Patterns · Mac-specific

Ten patterns the Mac app lives on.

The showcase on / covers the web kit. This page is for the SwiftUI engineer — each pattern is something AppViews.swift either already has in system- default styling, or needs to grow into. Preview + anatomy + SwiftUI skeleton per pattern. Copy names, sizes, and tokens as spec.
Jump toStatsGridLiveStepRowFlowChainViewInfoBannerBatchApprovalTableCompletionSummaryManagedRuntimeCardPermissionPromptSettingsFormAuditLogRow
01

StatsGrid / KPI tile

struct StatsGridView · used in Home, FlowDetail · Overview, AuditLog

Big number + tiny icon + one-line label. Four up on Home, three up on narrow detail panes. Rule: the number is Inter Tight 28/700, never bigger.

14
本月執行
420
累積替代(分鐘)
$2.18
本月 LLM 成本
3
已安裝 Flows
Anatomy
tile padding
18
tile radius
radius.lg (12)
tile background
bg.raised
icon container
32×32 · radius.md (8) · accent tint @ 8%
icon size
16pt
number type
Inter Tight 28/700 · -0.02em
label type
Inter 12/400 · text.muted
column gap
space.4 (14)
swiftui
struct KPITile: View {
    let icon: IconDagrun
    let value: String
    let label: String

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            ZStack {
                RoundedRectangle(cornerRadius: 8, style: .continuous)
                    .fill(.dagrunSignal.opacity(0.08))
                icon.view(size: 16, weight: .semibold)
                    .foregroundStyle(.dagrunSignal)
            }
            .frame(width: 32, height: 32)

            VStack(alignment: .leading, spacing: 6) {
                Text(value)
                    .font(.dagrunDisplay(28, weight: .bold))
                    .tracking(-0.6)
                Text(label)
                    .font(.dagrunBody(12))
                    .foregroundStyle(.dagrunTextMuted)
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(18)
        .background(
            RoundedRectangle(cornerRadius: 12, style: .continuous)
                .fill(.dagrunBgRaised)
                .overlay(
                    RoundedRectangle(cornerRadius: 12, style: .continuous)
                        .stroke(.dagrunBorder)
                )
        )
    }
}
02

LiveStepRow

struct LiveStepRow · used in FlowRunView · Running, FlowDetail · Steps

One row per step in the flow. Four visual states, three role badges (deterministic / LLM / human). The running state gets a pulsing dot + signal-orange left border + soft accent glow. This is the single most visible pattern in the app.

1
Load client profileDeterministic
~/Library/…/client_profile.yml
0.08s
2
Gmail searchDeterministic
(invoice OR 發票) has:attachment · 2026-04
1.4s
3
Propose namesLLM
claude-opus-4 · 7 attachments
00:14
4
Batch approvalHuman
Waiting for your review
5
Rename & moveDeterministic
→ ~/Lootex/2026_04_收據發票/
States
pending
4
Batch approvalHuman
Waiting
running
3
Propose namesLLM
claude-opus-4 · 7 files
00:14
done
2
Gmail searchDeterministic
18 hits
1.4s
halted
5
Rename & moveDeterministic
Collision at target folder
0.2s
Anatomy
row padding
14 vertical / 18 horizontal
row radius
radius.lg (10)
left border
3px · accent / success / danger / subtle
index chip
28×28 circle · bg.sunken · JetBrains Mono 11
status dot
8px · pulsing for running (8px accent glow)
title type
Inter Tight 14/600 · -0.01em
subtitle type
Inter 12/400 · text.muted
duration type
JetBrains Mono 11 · text.muted · right
swiftui
struct LiveStepRow: View {
    let index: Int
    let title: String
    let subtitle: String
    let state: StepState
    let role: StepRole
    let duration: String?

    var body: some View {
        HStack(spacing: 14) {
            Text("\(index)")
                .font(.dagrunMono(11))
                .foregroundStyle(.dagrunTextMuted)
                .frame(width: 28, height: 28)
                .background(Circle().fill(.dagrunBgSunken))

            VStack(alignment: .leading, spacing: 4) {
                HStack(spacing: 10) {
                    StatusDot(state: state)
                    Text(title).font(.dagrunDisplay(14, weight: .semibold))
                    RoleBadge(role: role)
                }
                Text(subtitle)
                    .font(.dagrunBody(12))
                    .foregroundStyle(.dagrunTextMuted)
            }

            Spacer()
            Text(duration ?? "—")
                .font(.dagrunMono(11))
                .foregroundStyle(.dagrunTextMuted)
        }
        .padding(.vertical, 14)
        .padding(.horizontal, 18)
        .background(
            RoundedRectangle(cornerRadius: 10, style: .continuous)
                .fill(.dagrunBgRaised)
        )
        .overlay(alignment: .leading) {
            Rectangle().fill(state.accentColor).frame(width: 3)
        }
        .overlay(
            RoundedRectangle(cornerRadius: 10, style: .continuous)
                .stroke(state == .running ? .dagrunSignal.opacity(0.35) : .dagrunBorder)
        )
        .shadow(
            color: state == .running ? .dagrunSignal.opacity(0.08) : .clear,
            radius: 3
        )
    }
}
03

