瀏覽代碼

Narrow the default window and scale card icons dynamically.

Centralize window dimensions in AppTheme so the app opens at 680px while preserving the existing layout with responsive icon sizing.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 17 小時之前
父節點
當前提交
0a655379e0

+ 2 - 2
smart_printer/AppDelegate.swift

@@ -23,9 +23,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
23
         window.styleMask.insert(.fullSizeContentView)
23
         window.styleMask.insert(.fullSizeContentView)
24
         window.isMovableByWindowBackground = true
24
         window.isMovableByWindowBackground = true
25
         window.backgroundColor = AppTheme.background
25
         window.backgroundColor = AppTheme.background
26
-        window.setContentSize(NSSize(width: 1120, height: 720))
26
+        window.setContentSize(NSSize(width: AppTheme.windowWidth, height: AppTheme.windowHeight))
27
         window.center()
27
         window.center()
28
-        window.minSize = NSSize(width: 900, height: 600)
28
+        window.minSize = NSSize(width: AppTheme.windowMinWidth, height: AppTheme.windowMinHeight)
29
     }
29
     }
30
 
30
 
31
     func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
31
     func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {

+ 23 - 1
smart_printer/AppTheme.swift

@@ -1,7 +1,29 @@
1
 import Cocoa
1
 import Cocoa
2
 
2
 
3
 enum AppTheme {
3
 enum AppTheme {
4
-    static let sidebarWidth: CGFloat = 220
4
+    static let windowWidth: CGFloat = 680
5
+    static let windowHeight: CGFloat = 720
6
+    static let windowMinWidth: CGFloat = 600
7
+    static let windowMinHeight: CGFloat = 600
8
+
9
+    static let sidebarWidth: CGFloat = 200
10
+    static let contentPadding: CGFloat = 20
11
+    static let quickStartSpacing: CGFloat = 12
12
+    static let featureGridSpacing: CGFloat = 10
13
+
14
+    static let quickStartIconMax: CGFloat = 80
15
+    static let quickStartIconMin: CGFloat = 56
16
+    static let featureIconMax: CGFloat = 56
17
+    static let featureIconMin: CGFloat = 36
18
+
19
+    static func quickStartIconSize(forCardWidth width: CGFloat) -> CGFloat {
20
+        min(quickStartIconMax, max(quickStartIconMin, width * 0.58))
21
+    }
22
+
23
+    static func featureIconSize(forCardWidth width: CGFloat) -> CGFloat {
24
+        min(featureIconMax, max(featureIconMin, width * 0.38))
25
+    }
26
+
5
     static let cornerRadius: CGFloat = 14
27
     static let cornerRadius: CGFloat = 14
6
     static let cardCornerRadius: CGFloat = 20
28
     static let cardCornerRadius: CGFloat = 20
7
     static let featureCardCornerRadius: CGFloat = 16
29
     static let featureCardCornerRadius: CGFloat = 16

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

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

+ 60 - 28
smart_printer/UIComponents.swift

@@ -184,7 +184,12 @@ struct QuickStartCardData {
184
 }
184
 }
185
 
185
 
186
 final class QuickStartCardView: NSView {
186
 final class QuickStartCardView: NSView {
187
+    private let iconView: QuickStartIconView
188
+    private var iconWidthConstraint: NSLayoutConstraint!
189
+    private var iconHeightConstraint: NSLayoutConstraint!
190
+
187
     init(data: QuickStartCardData) {
191
     init(data: QuickStartCardData) {
192
+        iconView = QuickStartIconView(kind: data.iconKind)
188
         super.init(frame: .zero)
193
         super.init(frame: .zero)
189
         translatesAutoresizingMaskIntoConstraints = false
194
         translatesAutoresizingMaskIntoConstraints = false
190
 
195
 
@@ -211,8 +216,6 @@ final class QuickStartCardView: NSView {
211
         let wavePattern = WavePatternView()
216
         let wavePattern = WavePatternView()
212
         wavePattern.translatesAutoresizingMaskIntoConstraints = false
217
         wavePattern.translatesAutoresizingMaskIntoConstraints = false
213
 
218
 
214
-        let iconView = QuickStartIconView(kind: data.iconKind)
215
-
216
         addSubview(gradient)
219
         addSubview(gradient)
217
         gradient.addSubview(wavePattern)
220
         gradient.addSubview(wavePattern)
218
         gradient.addSubview(titleLabel)
221
         gradient.addSubview(titleLabel)
@@ -225,32 +228,44 @@ final class QuickStartCardView: NSView {
225
             gradient.trailingAnchor.constraint(equalTo: trailingAnchor),
228
             gradient.trailingAnchor.constraint(equalTo: trailingAnchor),
226
             gradient.topAnchor.constraint(equalTo: topAnchor),
229
             gradient.topAnchor.constraint(equalTo: topAnchor),
227
             gradient.bottomAnchor.constraint(equalTo: bottomAnchor),
230
             gradient.bottomAnchor.constraint(equalTo: bottomAnchor),
228
-            heightAnchor.constraint(equalToConstant: 160),
231
+            heightAnchor.constraint(equalToConstant: 148),
229
 
232
 
230
-            titleLabel.leadingAnchor.constraint(equalTo: gradient.leadingAnchor, constant: 24),
231
-            titleLabel.topAnchor.constraint(equalTo: gradient.topAnchor, constant: 28),
233
+            titleLabel.leadingAnchor.constraint(equalTo: gradient.leadingAnchor, constant: 18),
234
+            titleLabel.topAnchor.constraint(equalTo: gradient.topAnchor, constant: 24),
232
 
235
 
233
             subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
236
             subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
234
-            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6),
235
-            subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: iconView.leadingAnchor, constant: -12),
237
+            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
238
+            subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: iconView.leadingAnchor, constant: -8),
236
 
239
 
237
             button.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
240
             button.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
238
-            button.bottomAnchor.constraint(equalTo: gradient.bottomAnchor, constant: -24),
241
+            button.bottomAnchor.constraint(equalTo: gradient.bottomAnchor, constant: -20),
239
 
242
 
240
             wavePattern.trailingAnchor.constraint(equalTo: gradient.trailingAnchor),
243
             wavePattern.trailingAnchor.constraint(equalTo: gradient.trailingAnchor),
241
             wavePattern.bottomAnchor.constraint(equalTo: gradient.bottomAnchor),
244
             wavePattern.bottomAnchor.constraint(equalTo: gradient.bottomAnchor),
242
-            wavePattern.widthAnchor.constraint(equalToConstant: 120),
243
-            wavePattern.heightAnchor.constraint(equalToConstant: 80),
245
+            wavePattern.widthAnchor.constraint(equalToConstant: 100),
246
+            wavePattern.heightAnchor.constraint(equalToConstant: 70),
244
 
247
 
245
-            iconView.trailingAnchor.constraint(equalTo: gradient.trailingAnchor, constant: -12),
248
+            iconView.trailingAnchor.constraint(equalTo: gradient.trailingAnchor, constant: -8),
246
             iconView.centerYAnchor.constraint(equalTo: gradient.centerYAnchor),
249
             iconView.centerYAnchor.constraint(equalTo: gradient.centerYAnchor),
247
-            iconView.widthAnchor.constraint(equalToConstant: 110),
248
-            iconView.heightAnchor.constraint(equalToConstant: 110),
249
         ])
250
         ])
251
+
252
+        iconWidthConstraint = iconView.widthAnchor.constraint(equalToConstant: AppTheme.quickStartIconMax)
253
+        iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.quickStartIconMax)
254
+        iconWidthConstraint.isActive = true
255
+        iconHeightConstraint.isActive = true
250
     }
