瀏覽代碼

Redesign Create & Print section to match reference layout.

Use custom 3D icons, horizontal cards, and a 4+2 grid so the section matches the intended dashboard design.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 15 小時之前
父節點
當前提交
6a9e45e463
共有 3 個文件被更改,包括 385 次插入50 次删除
  1. 307 0
      smart_printer/FeatureIcons.swift
  2. 22 37
      smart_printer/UIComponents.swift
  3. 56 13
      smart_printer/ViewController.swift

+ 307 - 0
smart_printer/FeatureIcons.swift

@@ -0,0 +1,307 @@
1
+import Cocoa
2
+
3
+enum FeatureIconKind {
4
+    case scanFile
5
+    case printText
6
+    case printContacts
7
+    case printWebsite
8
+    case drawPrint
9
+    case ocrFile
10
+}
11
+
12
+final class FeatureIconView: NSView {
13
+    private let kind: FeatureIconKind
14
+
15
+    init(kind: FeatureIconKind) {
16
+        self.kind = kind
17
+        super.init(frame: .zero)
18
+        translatesAutoresizingMaskIntoConstraints = false
19
+    }
20
+
21
+    @available(*, unavailable)
22
+    required init?(coder: NSCoder) { nil }
23
+
24
+    override var isFlipped: Bool { true }
25
+    override var isOpaque: Bool { false }
26
+
27
+    override func draw(_ dirtyRect: NSRect) {
28
+        super.draw(dirtyRect)
29
+        guard let context = NSGraphicsContext.current?.cgContext else { return }
30
+
31
+        switch kind {
32
+        case .scanFile: drawScanFile(in: context)
33
+        case .printText: drawPrintText(in: context)
34
+        case .printContacts: drawPrintContacts(in: context)
35
+        case .printWebsite: drawPrintWebsite(in: context)
36
+        case .drawPrint: drawDrawPrint(in: context)
37
+        case .ocrFile: drawOCRFile(in: context)
38
+        }
39
+    }
40
+
41
+    // MARK: - Scan File
42
+
43
+    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
47
+
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)
50
+
51
+        let blueTop = NSColor(red: 0.38, green: 0.66, blue: 1.0, alpha: 1)
52
+        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))
54
+
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)
56
+        context.setFillColor(NSColor.white.withAlphaComponent(0.25).cgColor)
57
+        context.addPath(roundedRect(glassRect, radius: s * 0.03))
58
+        context.fillPath()
59
+
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))
62
+
63
+        let paperRect = CGRect(x: ox + s * 0.28, y: oy + s * 0.02, width: s * 0.44, height: s * 0.22)
64
+        context.setFillColor(NSColor.white.cgColor)
65
+        context.addPath(roundedRect(paperRect, radius: s * 0.02))
66
+        context.fillPath()
67
+
68
+        for i in 0..<3 {
69
+            let lineY = paperRect.minY + s * 0.06 + CGFloat(i) * s * 0.045
70
+            let lineW = paperRect.width * (0.85 - CGFloat(i) * 0.1)
71
+            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))
73
+            context.fillPath()
74
+        }
75
+
76
+        let lightRect = CGRect(x: bodyRect.maxX - s * 0.14, y: bodyRect.minY + s * 0.08, width: s * 0.06, height: s * 0.06)
77
+        context.setFillColor(NSColor(red: 0.30, green: 0.90, blue: 0.55, alpha: 1).cgColor)
78
+        context.fillEllipse(in: lightRect)
79
+    }
80
+
81
+    // MARK: - Print Text
82
+
83
+    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
87
+
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)
90
+
91
+        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))
95
+        context.strokePath()
96
+        context.setLineDash(phase: 0, lengths: [])
97
+
98
+        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)
100
+        let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: purple]
101
+        let text = "T" as NSString
102
+        let textSize = text.size(withAttributes: attrs)
103
+        text.draw(at: CGPoint(x: frameRect.midX - textSize.width / 2, y: frameRect.midY - textSize.height / 2), withAttributes: attrs)
104
+    }
105
+
106
+    // MARK: - Print Contacts
107
+
108
+    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
112
+
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)
115
+
116
+        let greenTop = NSColor(red: 0.42, green: 0.86, blue: 0.52, alpha: 1)
117
+        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))
119
+
120
+        let spineRect = CGRect(x: bookRect.minX, y: bookRect.minY, width: s * 0.08, height: bookRect.height)
121
+        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))
123
+        context.fillPath()
124
+
125
+        let tabColors: [NSColor] = [
126
+            NSColor(red: 0.96, green: 0.55, blue: 0.55, alpha: 1),
127
+            NSColor(red: 0.55, green: 0.72, blue: 0.96, alpha: 1),
128
+            NSColor(red: 0.96, green: 0.82, blue: 0.40, alpha: 1),
129
+        ]
130
+        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)
132
+            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))
134
+            context.fillPath()
135
+        }
136
+
137
+        let personCenter = CGPoint(x: bookRect.midX - s * 0.02, y: bookRect.midY + s * 0.04)
138
+        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))
141
+    }
142
+
143
+    // MARK: - Print Website
144
+
145
+    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
149
+
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)
152
+
153
+        let blueTop = NSColor(red: 0.40, green: 0.70, blue: 1.0, alpha: 1)
154
+        let blueBottom = NSColor(red: 0.16, green: 0.44, blue: 0.92, alpha: 1)
155
+        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
+
157
+        context.setStrokeColor(NSColor.white.withAlphaComponent(0.55).cgColor)
158
+        context.setLineWidth(s * 0.018)
159
+        context.strokeEllipse(in: globeRect.insetBy(dx: globeRect.width * 0.15, dy: 0))
160
+        context.strokeEllipse(in: globeRect.insetBy(dx: 0, dy: globeRect.height * 0.22))
161
+        context.move(to: CGPoint(x: globeRect.minX, y: globeRect.midY))
162
+        context.addLine(to: CGPoint(x: globeRect.maxX, y: globeRect.midY))
163
+        context.strokePath()
164
+
165
+        let cursorSize = s * 0.30
166
+        let cursorOrigin = CGPoint(x: ox + s * 0.52, y: oy + s * 0.38)
167
+        context.setFillColor(NSColor.white.cgColor)
168
+        let cursor = CGMutablePath()
169
+        cursor.move(to: cursorOrigin)
170
+        cursor.addLine(to: CGPoint(x: cursorOrigin.x, y: cursorOrigin.y + cursorSize))
171
+        cursor.addLine(to: CGPoint(x: cursorOrigin.x + cursorSize * 0.35, y: cursorOrigin.y + cursorSize * 0.72))
172
+        cursor.closeSubpath()
173
+        context.addPath(cursor)
174
+        context.fillPath()
175
+        context.setStrokeColor(NSColor(red: 0.30, green: 0.50, blue: 0.90, alpha: 1).cgColor)
176
+        context.setLineWidth(s * 0.02)
177
+        context.addPath(cursor)
178
+        context.strokePath()
179
+    }
180
+
181
+    // MARK: - Draw & Print
182
+
183
+    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
187
+
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)
190
+
191
+        let woodTop = NSColor(red: 0.96, green: 0.78, blue: 0.48, alpha: 1)
192
+        let woodBottom = NSColor(red: 0.86, green: 0.58, blue: 0.28, alpha: 1)
193
+        let palettePath = paletteShape(in: paletteRect)
194
+        fillGradient(in: context, path: palettePath, colors: [woodTop.cgColor, woodBottom.cgColor], start: CGPoint(x: paletteRect.midX, y: paletteRect.minY), end: CGPoint(x: paletteRect.midX, y: paletteRect.maxY))
195
+
196
+        let paintColors: [NSColor] = [
197
+            NSColor(red: 0.96, green: 0.35, blue: 0.35, alpha: 1),
198
+            NSColor(red: 0.35, green: 0.65, blue: 0.96, alpha: 1),
199
+            NSColor(red: 0.35, green: 0.82, blue: 0.45, alpha: 1),
200
+            NSColor(red: 0.96, green: 0.78, blue: 0.25, alpha: 1),
201
+            NSColor(red: 0.72, green: 0.40, blue: 0.96, alpha: 1),
202
+        ]
203
+        let thumbCenter = CGPoint(x: paletteRect.minX + paletteRect.width * 0.18, y: paletteRect.midY)
204
+        for (i, color) in paintColors.enumerated() {
205
+            let angle = CGFloat(i) * .pi * 0.32 - .pi * 0.5
206
+            let radius = paletteRect.width * 0.28
207
+            let dotCenter = CGPoint(x: thumbCenter.x + cos(angle) * radius, y: thumbCenter.y + sin(angle) * radius * 0.55)
208
+            let dotSize = s * 0.09
209
+            context.setFillColor(color.cgColor)
210
+            context.fillEllipse(in: CGRect(x: dotCenter.x - dotSize / 2, y: dotCenter.y - dotSize / 2, width: dotSize, height: dotSize))
211
+        }
212
+
213
+        context.saveGState()
214
+        context.translateBy(x: ox + s * 0.58, y: oy + s * 0.08)
215
+        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)
219
+        context.setFillColor(NSColor(red: 0.96, green: 0.55, blue: 0.30, alpha: 1).cgColor)
220
+        context.fillEllipse(in: bristleRect)
221
+        context.restoreGState()
222
+    }
223
+
224
+    private func paletteShape(in rect: CGRect) -> CGPath {
225
+        let path = CGMutablePath()
226
+        path.addEllipse(in: rect)
227
+        let thumb = CGRect(x: rect.minX - rect.width * 0.08, y: rect.midY - rect.height * 0.18, width: rect.width * 0.22, height: rect.height * 0.36)
228
+        path.addEllipse(in: thumb)
229
+        return path
230
+    }
231
+
232
+    // MARK: - OCR File
233
+
234
+    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
238
+
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)
241
+
242
+        context.setFillColor(NSColor.white.cgColor)
243
+        context.addPath(roundedRect(docRect, radius: s * 0.04))
244
+        context.fillPath()
245
+
246
+        let purpleTop = NSColor(red: 0.62, green: 0.44, blue: 0.96, alpha: 1)
247
+        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))
250
+
251
+        let font = NSFont.systemFont(ofSize: s * 0.14, weight: .bold)
252
+        let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: NSColor.white]
253
+        let text = "OCR" as NSString
254
+        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)
256
+
257
+        let lineColor = NSColor(red: 0.78, green: 0.72, blue: 0.96, alpha: 1)
258
+        for i in 0..<4 {
259
+            let lineY = docRect.minY + s * 0.24 + CGFloat(i) * s * 0.10
260
+            let lineW = docRect.width * (0.80 - CGFloat(i) * 0.08)
261
+            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))
263
+            context.fillPath()
264
+        }
265
+    }
266
+
267
+    // MARK: - Helpers
268
+
269
+    private func drawSoftShadow(in context: CGContext, rect: CGRect, radius: CGFloat) {
270
+        context.saveGState()
271
+        context.setFillColor(NSColor.black.withAlphaComponent(0.10).cgColor)
272
+        context.addPath(roundedRect(rect.offsetBy(dx: 0, dy: radius * 0.2), radius: radius))
273
+        context.fillPath()
274
+        context.restoreGState()
275
+    }
276
+
277
+    private func fillGradient(in context: CGContext, path: CGPath, colors: [CGColor], start: CGPoint, end: CGPoint) {
278
+        context.saveGState()
279
+        context.addPath(path)
280
+        context.clip()
281
+        guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: [0, 1]) else {
282
+            context.restoreGState()
283
+            return
284
+        }
285
+        context.drawLinearGradient(gradient, start: start, end: end, options: [])
286
+        context.restoreGState()
287
+    }
288
+
289
+    private func roundedRect(_ rect: CGRect, radius: CGFloat) -> CGPath {
290
+        roundedRect(rect, radius: radius, topLeft: radius, topRight: radius, bottomLeft: radius, bottomRight: radius)
291
+    }
292
+
293
+    private func roundedRect(_ rect: CGRect, radius: CGFloat, topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) -> CGPath {
294
+        let path = CGMutablePath()
295
+        path.move(to: CGPoint(x: rect.minX + topLeft, y: rect.maxY))
296
+        path.addLine(to: CGPoint(x: rect.maxX - topRight, y: rect.maxY))
297
+        path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY - topRight), control: CGPoint(x: rect.maxX, y: rect.maxY))
298
+        path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + bottomRight))
299
+        path.addQuadCurve(to: CGPoint(x: rect.maxX - bottomRight, y: rect.minY), control: CGPoint(x: rect.maxX, y: rect.minY))
300
+        path.addLine(to: CGPoint(x: rect.minX + bottomLeft, y: rect.minY))
301
+        path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.minY + bottomLeft), control: CGPoint(x: rect.minX, y: rect.minY))
302
+        path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY - topLeft))
303
+        path.addQuadCurve(to: CGPoint(x: rect.minX + topLeft, y: rect.maxY), control: CGPoint(x: rect.minX, y: rect.maxY))
304
+        path.closeSubpath()
305
+        return path
306
+    }
307
+}