FlowChainView

struct FlowChainView · used in FlowDetail · Overview

Horizontal chip-strip of every step in the flow with role tint + done marker. Compresses 7-step flows into a scannable block. Arrow rule is 1px thin line, not a chevron — keep it calm.

profileDeterministic
searchDeterministic
proposeLLM
approveHuman
downloadDeterministic
renameDeterministic
auditDeterministic
Anatomy
chip padding
10 vertical / 14 horizontal
chip radius
radius.md (8)
connector
18px · 1px rgba(ink, 0.2)
chip gap
2 (the connector is the separator)
chip type
Inter Tight 13/500
04

InfoBanner

struct InfoBanner · used in Home, FlowRunView, Settings

Inline, not a toast. 3px colored left stripe, tinted background at 6-8% opacity, the tone label as a small mono eyebrow. Dismiss chevron is optional — errors are not dismissible.

info
Gmail authorized
You can now run receipts-to-folder.
success
Monthly budget reset
Your April LLM spend resets in 3 days.
warning
Target folder is in iCloud Drive
Files may not appear immediately if iCloud is syncing.
danger
LLM error · 429
Anthropic API rate limit hit. Waiting 60s before retry.
Anatomy
padding
12 vertical / 16 horizontal
radius
radius.md (8)
left stripe
3px · tone color
background
tone color @ 6-8% opacity
eyebrow
Mono 10 · 0.15em tracking · tone color
title
Inter Tight 13/600
body
Inter 12/400 · text.muted
05

BatchApprovalTable

struct ApprovalTableView · used in FlowRunView · Approval (human-in-the-loop gate)

The killer screen. The user spends most of their time here — scanning proposed filenames. Make editing a single tap/click (cell becomes an input). Green pill row = user accepted; amber = edited; red = rejected. Approve all must be a single keystroke (⌘⏎) and the primary action must be visually dominant.

請確認命名提案· 5 封信 · 7 個附件Awaiting approval
idxdatesenderoriginalproposed
120260401Google Cloud5540586479.pdf
20260401_蓋亞GCP_發票_5540586479.pdf
220260403Alchemyreceipt-apr.pdf
20260403_Alchemy_Apr.pdf
320260412Google Workspaceinvoice-0412.pdf
20260412_GoogleWorkspace_發票.pdf
大部分情況下整批通過即可。
Anatomy
row padding
14 vertical / 18 horizontal
row divider
1px · border
proposed cell bg
paper (#F5F2EA)
proposed cell radius
radius.xs (4)
header type
Mono 10 · 0.15em · text.muted · uppercase
primary action
accent filled · ⌘⏎ keyboard shortcut
secondary action
secondary outline
hint
Mono 11 · text.muted · left-aligned
swiftui
struct ApprovalTableView: View {
    @Binding var items: [Proposal]
    var onApproveAll: () -> Void
    var onRejectAll: () -> Void

    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 12) {
                IconDagrun.user.view(size: 14)
                Text("請確認命名提案")
                    .font(.dagrunDisplay(13, weight: .semibold))
                Text("· \(items.count) 項")
                    .font(.dagrunMono(11))
                    .foregroundStyle(.dagrunTextMuted)
                Spacer()
                Badge(text: "Awaiting approval", tone: .accent)
            }
            .padding(.horizontal, 18).padding(.vertical, 12)
            .background(.dagrunBgSunken)

            Divider()
            HeaderRow()
            ForEach($items) { \$item in
                ProposalRow(item: \$item) // inline TextField
                Divider().opacity(0.5)
            }

            HStack {
                Text("大部分情況下整批通過即可。")
                    .font(.dagrunMono(11))
                    .foregroundStyle(.dagrunTextMuted)
                Spacer()
                DagrunButton("Reject all", variant: .secondary, action: onRejectAll)
                DagrunButton("Approve all (⌘⏎)", variant: .accent, action: onApproveAll)
                    .keyboardShortcut(.return, modifiers: .command)
            }
            .padding(14)
        }
        .background(.dagrunBgRaised, in: RoundedRectangle(cornerRadius: 10, style: .continuous))
    }
}
06

