Forráskód Böngészése

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

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 hónapja
szülő
commit
d9c60d4bb2

+ 2 - 2
smart_printer/AppTheme.swift

@@ -13,7 +13,7 @@ enum AppTheme {
 
     static let quickStartIconMax: CGFloat = 80
     static let quickStartIconMin: CGFloat = 56
-    static let featureIconMax: CGFloat = 56
+    static let featureIconMax: CGFloat = 48
     static let featureIconMin: CGFloat = 36
 
     static func quickStartIconSize(forCardWidth width: CGFloat) -> CGFloat {
@@ -21,7 +21,7 @@ enum AppTheme {
     }
 
     static func featureIconSize(forCardWidth width: CGFloat) -> CGFloat {
-        min(featureIconMax, max(featureIconMin, width * 0.38))
+        min(featureIconMax, max(featureIconMin, width * 0.24))
     }
 
     static let cornerRadius: CGFloat = 14

+ 90 - 68
smart_printer/FeatureIcons.swift

@@ -16,6 +16,10 @@ final class FeatureIconView: NSView {
         self.kind = kind
         super.init(frame: .zero)
         translatesAutoresizingMaskIntoConstraints = false
+        setContentHuggingPriority(.required, for: .horizontal)
+        setContentHuggingPriority(.required, for: .vertical)
+        setContentCompressionResistancePriority(.required, for: .horizontal)
+        setContentCompressionResistancePriority(.required, for: .vertical)
     }
 
     @available(*, unavailable)
@@ -38,42 +42,70 @@ final class FeatureIconView: NSView {
         }
     }
 
+    // MARK: - Layout
+
+    /// Shared artboard so every icon fills the same visual area.
+    private struct IconLayout {
+        let fill: CGRect
+        let d: CGFloat
+
+        init(bounds: NSRect) {
+            let side = min(bounds.width, bounds.height)
+            let originX = (bounds.width - side) / 2
+            let originY = (bounds.height - side) / 2
+            let inset = side * 0.12
+            fill = CGRect(x: originX + inset, y: originY + inset, width: side - inset * 2, height: side - inset * 2)
+            d = fill.width
+        }
+
+        func rect(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) -> CGRect {
+            CGRect(
+                x: fill.minX + fill.width * x,
+                y: fill.minY + fill.height * y,
+                width: fill.width * w,
+                height: fill.height * h
+            )
+        }
+
+        func dim(_ fraction: CGFloat) -> CGFloat {
+            d * fraction
+        }
+    }
+
     // MARK: - Scan File
 
     private func drawScanFile(in context: CGContext) {
-        let s = min(bounds.width, bounds.height)
-        let ox = (bounds.width - s) / 2
-        let oy = (bounds.height - s) / 2
+        let layout = IconLayout(bounds: bounds)
 
-        let bodyRect = CGRect(x: ox + s * 0.08, y: oy + s * 0.30, width: s * 0.84, height: s * 0.52)
-        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)
+        let bodyRect = layout.rect(0.08, 0.38, 0.84, 0.50)
+        drawSoftShadow(in: context, rect: CGRect(x: bodyRect.minX, y: bodyRect.maxY - layout.dim(0.03), width: bodyRect.width, height: layout.dim(0.05)), radius: layout.dim(0.025))
 
         let blueTop = NSColor(red: 0.38, green: 0.66, blue: 1.0, alpha: 1)
         let blueBottom = NSColor(red: 0.18, green: 0.44, blue: 0.94, alpha: 1)
-        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))
+        fillGradient(in: context, path: roundedRect(bodyRect, radius: layout.dim(0.06)), colors: [blueTop.cgColor, blueBottom.cgColor], start: CGPoint(x: bodyRect.midX, y: bodyRect.minY), end: CGPoint(x: bodyRect.midX, y: bodyRect.maxY))
 
-        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)
+        let glassRect = CGRect(x: bodyRect.minX + layout.dim(0.06), y: bodyRect.minY + layout.dim(0.05), width: bodyRect.width * 0.84, height: bodyRect.height * 0.52)
         context.setFillColor(NSColor.white.withAlphaComponent(0.25).cgColor)
-        context.addPath(roundedRect(glassRect, radius: s * 0.03))
+        context.addPath(roundedRect(glassRect, radius: layout.dim(0.03)))
         context.fillPath()
 