+ 22 - 37
smart_printer/UIComponents.swift

@@ -258,8 +258,7 @@ final class QuickStartCardView: NSView {
258 258
 struct FeatureCardData {
259 259
     let title: String
260 260
     let subtitle: String
261
-    let accentColor: NSColor
262
-    let symbolName: String
261
+    let iconKind: FeatureIconKind
263 262
 }
264 263
 
265 264
 final class FeatureCardView: NSView {
@@ -271,19 +270,7 @@ final class FeatureCardView: NSView {
271 270
         layer?.cornerRadius = AppTheme.featureCardCornerRadius
272 271
         applyCardShadow()
273 272
 
274
-        let iconBackground = NSView()
275
-        iconBackground.wantsLayer = true
276
-        iconBackground.layer?.backgroundColor = data.accentColor.withAlphaComponent(0.12).cgColor
277
-        iconBackground.layer?.cornerRadius = 14
278
-        iconBackground.translatesAutoresizingMaskIntoConstraints = false
279
-
280
-        let iconView = NSImageView()
281
-        iconView.translatesAutoresizingMaskIntoConstraints = false
282
-        if let image = NSImage(systemSymbolName: data.symbolName, accessibilityDescription: data.title) {
283
-            let config = NSImage.SymbolConfiguration(pointSize: 22, weight: .medium)
284
-            iconView.image = image.withSymbolConfiguration(config)
285
-        }
286
-        iconView.contentTintColor = data.accentColor
273
+        let iconView = FeatureIconView(kind: data.iconKind)
287 274
 
288 275
         let titleLabel = NSTextField(labelWithString: data.title)
289 276
         titleLabel.font = AppTheme.semiboldFont(size: 15)
@@ -298,44 +285,42 @@ final class FeatureCardView: NSView {
298 285
         let arrowButton = NSButton()
299 286
         arrowButton.isBordered = false
300 287
         arrowButton.wantsLayer = true
301
-        arrowButton.layer?.backgroundColor = data.accentColor.withAlphaComponent(0.12).cgColor
302
-        arrowButton.layer?.cornerRadius = 16
288
+        arrowButton.layer?.backgroundColor = NSColor.white.cgColor
289
+        arrowButton.layer?.cornerRadius = 15
290
+        arrowButton.layer?.borderWidth = 1
291
+        arrowButton.layer?.borderColor = NSColor(calibratedWhite: 0.88, alpha: 1).cgColor
303 292
         arrowButton.translatesAutoresizingMaskIntoConstraints = false
304
-        if let arrow = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: "Open") {
305
-            let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
293
+        if let arrow = NSImage(systemSymbolName: "chevron.right", accessibilityDescription: "Open") {
294
+            let config = NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold)
306 295
             arrowButton.image = arrow.withSymbolConfiguration(config)
307 296
         }
308
-        arrowButton.contentTintColor = data.accentColor
297
+        arrowButton.contentTintColor = AppTheme.textSecondary
309 298
 
310
-        addSubview(iconBackground)
311
-        iconBackground.addSubview(iconView)
299
+        addSubview(iconView)
312 300
         addSubview(titleLabel)
313 301
         addSubview(subtitleLabel)
314 302
         addSubview(arrowButton)
315 303
 
316 304
         NSLayoutConstraint.activate([
317
-            heightAnchor.constraint(equalToConstant: 130),
305
+            heightAnchor.constraint(equalToConstant: 118),
318 306
 
319
-            iconBackground.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
320
-            iconBackground.topAnchor.constraint(equalTo: topAnchor, constant: 18),
321
-            iconBackground.widthAnchor.constraint(equalToConstant: 48),
322
-            iconBackground.heightAnchor.constraint(equalToConstant: 48),
307
+            iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
308
+            iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
309
+            iconView.widthAnchor.constraint(equalToConstant: 72),
310
+            iconView.heightAnchor.constraint(equalToConstant: 72),
323 311
 
324
-            iconView.centerXAnchor.constraint(equalTo: iconBackground.centerXAnchor),
325
-            iconView.centerYAnchor.constraint(equalTo: iconBackground.centerYAnchor),
326
-
327
-            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
328
-            titleLabel.topAnchor.constraint(equalTo: iconBackground.bottomAnchor, constant: 12),
329
-            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -18),
312
+            titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
313
+            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 28),
314
+            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -40),
330 315
 
331 316
             subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
332 317
             subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
333 318
             subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
334 319
 
335
-            arrowButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
336
-            arrowButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16),
337
-            arrowButton.widthAnchor.constraint(equalToConstant: 32),
338
-            arrowButton.heightAnchor.constraint(equalToConstant: 32),
320
+            arrowButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14),
321
+            arrowButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -14),
322
+            arrowButton.widthAnchor.constraint(equalToConstant: 30),
323
+            arrowButton.heightAnchor.constraint(equalToConstant: 30),
339 324
         ])
