浏览代码

Add the Smart Printer home dashboard UI.

Replace the default template window with a sidebar, quick start cards with custom 3D icons, and a create-and-print feature grid, and ignore Xcode build artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 9 小时之前
父节点
当前提交
0843bdce1f

+ 32 - 0
.gitignore

@@ -0,0 +1,32 @@
1
+# Xcode
2
+DerivedData/
3
+.derivedData/
4
+build/
5
+*.pbxuser
6
+!default.pbxuser
7
+*.mode1v3
8
+!default.mode1v3
9
+*.mode2v3
10
+!default.mode2v3
11
+*.perspectivev3
12
+!default.perspectivev3
13
+xcuserdata/
14
+*.xccheckout
15
+*.moved-aside
16
+*.xcuserstate
17
+*.xcscmblueprint
18
+
19
+# macOS
20
+.DS_Store
21
+.AppleDouble
22
+.LSOverride
23
+
24
+# Swift Package Manager
25
+.build/
26
+.swiftpm/
27
+
28
+# CocoaPods
29
+Pods/
30
+
31
+# Carthage
32
+Carthage/Build/

+ 17 - 13
smart_printer/AppDelegate.swift

@@ -2,29 +2,33 @@
2 2
 //  AppDelegate.swift
3 3
 //  smart_printer
4 4
 //
5
-//  Created by Mql Mac 2 on 04/06/2026.
6
-//
7 5
 
8 6
 import Cocoa
9 7
 
10 8
 @main
11 9
 class AppDelegate: NSObject, NSApplicationDelegate {
12 10
 
13
-    
14
-
15
-
16
-    func applicationDidFinishLaunching(_ aNotification: Notification) {
17
-        // Insert code here to initialize your application
11
+    func applicationDidFinishLaunching(_ notification: Notification) {
12
+        DispatchQueue.main.async {
13
+            self.configureMainWindow()
14
+        }
18 15
     }
19 16
 
20
-    func applicationWillTerminate(_ aNotification: Notification) {
21
-        // Insert code here to tear down your application
17
+    private func configureMainWindow() {
18
+        guard let window = NSApplication.shared.windows.first else { return }
19
+
20
+        window.title = "Smart Printer"
21
+        window.titlebarAppearsTransparent = true
22
+        window.titleVisibility = .hidden
23
+        window.styleMask.insert(.fullSizeContentView)
24
+        window.isMovableByWindowBackground = true
25
+        window.backgroundColor = AppTheme.background
26
+        window.setContentSize(NSSize(width: 1120, height: 720))
27
+        window.center()
28
+        window.minSize = NSSize(width: 900, height: 600)
22 29
     }
23 30
 
24 31
     func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
25
-        return true
32
+        true
26 33
     }
27
-
28
-
29 34
 }
30
-

+ 57 - 0
smart_printer/AppTheme.swift

@@ -0,0 +1,57 @@
1
+import Cocoa
2
+
3
+enum AppTheme {
4
+    static let sidebarWidth: CGFloat = 220
5
+    static let cornerRadius: CGFloat = 14
6
+    static let cardCornerRadius: CGFloat = 20
7
+    static let featureCardCornerRadius: CGFloat = 16
8
+
9
+    static let background = NSColor(calibratedWhite: 0.965, alpha: 1)
10
+    static let sidebarBackground = NSColor.white
11
+    static let cardBackground = NSColor.white
12
+    static let textPrimary = NSColor(calibratedWhite: 0.12, alpha: 1)
13
+    static let textSecondary = NSColor(calibratedWhite: 0.48, alpha: 1)
14
+
15
+    static let homeActiveBackground = NSColor(red: 0.91, green: 0.94, blue: 1.0, alpha: 1)
16
+    static let homeActiveForeground = NSColor(red: 0.22, green: 0.47, blue: 0.96, alpha: 1)
17
+    static let premiumBackground = NSColor(red: 0.95, green: 0.93, blue: 1.0, alpha: 1)
18
+    static let premiumForeground = NSColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1)
19
+
20
+    static let blue = NSColor(red: 0.22, green: 0.47, blue: 0.96, alpha: 1)
21
+    static let blueLight = NSColor(red: 0.88, green: 0.93, blue: 1.0, alpha: 1)
22
+    static let green = NSColor(red: 0.13, green: 0.68, blue: 0.42, alpha: 1)
23
+    static let greenLight = NSColor(red: 0.88, green: 0.97, blue: 0.91, alpha: 1)
24
+    static let orange = NSColor(red: 0.96, green: 0.52, blue: 0.18, alpha: 1)
25
+    static let orangeLight = NSColor(red: 1.0, green: 0.94, blue: 0.88, alpha: 1)
26
+    static let purple = NSColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1)
27
+    static let teal = NSColor(red: 0.18, green: 0.72, blue: 0.82, alpha: 1)
28
+
29
+    static func semiboldFont(size: CGFloat) -> NSFont {
30
+        .systemFont(ofSize: size, weight: .semibold)
31
+    }
32
+
33
+    static func mediumFont(size: CGFloat) -> NSFont {
34
+        .systemFont(ofSize: size, weight: .medium)
35
+    }
36
+
37
+    static func regularFont(size: CGFloat) -> NSFont {
38
+        .systemFont(ofSize: size, weight: .regular)
39
+    }
40
+}
41
+
42
+extension NSView {
43
+    func applyCardShadow() {
44
+        wantsLayer = true
45
+        layer?.shadowColor = NSColor.black.cgColor
46
+        layer?.shadowOpacity = 0.07
47
+        layer?.shadowOffset = NSSize(width: 0, height: -3)
48
+        layer?.shadowRadius = 14
49
+        layer?.masksToBounds = false
50
+    }
51
+
52
+    func roundCorners(_ radius: CGFloat) {
53
+        wantsLayer = true
54
+        layer?.cornerRadius = radius
55
+        layer?.masksToBounds = true
56
+    }
57
+}

+ 3 - 3
smart_printer/Base.lproj/Main.storyboard

@@ -683,10 +683,10 @@
683 683
         <scene sceneID="R2V-B0-nI4">
684 684
             <objects>
685 685
                 <windowController id="B8D-0N-5wS" sceneMemberID="viewController">
686
-                    <window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
686
+                    <window key="window" title="Smart Printer" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="YES" animationBehavior="default" id="IQv-IB-iLA">
687 687
                         <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
688 688
                         <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
689
-                        <rect key="contentRect" x="196" y="240" width="480" height="270"/>
689
+                        <rect key="contentRect" x="196" y="240" width="1120" height="720"/>
690 690
                         <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
691 691
                         <connections>
692 692
                             <outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
@@ -705,7 +705,7 @@
705 705
             <objects>
706 706
                 <viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
707 707
                     <view key="view" id="m2S-Jp-Qdl">
708
-                        <rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
708
+                        <rect key="frame" x="0.0" y="0.0" width="1120" height="720"/>
709 709
                         <autoresizingMask key="autoresizingMask"/>
710 710
                     </view>
711 711
                 </viewController>

+ 295 - 0
smart_printer/QuickStartIcons.swift