CompletionSummary

struct CompletionSummaryView · used in FlowRunView · Complete

The ROI moment. 替代 47 分鐘 is the hero — 36pt display, rest is mono. This is also the only place in the app where a Reveal in Finder button appears as the primary CTA — it gives the user immediate physical proof the flow did something.

Complete · 2026-04-18 14:22
替代 47 分鐘。
歸檔
7 files
LLM 成本
$0.12
耗時
42.1s
累積節省
7h 00m
Anatomy
check avatar
96×96 · circle · success (#2E7D32) · 44pt icon
hero number
Inter Tight 36/700 · -0.03em
eyebrow
Mono 10 · 0.15em · success · uppercase
metric label
Mono 10 · 0.1em · text.muted
metric value
Inter Tight 16/600
primary cta
ink button · Reveal in Finder
07

ManagedRuntimeCard

struct ManagedRuntimeCard · used in Settings · Managed Script Runtimes

Shows an embedded Python/Node runtime with version, size, install path, and live progress. Dagrun is local-first — the card should communicate “we ship this runtime in the app bundle, never from your system PATH”. Progress bar uses signal orange; idle state hides it.

Python runtime
3.12.4 · 42.1 MB · managed · ~/Library/…/managed-runtimes/python/
Downloading · 28.6 MB / 42.1 MB
Installing
Anatomy
card padding
20
icon avatar
44×44 · radius.lg (10) · bg.sunken
progress bar
6px · radius 3 · signal
progress label
Mono 10 · text.muted
status pill
Mono 10 · uppercase · tone-appropriate
08

PermissionPrompt

struct PermissionPromptView · used in FlowRunView · First run (Gmail, filesystem, etc.)

The permission prompt is Dagrun's main trust moment. Rule of three bullets: (1) what we will do, (2) what we will never do, (3) where to revoke. Signal-orange shield icon, not system padlock. Decline is always the easier button visually.

Allow Dagrun to access Gmail?
receipts-to-folder needs read-only Gmail access to find invoices with attachments. Dagrun never uploads your mail. You can revoke any time from Settings → Permissions.
read messages matching the search query
download attachment bytes into your local staging folder
send, delete, or modify your mail — never
Anatomy
modal width
520
modal radius
radius.lg (12)
shadow
shadow.xl
shield avatar
52×52 · radius.lg (12) · signal @ 10%
headline
Inter Tight 17/700 · -0.02em
body
Inter 13/400 · text.muted · lh 1.5
scope box
bg.sunken · radius.md (8) · Mono 11
primary cta
accent filled
secondary cta
secondary outline · Not now (never dangerous)
09

SettingsForm · Preferences

struct SettingsView · FormLikeRows · used in Settings scene · 900×720

Mac System-Preferences convention: 200px label column, 1fr control column, divider between rows, section header as a small bold eyebrow. Help text below the label — not beside the control. Toggles use AccentColor (signal).

LLM Providers
Anthropic API key
Stored in macOS Keychain. Never leaves this device.
sk-ant-…·····
Default planner
Used when no step override is set.
claude-opus-4
Monthly budget cap
Halt all runs if this month's spend exceeds the cap.
$10.00
Local LLM fallback
Anatomy
label column
200px fixed · right-aligned optional
row padding
14 vertical
row divider
1px border · 6% opacity
section header
Inter Tight 14/700 · -0.01em · padding 16 top
input height
32
toggle
36×20 · accent when on
10

AuditLogRow

struct AuditRow · used in AuditLog, Home · Recent activity

Timestamp → flow name → status → 4 metrics (output, saved, cost, duration). Metrics are Mono 12. Status pill uses success/danger tokens. Designed to be scannable at 48px row height.

Apr 18 · 14:22receipts-to-folderSuccess
產出
7 files
替代
47 min
LLM
$0.12
耗時
42.1s
Apr 18 · 09:11python-hello-worldSuccess
產出
1 file
替代
2 min
LLM
$0.00
耗時
0.8s
Apr 17 · 21:40receipts-to-folderHalted
產出
0 files
替代
0 min
LLM
$0.04
耗時
12.6s
Anatomy
row padding
14 vertical / 18 horizontal
timestamp col
160px · Mono 11 · text.muted
flow col
fluid · Inter 13 with leading icon 14pt
status col
120px · pill · tone success/warning/danger
metric col
100px each · small label above value
divider
1px border @ 6%

What’s not here yet

Three patterns still waiting on design: OAuthWebView (the in-app Google consent flow), EmptyState (no runs yet / no flows installed), and OnboardingSheet (first-run walkthrough). Flag these when the SwiftUI engineer hits them and we’ll spec alongside the first real usage.