256
     }
251
 
257
 
252
     @available(*, unavailable)
258
     @available(*, unavailable)
253
     required init?(coder: NSCoder) { nil }
259
     required init?(coder: NSCoder) { nil }
260
+
261
+    override func layout() {
262
+        super.layout()
263
+        let size = AppTheme.quickStartIconSize(forCardWidth: bounds.width)
264
+        if iconWidthConstraint.constant != size {
265
+            iconWidthConstraint.constant = size
266
+            iconHeightConstraint.constant = size
267
+        }
268
+    }
254
 }
269
 }
255
 
270
 
256
 // MARK: - Feature Card
271
 // MARK: - Feature Card
@@ -262,16 +277,21 @@ struct FeatureCardData {
262
 }
277
 }
263
 
278
 
264
 final class FeatureCardView: NSView {
279
 final class FeatureCardView: NSView {
280
+    private let iconView: FeatureIconView
281
+    private var iconWidthConstraint: NSLayoutConstraint!
282
+    private var iconHeightConstraint: NSLayoutConstraint!
283
+
265
     init(data: FeatureCardData) {
284
     init(data: FeatureCardData) {
285
+        iconView = FeatureIconView(kind: data.iconKind)
266
         super.init(frame: .zero)
286
         super.init(frame: .zero)
267
         translatesAutoresizingMaskIntoConstraints = false
287
         translatesAutoresizingMaskIntoConstraints = false
288
+        setContentHuggingPriority(.defaultLow, for: .horizontal)
289
+        setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
268
         wantsLayer = true
290
         wantsLayer = true
269
         layer?.backgroundColor = AppTheme.cardBackground.cgColor
291
         layer?.backgroundColor = AppTheme.cardBackground.cgColor
270
         layer?.cornerRadius = AppTheme.featureCardCornerRadius
292
         layer?.cornerRadius = AppTheme.featureCardCornerRadius
271
         applyCardShadow()
293
         applyCardShadow()
272
 
294
 
273
-        let iconView = FeatureIconView(kind: data.iconKind)
274
-
275
         let titleLabel = NSTextField(labelWithString: data.title)
295
         let titleLabel = NSTextField(labelWithString: data.title)
276
         titleLabel.font = AppTheme.semiboldFont(size: 15)
296
         titleLabel.font = AppTheme.semiboldFont(size: 15)
277
         titleLabel.textColor = AppTheme.textPrimary
297
         titleLabel.textColor = AppTheme.textPrimary
@@ -286,7 +306,7 @@ final class FeatureCardView: NSView {
286
         arrowButton.isBordered = false
306
         arrowButton.isBordered = false
287
         arrowButton.wantsLayer = true
307
         arrowButton.wantsLayer = true
288
         arrowButton.layer?.backgroundColor = NSColor.white.cgColor
308
         arrowButton.layer?.backgroundColor = NSColor.white.cgColor
289
-        arrowButton.layer?.cornerRadius = 15
309
+        arrowButton.layer?.cornerRadius = 13
290
         arrowButton.layer?.borderWidth = 1
310
         arrowButton.layer?.borderWidth = 1
291
         arrowButton.layer?.borderColor = NSColor(calibratedWhite: 0.88, alpha: 1).cgColor
311
         arrowButton.layer?.borderColor = NSColor(calibratedWhite: 0.88, alpha: 1).cgColor
292
         arrowButton.translatesAutoresizingMaskIntoConstraints = false
312
         arrowButton.translatesAutoresizingMaskIntoConstraints = false
@@ -302,31 +322,43 @@ final class FeatureCardView: NSView {
302
         addSubview(arrowButton)
322
         addSubview(arrowButton)
303
 
323
 
304
         NSLayoutConstraint.activate([
324
         NSLayoutConstraint.activate([
305
-            heightAnchor.constraint(equalToConstant: 118),
325
+            heightAnchor.constraint(equalToConstant: 104),
306
 
326
 
307
-            iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
327
+            iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
308
             iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
328
             iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
309
-            iconView.widthAnchor.constraint(equalToConstant: 72),
310
-            iconView.heightAnchor.constraint(equalToConstant: 72),
311
 
329
 
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
+            titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8),
331
+            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 22),
332
+            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32),
315
 
333
 
316
             subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
334
             subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
317
-            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
335
+            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 3),
318
             subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
336
             subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
319
 
337
 
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),
338
+            arrowButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
339
+            arrowButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10),
340
+            arrowButton.widthAnchor.constraint(equalToConstant: 26),
341
+            arrowButton.heightAnchor.constraint(equalToConstant: 26),
324
         ])
342
         ])