-        let lidRect = CGRect(x: ox + s * 0.04, y: oy + s * 0.14, width: s * 0.92, height: s * 0.20)
-        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))
+        let lidRect = layout.rect(0.06, 0.22, 0.88, 0.18)
+        fillGradient(in: context, path: roundedRect(lidRect, radius: layout.dim(0.04)), colors: [NSColor(red: 0.55, green: 0.76, blue: 1.0, alpha: 1).cgColor, blueTop.cgColor], start: CGPoint(x: lidRect.midX, y: lidRect.minY), end: CGPoint(x: lidRect.midX, y: lidRect.maxY))
 
-        let paperRect = CGRect(x: ox + s * 0.28, y: oy + s * 0.02, width: s * 0.44, height: s * 0.22)
+        let paperRect = layout.rect(0.28, 0.06, 0.44, 0.20)
         context.setFillColor(NSColor.white.cgColor)
-        context.addPath(roundedRect(paperRect, radius: s * 0.02))
+        context.addPath(roundedRect(paperRect, radius: layout.dim(0.02)))
         context.fillPath()
 
         for i in 0..<3 {
-            let lineY = paperRect.minY + s * 0.06 + CGFloat(i) * s * 0.045
+            let lineY = paperRect.minY + layout.dim(0.06) + CGFloat(i) * layout.dim(0.045)
             let lineW = paperRect.width * (0.85 - CGFloat(i) * 0.1)
             context.setFillColor(NSColor(red: 0.72, green: 0.82, blue: 0.98, alpha: 1).cgColor)
-            context.addPath(roundedRect(CGRect(x: paperRect.minX + s * 0.05, y: lineY, width: lineW, height: s * 0.025), radius: s * 0.01))
+            context.addPath(roundedRect(CGRect(x: paperRect.minX + layout.dim(0.05), y: lineY, width: lineW, height: layout.dim(0.025)), radius: layout.dim(0.01)))
             context.fillPath()
         }
 
-        let lightRect = CGRect(x: bodyRect.maxX - s * 0.14, y: bodyRect.minY + s * 0.08, width: s * 0.06, height: s * 0.06)
+        let lightRect = CGRect(x: bodyRect.maxX - layout.dim(0.14), y: bodyRect.minY + layout.dim(0.08), width: layout.dim(0.06), height: layout.dim(0.06))
         context.setFillColor(NSColor(red: 0.30, green: 0.90, blue: 0.55, alpha: 1).cgColor)
         context.fillEllipse(in: lightRect)
     }
@@ -81,22 +113,20 @@ final class FeatureIconView: NSView {
     // MARK: - Print Text
 
     private func drawPrintText(in context: CGContext) {
-        let s = min(bounds.width, bounds.height)
-        let ox = (bounds.width - s) / 2
-        let oy = (bounds.height - s) / 2
+        let layout = IconLayout(bounds: bounds)
 
-        let frameRect = CGRect(x: ox + s * 0.14, y: oy + s * 0.14, width: s * 0.72, height: s * 0.72)
-        drawSoftShadow(in: context, rect: frameRect.insetBy(dx: s * 0.04, dy: -s * 0.02), radius: s * 0.03)
+        let frameRect = layout.rect(0.08, 0.08, 0.84, 0.84)
+        drawSoftShadow(in: context, rect: frameRect.insetBy(dx: layout.dim(0.04), dy: -layout.dim(0.02)), radius: layout.dim(0.03))
 
         context.setStrokeColor(NSColor(red: 0.62, green: 0.44, blue: 0.96, alpha: 1).cgColor)
-        context.setLineWidth(s * 0.025)
-        context.setLineDash(phase: 0, lengths: [s * 0.04, s * 0.04])
-        context.addPath(roundedRect(frameRect, radius: s * 0.04))
+        context.setLineWidth(layout.dim(0.03))
+        context.setLineDash(phase: 0, lengths: [layout.dim(0.04), layout.dim(0.04)])
+        context.addPath(roundedRect(frameRect, radius: layout.dim(0.04)))
         context.strokePath()
         context.setLineDash(phase: 0, lengths: [])
 
         let purple = NSColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1)
-        let font = NSFont.systemFont(ofSize: s * 0.42, weight: .bold)
+        let font = NSFont.systemFont(ofSize: layout.dim(0.40), weight: .bold)
         let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: purple]
         let text = "T" as NSString
         let textSize = text.size(withAttributes: attrs)