@@ -0,0 +1,295 @@
1
+import Cocoa
2
+
3
+enum QuickStartIconKind {
4
+    case photos
5
+    case files
6
+    case importFile
7
+}
8
+
9
+final class QuickStartIconView: NSView {
10
+    private let kind: QuickStartIconKind
11
+
12
+    init(kind: QuickStartIconKind) {
13
+        self.kind = kind
14
+        super.init(frame: .zero)
15
+        translatesAutoresizingMaskIntoConstraints = false
16
+    }
17
+
18
+    @available(*, unavailable)
19
+    required init?(coder: NSCoder) { nil }
20
+
21
+    override var isFlipped: Bool { true }
22
+    override var isOpaque: Bool { false }
23
+
24
+    override func draw(_ dirtyRect: NSRect) {
25
+        super.draw(dirtyRect)
26
+        guard let context = NSGraphicsContext.current?.cgContext else { return }
27
+
28
+        switch kind {
29
+        case .photos:
30
+            drawPhotosIcon(in: context)
31
+        case .files:
32
+            drawFilesIcon(in: context)
33
+        case .importFile:
34
+            drawImportIcon(in: context)
35
+        }
36
+    }
37
+
38
+    // MARK: - Photos
39
+
40
+    private func drawPhotosIcon(in context: CGContext) {
41
+        let s = min(bounds.width, bounds.height)
42
+        let ox = (bounds.width - s) / 2
43
+        let oy = (bounds.height - s) / 2
44
+
45
+        drawSoftShadow(in: context, rect: CGRect(x: ox + s * 0.30, y: oy + s * 0.52, width: s * 0.50, height: s * 0.08), radius: s * 0.04)
46
+
47
+        context.saveGState()
48
+        context.translateBy(x: ox + s * 0.62, y: oy + s * 0.30)
49
+        context.rotate(by: .pi / 7.5)
50
+        context.translateBy(x: -(s * 0.24), y: -(s * 0.28))
51
+
52
+        let backRect = CGRect(x: 0, y: 0, width: s * 0.48, height: s * 0.56)
53
+        let backPath = roundedRect(backRect, radius: s * 0.085)
54
+        context.setFillColor(NSColor.white.withAlphaComponent(0.92).cgColor)
55
+        context.addPath(backPath)
56
+        context.fillPath()
57
+
58
+        context.setStrokeColor(NSColor.white.cgColor)
59
+        context.setLineWidth(s * 0.014)
60
+        context.addPath(backPath)
61
+        context.strokePath()
62
+
63
+        drawMountainScene(
64
+            in: context,
65
+            rect: CGRect(x: backRect.minX + s * 0.05, y: backRect.minY + s * 0.07, width: backRect.width - s * 0.10, height: backRect.height - s * 0.12),
66
+            sunColor: NSColor(red: 0.62, green: 0.86, blue: 0.95, alpha: 1),
67
+            mountainColor: NSColor(red: 0.38, green: 0.80, blue: 0.90, alpha: 1)
68
+        )
69
+        context.restoreGState()
70
+
71
+        let frontRect = CGRect(x: ox + s * 0.20, y: oy + s * 0.22, width: s * 0.50, height: s * 0.58)
72
+        let frontPath = roundedRect(frontRect, radius: s * 0.085)
73
+
74
+        let blueTop = NSColor(red: 0.34, green: 0.62, blue: 1.0, alpha: 1)
75
+        let blueBottom = NSColor(red: 0.16, green: 0.40, blue: 0.94, alpha: 1)
76
+        fillGradient(in: context, path: frontPath, colors: [blueTop.cgColor, blueBottom.cgColor], start: CGPoint(x: frontRect.midX, y: frontRect.minY), end: CGPoint(x: frontRect.midX, y: frontRect.maxY))
77
+
78
+        drawMountainScene(
79
+            in: context,
80
+            rect: CGRect(x: frontRect.minX + s * 0.05, y: frontRect.minY + s * 0.07, width: frontRect.width - s * 0.10, height: frontRect.height - s * 0.12),
81
+            sunColor: .white,
82
+            mountainColor: .white
83
+        )
84
+
85
+        context.setStrokeColor(NSColor.white.withAlphaComponent(0.4).cgColor)
86
+        context.setLineWidth(s * 0.01)
87
+        context.addPath(frontPath)
88
+        context.strokePath()
89
+    }
90
+
91
+    private func drawMountainScene(in context: CGContext, rect: CGRect, sunColor: NSColor, mountainColor: NSColor) {
92
+        let sunRadius = rect.width * 0.09
93
+        context.setFillColor(sunColor.cgColor)
94
+        context.fillEllipse(in: CGRect(x: rect.minX + rect.width * 0.10, y: rect.minY + rect.height * 0.08, width: sunRadius * 2, height: sunRadius * 2))
95
+
96
+        let baseY = rect.maxY - rect.height * 0.10
97
+        context.setFillColor(mountainColor.cgColor)
98
+        let leftPeak = mountainPath(
99
+            baseY: baseY,
100
+            peakX: rect.minX + rect.width * 0.22,
101
+            peakY: rect.minY + rect.height * 0.48,
102
+            leftX: rect.minX,
103
+            rightX: rect.minX + rect.width * 0.48
104
+        )
105
+        context.addPath(leftPeak)
106
+        context.fillPath()
107
+
108
+        let rightPeak = mountainPath(
109
+            baseY: baseY,
110
+            peakX: rect.minX + rect.width * 0.72,
111
+            peakY: rect.minY + rect.height * 0.62,
112
+            leftX: rect.minX + rect.width * 0.34,
113
+            rightX: rect.maxX
114
+        )
115
+        context.addPath(rightPeak)
116
+        context.fillPath()
117
+    }
118
+
119
+    // MARK: - Files
120
+
121
+    private func drawFilesIcon(in context: CGContext) {
122
+        let s = min(bounds.width, bounds.height)
123
+        let ox = (bounds.width - s) / 2
124
+        let oy = (bounds.height - s) / 2
125
+
126
+        let folderRect = CGRect(x: ox + s * 0.10, y: oy + s * 0.28, width: s * 0.78, height: s * 0.52)
127
+        drawSoftShadow(in: context, rect: CGRect(x: folderRect.minX, y: folderRect.maxY - s * 0.04, width: folderRect.width, height: s * 0.06), radius: s * 0.03)
128
+
129
+        let backGreen = NSColor(red: 0.42, green: 0.80, blue: 0.36, alpha: 1)
130
+        let frontGreen = NSColor(red: 0.62, green: 0.92, blue: 0.44, alpha: 1)
131
+
132
+        let tabRect = CGRect(x: folderRect.minX + s * 0.02, y: folderRect.minY - s * 0.08, width: s * 0.30, height: s * 0.10)
133
+        context.setFillColor(backGreen.cgColor)
134
+        context.addPath(roundedRect(tabRect, radius: s * 0.025, topLeft: s * 0.025, topRight: s * 0.025, bottomLeft: 0, bottomRight: 0))
135
+        context.fillPath()
136
+
137
+        let backBody = CGRect(x: folderRect.minX, y: folderRect.minY, width: folderRect.width, height: folderRect.height)
138
+        fillGradient(in: context, path: roundedRect(backBody, radius: s * 0.055), colors: [backGreen.cgColor, NSColor(red: 0.24, green: 0.66, blue: 0.28, alpha: 1).cgColor], start: CGPoint(x: backBody.midX, y: backBody.minY), end: CGPoint(x: backBody.midX, y: backBody.maxY))
139
+
140
+        let docBack = CGRect(x: folderRect.minX + s * 0.14, y: folderRect.minY - s * 0.18, width: s * 0.32, height: s * 0.40)
141
+        context.setFillColor(NSColor.white.withAlphaComponent(0.85).cgColor)
142
+        context.addPath(roundedRect(docBack, radius: s * 0.02))
143
+        context.fillPath()
144
+
145
+        let docFront = CGRect(x: folderRect.minX + s * 0.22, y: folderRect.minY - s * 0.28, width: s * 0.40, height: s * 0.50)
146
+        context.setFillColor(NSColor.white.cgColor)
147
+        context.addPath(roundedRect(docFront, radius: s * 0.03))
148
+        context.fillPath()
149
+
150
+        let header = CGRect(x: docFront.minX, y: docFront.minY, width: docFront.width, height: s * 0.12)
151
+        context.setFillColor(NSColor(red: 0.20, green: 0.48, blue: 0.96, alpha: 1).cgColor)
152
+        context.addPath(roundedRect(header, radius: s * 0.03, topLeft: s * 0.03, topRight: s * 0.03, bottomLeft: 0, bottomRight: 0))
153
+        context.fillPath()
154
+
155
+        let lineColor = NSColor(red: 0.66, green: 0.74, blue: 0.98, alpha: 1)
156
+        for (index, widthFactor) in [0.78, 0.62, 0.70].enumerated() {
157
+            let lineY = docFront.minY + s * 0.18 + CGFloat(index) * s * 0.075
158
+            let lineRect = CGRect(x: docFront.minX + s * 0.06, y: lineY, width: docFront.width * widthFactor - s * 0.08, height: s * 0.038)
159
+            context.setFillColor(lineColor.cgColor)
160
+            context.addPath(roundedRect(lineRect, radius: s * 0.014))
161
+            context.fillPath()
162
+        }
163
+
164
+        let frontFlap = CGRect(x: folderRect.minX, y: folderRect.minY + folderRect.height * 0.38, width: folderRect.width, height: folderRect.height * 0.62)
165
+        fillGradient(in: context, path: roundedRect(frontFlap, radius: s * 0.055, topLeft: 0, topRight: 0, bottomLeft: s * 0.055, bottomRight: s * 0.055), colors: [frontGreen.cgColor, NSColor(red: 0.38, green: 0.76, blue: 0.32, alpha: 1).cgColor], start: CGPoint(x: frontFlap.midX, y: frontFlap.minY), end: CGPoint(x: frontFlap.midX, y: frontFlap.maxY))
166
+    }
167
+
168
+    // MARK: - Import
169
+
170
+    private func drawImportIcon(in context: CGContext) {
171
+        let s = min(bounds.width, bounds.height)
172
+        let ox = (bounds.width - s) / 2
173
+        let oy = (bounds.height - s) / 2
174
+
175
+        let folderRect = CGRect(x: ox + s * 0.08, y: oy + s * 0.30, width: s * 0.74, height: s * 0.46)
176
+        drawSoftShadow(in: context, rect: CGRect(x: folderRect.minX, y: folderRect.maxY - s * 0.03, width: folderRect.width, height: s * 0.05), radius: s * 0.025)
177
+
178
+        let orangeTop = NSColor(red: 1.0, green: 0.84, blue: 0.40, alpha: 1)
179
+        let orangeBottom = NSColor(red: 0.95, green: 0.60, blue: 0.16, alpha: 1)
180
+
181
+        let tabRect = CGRect(x: folderRect.minX + s * 0.02, y: folderRect.minY - s * 0.07, width: s * 0.28, height: s * 0.09)
182
+        context.setFillColor(orangeBottom.cgColor)
183
+        context.addPath(roundedRect(tabRect, radius: s * 0.022, topLeft: s * 0.022, topRight: s * 0.022, bottomLeft: 0, bottomRight: 0))
184
+        context.fillPath()
185
+
186
+        fillGradient(in: context, path: roundedRect(folderRect, radius: s * 0.048), colors: [orangeTop.cgColor, orangeBottom.cgColor], start: CGPoint(x: folderRect.midX, y: folderRect.minY), end: CGPoint(x: folderRect.midX, y: folderRect.maxY))
187
+
188
+        let paper = CGRect(x: folderRect.minX + s * 0.12, y: folderRect.minY + s * 0.08, width: folderRect.width * 0.76, height: folderRect.height * 0.38)
189
+        context.setFillColor(NSColor.white.withAlphaComponent(0.9).cgColor)
190
+        context.addPath(roundedRect(paper, radius: s * 0.018))
191
+        context.fillPath()
192
+
193
+        let frontFlap = CGRect(x: folderRect.minX, y: folderRect.minY + folderRect.height * 0.42, width: folderRect.width, height: folderRect.height * 0.58)
194
+        fillGradient(in: context, path: roundedRect(frontFlap, radius: s * 0.048, topLeft: 0, topRight: 0, bottomLeft: s * 0.048, bottomRight: s * 0.048), colors: [NSColor(red: 1.0, green: 0.90, blue: 0.50, alpha: 1).cgColor, orangeBottom.cgColor], start: CGPoint(x: frontFlap.midX, y: frontFlap.minY), end: CGPoint(x: frontFlap.midX, y: frontFlap.maxY))
195
+
196
+        let badgeRadius = s * 0.17
197
+        let badgeCenter = CGPoint(x: folderRect.midX, y: folderRect.maxY - s * 0.02)
198
+        drawSoftShadow(in: context, rect: CGRect(x: badgeCenter.x - badgeRadius, y: badgeCenter.y - badgeRadius * 0.4, width: badgeRadius * 2, height: badgeRadius * 0.5), radius: s * 0.03)
199
+
200
+        let badgeRect = CGRect(x: badgeCenter.x - badgeRadius, y: badgeCenter.y - badgeRadius, width: badgeRadius * 2, height: badgeRadius * 2)
201
+        let badgeColors = [NSColor(red: 0.66, green: 0.78, blue: 1.0, alpha: 1).cgColor, NSColor(red: 0.40, green: 0.56, blue: 0.96, alpha: 1).cgColor]
202
+        fillGradient(in: context, path: CGPath(ellipseIn: badgeRect, transform: nil), colors: badgeColors, start: CGPoint(x: badgeRect.midX, y: badgeRect.minY), end: CGPoint(x: badgeRect.midX, y: badgeRect.maxY))
203
+
204
+        drawCloudUpload(in: context, center: badgeCenter, size: badgeRadius * 1.15)
205
+    }
206
+
207
+    private func drawCloudUpload(in context: CGContext, center: CGPoint, size: CGFloat) {
208
+        let cloudColor = NSColor.white
209
+        let arrowColor = NSColor(red: 0.28, green: 0.42, blue: 0.86, alpha: 1)
210
+
211
+        let w = size
212
+        let h = size * 0.62
213
+        let cloudRect = CGRect(x: center.x - w / 2, y: center.y - h / 2 + size * 0.04, width: w, height: h)
214
+        let cloud = cloudPath(in: cloudRect)
215
+        context.setFillColor(cloudColor.cgColor)
216
+        context.addPath(cloud)
217
+        context.fillPath()
218
+
219
+        let arrowWidth = size * 0.14
220
+        let arrowHeight = size * 0.28
221
+        let arrowTop = center.y + size * 0.02
222
+        let arrow = CGMutablePath()
223
+        arrow.move(to: CGPoint(x: center.x, y: arrowTop + arrowHeight))
224
+        arrow.addLine(to: CGPoint(x: center.x - arrowWidth, y: arrowTop + arrowHeight * 0.45))
225
+        arrow.addLine(to: CGPoint(x: center.x - arrowWidth * 0.45, y: arrowTop + arrowHeight * 0.45))
226
+        arrow.addLine(to: CGPoint(x: center.x - arrowWidth * 0.45, y: arrowTop))
227
+        arrow.addLine(to: CGPoint(x: center.x + arrowWidth * 0.45, y: arrowTop))
228
+        arrow.addLine(to: CGPoint(x: center.x + arrowWidth * 0.45, y: arrowTop + arrowHeight * 0.45))
229
+        arrow.addLine(to: CGPoint(x: center.x + arrowWidth, y: arrowTop + arrowHeight * 0.45))
230
+        arrow.closeSubpath()
231
+        context.setFillColor(arrowColor.cgColor)
232
+        context.addPath(arrow)
233
+        context.fillPath()
234
+    }
235
+
236
+    // MARK: - Helpers
237
+
238
+    private func drawSoftShadow(in context: CGContext, rect: CGRect, radius: CGFloat) {
239
+        context.saveGState()
240
+        context.setFillColor(NSColor.black.withAlphaComponent(0.12).cgColor)
241
+        context.addPath(roundedRect(rect.offsetBy(dx: 0, dy: -radius * 0.3), radius: radius))
242
+        context.fillPath()
243
+        context.restoreGState()
244
+    }
245
+
246
+    private func fillGradient(in context: CGContext, path: CGPath, colors: [CGColor], start: CGPoint, end: CGPoint) {
247
+        context.saveGState()
248
+        context.addPath(path)
249
+        context.clip()
250
+        guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: [0, 1]) else {
251
+            context.restoreGState()
252
+            return
253
+        }
254
+        context.drawLinearGradient(gradient, start: start, end: end, options: [])
255
+        context.restoreGState()
256
+    }
257
+
258
+    private func roundedRect(_ rect: CGRect, radius: CGFloat) -> CGPath {
259
+        roundedRect(rect, radius: radius, topLeft: radius, topRight: radius, bottomLeft: radius, bottomRight: radius)
260
+    }
261
+
262
+    private func roundedRect(_ rect: CGRect, radius: CGFloat, topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) -> CGPath {
263
+        let path = CGMutablePath()
264
+        path.move(to: CGPoint(x: rect.minX + topLeft, y: rect.maxY))
265
+        path.addLine(to: CGPoint(x: rect.maxX - topRight, y: rect.maxY))
266
+        path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY - topRight), control: CGPoint(x: rect.maxX, y: rect.maxY))
267
+        path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + bottomRight))
268
+        path.addQuadCurve(to: CGPoint(x: rect.maxX - bottomRight, y: rect.minY), control: CGPoint(x: rect.maxX, y: rect.minY))
269
+        path.addLine(to: CGPoint(x: rect.minX + bottomLeft, y: rect.minY))
270
+        path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.minY + bottomLeft), control: CGPoint(x: rect.minX, y: rect.minY))
271
+        path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY - topLeft))
272
+        path.addQuadCurve(to: CGPoint(x: rect.minX + topLeft, y: rect.maxY), control: CGPoint(x: rect.minX, y: rect.maxY))
273
+        path.closeSubpath()
274
+        return path
275
+    }
276
+
277
+    private func mountainPath(baseY: CGFloat, peakX: CGFloat, peakY: CGFloat, leftX: CGFloat, rightX: CGFloat) -> CGPath {
278
+        let path = CGMutablePath()
279
+        path.move(to: CGPoint(x: leftX, y: baseY))
280
+        path.addLine(to: CGPoint(x: peakX, y: peakY))
281
+        path.addLine(to: CGPoint(x: rightX, y: baseY))
282
+        path.closeSubpath()
283
+        return path
284
+    }
285
+
286
+    private func cloudPath(in rect: CGRect) -> CGPath {
287
+        let path = CGMutablePath()
288
+        let r = rect.height * 0.38
289
+        path.addEllipse(in: CGRect(x: rect.minX + rect.width * 0.08, y: rect.minY + rect.height * 0.18, width: r * 2, height: r * 2))
290
+        path.addEllipse(in: CGRect(x: rect.minX + rect.width * 0.30, y: rect.minY + rect.height * 0.30, width: r * 2.1, height: r * 2.1))
291
+        path.addEllipse(in: CGRect(x: rect.minX + rect.width * 0.52, y: rect.minY + rect.height * 0.16, width: r * 1.9, height: r * 1.9))
292
+        path.addRect(CGRect(x: rect.minX + rect.width * 0.12, y: rect.minY + rect.height * 0.18, width: rect.width * 0.76, height: rect.height * 0.42))
293
+        return path
294
+    }
295
+}