343
+
344
+        iconWidthConstraint = iconView.widthAnchor.constraint(equalToConstant: AppTheme.featureIconMax)
345
+        iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.featureIconMax)
346
+        iconWidthConstraint.isActive = true
347
+        iconHeightConstraint.isActive = true
325
     }
348
     }
326
 
349
 
327
     @available(*, unavailable)
350
     @available(*, unavailable)
328
     required init?(coder: NSCoder) { nil }
351
     required init?(coder: NSCoder) { nil }
329
 
352
 
353
+    override func layout() {
354
+        super.layout()
355
+        let size = AppTheme.featureIconSize(forCardWidth: bounds.width)
356
+        if iconWidthConstraint.constant != size {
357
+            iconWidthConstraint.constant = size
358
+            iconHeightConstraint.constant = size
359
+        }
360
+    }
361
+
330
     override func resetCursorRects() {
362
     override func resetCursorRects() {
331
         addCursorRect(bounds, cursor: .pointingHand)
363
         addCursorRect(bounds, cursor: .pointingHand)
332
     }
364
     }

+ 51 - 37
smart_printer/ViewController.swift

@@ -8,7 +8,7 @@ import Cocoa
8
 class ViewController: NSViewController {
8
 class ViewController: NSViewController {
9
 
9
 
10
     override func loadView() {
10
     override func loadView() {
11
-        let container = NSView(frame: NSRect(x: 0, y: 0, width: 1120, height: 720))
11
+        let container = NSView(frame: NSRect(x: 0, y: 0, width: AppTheme.windowWidth, height: AppTheme.windowHeight))
12
         container.autoresizingMask = [.width, .height]
12
         container.autoresizingMask = [.width, .height]
13
         container.wantsLayer = true
13
         container.wantsLayer = true
14
         container.layer?.backgroundColor = AppTheme.background.cgColor
14
         container.layer?.backgroundColor = AppTheme.background.cgColor
@@ -62,8 +62,8 @@ class ViewController: NSViewController {
62
 
62
 
63
             wavePattern.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
63
             wavePattern.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
64
             wavePattern.bottomAnchor.constraint(equalTo: mainContent.bottomAnchor),
64
             wavePattern.bottomAnchor.constraint(equalTo: mainContent.bottomAnchor),
65
-            wavePattern.widthAnchor.constraint(equalToConstant: 280),
66
-            wavePattern.heightAnchor.constraint(equalToConstant: 180),
65
+            wavePattern.widthAnchor.constraint(equalToConstant: 200),
66
+            wavePattern.heightAnchor.constraint(equalToConstant: 140),
67
         ])
67
         ])
68
     }