340 325
     }
341 326
 

+ 56 - 13
smart_printer/ViewController.swift

@@ -142,12 +142,45 @@ class ViewController: NSViewController {
142 142
         return scrollView
143 143
     }
144 144
 
145
-    private func makeSectionTitle(_ text: String) -> NSTextField {
145
+    private func makeSectionTitle(_ text: String, showGridIcon: Bool = false) -> NSView {
146
+        let container = NSView()
147
+        container.translatesAutoresizingMaskIntoConstraints = false
148
+
149
+        var leadingAnchor = container.leadingAnchor
150
+
151
+        if showGridIcon {
152
+            let gridIcon = NSImageView()
153
+            gridIcon.translatesAutoresizingMaskIntoConstraints = false
154
+            if let image = NSImage(systemSymbolName: "square.grid.2x2", accessibilityDescription: nil) {
155
+                let config = NSImage.SymbolConfiguration(pointSize: 16, weight: .medium)
156
+                gridIcon.image = image.withSymbolConfiguration(config)
157
+            }
158
+            gridIcon.contentTintColor = AppTheme.textPrimary
159
+            container.addSubview(gridIcon)
160
+
161
+            NSLayoutConstraint.activate([
162
+                gridIcon.leadingAnchor.constraint(equalTo: container.leadingAnchor),
163
+                gridIcon.centerYAnchor.constraint(equalTo: container.centerYAnchor),
164
+                gridIcon.widthAnchor.constraint(equalToConstant: 20),
165
+                gridIcon.heightAnchor.constraint(equalToConstant: 20),
166
+            ])
167
+            leadingAnchor = gridIcon.trailingAnchor
168
+        }
169
+
146 170
         let label = NSTextField(labelWithString: text)
147 171
         label.font = AppTheme.semiboldFont(size: 20)
148 172
         label.textColor = AppTheme.textPrimary
149 173
         label.translatesAutoresizingMaskIntoConstraints = false
150
-        return label
174
+        container.addSubview(label)
175
+
176
+        NSLayoutConstraint.activate([
177
+            container.heightAnchor.constraint(equalToConstant: 28),
178
+            label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: showGridIcon ? 8 : 0),
179
+            label.centerYAnchor.constraint(equalTo: container.centerYAnchor),
180
+            label.trailingAnchor.constraint(equalTo: container.trailingAnchor),
181
+        ])
182
+
183
+        return container
151 184
     }