+ 101 - 0
smart_printer/SidebarView.swift

@@ -0,0 +1,101 @@
1
+import Cocoa
2
+
3
+final class SidebarView: NSView {
4
+    init() {
5
+        super.init(frame: .zero)
6
+        wantsLayer = true
7
+        layer?.backgroundColor = AppTheme.sidebarBackground.cgColor
8
+        translatesAutoresizingMaskIntoConstraints = false
9
+        setup()
10
+    }
11
+
12
+    @available(*, unavailable)
13
+    required init?(coder: NSCoder) { nil }
14
+
15
+    private func setup() {
16
+        let logoContainer = NSView()
17
+        logoContainer.translatesAutoresizingMaskIntoConstraints = false
18
+
19
+        let logoIcon = NSImageView()
20
+        logoIcon.translatesAutoresizingMaskIntoConstraints = false
21
+        if let image = NSImage(systemSymbolName: "printer.fill", accessibilityDescription: "Smart Printer") {
22
+            let config = NSImage.SymbolConfiguration(pointSize: 28, weight: .medium)
23
+            logoIcon.image = image.withSymbolConfiguration(config)
24
+        }
25
+        logoIcon.contentTintColor = AppTheme.blue
26
+
27
+        let wifiBadge = NSImageView()
28
+        wifiBadge.translatesAutoresizingMaskIntoConstraints = false
29
+        if let image = NSImage(systemSymbolName: "wifi", accessibilityDescription: "Wi-Fi") {
30
+            let config = NSImage.SymbolConfiguration(pointSize: 10, weight: .bold)
31
+            wifiBadge.image = image.withSymbolConfiguration(config)
32
+        }
33
+        wifiBadge.contentTintColor = AppTheme.blue
34
+
35
+        let appNameLabel = NSTextField(labelWithString: "Smart Printer")
36
+        appNameLabel.font = AppTheme.semiboldFont(size: 15)
37
+        appNameLabel.textColor = AppTheme.textPrimary
38
+        appNameLabel.alignment = .center
39
+        appNameLabel.translatesAutoresizingMaskIntoConstraints = false
40
+
41
+        let navStack = NSStackView()
42
+        navStack.orientation = .vertical
43
+        navStack.spacing = 4
44
+        navStack.alignment = .leading
45
+        navStack.translatesAutoresizingMaskIntoConstraints = false
46
+
47
+        let homeItem = SidebarNavItem(title: "Home", symbolName: "house.fill", style: .active)
48
+        let scanItem = SidebarNavItem(title: "Scan", symbolName: "viewfinder", style: .normal)
49
+        let premiumItem = SidebarNavItem(title: "Premium", symbolName: "diamond.fill", style: .premium)
50
+
51
+        navStack.addArrangedSubview(homeItem)
52
+        navStack.addArrangedSubview(scanItem)
53
+        navStack.addArrangedSubview(premiumItem)
54
+
55
+        addSubview(logoContainer)
56
+        logoContainer.addSubview(logoIcon)
57
+        logoContainer.addSubview(wifiBadge)
58
+        addSubview(appNameLabel)
59
+        addSubview(navStack)
60
+
61
+        let divider = NSBox()
62
+        divider.boxType = .separator
63
+        divider.translatesAutoresizingMaskIntoConstraints = false
64
+        addSubview(divider)
65
+
66
+        NSLayoutConstraint.activate([
67
+            widthAnchor.constraint(equalToConstant: AppTheme.sidebarWidth),
68
+
69
+            logoContainer.topAnchor.constraint(equalTo: topAnchor, constant: 52),
70
+            logoContainer.centerXAnchor.constraint(equalTo: centerXAnchor),
71
+            logoContainer.widthAnchor.constraint(equalToConstant: 48),
72
+            logoContainer.heightAnchor.constraint(equalToConstant: 48),
73
+
74
+            logoIcon.centerXAnchor.constraint(equalTo: logoContainer.centerXAnchor),
75
+            logoIcon.centerYAnchor.constraint(equalTo: logoContainer.centerYAnchor),
76
+            logoIcon.widthAnchor.constraint(equalToConstant: 36),
77
+            logoIcon.heightAnchor.constraint(equalToConstant: 36),
78
+
79
+            wifiBadge.topAnchor.constraint(equalTo: logoContainer.topAnchor, constant: -2),
80
+            wifiBadge.trailingAnchor.constraint(equalTo: logoContainer.trailingAnchor, constant: 4),
81
+            wifiBadge.widthAnchor.constraint(equalToConstant: 14),
82
+            wifiBadge.heightAnchor.constraint(equalToConstant: 14),
83
+
84
+            appNameLabel.topAnchor.constraint(equalTo: logoContainer.bottomAnchor, constant: 8),
85
+            appNameLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
86
+
87
+            navStack.topAnchor.constraint(equalTo: appNameLabel.bottomAnchor, constant: 32),
88
+            navStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
89
+            navStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
90
+
91
+            homeItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
92
+            scanItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
93
+            premiumItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
94
+
95
+            divider.trailingAnchor.constraint(equalTo: trailingAnchor),
96
+            divider.topAnchor.constraint(equalTo: topAnchor),
97
+            divider.bottomAnchor.constraint(equalTo: bottomAnchor),
98
+            divider.widthAnchor.constraint(equalToConstant: 1),
99
+        ])
100
+    }
101
+}

