Pārlūkot izejas kodu

Normalize feature card icons and make the Create & Print grid resize equally with the window.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 12 stundas atpakaļ
vecāks
revīzija
d9c60d4bb2

+ 2 - 2
smart_printer/AppTheme.swift

@@ -13,7 +13,7 @@ enum AppTheme {
13 13
 
14 14
     static let quickStartIconMax: CGFloat = 80
15 15
     static let quickStartIconMin: CGFloat = 56
16
-    static let featureIconMax: CGFloat = 56
16
+    static let featureIconMax: CGFloat = 48
17 17
     static let featureIconMin: CGFloat = 36
18 18
 
19 19
     static func quickStartIconSize(forCardWidth width: CGFloat) -> CGFloat {
@@ -21,7 +21,7 @@ enum AppTheme {
21 21
     }
22 22
 
23 23
     static func featureIconSize(forCardWidth width: CGFloat) -> CGFloat {
24
-        min(featureIconMax, max(featureIconMin, width * 0.38))
24
+        min(featureIconMax, max(featureIconMin, width * 0.24))
25 25
     }
26 26
 
27 27
     static let cornerRadius: CGFloat = 14

+ 90 - 68
smart_printer/FeatureIcons.swift

@@ -16,6 +16,10 @@ final class FeatureIconView: NSView {
16 16
         self.kind = kind
17 17
         super.init(frame: .zero)
18 18
         translatesAutoresizingMaskIntoConstraints = false
19
+        setContentHuggingPriority(.required, for: .horizontal)
20
+        setContentHuggingPriority(.required, for: .vertical)
21
+        setContentCompressionResistancePriority(.required, for: .horizontal)
22
+        setContentCompressionResistancePriority(.required, for: .vertical)
19 23
     }
20 24
 
21 25
     @available(*, unavailable)
@@ -38,42 +42,70 @@ final class FeatureIconView: NSView {
38 42
         }
39 43
     }
40 44
 
45
+    // MARK: - Layout
46
+
47
+    /// Shared artboard so every icon fills the same visual area.
48
+    private struct IconLayout {
49
+        let fill: CGRect
50
+        let d: CGFloat
51
+
52
+        init(bounds: NSRect) {
53
+            let side = min(bounds.width, bounds.height)
54
+            let originX = (bounds.width - side) / 2
55
+            let originY = (bounds.height - side) / 2
56
+            let inset = side * 0.12
57
+            fill = CGRect(x: originX + inset, y: originY + inset, width: side - inset * 2, height: side - inset * 2)
58
+            d = fill.width
59
+        }
60
+
61
+        func rect(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) -> CGRect {
62
+            CGRect(
63
+                x: fill.minX + fill.width * x,
64
+                y: fill.minY + fill.height * y,
65
+                width: fill.width * w,
66
+                height: fill.height * h
67
+            )
68
+        }
69
+
70
+        func dim(_ fraction: CGFloat) -> CGFloat {
71
+            d * fraction
72
+        }
73
+    }
74
+
41 75
     // MARK: - Scan File
42 76
 
43 77
     private func drawScanFile(in context: CGContext) {
44
-        let s = min(bounds.width, bounds.height)
45
-        let ox = (bounds.width - s) / 2
46
-        let oy = (bounds.height - s) / 2
78
+        let layout = IconLayout(bounds: bounds)
47 79
 
48
-        let bodyRect = CGRect(x: ox + s * 0.08, y: oy + s * 0.30, width: s * 0.84, height: s * 0.52)
49
-        drawSoftShadow(in: context, rect: CGRect(x: bodyRect.minX, y: bodyRect.maxY - s * 0.03, width: bodyRect.width, height: s * 0.05), radius: s * 0.025)
80
+        let bodyRect = layout.rect(0.08, 0.38, 0.84, 0.50)
81
+        drawSoftShadow(in: context, rect: CGRect(x: bodyRect.minX, y: bodyRect.maxY - layout.dim(0.03), width: bodyRect.width, height: layout.dim(0.05)), radius: layout.dim(0.025))
50 82
 
51 83
         let blueTop = NSColor(red: 0.38, green: 0.66, blue: 1.0, alpha: 1)
52 84
         let blueBottom = NSColor(red: 0.18, green: 0.44, blue: 0.94, alpha: 1)
53
-        fillGradient(in: context, path: roundedRect(bodyRect, radius: s * 0.06), colors: [blueTop.cgColor, blueBottom.cgColor], start: CGPoint(x: bodyRect.midX, y: bodyRect.minY), end: CGPoint(x: bodyRect.midX, y: bodyRect.maxY))
85
+        fillGradient(in: context, path: roundedRect(bodyRect, radius: layout.dim(0.06)), colors: [blueTop.cgColor, blueBottom.cgColor], start: CGPoint(x: bodyRect.midX, y: bodyRect.minY), end: CGPoint(x: bodyRect.midX, y: bodyRect.maxY))
54 86
 
55
-        let glassRect = CGRect(x: bodyRect.minX + s * 0.08, y: bodyRect.minY + s * 0.06, width: bodyRect.width * 0.84, height: bodyRect.height * 0.52)
87
+        let glassRect = CGRect(x: bodyRect.minX + layout.dim(0.06), y: bodyRect.minY + layout.dim(0.05), width: bodyRect.width * 0.84, height: bodyRect.height * 0.52)
56 88
         context.setFillColor(NSColor.white.withAlphaComponent(0.25).cgColor)
57
-        context.addPath(roundedRect(glassRect, radius: s * 0.03))
89
+        context.addPath(roundedRect(glassRect, radius: layout.dim(0.03)))
58 90
         context.fillPath()
59 91
 
60
-        let lidRect = CGRect(x: ox + s * 0.04, y: oy + s * 0.14, width: s * 0.92, height: s * 0.20)
61
-        fillGradient(in: context, path: roundedRect(lidRect, radius: s * 0.04), colors: [NSColor(red: 0.55, green: 0.76, blue: 1.0, alpha: 1).cgColor, blueTop.cgColor], start: CGPoint(x: lidRect.midX, y: lidRect.minY), end: CGPoint(x: lidRect.midX, y: lidRect.maxY))
92
+        let lidRect = layout.rect(0.06, 0.22, 0.88, 0.18)
93
+        fillGradient(in: context, path: roundedRect(lidRect, radius: layout.dim(0.04)), colors: [NSColor(red: 0.55, green: 0.76, blue: 1.0, alpha: 1).cgColor, blueTop.cgColor], start: CGPoint(x: lidRect.midX, y: lidRect.minY), end: CGPoint(x: lidRect.midX, y: lidRect.maxY))
62 94
 
63
-        let paperRect = CGRect(x: ox + s * 0.28, y: oy + s * 0.02, width: s * 0.44, height: s * 0.22)
95
+        let paperRect = layout.rect(0.28, 0.06, 0.44, 0.20)
64 96
         context.setFillColor(NSColor.white.cgColor)
65
-        context.addPath(roundedRect(paperRect, radius: s * 0.02))
97
+        context.addPath(roundedRect(paperRect, radius: layout.dim(0.02)))
66 98
         context.fillPath()
67 99
 
68 100
         for i in 0..<3 {
69
-            let lineY = paperRect.minY + s * 0.06 + CGFloat(i) * s * 0.045
101
+            let lineY = paperRect.minY + layout.dim(0.06) + CGFloat(i) * layout.dim(0.045)
70 102
             let lineW = paperRect.width * (0.85 - CGFloat(i) * 0.1)
71 103
             context.setFillColor(NSColor(red: 0.72, green: 0.82, blue: 0.98, alpha: 1).cgColor)
72
-            context.addPath(roundedRect(CGRect(x: paperRect.minX + s * 0.05, y: lineY, width: lineW, height: s * 0.025), radius: s * 0.01))
104
+            context.addPath(roundedRect(CGRect(x: paperRect.minX + layout.dim(0.05), y: lineY, width: lineW, height: layout.dim(0.025)), radius: layout.dim(0.01)))
73 105
             context.fillPath()
74 106
         }
75 107
 
76
-        let lightRect = CGRect(x: bodyRect.maxX - s * 0.14, y: bodyRect.minY + s * 0.08, width: s * 0.06, height: s * 0.06)
108
+        let lightRect = CGRect(x: bodyRect.maxX - layout.dim(0.14), y: bodyRect.minY + layout.dim(0.08), width: layout.dim(0.06), height: layout.dim(0.06))
77 109
         context.setFillColor(NSColor(red: 0.30, green: 0.90, blue: 0.55, alpha: 1).cgColor)
78 110
         context.fillEllipse(in: lightRect)
79 111
     }
@@ -81,22 +113,20 @@ final class FeatureIconView: NSView {
81 113
     // MARK: - Print Text
82 114
 
83 115
     private func drawPrintText(in context: CGContext) {
84
-        let s = min(bounds.width, bounds.height)
85
-        let ox = (bounds.width - s) / 2
86
-        let oy = (bounds.height - s) / 2
116
+        let layout = IconLayout(bounds: bounds)
87 117
 
88
-        let frameRect = CGRect(x: ox + s * 0.14, y: oy + s * 0.14, width: s * 0.72, height: s * 0.72)
89
-        drawSoftShadow(in: context, rect: frameRect.insetBy(dx: s * 0.04, dy: -s * 0.02), radius: s * 0.03)
118
+        let frameRect = layout.rect(0.08, 0.08, 0.84, 0.84)
119
+        drawSoftShadow(in: context, rect: frameRect.insetBy(dx: layout.dim(0.04), dy: -layout.dim(0.02)), radius: layout.dim(0.03))
90 120
 
91 121
         context.setStrokeColor(NSColor(red: 0.62, green: 0.44, blue: 0.96, alpha: 1).cgColor)
92
-        context.setLineWidth(s * 0.025)
93
-        context.setLineDash(phase: 0, lengths: [s * 0.04, s * 0.04])
94
-        context.addPath(roundedRect(frameRect, radius: s * 0.04))
122
+        context.setLineWidth(layout.dim(0.03))
123
+        context.setLineDash(phase: 0, lengths: [layout.dim(0.04), layout.dim(0.04)])
124
+        context.addPath(roundedRect(frameRect, radius: layout.dim(0.04)))
95 125
         context.strokePath()
96 126
         context.setLineDash(phase: 0, lengths: [])
97 127
 
98 128
         let purple = NSColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1)
99
-        let font = NSFont.systemFont(ofSize: s * 0.42, weight: .bold)
129
+        let font = NSFont.systemFont(ofSize: layout.dim(0.40), weight: .bold)
100 130
         let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: purple]
101 131
         let text = "T" as NSString
102 132
         let textSize = text.size(withAttributes: attrs)
@@ -106,20 +136,18 @@ final class FeatureIconView: NSView {
106 136
     // MARK: - Print Contacts
107 137
 
108 138
     private func drawPrintContacts(in context: CGContext) {
109
-        let s = min(bounds.width, bounds.height)
110
-        let ox = (bounds.width - s) / 2
111
-        let oy = (bounds.height - s) / 2
139
+        let layout = IconLayout(bounds: bounds)
112 140
 
113
-        let bookRect = CGRect(x: ox + s * 0.12, y: oy + s * 0.18, width: s * 0.62, height: s * 0.68)
114
-        drawSoftShadow(in: context, rect: CGRect(x: bookRect.minX, y: bookRect.maxY - s * 0.03, width: bookRect.width, height: s * 0.05), radius: s * 0.025)
141
+        let bookRect = layout.rect(0.08, 0.08, 0.84, 0.84)
142
+        drawSoftShadow(in: context, rect: CGRect(x: bookRect.minX, y: bookRect.maxY - layout.dim(0.03), width: bookRect.width, height: layout.dim(0.05)), radius: layout.dim(0.025))
115 143
 
116 144
         let greenTop = NSColor(red: 0.42, green: 0.86, blue: 0.52, alpha: 1)
117 145
         let greenBottom = NSColor(red: 0.18, green: 0.68, blue: 0.38, alpha: 1)
118
-        fillGradient(in: context, path: roundedRect(bookRect, radius: s * 0.05), colors: [greenTop.cgColor, greenBottom.cgColor], start: CGPoint(x: bookRect.midX, y: bookRect.minY), end: CGPoint(x: bookRect.midX, y: bookRect.maxY))
146
+        fillGradient(in: context, path: roundedRect(bookRect, radius: layout.dim(0.05)), colors: [greenTop.cgColor, greenBottom.cgColor], start: CGPoint(x: bookRect.midX, y: bookRect.minY), end: CGPoint(x: bookRect.midX, y: bookRect.maxY))
119 147
 
120
-        let spineRect = CGRect(x: bookRect.minX, y: bookRect.minY, width: s * 0.08, height: bookRect.height)
148
+        let spineRect = CGRect(x: bookRect.minX, y: bookRect.minY, width: layout.dim(0.08), height: bookRect.height)
121 149
         context.setFillColor(NSColor(red: 0.12, green: 0.52, blue: 0.30, alpha: 1).cgColor)
122
-        context.addPath(roundedRect(spineRect, radius: s * 0.02, topLeft: s * 0.05, topRight: 0, bottomLeft: s * 0.05, bottomRight: 0))
150
+        context.addPath(roundedRect(spineRect, radius: layout.dim(0.02), topLeft: layout.dim(0.05), topRight: 0, bottomLeft: layout.dim(0.05), bottomRight: 0))
123 151
         context.fillPath()
124 152
 
125 153
         let tabColors: [NSColor] = [
@@ -128,42 +156,40 @@ final class FeatureIconView: NSView {
128 156
             NSColor(red: 0.96, green: 0.82, blue: 0.40, alpha: 1),
129 157
         ]
130 158
         for (i, color) in tabColors.enumerated() {
131
-            let tabRect = CGRect(x: bookRect.maxX - s * 0.02, y: bookRect.minY + s * 0.10 + CGFloat(i) * s * 0.14, width: s * 0.10, height: s * 0.10)
159
+            let tabRect = CGRect(x: bookRect.maxX - layout.dim(0.02), y: bookRect.minY + layout.dim(0.10) + CGFloat(i) * layout.dim(0.14), width: layout.dim(0.10), height: layout.dim(0.10))
132 160
             context.setFillColor(color.cgColor)
133
-            context.addPath(roundedRect(tabRect, radius: s * 0.02, topLeft: 0, topRight: s * 0.02, bottomLeft: 0, bottomRight: s * 0.02))
161
+            context.addPath(roundedRect(tabRect, radius: layout.dim(0.02), topLeft: 0, topRight: layout.dim(0.02), bottomLeft: 0, bottomRight: layout.dim(0.02)))
134 162
             context.fillPath()
135 163
         }
136 164
 
137
-        let personCenter = CGPoint(x: bookRect.midX - s * 0.02, y: bookRect.midY + s * 0.04)
165
+        let personCenter = CGPoint(x: bookRect.midX - layout.dim(0.02), y: bookRect.midY + layout.dim(0.04))
138 166
         context.setFillColor(NSColor.white.cgColor)
139
-        context.fillEllipse(in: CGRect(x: personCenter.x - s * 0.10, y: personCenter.y - s * 0.16, width: s * 0.20, height: s * 0.20))
140
-        context.fillEllipse(in: CGRect(x: personCenter.x - s * 0.14, y: personCenter.y + s * 0.02, width: s * 0.28, height: s * 0.22))
167
+        context.fillEllipse(in: CGRect(x: personCenter.x - layout.dim(0.10), y: personCenter.y - layout.dim(0.16), width: layout.dim(0.20), height: layout.dim(0.20)))
168
+        context.fillEllipse(in: CGRect(x: personCenter.x - layout.dim(0.14), y: personCenter.y + layout.dim(0.02), width: layout.dim(0.28), height: layout.dim(0.22)))
141 169
     }
142 170
 
143 171
     // MARK: - Print Website
144 172
 
145 173
     private func drawPrintWebsite(in context: CGContext) {
146
-        let s = min(bounds.width, bounds.height)
147
-        let ox = (bounds.width - s) / 2
148
-        let oy = (bounds.height - s) / 2
174
+        let layout = IconLayout(bounds: bounds)
149 175
 
150
-        let globeRect = CGRect(x: ox + s * 0.10, y: oy + s * 0.14, width: s * 0.58, height: s * 0.58)
151
-        drawSoftShadow(in: context, rect: globeRect.insetBy(dx: 0, dy: -s * 0.03), radius: s * 0.03)
176
+        let globeRect = layout.rect(0.08, 0.08, 0.84, 0.84)
177
+        drawSoftShadow(in: context, rect: globeRect.insetBy(dx: 0, dy: -layout.dim(0.03)), radius: layout.dim(0.03))
152 178
 
153 179
         let blueTop = NSColor(red: 0.40, green: 0.70, blue: 1.0, alpha: 1)
154 180
         let blueBottom = NSColor(red: 0.16, green: 0.44, blue: 0.92, alpha: 1)
155 181
         fillGradient(in: context, path: CGPath(ellipseIn: globeRect, transform: nil), colors: [blueTop.cgColor, blueBottom.cgColor], start: CGPoint(x: globeRect.midX, y: globeRect.minY), end: CGPoint(x: globeRect.midX, y: globeRect.maxY))
156 182
 
157 183
         context.setStrokeColor(NSColor.white.withAlphaComponent(0.55).cgColor)
158
-        context.setLineWidth(s * 0.018)
184
+        context.setLineWidth(layout.dim(0.018))
159 185
         context.strokeEllipse(in: globeRect.insetBy(dx: globeRect.width * 0.15, dy: 0))
160 186
         context.strokeEllipse(in: globeRect.insetBy(dx: 0, dy: globeRect.height * 0.22))
161 187
         context.move(to: CGPoint(x: globeRect.minX, y: globeRect.midY))
162 188
         context.addLine(to: CGPoint(x: globeRect.maxX, y: globeRect.midY))
163 189
         context.strokePath()
164 190
 
165
-        let cursorSize = s * 0.30
166
-        let cursorOrigin = CGPoint(x: ox + s * 0.52, y: oy + s * 0.38)
191
+        let cursorSize = layout.dim(0.22)
192
+        let cursorOrigin = CGPoint(x: layout.fill.maxX - layout.dim(0.12), y: layout.fill.minY + layout.fill.height * 0.42)
167 193
         context.setFillColor(NSColor.white.cgColor)
168 194
         let cursor = CGMutablePath()
169 195
         cursor.move(to: cursorOrigin)
@@ -173,7 +199,7 @@ final class FeatureIconView: NSView {
173 199
         context.addPath(cursor)
174 200
         context.fillPath()
175 201
         context.setStrokeColor(NSColor(red: 0.30, green: 0.50, blue: 0.90, alpha: 1).cgColor)
176
-        context.setLineWidth(s * 0.02)
202
+        context.setLineWidth(layout.dim(0.02))
177 203
         context.addPath(cursor)
178 204
         context.strokePath()
179 205
     }
@@ -181,12 +207,10 @@ final class FeatureIconView: NSView {
181 207
     // MARK: - Draw & Print
182 208
 
183 209
     private func drawDrawPrint(in context: CGContext) {
184
-        let s = min(bounds.width, bounds.height)
185
-        let ox = (bounds.width - s) / 2
186
-        let oy = (bounds.height - s) / 2
210
+        let layout = IconLayout(bounds: bounds)
187 211
 
188
-        let paletteRect = CGRect(x: ox + s * 0.08, y: oy + s * 0.30, width: s * 0.72, height: s * 0.48)
189
-        drawSoftShadow(in: context, rect: CGRect(x: paletteRect.minX, y: paletteRect.maxY - s * 0.02, width: paletteRect.width, height: s * 0.04), radius: s * 0.025)
212
+        let paletteRect = layout.rect(0.06, 0.14, 0.80, 0.72)
213
+        drawSoftShadow(in: context, rect: CGRect(x: paletteRect.minX, y: paletteRect.maxY - layout.dim(0.02), width: paletteRect.width, height: layout.dim(0.04)), radius: layout.dim(0.025))
190 214
 
191 215
         let woodTop = NSColor(red: 0.96, green: 0.78, blue: 0.48, alpha: 1)
192 216
         let woodBottom = NSColor(red: 0.86, green: 0.58, blue: 0.28, alpha: 1)
@@ -205,17 +229,17 @@ final class FeatureIconView: NSView {
205 229
             let angle = CGFloat(i) * .pi * 0.32 - .pi * 0.5
206 230
             let radius = paletteRect.width * 0.28
207 231
             let dotCenter = CGPoint(x: thumbCenter.x + cos(angle) * radius, y: thumbCenter.y + sin(angle) * radius * 0.55)
208
-            let dotSize = s * 0.09
232
+            let dotSize = layout.dim(0.09)
209 233
             context.setFillColor(color.cgColor)
210 234
             context.fillEllipse(in: CGRect(x: dotCenter.x - dotSize / 2, y: dotCenter.y - dotSize / 2, width: dotSize, height: dotSize))
211 235
         }
212 236
 
213 237
         context.saveGState()
214
-        context.translateBy(x: ox + s * 0.58, y: oy + s * 0.08)
238
+        context.translateBy(x: layout.fill.minX + layout.fill.width * 0.58, y: layout.fill.minY + layout.fill.height * 0.04)
215 239
         context.rotate(by: -.pi / 5)
216
-        let brushRect = CGRect(x: 0, y: 0, width: s * 0.10, height: s * 0.42)
217
-        fillGradient(in: context, path: roundedRect(brushRect, radius: s * 0.02), colors: [NSColor(red: 0.72, green: 0.48, blue: 0.28, alpha: 1).cgColor, NSColor(red: 0.52, green: 0.32, blue: 0.18, alpha: 1).cgColor], start: CGPoint(x: brushRect.midX, y: brushRect.minY), end: CGPoint(x: brushRect.midX, y: brushRect.maxY))
218
-        let bristleRect = CGRect(x: brushRect.midX - s * 0.05, y: brushRect.maxY - s * 0.02, width: s * 0.10, height: s * 0.10)
240
+        let brushRect = CGRect(x: 0, y: 0, width: layout.dim(0.10), height: layout.dim(0.44))
241
+        fillGradient(in: context, path: roundedRect(brushRect, radius: layout.dim(0.02)), colors: [NSColor(red: 0.72, green: 0.48, blue: 0.28, alpha: 1).cgColor, NSColor(red: 0.52, green: 0.32, blue: 0.18, alpha: 1).cgColor], start: CGPoint(x: brushRect.midX, y: brushRect.minY), end: CGPoint(x: brushRect.midX, y: brushRect.maxY))
242
+        let bristleRect = CGRect(x: brushRect.midX - layout.dim(0.05), y: brushRect.maxY - layout.dim(0.02), width: layout.dim(0.10), height: layout.dim(0.10))
219 243
         context.setFillColor(NSColor(red: 0.96, green: 0.55, blue: 0.30, alpha: 1).cgColor)
220 244
         context.fillEllipse(in: bristleRect)
221 245
         context.restoreGState()
@@ -232,34 +256,32 @@ final class FeatureIconView: NSView {
232 256
     // MARK: - OCR File
233 257
 
234 258
     private func drawOCRFile(in context: CGContext) {
235
-        let s = min(bounds.width, bounds.height)
236
-        let ox = (bounds.width - s) / 2
237
-        let oy = (bounds.height - s) / 2
259
+        let layout = IconLayout(bounds: bounds)
238 260
 
239
-        let docRect = CGRect(x: ox + s * 0.18, y: oy + s * 0.10, width: s * 0.56, height: s * 0.72)
240
-        drawSoftShadow(in: context, rect: CGRect(x: docRect.minX, y: docRect.maxY - s * 0.03, width: docRect.width, height: s * 0.05), radius: s * 0.025)
261
+        let docRect = layout.rect(0.08, 0.06, 0.84, 0.88)
262
+        drawSoftShadow(in: context, rect: CGRect(x: docRect.minX, y: docRect.maxY - layout.dim(0.03), width: docRect.width, height: layout.dim(0.05)), radius: layout.dim(0.025))
241 263
 
242 264
         context.setFillColor(NSColor.white.cgColor)
243
-        context.addPath(roundedRect(docRect, radius: s * 0.04))
265
+        context.addPath(roundedRect(docRect, radius: layout.dim(0.04)))
244 266
         context.fillPath()
245 267
 
246 268
         let purpleTop = NSColor(red: 0.62, green: 0.44, blue: 0.96, alpha: 1)
247 269
         let purpleBottom = NSColor(red: 0.45, green: 0.28, blue: 0.86, alpha: 1)
248
-        let headerRect = CGRect(x: docRect.minX, y: docRect.minY, width: docRect.width, height: s * 0.18)
249
-        fillGradient(in: context, path: roundedRect(headerRect, radius: s * 0.04, topLeft: s * 0.04, topRight: s * 0.04, bottomLeft: 0, bottomRight: 0), colors: [purpleTop.cgColor, purpleBottom.cgColor], start: CGPoint(x: headerRect.midX, y: headerRect.minY), end: CGPoint(x: headerRect.midX, y: headerRect.maxY))
270
+        let headerRect = CGRect(x: docRect.minX, y: docRect.minY, width: docRect.width, height: layout.dim(0.18))
271
+        fillGradient(in: context, path: roundedRect(headerRect, radius: layout.dim(0.04), topLeft: layout.dim(0.04), topRight: layout.dim(0.04), bottomLeft: 0, bottomRight: 0), colors: [purpleTop.cgColor, purpleBottom.cgColor], start: CGPoint(x: headerRect.midX, y: headerRect.minY), end: CGPoint(x: headerRect.midX, y: headerRect.maxY))
250 272
 
251
-        let font = NSFont.systemFont(ofSize: s * 0.14, weight: .bold)
273
+        let font = NSFont.systemFont(ofSize: layout.dim(0.14), weight: .bold)
252 274
         let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: NSColor.white]
253 275
         let text = "OCR" as NSString
254 276
         let textSize = text.size(withAttributes: attrs)
255
-        text.draw(at: CGPoint(x: docRect.midX - textSize.width / 2, y: docRect.minY + s * 0.04), withAttributes: attrs)
277
+        text.draw(at: CGPoint(x: docRect.midX - textSize.width / 2, y: docRect.minY + layout.dim(0.04)), withAttributes: attrs)
256 278
 
257 279
         let lineColor = NSColor(red: 0.78, green: 0.72, blue: 0.96, alpha: 1)
258 280
         for i in 0..<4 {
259
-            let lineY = docRect.minY + s * 0.24 + CGFloat(i) * s * 0.10
281
+            let lineY = docRect.minY + layout.dim(0.24) + CGFloat(i) * layout.dim(0.10)
260 282
             let lineW = docRect.width * (0.80 - CGFloat(i) * 0.08)
261 283
             context.setFillColor(lineColor.cgColor)
262
-            context.addPath(roundedRect(CGRect(x: docRect.minX + s * 0.08, y: lineY, width: lineW, height: s * 0.035), radius: s * 0.012))
284
+            context.addPath(roundedRect(CGRect(x: docRect.minX + layout.dim(0.08), y: lineY, width: lineW, height: layout.dim(0.035)), radius: layout.dim(0.012)))
263 285
             context.fillPath()
264 286
         }
265 287
     }

+ 6 - 6
smart_printer/UIComponents.swift

@@ -308,12 +308,12 @@ final class FeatureCardView: NSView {
308 308
         applyCardShadow()
309 309
 
310 310
         let titleLabel = NSTextField(labelWithString: data.title)
311
-        titleLabel.font = AppTheme.semiboldFont(size: 15)
311
+        titleLabel.font = AppTheme.semiboldFont(size: 14)
312 312
         titleLabel.textColor = AppTheme.textPrimary
313 313
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
314 314
 
315 315
         let subtitleLabel = NSTextField(labelWithString: data.subtitle)
316
-        subtitleLabel.font = AppTheme.regularFont(size: 12)
316
+        subtitleLabel.font = AppTheme.regularFont(size: 11)
317 317
         subtitleLabel.textColor = AppTheme.textSecondary
318 318
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
319 319
 
@@ -337,13 +337,13 @@ final class FeatureCardView: NSView {
337 337
         addSubview(arrowButton)
338 338
 
339 339
         NSLayoutConstraint.activate([
340
-            heightAnchor.constraint(equalToConstant: 104),
340
+            heightAnchor.constraint(equalToConstant: 96),
341 341
 
342
-            iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
342
+            iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
343 343
             iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
344 344
 
345
-            titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8),
346
-            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 22),
345
+            titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
346
+            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
347 347
             titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32),
348 348
 
349 349
             subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),

+ 18 - 41
smart_printer/ViewController.swift

@@ -258,7 +258,15 @@ class ViewController: NSViewController {
258 258
             FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
259 259
         ]
260 260
 
261
-        let grid = makeFeatureGrid(features: features, columns: 4)
261
+        let topRow = makeFeatureRow(features: Array(features.prefix(4)))
262
+        let bottomRow = makeFeatureRow(features: Array(features.suffix(2)))
263
+
264
+        let grid = NSStackView(views: [topRow, bottomRow])
265
+        grid.orientation = .vertical
266
+        grid.spacing = AppTheme.featureGridSpacing
267
+        grid.distribution = .fillEqually
268
+        grid.translatesAutoresizingMaskIntoConstraints = false
269
+
262 270
         section.addSubview(grid)
263 271
 
264 272
         NSLayoutConstraint.activate([
@@ -274,49 +282,18 @@ class ViewController: NSViewController {
274 282
         return section
275 283
     }
276 284
 
277
-    private func makeFeatureGrid(features: [FeatureCardData], columns: Int) -> NSView {
278
-        let grid = NSView()
279
-        grid.translatesAutoresizingMaskIntoConstraints = false
280
-
281
-        let spacing = AppTheme.featureGridSpacing
282
-        let cards = features.map { FeatureCardView(data: $0) }
283
-        let rowCount = (cards.count + columns - 1) / columns
284
-
285
-        for card in cards {
286
-            grid.addSubview(card)
287
-        }
288
-
289
-        for (index, card) in cards.enumerated() {
290
-            let row = index / columns
291
-            let col = index % columns
292
-            let columnAnchor = cards[col]
285
+    private func makeFeatureRow(features: [FeatureCardData]) -> NSStackView {
286
+        let row = NSStackView()
287
+        row.orientation = .horizontal
288
+        row.spacing = AppTheme.featureGridSpacing
289
+        row.distribution = .fillEqually
290
+        row.translatesAutoresizingMaskIntoConstraints = false
293 291
 
294
-            if col == 0 {
295
-                card.leadingAnchor.constraint(equalTo: grid.leadingAnchor).isActive = true
296
-            } else {
297
-                let leftCard = cards[index - 1]
298
-                card.leadingAnchor.constraint(equalTo: leftCard.trailingAnchor, constant: spacing).isActive = true
299
-            }
300
-
301
-            card.widthAnchor.constraint(equalTo: columnAnchor.widthAnchor).isActive = true
302
-
303
-            if row == 0 {
304
-                card.topAnchor.constraint(equalTo: grid.topAnchor).isActive = true
305
-            } else {
306
-                let aboveCard = cards[index - columns]
307
-                card.topAnchor.constraint(equalTo: aboveCard.bottomAnchor, constant: spacing).isActive = true
308
-            }
309
-
310
-            if col == columns - 1 {
311
-                card.trailingAnchor.constraint(equalTo: grid.trailingAnchor).isActive = true
312
-            }
313
-
314
-            if row == rowCount - 1 {
315
-                card.bottomAnchor.constraint(equalTo: grid.bottomAnchor).isActive = true
316
-            }
292
+        for data in features {
293
+            row.addArrangedSubview(FeatureCardView(data: data))
317 294
         }
318 295
 
319
-        return grid
296
+        return row
320 297
     }
321 298
 }
322 299