68
     }
69
 
69
 
@@ -129,12 +129,12 @@ class ViewController: NSViewController {
129
             documentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
129
             documentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
130
             documentView.widthAnchor.constraint(equalTo: contentGuide.widthAnchor),
130
             documentView.widthAnchor.constraint(equalTo: contentGuide.widthAnchor),
131
 
131
 
132
-            quickStartSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: 32),
133
-            quickStartSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -32),
132
+            quickStartSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
133
+            quickStartSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
134
             quickStartSection.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
134
             quickStartSection.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
135
 
135
 
136
-            createPrintSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: 32),
137
-            createPrintSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -32),
136
+            createPrintSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
137
+            createPrintSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
138
             createPrintSection.topAnchor.constraint(equalTo: quickStartSection.bottomAnchor, constant: 36),
138
             createPrintSection.topAnchor.constraint(equalTo: quickStartSection.bottomAnchor, constant: 36),
139
             createPrintSection.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -40),
139
             createPrintSection.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -40),
140
         ])
140
         ])
@@ -192,7 +192,7 @@ class ViewController: NSViewController {
192
 
192
 
193
         let cardsStack = NSStackView()
193
         let cardsStack = NSStackView()
194
         cardsStack.orientation = .horizontal
194
         cardsStack.orientation = .horizontal
195
-        cardsStack.spacing = 20
195
+        cardsStack.spacing = AppTheme.quickStartSpacing
196
         cardsStack.distribution = .fillEqually
196
         cardsStack.distribution = .fillEqually
197
         cardsStack.translatesAutoresizingMaskIntoConstraints = false
197
         cardsStack.translatesAutoresizingMaskIntoConstraints = false
198
 
198
 
@@ -258,51 +258,65 @@ class ViewController: NSViewController {
258
             FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
258
             FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
259
         ]
259
         ]
