Sfoglia il codice sorgente

Redesign Create & Print section to match reference layout.

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

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 mese fa
parent
commit
6a9e45e463

+ 307 - 0
smart_printer/FeatureIcons.swift

@@ -0,0 +1,307 @@
+import Cocoa
+
+enum FeatureIconKind {
+    case scanFile
+    case printText
+    case printContacts
+    case printWebsite
+    case drawPrint
+    case ocrFile
+}
+
+final class FeatureIconView: NSView {
+    private let kind: FeatureIconKind
+
+    init(kind: FeatureIconKind) {
+        self.kind = kind
+        super.init(frame: .zero)
+        translatesAutoresizingMaskIntoConstraints = false
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) { nil }
+
+    override var isFlipped: Bool { true }
+    override var isOpaque: Bool { false }
+
+    override func draw(_ dirtyRect: NSRect) {
+        super.draw(dirtyRect)
+        guard let context = NSGraphicsContext.current?.cgContext else { return }
+
+        switch kind {
+        case .scanFile: drawScanFile(in: context)
+        case .printText: drawPrintText(in: context)
+        case .printContacts: drawPrintContacts(in: context)
+        case .printWebsite: drawPrintWebsite(in: context)
+        case .drawPrint: drawDrawPrint(in: context)
+        case .ocrFile: drawOCRFile(in: context)
+        }
+    }
+
+    // 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 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 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))
+
+        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)
+        context.setFillColor(NSColor.white.withAlphaComponent(0.25).cgColor)
+        context.addPath(roundedRect(glassRect, radius: s * 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 paperRect = CGRect(x: ox + s * 0.28, y: oy + s * 0.02, width: s * 0.44, height: s * 0.22)
+        context.setFillColor(NSColor.white.cgColor)
+        context.addPath(roundedRect(paperRect, radius: s * 0.02))
+        context.fillPath()
+
+        for i in 0..<3 {
+            let lineY = paperRect.minY + s * 0.06 + CGFloat(i) * s * 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.fillPath()
+        }
+
+        let lightRect = CGRect(x: bodyRect.maxX - s * 0.14, y: bodyRect.minY + s * 0.08, width: s * 0.06, height: s * 0.06)
+        context.setFillColor(NSColor(red: 0.30, green: 0.90, blue: 0.55, alpha: 1).cgColor)
+        context.fillEllipse(in: lightRect)
+    }
+
+    // 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 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)
+
+        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.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 attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: purple]
+        let text = "T" as NSString
+        let textSize = text.size(withAttributes: attrs)
+        text.draw(at: CGPoint(x: frameRect.midX - textSize.width / 2, y: frameRect.midY - textSize.height / 2), withAttributes: attrs)
+    }
+
+    // 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 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 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))
+
+        let spineRect = CGRect(x: bookRect.minX, y: bookRect.minY, width: s * 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.fillPath()
+
+        let tabColors: [NSColor] = [
+            NSColor(red: 0.96, green: 0.55, blue: 0.55, alpha: 1),
+            NSColor(red: 0.55, green: 0.72, blue: 0.96, alpha: 1),
+            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)
+            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.fillPath()
+        }
+
+        let personCenter = CGPoint(x: bookRect.midX - s * 0.02, y: bookRect.midY + s * 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))
+    }
+
+    // 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 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 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.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)
+        context.setFillColor(NSColor.white.cgColor)
+        let cursor = CGMutablePath()
+        cursor.move(to: cursorOrigin)
+        cursor.addLine(to: CGPoint(x: cursorOrigin.x, y: cursorOrigin.y + cursorSize))
+        cursor.addLine(to: CGPoint(x: cursorOrigin.x + cursorSize * 0.35, y: cursorOrigin.y + cursorSize * 0.72))
+        cursor.closeSubpath()
+        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.addPath(cursor)
+        context.strokePath()
+    }
+
+    // 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 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 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)
+        let palettePath = paletteShape(in: paletteRect)
+        fillGradient(in: context, path: palettePath, colors: [woodTop.cgColor, woodBottom.cgColor], start: CGPoint(x: paletteRect.midX, y: paletteRect.minY), end: CGPoint(x: paletteRect.midX, y: paletteRect.maxY))
+
+        let paintColors: [NSColor] = [
+            NSColor(red: 0.96, green: 0.35, blue: 0.35, alpha: 1),
+            NSColor(red: 0.35, green: 0.65, blue: 0.96, alpha: 1),
+            NSColor(red: 0.35, green: 0.82, blue: 0.45, alpha: 1),
+            NSColor(red: 0.96, green: 0.78, blue: 0.25, alpha: 1),
+            NSColor(red: 0.72, green: 0.40, blue: 0.96, alpha: 1),
+        ]
+        let thumbCenter = CGPoint(x: paletteRect.minX + paletteRect.width * 0.18, y: paletteRect.midY)
+        for (i, color) in paintColors.enumerated() {
+            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
+            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.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)
+        context.setFillColor(NSColor(red: 0.96, green: 0.55, blue: 0.30, alpha: 1).cgColor)
+        context.fillEllipse(in: bristleRect)
+        context.restoreGState()
+    }
+
+    private func paletteShape(in rect: CGRect) -> CGPath {
+        let path = CGMutablePath()
+        path.addEllipse(in: rect)
+        let thumb = CGRect(x: rect.minX - rect.width * 0.08, y: rect.midY - rect.height * 0.18, width: rect.width * 0.22, height: rect.height * 0.36)
+        path.addEllipse(in: thumb)
+        return path
+    }
+
+    // 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 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)
+
+        context.setFillColor(NSColor.white.cgColor)
+        context.addPath(roundedRect(docRect, radius: s * 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 font = NSFont.systemFont(ofSize: s * 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)
+
+        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 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.fillPath()
+        }
+    }
+
+    // MARK: - Helpers
+
+    private func drawSoftShadow(in context: CGContext, rect: CGRect, radius: CGFloat) {
+        context.saveGState()
+        context.setFillColor(NSColor.black.withAlphaComponent(0.10).cgColor)
+        context.addPath(roundedRect(rect.offsetBy(dx: 0, dy: radius * 0.2), radius: radius))
+        context.fillPath()
+        context.restoreGState()
+    }
+
+    private func fillGradient(in context: CGContext, path: CGPath, colors: [CGColor], start: CGPoint, end: CGPoint) {
+        context.saveGState()
+        context.addPath(path)
+        context.clip()
+        guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: [0, 1]) else {
+            context.restoreGState()
+            return
+        }
+        context.drawLinearGradient(gradient, start: start, end: end, options: [])
+        context.restoreGState()
+    }
+
+    private func roundedRect(_ rect: CGRect, radius: CGFloat) -> CGPath {
+        roundedRect(rect, radius: radius, topLeft: radius, topRight: radius, bottomLeft: radius, bottomRight: radius)
+    }
+
+    private func roundedRect(_ rect: CGRect, radius: CGFloat, topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) -> CGPath {
+        let path = CGMutablePath()
+        path.move(to: CGPoint(x: rect.minX + topLeft, y: rect.maxY))
+        path.addLine(to: CGPoint(x: rect.maxX - topRight, y: rect.maxY))
+        path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY - topRight), control: CGPoint(x: rect.maxX, y: rect.maxY))
+        path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + bottomRight))
+        path.addQuadCurve(to: CGPoint(x: rect.maxX - bottomRight, y: rect.minY), control: CGPoint(x: rect.maxX, y: rect.minY))
+        path.addLine(to: CGPoint(x: rect.minX + bottomLeft, y: rect.minY))
+        path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.minY + bottomLeft), control: CGPoint(x: rect.minX, y: rect.minY))
+        path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY - topLeft))
+        path.addQuadCurve(to: CGPoint(x: rect.minX + topLeft, y: rect.maxY), control: CGPoint(x: rect.minX, y: rect.maxY))
+        path.closeSubpath()
+        return path
+    }
+}