+ 348 - 0
smart_printer/UIComponents.swift

@@ -0,0 +1,348 @@
1
+import Cocoa
2
+
3
+// MARK: - Gradient Card View
4
+
5
+final class GradientCardView: NSView {
6
+    private let gradientLayer = CAGradientLayer()
7
+
8
+    init(colors: [NSColor], startPoint: CGPoint = CGPoint(x: 0, y: 0.5), endPoint: CGPoint = CGPoint(x: 1, y: 0.5)) {
9
+        super.init(frame: .zero)
10
+        wantsLayer = true
11
+        gradientLayer.colors = colors.map { $0.cgColor }
12
+        gradientLayer.startPoint = startPoint
13
+        gradientLayer.endPoint = endPoint
14
+        gradientLayer.cornerRadius = AppTheme.cardCornerRadius
15
+        layer?.addSublayer(gradientLayer)
16
+        applyCardShadow()
17
+    }
18
+
19
+    @available(*, unavailable)
20
+    required init?(coder: NSCoder) { nil }
21
+
22
+    override func layout() {
23
+        super.layout()
24
+        gradientLayer.frame = bounds
25
+        gradientLayer.cornerRadius = AppTheme.cardCornerRadius
26
+    }
27
+}
28
+
29
+// MARK: - Wave Pattern
30
+
31
+final class WavePatternView: NSView {
32
+    override var isOpaque: Bool { false }
33
+
34
+    override func draw(_ dirtyRect: NSRect) {
35
+        super.draw(dirtyRect)
36
+        guard let context = NSGraphicsContext.current?.cgContext else { return }
37
+
38
+        let dotColor = NSColor(calibratedWhite: 0.82, alpha: 0.55)
39
+        context.setFillColor(dotColor.cgColor)
40
+
41
+        let spacing: CGFloat = 14
42
+        let dotSize: CGFloat = 3
43
+        let rows = Int(bounds.height / spacing) + 2
44
+        let cols = Int(bounds.width / spacing) + 2
45
+
46
+        for row in 0..<rows {
47
+            for col in 0..<cols {
48
+                let wave = sin(Double(col) * 0.35 + Double(row) * 0.25) * 8
49
+                let x = CGFloat(col) * spacing + CGFloat(wave)
50
+                let y = CGFloat(row) * spacing
51
+                let rect = CGRect(x: x, y: y, width: dotSize, height: dotSize)
52
+                context.fillEllipse(in: rect)
53
+            }
54
+        }
55
+    }
56
+}
57
+
58
+// MARK: - Sidebar Nav Item
59
+
60
+final class SidebarNavItem: NSControl {
61
+    enum Style {
62
+        case normal
63
+        case active
64
+        case premium
65
+    }
66
+
67
+    private let iconView = NSImageView()
68
+    private let titleLabel = NSTextField(labelWithString: "")
69
+    private let container = NSView()
70
+    private var style: Style = .normal
71
+
72
+    init(title: String, symbolName: String, style: Style = .normal) {
73
+        super.init(frame: .zero)
74
+        self.style = style
75
+
76
+        titleLabel.stringValue = title
77
+        titleLabel.font = AppTheme.mediumFont(size: 14)
78
+        titleLabel.textColor = AppTheme.textPrimary
79
+
80
+        if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: title) {
81
+            let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
82
+            iconView.image = image.withSymbolConfiguration(config)
83
+        }
84
+        iconView.contentTintColor = AppTheme.textPrimary
85
+
86
+        container.translatesAutoresizingMaskIntoConstraints = false
87
+        iconView.translatesAutoresizingMaskIntoConstraints = false
88
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
89
+        translatesAutoresizingMaskIntoConstraints = false
90
+
91
+        addSubview(container)
92
+        container.addSubview(iconView)
93
+        container.addSubview(titleLabel)
94
+
95
+        NSLayoutConstraint.activate([
96
+            heightAnchor.constraint(equalToConstant: 44),
97
+
98
+            container.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
99
+            container.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
100
+            container.topAnchor.constraint(equalTo: topAnchor),
101
+            container.bottomAnchor.constraint(equalTo: bottomAnchor),
102
+
103
+            iconView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12),
104
+            iconView.centerYAnchor.constraint(equalTo: container.centerYAnchor),
105
+            iconView.widthAnchor.constraint(equalToConstant: 20),
106
+            iconView.heightAnchor.constraint(equalToConstant: 20),
107
+
108
+            titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
109
+            titleLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor),
110
+            titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor, constant: -12),
111
+        ])
112
+
113
+        applyStyle(style)
114
+    }
115
+
116
+    @available(*, unavailable)
117
+    required init?(coder: NSCoder) { nil }
118
+
119
+    private func applyStyle(_ style: Style) {
120
+        container.wantsLayer = true
121
+        container.layer?.cornerRadius = AppTheme.cornerRadius
122
+
123
+        switch style {
124
+        case .normal:
125
+            container.layer?.backgroundColor = NSColor.clear.cgColor
126
+            titleLabel.textColor = AppTheme.textPrimary
127
+            iconView.contentTintColor = AppTheme.textPrimary
128
+        case .active:
129
+            container.layer?.backgroundColor = AppTheme.homeActiveBackground.cgColor
130
+            titleLabel.textColor = AppTheme.homeActiveForeground
131
+            iconView.contentTintColor = AppTheme.homeActiveForeground
132
+        case .premium:
133
+            container.layer?.backgroundColor = AppTheme.premiumBackground.cgColor
134
+            titleLabel.textColor = AppTheme.premiumForeground
135
+            iconView.contentTintColor = AppTheme.premiumForeground
136
+        }
137
+    }
138
+
139
+    override func resetCursorRects() {
140
+        addCursorRect(bounds, cursor: .pointingHand)
141
+    }
142
+}
143
+
144
+// MARK: - Action Button
145
+
146
+final class PillButton: NSButton {
147
+    init(title: String, color: NSColor) {
148
+        super.init(frame: .zero)
149
+        self.title = title
150
+        isBordered = false
151
+        wantsLayer = true
152
+        layer?.backgroundColor = color.cgColor
153
+        layer?.cornerRadius = 10
154
+        font = AppTheme.semiboldFont(size: 13)
155
+        contentTintColor = .white
156
+
157
+        if let arrow = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil) {
158
+            let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
159
+            image = arrow.withSymbolConfiguration(config)
160
+            imagePosition = .imageTrailing
161
+            imageHugsTitle = true
162
+        }
163
+
164
+        heightAnchor.constraint(equalToConstant: 36).isActive = true
165
+    }
166
+
167
+    @available(*, unavailable)
168
+    required init?(coder: NSCoder) { nil }
169
+
170
+    override func resetCursorRects() {
171
+        addCursorRect(bounds, cursor: .pointingHand)
172
+    }
173
+}
174
+
175
+// MARK: - Quick Start Card
176
+
177
+struct QuickStartCardData {
178
+    let title: String
179
+    let subtitle: String
180
+    let buttonTitle: String
181
+    let accentColor: NSColor
182
+    let gradientColors: [NSColor]
183
+    let iconKind: QuickStartIconKind
184
+}
185
+
186
+final class QuickStartCardView: NSView {
187
+    init(data: QuickStartCardData) {
188
+        super.init(frame: .zero)
189
+        translatesAutoresizingMaskIntoConstraints = false
190
+
191
+        let gradient = GradientCardView(
192
+            colors: data.gradientColors,
193
+            startPoint: CGPoint(x: 0, y: 0.5),
194
+            endPoint: CGPoint(x: 1, y: 0.5)
195
+        )
196
+        gradient.translatesAutoresizingMaskIntoConstraints = false
197
+
198
+        let titleLabel = NSTextField(labelWithString: data.title)
199
+        titleLabel.font = AppTheme.semiboldFont(size: 22)
200
+        titleLabel.textColor = data.accentColor
201
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
202
+
203
+        let subtitleLabel = NSTextField(labelWithString: data.subtitle)
204
+        subtitleLabel.font = AppTheme.regularFont(size: 13)
205
+        subtitleLabel.textColor = AppTheme.textSecondary
206
+        subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
207
+
208
+        let button = PillButton(title: data.buttonTitle, color: data.accentColor)
209
+        button.translatesAutoresizingMaskIntoConstraints = false
210
+
211
+        let wavePattern = WavePatternView()
212
+        wavePattern.translatesAutoresizingMaskIntoConstraints = false
213
+
214
+        let iconView = QuickStartIconView(kind: data.iconKind)
215
+
216
+        addSubview(gradient)
217
+        gradient.addSubview(wavePattern)
218
+        gradient.addSubview(titleLabel)
219
+        gradient.addSubview(subtitleLabel)
220
+        gradient.addSubview(button)
221
+        gradient.addSubview(iconView)
222
+
223
+        NSLayoutConstraint.activate([
224
+            gradient.leadingAnchor.constraint(equalTo: leadingAnchor),
225
+            gradient.trailingAnchor.constraint(equalTo: trailingAnchor),
226
+            gradient.topAnchor.constraint(equalTo: topAnchor),
227
+            gradient.bottomAnchor.constraint(equalTo: bottomAnchor),
228
+            heightAnchor.constraint(equalToConstant: 160),
229
+
230
+            titleLabel.leadingAnchor.constraint(equalTo: gradient.leadingAnchor, constant: 24),
231
+            titleLabel.topAnchor.constraint(equalTo: gradient.topAnchor, constant: 28),
232
+
233
+            subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
234
+            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6),
235
+            subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: iconView.leadingAnchor, constant: -12),
236
+
237
+            button.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
238
+            button.bottomAnchor.constraint(equalTo: gradient.bottomAnchor, constant: -24),
239
+
240
+            wavePattern.trailingAnchor.constraint(equalTo: gradient.trailingAnchor),
241
+            wavePattern.bottomAnchor.constraint(equalTo: gradient.bottomAnchor),
242
+            wavePattern.widthAnchor.constraint(equalToConstant: 120),
243
+            wavePattern.heightAnchor.constraint(equalToConstant: 80),
244
+
245
+            iconView.trailingAnchor.constraint(equalTo: gradient.trailingAnchor, constant: -12),
246
+            iconView.centerYAnchor.constraint(equalTo: gradient.centerYAnchor),
247
+            iconView.widthAnchor.constraint(equalToConstant: 110),
248
+            iconView.heightAnchor.constraint(equalToConstant: 110),
249
+        ])
250
+    }
251
+
252
+    @available(*, unavailable)
253
+    required init?(coder: NSCoder) { nil }
254
+}
255
+
256
+// MARK: - Feature Card
257
+
258
+struct FeatureCardData {
259
+    let title: String
260
+    let subtitle: String
261
+    let accentColor: NSColor
262
+    let symbolName: String
263
+}
264
+
265
+final class FeatureCardView: NSView {
266
+    init(data: FeatureCardData) {
267
+        super.init(frame: .zero)
268
+        translatesAutoresizingMaskIntoConstraints = false
269
+        wantsLayer = true
270
+        layer?.backgroundColor = AppTheme.cardBackground.cgColor
271
+        layer?.cornerRadius = AppTheme.featureCardCornerRadius
272
+        applyCardShadow()
273
+
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
287
+
288
+        let titleLabel = NSTextField(labelWithString: data.title)
289
+        titleLabel.font = AppTheme.semiboldFont(size: 15)
290
+        titleLabel.textColor = AppTheme.textPrimary
291
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
292
+
293
+        let subtitleLabel = NSTextField(labelWithString: data.subtitle)
294
+        subtitleLabel.font = AppTheme.regularFont(size: 12)
295
+        subtitleLabel.textColor = AppTheme.textSecondary
296
+        subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
297
+
298
+        let arrowButton = NSButton()
299
+        arrowButton.isBordered = false
300
+        arrowButton.wantsLayer = true
301
+        arrowButton.layer?.backgroundColor = data.accentColor.withAlphaComponent(0.12).cgColor
302
+        arrowButton.layer?.cornerRadius = 16
303
+        arrowButton.translatesAutoresizingMaskIntoConstraints = false
304
+        if let arrow = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: "Open") {
305
+            let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
306
+            arrowButton.image = arrow.withSymbolConfiguration(config)
307
+        }
308
+        arrowButton.contentTintColor = data.accentColor
309
+
310
+        addSubview(iconBackground)
311
+        iconBackground.addSubview(iconView)
312
+        addSubview(titleLabel)
313
+        addSubview(subtitleLabel)
314
+        addSubview(arrowButton)
315
+
316
+        NSLayoutConstraint.activate([
317
+            heightAnchor.constraint(equalToConstant: 130),
318
+
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),
323
+
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),
330
+
331
+            subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
332
+            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
333
+            subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
334
+
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),
339
+        ])
340
+    }
341
+
342
+    @available(*, unavailable)
343
+    required init?(coder: NSCoder) { nil }
344
+
345
+    override func resetCursorRects() {
346
+        addCursorRect(bounds, cursor: .pointingHand)
347
+    }
348
+}