@@ -106,20 +136,18 @@ final class FeatureIconView: NSView {
     // MARK: - Print Contacts
 
     private func drawPrintContacts(in context: CGContext) {
-        let s = min(bounds.width, bounds.height)
-        let ox = (bounds.width - s) / 2
-        let oy = (bounds.height - s) / 2
+        let layout = IconLayout(bounds: bounds)
 
-        let bookRect = CGRect(x: ox + s * 0.12, y: oy + s * 0.18, width: s * 0.62, height: s * 0.68)
-        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)
+        let bookRect = layout.rect(0.08, 0.08, 0.84, 0.84)
+        drawSoftShadow(in: context, rect: CGRect(x: bookRect.minX, y: bookRect.maxY - layout.dim(0.03), width: bookRect.width, height: layout.dim(0.05)), radius: layout.dim(0.025))
 
         let greenTop = NSColor(red: 0.42, green: 0.86, blue: 0.52, alpha: 1)
         let greenBottom = NSColor(red: 0.18, green: 0.68, blue: 0.38, alpha: 1)
-        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))
+        fillGradient(in: context, path: roundedRect(bookRect, radius: layout.dim(0.05)), colors: [greenTop.cgColor, greenBottom.cgColor], start: CGPoint(x: bookRect.midX, y: bookRect.minY), end: CGPoint(x: bookRect.midX, y: bookRect.maxY))
 
-        let spineRect = CGRect(x: bookRect.minX, y: bookRect.minY, width: s * 0.08, height: bookRect.height)
+        let spineRect = CGRect(x: bookRect.minX, y: bookRect.minY, width: layout.dim(0.08), height: bookRect.height)
         context.setFillColor(NSColor(red: 0.12, green: 0.52, blue: 0.30, alpha: 1).cgColor)
-        context.addPath(roundedRect(spineRect, radius: s * 0.02, topLeft: s * 0.05, topRight: 0, bottomLeft: s * 0.05, bottomRight: 0))
+        context.addPath(roundedRect(spineRect, radius: layout.dim(0.02), topLeft: layout.dim(0.05), topRight: 0, bottomLeft: layout.dim(0.05), bottomRight: 0))
         context.fillPath()
 
         let tabColors: [NSColor] = [
@@ -128,42 +156,40 @@ final class FeatureIconView: NSView {
             NSColor(red: 0.96, green: 0.82, blue: 0.40, alpha: 1),
         ]
         for (i, color) in tabColors.enumerated() {
-            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)
+            let tabRect = CGRect(x: bookRect.maxX - layout.dim(0.02), y: bookRect.minY + layout.dim(0.10) + CGFloat(i) * layout.dim(0.14), width: layout.dim(0.10), height: layout.dim(0.10))
             context.setFillColor(color.cgColor)
-            context.addPath(roundedRect(tabRect, radius: s * 0.02, topLeft: 0, topRight: s * 0.02, bottomLeft: 0, bottomRight: s * 0.02))
+            context.addPath(roundedRect(tabRect, radius: layout.dim(0.02), topLeft: 0, topRight: layout.dim(0.02), bottomLeft: 0, bottomRight: layout.dim(0.02)))
             context.fillPath()
         }
 
-        let personCenter = CGPoint(x: bookRect.midX - s * 0.02, y: bookRect.midY + s * 0.04)
+        let personCenter = CGPoint(x: bookRect.midX - layout.dim(0.02), y: bookRect.midY + layout.dim(0.04))
         context.setFillColor(NSColor.white.cgColor)
-        context.fillEllipse(in: CGRect(x: personCenter.x - s * 0.10, y: personCenter.y - s * 0.16, width: s * 0.20, height: s * 0.20))
-        context.fillEllipse(in: CGRect(x: personCenter.x - s * 0.14, y: personCenter.y + s * 0.02, width: s * 0.28, height: s * 0.22))
+        context.fillEllipse(in: CGRect(x: personCenter.x - layout.dim(0.10), y: personCenter.y - layout.dim(0.16), width: layout.dim(0.20), height: layout.dim(0.20)))
+        context.fillEllipse(in: CGRect(x: personCenter.x - layout.dim(0.14), y: personCenter.y + layout.dim(0.02), width: layout.dim(0.28), height: layout.dim(0.22)))
     }
 
     // MARK: - Print Website
 
     private func drawPrintWebsite(in context: CGContext) {
-        let s = min(bounds.width, bounds.height)
-        let ox = (bounds.width - s) / 2
-        let oy = (bounds.height - s) / 2
+        let layout = IconLayout(bounds: bounds)
 
-        let globeRect = CGRect(x: ox + s * 0.10, y: oy + s * 0.14, width: s * 0.58, height: s * 0.58)
-        drawSoftShadow(in: context, rect: globeRect.insetBy(dx: 0, dy: -s * 0.03), radius: s * 0.03)
+        let globeRect = layout.rect(0.08, 0.08, 0.84, 0.84)
+        drawSoftShadow(in: context, rect: globeRect.insetBy(dx: 0, dy: -layout.dim(0.03)), radius: layout.dim(0.03))
 
         let blueTop = NSColor(red: 0.40, green: 0.70, blue: 1.0, alpha: 1)
         let blueBottom = NSColor(red: 0.16, green: 0.44, blue: 0.92, alpha: 1)
         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))
 
         context.setStrokeColor(NSColor.white.withAlphaComponent(0.55).cgColor)