+ 22 - 37
smart_printer/UIComponents.swift

@@ -258,8 +258,7 @@ final class QuickStartCardView: NSView {
 struct FeatureCardData {
     let title: String
     let subtitle: String
-    let accentColor: NSColor
-    let symbolName: String
+    let iconKind: FeatureIconKind
 }
 
 final class FeatureCardView: NSView {
@@ -271,19 +270,7 @@ final class FeatureCardView: NSView {
         layer?.cornerRadius = AppTheme.featureCardCornerRadius
         applyCardShadow()
 
-        let iconBackground = NSView()
-        iconBackground.wantsLayer = true
-        iconBackground.layer?.backgroundColor = data.accentColor.withAlphaComponent(0.12).cgColor
-        iconBackground.layer?.cornerRadius = 14
-        iconBackground.translatesAutoresizingMaskIntoConstraints = false
-
-        let iconView = NSImageView()
-        iconView.translatesAutoresizingMaskIntoConstraints = false
-        if let image = NSImage(systemSymbolName: data.symbolName, accessibilityDescription: data.title) {
-            let config = NSImage.SymbolConfiguration(pointSize: 22, weight: .medium)
-            iconView.image = image.withSymbolConfiguration(config)
-        }
-        iconView.contentTintColor = data.accentColor
+        let iconView = FeatureIconView(kind: data.iconKind)
 
         let titleLabel = NSTextField(labelWithString: data.title)
         titleLabel.font = AppTheme.semiboldFont(size: 15)
@@ -298,44 +285,42 @@ final class FeatureCardView: NSView {
         let arrowButton = NSButton()
         arrowButton.isBordered = false
         arrowButton.wantsLayer = true
-        arrowButton.layer?.backgroundColor = data.accentColor.withAlphaComponent(0.12).cgColor
-        arrowButton.layer?.cornerRadius = 16
+        arrowButton.layer?.backgroundColor = NSColor.white.cgColor
+        arrowButton.layer?.cornerRadius = 15
+        arrowButton.layer?.borderWidth = 1
+        arrowButton.layer?.borderColor = NSColor(calibratedWhite: 0.88, alpha: 1).cgColor
         arrowButton.translatesAutoresizingMaskIntoConstraints = false
-        if let arrow = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: "Open") {
-            let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
+        if let arrow = NSImage(systemSymbolName: "chevron.right", accessibilityDescription: "Open") {
+            let config = NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold)
             arrowButton.image = arrow.withSymbolConfiguration(config)
         }
-        arrowButton.contentTintColor = data.accentColor
+        arrowButton.contentTintColor = AppTheme.textSecondary
 
-        addSubview(iconBackground)
-        iconBackground.addSubview(iconView)
+        addSubview(iconView)
         addSubview(titleLabel)
         addSubview(subtitleLabel)
         addSubview(arrowButton)
 
         NSLayoutConstraint.activate([
-            heightAnchor.constraint(equalToConstant: 130),
+            heightAnchor.constraint(equalToConstant: 118),
 
-            iconBackground.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
-            iconBackground.topAnchor.constraint(equalTo: topAnchor, constant: 18),
-            iconBackground.widthAnchor.constraint(equalToConstant: 48),
-            iconBackground.heightAnchor.constraint(equalToConstant: 48),
+            iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
+            iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
+            iconView.widthAnchor.constraint(equalToConstant: 72),
+            iconView.heightAnchor.constraint(equalToConstant: 72),
 
-            iconView.centerXAnchor.constraint(equalTo: iconBackground.centerXAnchor),
-            iconView.centerYAnchor.constraint(equalTo: iconBackground.centerYAnchor),
-
-            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
-            titleLabel.topAnchor.constraint(equalTo: iconBackground.bottomAnchor, constant: 12),
-            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -18),
+            titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
+            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 28),
+            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -40),
 
             subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
             subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
             subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
 
-            arrowButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
-            arrowButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16),
-            arrowButton.widthAnchor.constraint(equalToConstant: 32),
-            arrowButton.heightAnchor.constraint(equalToConstant: 32),
+            arrowButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14),
+            arrowButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -14),
+            arrowButton.widthAnchor.constraint(equalToConstant: 30),
+            arrowButton.heightAnchor.constraint(equalToConstant: 30),
         ])
     }
 