260
 
260
 
261
-        let row1 = makeFeatureRow(features: Array(features[0..<4]), columns: 4)
262
-        let row2 = makeFeatureRow(features: Array(features[4..<6]), columns: 4)
263
-
264
-        let gridStack = NSStackView(views: [row1, row2])
265
-        gridStack.orientation = .vertical
266
-        gridStack.spacing = 16
267
-        gridStack.distribution = .fill
268
-        gridStack.translatesAutoresizingMaskIntoConstraints = false
269
-        section.addSubview(gridStack)
261
+        let grid = makeFeatureGrid(features: features, columns: 4)
262
+        section.addSubview(grid)
270
 
263
 
271
         NSLayoutConstraint.activate([
264
         NSLayoutConstraint.activate([
272
             title.leadingAnchor.constraint(equalTo: section.leadingAnchor),
265
             title.leadingAnchor.constraint(equalTo: section.leadingAnchor),
273
             title.topAnchor.constraint(equalTo: section.topAnchor),
266
             title.topAnchor.constraint(equalTo: section.topAnchor),
274
 
267
 
275
-            gridStack.leadingAnchor.constraint(equalTo: section.leadingAnchor),
276
-            gridStack.trailingAnchor.constraint(equalTo: section.trailingAnchor),
277
-            gridStack.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16),
278
-            gridStack.bottomAnchor.constraint(equalTo: section.bottomAnchor),
268
+            grid.leadingAnchor.constraint(equalTo: section.leadingAnchor),
269
+            grid.trailingAnchor.constraint(equalTo: section.trailingAnchor),
270
+            grid.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16),
271
+            grid.bottomAnchor.constraint(equalTo: section.bottomAnchor),
279
         ])
272
         ])
280
 
273
 
281
         return section
274
         return section
282
     }
275
     }
283
 
276
 
284
-    private func makeFeatureRow(features: [FeatureCardData], columns: Int) -> NSStackView {
285
-        let row = NSStackView()
286
-        row.orientation = .horizontal
287
-        row.spacing = 16
288
-        row.distribution = .fillEqually
289
-        row.alignment = .centerY
290
-        row.translatesAutoresizingMaskIntoConstraints = false
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
291
 
284
 
292
-        for feature in features {
293
-            row.addArrangedSubview(FeatureCardView(data: feature))
285
+        for card in cards {
286
+            grid.addSubview(card)
294
         }
287
         }
295
 
288
 
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)
289
+        for (index, card) in cards.enumerated() {
290
+            let row = index / columns
291
+            let col = index % columns
292
+            let columnAnchor = cards[col]
293
+
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
302
             }
316
             }
303
         }
317
         }
304
 
318
 
305
-        return row
319
+        return grid
306
     }
320
     }
307
 }
321
 }
308
 
322