+ 248 - 6
smart_printer/ViewController.swift

@@ -2,25 +2,267 @@
2 2
 //  ViewController.swift
3 3
 //  smart_printer
4 4
 //
5
-//  Created by Mql Mac 2 on 04/06/2026.
6
-//
7 5
 
8 6
 import Cocoa
9 7
 
10 8
 class ViewController: NSViewController {
11 9
 
10
+    override func loadView() {
11
+        let container = NSView(frame: NSRect(x: 0, y: 0, width: 1120, height: 720))
12
+        container.autoresizingMask = [.width, .height]
13
+        container.wantsLayer = true
14
+        container.layer?.backgroundColor = AppTheme.background.cgColor
15
+        view = container
16
+    }
17
+
12 18
     override func viewDidLoad() {
13 19
         super.viewDidLoad()
20
+        setupLayout()
21
+    }
22
+
23
+    private func setupLayout() {
24
+        let sidebar = SidebarView()
25
+
26
+        let mainContent = NSView()
27
+        mainContent.translatesAutoresizingMaskIntoConstraints = false
28
+        mainContent.wantsLayer = true
29
+        mainContent.layer?.backgroundColor = AppTheme.background.cgColor
30
+
31
+        let header = makeHeader()
32
+        let scrollView = makeScrollView()
14 33
 
15
-        // Do any additional setup after loading the view.
34
+        let wavePattern = WavePatternView()
35
+        wavePattern.translatesAutoresizingMaskIntoConstraints = false
36
+
37
+        view.addSubview(sidebar)
38
+        view.addSubview(mainContent)
39
+        mainContent.addSubview(header)
40
+        mainContent.addSubview(scrollView)
41
+        mainContent.addSubview(wavePattern)
42
+
43
+        NSLayoutConstraint.activate([
44
+            sidebar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
45
+            sidebar.topAnchor.constraint(equalTo: view.topAnchor),
46
+            sidebar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
47
+
48
+            mainContent.leadingAnchor.constraint(equalTo: sidebar.trailingAnchor),
49
+            mainContent.trailingAnchor.constraint(equalTo: view.trailingAnchor),
50
+            mainContent.topAnchor.constraint(equalTo: view.topAnchor),
51
+            mainContent.bottomAnchor.constraint(equalTo: view.bottomAnchor),
52
+
53
+            header.leadingAnchor.constraint(equalTo: mainContent.leadingAnchor),
54
+            header.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
55
+            header.topAnchor.constraint(equalTo: mainContent.topAnchor, constant: 16),
56
+            header.heightAnchor.constraint(equalToConstant: 44),
57
+
58
+            scrollView.leadingAnchor.constraint(equalTo: mainContent.leadingAnchor),
59
+            scrollView.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
60
+            scrollView.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 8),
61
+            scrollView.bottomAnchor.constraint(equalTo: mainContent.bottomAnchor),
62
+
63
+            wavePattern.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
64
+            wavePattern.bottomAnchor.constraint(equalTo: mainContent.bottomAnchor),
65
+            wavePattern.widthAnchor.constraint(equalToConstant: 280),
66
+            wavePattern.heightAnchor.constraint(equalToConstant: 180),
67
+        ])
16 68
     }