+ 56 - 13
smart_printer/ViewController.swift

@@ -142,12 +142,45 @@ class ViewController: NSViewController {
         return scrollView
     }
 
-    private func makeSectionTitle(_ text: String) -> NSTextField {
+    private func makeSectionTitle(_ text: String, showGridIcon: Bool = false) -> NSView {
+        let container = NSView()
+        container.translatesAutoresizingMaskIntoConstraints = false
+
+        var leadingAnchor = container.leadingAnchor
+
+        if showGridIcon {
+            let gridIcon = NSImageView()
+            gridIcon.translatesAutoresizingMaskIntoConstraints = false
+            if let image = NSImage(systemSymbolName: "square.grid.2x2", accessibilityDescription: nil) {
+                let config = NSImage.SymbolConfiguration(pointSize: 16, weight: .medium)
+                gridIcon.image = image.withSymbolConfiguration(config)
+            }
+            gridIcon.contentTintColor = AppTheme.textPrimary
+            container.addSubview(gridIcon)
+
+            NSLayoutConstraint.activate([
+                gridIcon.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+                gridIcon.centerYAnchor.constraint(equalTo: container.centerYAnchor),
+                gridIcon.widthAnchor.constraint(equalToConstant: 20),
+                gridIcon.heightAnchor.constraint(equalToConstant: 20),
+            ])
+            leadingAnchor = gridIcon.trailingAnchor
+        }
+
         let label = NSTextField(labelWithString: text)
         label.font = AppTheme.semiboldFont(size: 20)
         label.textColor = AppTheme.textPrimary
         label.translatesAutoresizingMaskIntoConstraints = false
-        return label
+        container.addSubview(label)
+
+        NSLayoutConstraint.activate([
+            container.heightAnchor.constraint(equalToConstant: 28),
+            label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: showGridIcon ? 8 : 0),
+            label.centerYAnchor.constraint(equalTo: container.centerYAnchor),
+            label.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+        ])
+
+        return container
     }
 
     private func makeQuickStartSection() -> NSView {
@@ -213,20 +246,20 @@ class ViewController: NSViewController {
         let section = NSView()
         section.translatesAutoresizingMaskIntoConstraints = false
 
-        let title = makeSectionTitle("Create & Print")
+        let title = makeSectionTitle("Create & Print", showGridIcon: true)
         section.addSubview(title)
 
         let features: [FeatureCardData] = [
-            FeatureCardData(title: "Scan File", subtitle: "Scan any document", accentColor: AppTheme.blue, symbolName: "scanner.fill"),
-            FeatureCardData(title: "Print Text", subtitle: "Type text and print", accentColor: AppTheme.purple, symbolName: "textformat"),
-            FeatureCardData(title: "Print Contacts", subtitle: "Print your contacts", accentColor: AppTheme.green, symbolName: "person.crop.rectangle.fill"),
-            FeatureCardData(title: "Print Website", subtitle: "Print any website", accentColor: AppTheme.teal, symbolName: "globe"),
-            FeatureCardData(title: "Draw & Print", subtitle: "Add drawings, text and more", accentColor: AppTheme.orange, symbolName: "paintpalette.fill"),
-            FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", accentColor: AppTheme.purple, symbolName: "doc.text.viewfinder"),
+            FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
+            FeatureCardData(title: "Print Text", subtitle: "Type text and print", iconKind: .printText),
+            FeatureCardData(title: "Print Contacts", subtitle: "Print your contacts", iconKind: .printContacts),
+            FeatureCardData(title: "Print Website", subtitle: "Print any website", iconKind: .printWebsite),
+            FeatureCardData(title: "Draw & Print", subtitle: "Add drawings, text and more", iconKind: .drawPrint),
+            FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
         ]
 
-        let row1 = makeFeatureRow(features: Array(features[0..<3]))
-        let row2 = makeFeatureRow(features: Array(features[3..<6]))
+        let row1 = makeFeatureRow(features: Array(features[0..<4]), columns: 4)
+        let row2 = makeFeatureRow(features: Array(features[4..<6]), columns: 4)
 
         let gridStack = NSStackView(views: [row1, row2])
         gridStack.orientation = .vertical
@@ -248,17 +281,27 @@ class ViewController: NSViewController {
         return section
     }
 
-    private func makeFeatureRow(features: [FeatureCardData]) -> NSStackView {
+    private func makeFeatureRow(features: [FeatureCardData], columns: Int) -> NSStackView {
         let row = NSStackView()
         row.orientation = .horizontal
-        row.spacing = 20
+        row.spacing = 16
         row.distribution = .fillEqually
+        row.alignment = .centerY
         row.translatesAutoresizingMaskIntoConstraints = false
 
         for feature in features {
             row.addArrangedSubview(FeatureCardView(data: feature))
         }
 
+        if features.count < columns {
+            let spacerCount = columns - features.count
+            for _ in 0..<spacerCount {
+                let spacer = NSView()
+                spacer.translatesAutoresizingMaskIntoConstraints = false
+                row.addArrangedSubview(spacer)
+            }
+        }
+
         return row
     }
 }