-        context.setLineWidth(s * 0.018)
+        context.setLineWidth(layout.dim(0.018))
         context.strokeEllipse(in: globeRect.insetBy(dx: globeRect.width * 0.15, dy: 0))
         context.strokeEllipse(in: globeRect.insetBy(dx: 0, dy: globeRect.height * 0.22))
         context.move(to: CGPoint(x: globeRect.minX, y: globeRect.midY))
         context.addLine(to: CGPoint(x: globeRect.maxX, y: globeRect.midY))
         context.strokePath()
 
-        let cursorSize = s * 0.30
-        let cursorOrigin = CGPoint(x: ox + s * 0.52, y: oy + s * 0.38)
+        let cursorSize = layout.dim(0.22)
+        let cursorOrigin = CGPoint(x: layout.fill.maxX - layout.dim(0.12), y: layout.fill.minY + layout.fill.height * 0.42)
         context.setFillColor(NSColor.white.cgColor)
         let cursor = CGMutablePath()
         cursor.move(to: cursorOrigin)
@@ -173,7 +199,7 @@ final class FeatureIconView: NSView {
         context.addPath(cursor)
         context.fillPath()
         context.setStrokeColor(NSColor(red: 0.30, green: 0.50, blue: 0.90, alpha: 1).cgColor)
-        context.setLineWidth(s * 0.02)
+        context.setLineWidth(layout.dim(0.02))
         context.addPath(cursor)
         context.strokePath()
     }
@@ -181,12 +207,10 @@ final class FeatureIconView: NSView {
     // MARK: - Draw & Print
 
     private func drawDrawPrint(in context: CGContext) {
-        let s = min(bounds.width, bounds.height)
-        let ox = (bounds.width - s) / 2
-        let oy = (bounds.height - s) / 2
+        let layout = IconLayout(bounds: bounds)
 
-        let paletteRect = CGRect(x: ox + s * 0.08, y: oy + s * 0.30, width: s * 0.72, height: s * 0.48)
-        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)
+        let paletteRect = layout.rect(0.06, 0.14, 0.80, 0.72)
+        drawSoftShadow(in: context, rect: CGRect(x: paletteRect.minX, y: paletteRect.maxY - layout.dim(0.02), width: paletteRect.width, height: layout.dim(0.04)), radius: layout.dim(0.025))
 
         let woodTop = NSColor(red: 0.96, green: 0.78, blue: 0.48, alpha: 1)
         let woodBottom = NSColor(red: 0.86, green: 0.58, blue: 0.28, alpha: 1)
@@ -205,17 +229,17 @@ final class FeatureIconView: NSView {
             let angle = CGFloat(i) * .pi * 0.32 - .pi * 0.5
             let radius = paletteRect.width * 0.28
             let dotCenter = CGPoint(x: thumbCenter.x + cos(angle) * radius, y: thumbCenter.y + sin(angle) * radius * 0.55)
-            let dotSize = s * 0.09
+            let dotSize = layout.dim(0.09)
             context.setFillColor(color.cgColor)
             context.fillEllipse(in: CGRect(x: dotCenter.x - dotSize / 2, y: dotCenter.y - dotSize / 2, width: dotSize, height: dotSize))
         }
 
         context.saveGState()
-        context.translateBy(x: ox + s * 0.58, y: oy + s * 0.08)
+        context.translateBy(x: layout.fill.minX + layout.fill.width * 0.58, y: layout.fill.minY + layout.fill.height * 0.04)
         context.rotate(by: -.pi / 5)
-        let brushRect = CGRect(x: 0, y: 0, width: s * 0.10, height: s * 0.42)
-        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))
-        let bristleRect = CGRect(x: brushRect.midX - s * 0.05, y: brushRect.maxY - s * 0.02, width: s * 0.10, height: s * 0.10)
+        let brushRect = CGRect(x: 0, y: 0, width: layout.dim(0.10), height: layout.dim(0.44))
+        fillGradient(in: context, path: roundedRect(brushRect, radius: layout.dim(0.02)), colors: [NSColor(red: 0.72, green: 0.48, blue: 0.28, alpha: 1).cgColor, NSColor(red: 0.52, green: 0.32, blue: 0.18, alpha: 1).cgColor], start: CGPoint(x: brushRect.midX, y: brushRect.minY), end: CGPoint(x: brushRect.midX, y: brushRect.maxY))
+        let bristleRect = CGRect(x: brushRect.midX - layout.dim(0.05), y: brushRect.maxY - layout.dim(0.02), width: layout.dim(0.10), height: layout.dim(0.10))
         context.setFillColor(NSColor(red: 0.96, green: 0.55, blue: 0.30, alpha: 1).cgColor)
         context.fillEllipse(in: bristleRect)
         context.restoreGState()