17 69
 
18
-    override var representedObject: Any? {
19
-        didSet {
20
-        // Update the view, if already loaded.
70
+    private func makeHeader() -> NSView {
71
+        let header = NSView()
72
+        header.translatesAutoresizingMaskIntoConstraints = false
73
+
74
+        let titleLabel = NSTextField(labelWithString: "Smart Printer")
75
+        titleLabel.font = AppTheme.semiboldFont(size: 18)
76
+        titleLabel.textColor = AppTheme.textPrimary
77
+        titleLabel.alignment = .center
78
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
79
+
80
+        let settingsButton = NSButton()
81
+        settingsButton.isBordered = false
82
+        settingsButton.translatesAutoresizingMaskIntoConstraints = false
83
+        if let image = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings") {
84
+            let config = NSImage.SymbolConfiguration(pointSize: 18, weight: .regular)
85
+            settingsButton.image = image.withSymbolConfiguration(config)
21 86
         }
87
+        settingsButton.contentTintColor = AppTheme.textSecondary
88
+
89
+        header.addSubview(titleLabel)
90
+        header.addSubview(settingsButton)
91
+
92
+        NSLayoutConstraint.activate([
93
+            titleLabel.centerXAnchor.constraint(equalTo: header.centerXAnchor),
94
+            titleLabel.centerYAnchor.constraint(equalTo: header.centerYAnchor),
95
+
96
+            settingsButton.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -28),
97
+            settingsButton.centerYAnchor.constraint(equalTo: header.centerYAnchor),
98
+            settingsButton.widthAnchor.constraint(equalToConstant: 32),
99
+            settingsButton.heightAnchor.constraint(equalToConstant: 32),
100
+        ])
101
+
102
+        return header
22 103
     }