152 185
 
153 186
     private func makeQuickStartSection() -> NSView {
@@ -213,20 +246,20 @@ class ViewController: NSViewController {
213 246
         let section = NSView()
214 247
         section.translatesAutoresizingMaskIntoConstraints = false
215 248
 
216
-        let title = makeSectionTitle("Create & Print")
249
+        let title = makeSectionTitle("Create & Print", showGridIcon: true)
217 250
         section.addSubview(title)
218 251
 
219 252
         let features: [FeatureCardData] = [
220
-            FeatureCardData(title: "Scan File", subtitle: "Scan any document", accentColor: AppTheme.blue, symbolName: "scanner.fill"),
221
-            FeatureCardData(title: "Print Text", subtitle: "Type text and print", accentColor: AppTheme.purple, symbolName: "textformat"),
222
-            FeatureCardData(title: "Print Contacts", subtitle: "Print your contacts", accentColor: AppTheme.green, symbolName: "person.crop.rectangle.fill"),
223
-            FeatureCardData(title: "Print Website", subtitle: "Print any website", accentColor: AppTheme.teal, symbolName: "globe"),
224
-            FeatureCardData(title: "Draw & Print", subtitle: "Add drawings, text and more", accentColor: AppTheme.orange, symbolName: "paintpalette.fill"),
225
-            FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", accentColor: AppTheme.purple, symbolName: "doc.text.viewfinder"),
253
+            FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
254
+            FeatureCardData(title: "Print Text", subtitle: "Type text and print", iconKind: .printText),
255
+            FeatureCardData(title: "Print Contacts", subtitle: "Print your contacts", iconKind: .printContacts),
256
+            FeatureCardData(title: "Print Website", subtitle: "Print any website", iconKind: .printWebsite),
257
+            FeatureCardData(title: "Draw & Print", subtitle: "Add drawings, text and more", iconKind: .drawPrint),
258
+            FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
226 259
         ]
227 260
 
228
-        let row1 = makeFeatureRow(features: Array(features[0..<3]))
229
-        let row2 = makeFeatureRow(features: Array(features[3..<6]))
261
+        let row1 = makeFeatureRow(features: Array(features[0..<4]), columns: 4)
262
+        let row2 = makeFeatureRow(features: Array(features[4..<6]), columns: 4)
230 263
 
231 264
         let gridStack = NSStackView(views: [row1, row2])
232 265
         gridStack.orientation = .vertical
@@ -248,17 +281,27 @@ class ViewController: NSViewController {
248 281
         return section
249 282
     }
250 283
 
251
-    private func makeFeatureRow(features: [FeatureCardData]) -> NSStackView {
284
+    private func makeFeatureRow(features: [FeatureCardData], columns: Int) -> NSStackView {
252 285
         let row = NSStackView()
253 286
         row.orientation = .horizontal
254
-        row.spacing = 20
287
+        row.spacing = 16
255 288
         row.distribution = .fillEqually
289
+        row.alignment = .centerY
256 290
         row.translatesAutoresizingMaskIntoConstraints = false
257 291
 
258 292
         for feature in features {
259 293
             row.addArrangedSubview(FeatureCardView(data: feature))
260 294
         }
261 295
 
296
+        if features.count < columns {
297
+            let spacerCount = columns - features.count
298
+            for _ in 0..<spacerCount {
299
+                let spacer = NSView()
300
+                spacer.translatesAutoresizingMaskIntoConstraints = false
301
+                row.addArrangedSubview(spacer)
302
+            }
303
+        }
304
+
262 305
         return row
263 306
     }
264 307
 }