@@ -232,34 +256,32 @@ final class FeatureIconView: NSView {
     // MARK: - OCR File
 
     private func drawOCRFile(in context: CGContext) {
-        let s = min(bounds.width, bounds.height)
-        let ox = (bounds.width - s) / 2
-        let oy = (bounds.height - s) / 2
+        let layout = IconLayout(bounds: bounds)
 
-        let docRect = CGRect(x: ox + s * 0.18, y: oy + s * 0.10, width: s * 0.56, height: s * 0.72)
-        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)
+        let docRect = layout.rect(0.08, 0.06, 0.84, 0.88)
+        drawSoftShadow(in: context, rect: CGRect(x: docRect.minX, y: docRect.maxY - layout.dim(0.03), width: docRect.width, height: layout.dim(0.05)), radius: layout.dim(0.025))
 
         context.setFillColor(NSColor.white.cgColor)
-        context.addPath(roundedRect(docRect, radius: s * 0.04))
+        context.addPath(roundedRect(docRect, radius: layout.dim(0.04)))
         context.fillPath()
 
         let purpleTop = NSColor(red: 0.62, green: 0.44, blue: 0.96, alpha: 1)
         let purpleBottom = NSColor(red: 0.45, green: 0.28, blue: 0.86, alpha: 1)
-        let headerRect = CGRect(x: docRect.minX, y: docRect.minY, width: docRect.width, height: s * 0.18)
-        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))
+        let headerRect = CGRect(x: docRect.minX, y: docRect.minY, width: docRect.width, height: layout.dim(0.18))
+        fillGradient(in: context, path: roundedRect(headerRect, radius: layout.dim(0.04), topLeft: layout.dim(0.04), topRight: layout.dim(0.04), bottomLeft: 0, bottomRight: 0), colors: [purpleTop.cgColor, purpleBottom.cgColor], start: CGPoint(x: headerRect.midX, y: headerRect.minY), end: CGPoint(x: headerRect.midX, y: headerRect.maxY))
 
-        let font = NSFont.systemFont(ofSize: s * 0.14, weight: .bold)
+        let font = NSFont.systemFont(ofSize: layout.dim(0.14), weight: .bold)
         let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: NSColor.white]
         let text = "OCR" as NSString
         let textSize = text.size(withAttributes: attrs)
-        text.draw(at: CGPoint(x: docRect.midX - textSize.width / 2, y: docRect.minY + s * 0.04), withAttributes: attrs)
+        text.draw(at: CGPoint(x: docRect.midX - textSize.width / 2, y: docRect.minY + layout.dim(0.04)), withAttributes: attrs)
 
         let lineColor = NSColor(red: 0.78, green: 0.72, blue: 0.96, alpha: 1)
         for i in 0..<4 {
-            let lineY = docRect.minY + s * 0.24 + CGFloat(i) * s * 0.10
+            let lineY = docRect.minY + layout.dim(0.24) + CGFloat(i) * layout.dim(0.10)
             let lineW = docRect.width * (0.80 - CGFloat(i) * 0.08)
             context.setFillColor(lineColor.cgColor)
-            context.addPath(roundedRect(CGRect(x: docRect.minX + s * 0.08, y: lineY, width: lineW, height: s * 0.035), radius: s * 0.012))
+            context.addPath(roundedRect(CGRect(x: docRect.minX + layout.dim(0.08), y: lineY, width: lineW, height: layout.dim(0.035)), radius: layout.dim(0.012)))
             context.fillPath()
         }
     }