23 104
 
105
+    private func makeScrollView() -> NSScrollView {
106
+        let scrollView = NSScrollView()
107
+        scrollView.translatesAutoresizingMaskIntoConstraints = false
108
+        scrollView.hasVerticalScroller = true
109
+        scrollView.hasHorizontalScroller = false
110
+        scrollView.autohidesScrollers = true
111
+        scrollView.drawsBackground = false
112
+        scrollView.borderType = .noBorder
113
+
114
+        let documentView = FlippedDocumentView()
115
+        documentView.translatesAutoresizingMaskIntoConstraints = false
116
+
117
+        let quickStartSection = makeQuickStartSection()
118
+        let createPrintSection = makeCreatePrintSection()
119
+
120
+        documentView.addSubview(quickStartSection)
121
+        documentView.addSubview(createPrintSection)
122
+        scrollView.documentView = documentView
123
+
124
+        let contentGuide = scrollView.contentView
125
+
126
+        NSLayoutConstraint.activate([
127
+            documentView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
128
+            documentView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
129
+            documentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
130
+            documentView.widthAnchor.constraint(equalTo: contentGuide.widthAnchor),
131
+
132
+            quickStartSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: 32),
133
+            quickStartSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -32),
134
+            quickStartSection.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
135
+
136
+            createPrintSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: 32),
137
+            createPrintSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -32),
138
+            createPrintSection.topAnchor.constraint(equalTo: quickStartSection.bottomAnchor, constant: 36),
139
+            createPrintSection.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -40),
140
+        ])
141
+
142
+        return scrollView
143
+    }
24 144
 
145
+    private func makeSectionTitle(_ text: String) -> NSTextField {
146
+        let label = NSTextField(labelWithString: text)
147
+        label.font = AppTheme.semiboldFont(size: 20)
148
+        label.textColor = AppTheme.textPrimary
149
+        label.translatesAutoresizingMaskIntoConstraints = false
150
+        return label
151
+    }
152
+
153
+    private func makeQuickStartSection() -> NSView {
154
+        let section = NSView()
155
+        section.translatesAutoresizingMaskIntoConstraints = false
156
+
157
+        let title = makeSectionTitle("Quick Start")
158
+        section.addSubview(title)
159
+
160
+        let cardsStack = NSStackView()
161
+        cardsStack.orientation = .horizontal
162
+        cardsStack.spacing = 20
163
+        cardsStack.distribution = .fillEqually
164
+        cardsStack.translatesAutoresizingMaskIntoConstraints = false
165
+
166
+        let cards: [QuickStartCardData] = [
167
+            QuickStartCardData(
168
+                title: "From Photos",
169
+                subtitle: "Take a photo from gallery",
170
+                buttonTitle: "Open Gallery",
171
+                accentColor: AppTheme.blue,
172
+                gradientColors: [AppTheme.blueLight, NSColor(red: 0.82, green: 0.90, blue: 1.0, alpha: 1)],
173
+                iconKind: .photos
174
+            ),
175
+            QuickStartCardData(
176
+                title: "From Files",
177
+                subtitle: "Take a photo from file manager",
178
+                buttonTitle: "Browse Files",
179
+                accentColor: AppTheme.green,
180
+                gradientColors: [AppTheme.greenLight, NSColor(red: 0.78, green: 0.94, blue: 0.84, alpha: 1)],
181
+                iconKind: .files
182
+            ),
183
+            QuickStartCardData(
184
+                title: "Import File",
185
+                subtitle: "Import a file from storage",
186
+                buttonTitle: "Import Now",
187
+                accentColor: AppTheme.orange,
188
+                gradientColors: [AppTheme.orangeLight, NSColor(red: 1.0, green: 0.88, blue: 0.76, alpha: 1)],
189
+                iconKind: .importFile
190
+            ),
191
+        ]
192
+
193
+        for cardData in cards {
194
+            cardsStack.addArrangedSubview(QuickStartCardView(data: cardData))
195
+        }
196
+
197
+        section.addSubview(cardsStack)
198
+
199
+        NSLayoutConstraint.activate([
200
+            title.leadingAnchor.constraint(equalTo: section.leadingAnchor),
201
+            title.topAnchor.constraint(equalTo: section.topAnchor),
202
+
203
+            cardsStack.leadingAnchor.constraint(equalTo: section.leadingAnchor),
204
+            cardsStack.trailingAnchor.constraint(equalTo: section.trailingAnchor),
205
+            cardsStack.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16),
206
+            cardsStack.bottomAnchor.constraint(equalTo: section.bottomAnchor),
207
+        ])
208
+
209
+        return section
210
+    }
211
+
212
+    private func makeCreatePrintSection() -> NSView {
213
+        let section = NSView()
214
+        section.translatesAutoresizingMaskIntoConstraints = false
215
+
216
+        let title = makeSectionTitle("Create & Print")
217
+        section.addSubview(title)
218
+
219
+        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"),
226
+        ]
227
+
228
+        let row1 = makeFeatureRow(features: Array(features[0..<3]))
229
+        let row2 = makeFeatureRow(features: Array(features[3..<6]))
230
+
231
+        let gridStack = NSStackView(views: [row1, row2])
232
+        gridStack.orientation = .vertical
233
+        gridStack.spacing = 16
234
+        gridStack.distribution = .fill
235
+        gridStack.translatesAutoresizingMaskIntoConstraints = false
236
+        section.addSubview(gridStack)
237
+
238
+        NSLayoutConstraint.activate([
239
+            title.leadingAnchor.constraint(equalTo: section.leadingAnchor),
240
+            title.topAnchor.constraint(equalTo: section.topAnchor),
241
+
242
+            gridStack.leadingAnchor.constraint(equalTo: section.leadingAnchor),
243
+            gridStack.trailingAnchor.constraint(equalTo: section.trailingAnchor),
244
+            gridStack.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16),
245
+            gridStack.bottomAnchor.constraint(equalTo: section.bottomAnchor),
246
+        ])
247
+
248
+        return section
249
+    }
250
+
251
+    private func makeFeatureRow(features: [FeatureCardData]) -> NSStackView {
252
+        let row = NSStackView()
253
+        row.orientation = .horizontal
254
+        row.spacing = 20
255
+        row.distribution = .fillEqually
256
+        row.translatesAutoresizingMaskIntoConstraints = false
257
+
258
+        for feature in features {
259
+            row.addArrangedSubview(FeatureCardView(data: feature))
260
+        }
261
+
262
+        return row
263
+    }
25 264
 }
26 265
 
266
+private final class FlippedDocumentView: NSView {
267
+    override var isFlipped: Bool { true }
268
+}