+ 6 - 6
smart_printer/UIComponents.swift

@@ -308,12 +308,12 @@ final class FeatureCardView: NSView {
         applyCardShadow()
 
         let titleLabel = NSTextField(labelWithString: data.title)
-        titleLabel.font = AppTheme.semiboldFont(size: 15)
+        titleLabel.font = AppTheme.semiboldFont(size: 14)
         titleLabel.textColor = AppTheme.textPrimary
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
 
         let subtitleLabel = NSTextField(labelWithString: data.subtitle)
-        subtitleLabel.font = AppTheme.regularFont(size: 12)
+        subtitleLabel.font = AppTheme.regularFont(size: 11)
         subtitleLabel.textColor = AppTheme.textSecondary
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
 
@@ -337,13 +337,13 @@ final class FeatureCardView: NSView {
         addSubview(arrowButton)
 
         NSLayoutConstraint.activate([
-            heightAnchor.constraint(equalToConstant: 104),
+            heightAnchor.constraint(equalToConstant: 96),
 
-            iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
+            iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
             iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
 
-            titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8),
-            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 22),
+            titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
+            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
             titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32),
 
             subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),

+ 18 - 41
smart_printer/ViewController.swift

@@ -258,7 +258,15 @@ class ViewController: NSViewController {
             FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
         ]
 
-        let grid = makeFeatureGrid(features: features, columns: 4)
+        let topRow = makeFeatureRow(features: Array(features.prefix(4)))
+        let bottomRow = makeFeatureRow(features: Array(features.suffix(2)))
+
+        let grid = NSStackView(views: [topRow, bottomRow])
+        grid.orientation = .vertical
+        grid.spacing = AppTheme.featureGridSpacing
+        grid.distribution = .fillEqually
+        grid.translatesAutoresizingMaskIntoConstraints = false
+
         section.addSubview(grid)
 
         NSLayoutConstraint.activate([
@@ -274,49 +282,18 @@ class ViewController: NSViewController {
         return section
     }
 
-    private func makeFeatureGrid(features: [FeatureCardData], columns: Int) -> NSView {
-        let grid = NSView()
-        grid.translatesAutoresizingMaskIntoConstraints = false
-
-        let spacing = AppTheme.featureGridSpacing
-        let cards = features.map { FeatureCardView(data: $0) }
-        let rowCount = (cards.count + columns - 1) / columns
-
-        for card in cards {
-            grid.addSubview(card)
-        }
-
-        for (index, card) in cards.enumerated() {
-            let row = index / columns
-            let col = index % columns
-            let columnAnchor = cards[col]
+    private func makeFeatureRow(features: [FeatureCardData]) -> NSStackView {
+        let row = NSStackView()
+        row.orientation = .horizontal
+        row.spacing = AppTheme.featureGridSpacing
+        row.distribution = .fillEqually
+        row.translatesAutoresizingMaskIntoConstraints = false
 
-            if col == 0 {
-                card.leadingAnchor.constraint(equalTo: grid.leadingAnchor).isActive = true
-            } else {
-                let leftCard = cards[index - 1]
-                card.leadingAnchor.constraint(equalTo: leftCard.trailingAnchor, constant: spacing).isActive = true
-            }
-
-            card.widthAnchor.constraint(equalTo: columnAnchor.widthAnchor).isActive = true
-
-            if row == 0 {
-                card.topAnchor.constraint(equalTo: grid.topAnchor).isActive = true
-            } else {
-                let aboveCard = cards[index - columns]
-                card.topAnchor.constraint(equalTo: aboveCard.bottomAnchor, constant: spacing).isActive = true
-            }
-
-            if col == columns - 1 {
-                card.trailingAnchor.constraint(equalTo: grid.trailingAnchor).isActive = true
-            }
-
-            if row == rowCount - 1 {
-                card.bottomAnchor.constraint(equalTo: grid.bottomAnchor).isActive = true
-            }
+        for data in features {
+            row.addArrangedSubview(FeatureCardView(data: data))
         }
 
-        return grid
+        return row
     }
 }