Send to Miro — shipped SPA flow vs the native Swift build to approve

Left = what ships today — the React MiroExportPanel (faithful to MiroExportPanel.tsx). On desktop this same panel is reached via a native entry (toolbar "Send to Miro…" row + Quotes menu) with token persistence in the Keychain — built & verified 28 Jun (board created end-to-end; token survives relaunch via iCloud Keychain). Right = the proposed native-Swift build to approve — the same flow rendered as a modern macOS sheet (centred rounded card over the dimmed window, one constant size, leading-aligned, our SF glyphs). The build details are spec'd at the bottom: "Native Swift build — decisions to approve."

Ships today — React panelWhat desktop shows now (native entry → this panel in the WKWebView). The flow to match.
To build — native Swift sheetSame flow, rendered in SwiftUI. The proposal to approve (spec below).
Entry · the Export affordance gains a Miro item
Export Report…
Copy 11 Quotes
Save as Spreadsheet
Extract Video Clips…
Send to Miro…
Ikea tom · 11 Quotes
Export Report…
Copy Quotes
Save as Spreadsheet…
Extract Video Clips…
Send to Miro…
Built: the SPA ExportDropdown already has the Send to Miro… row → onSendToMiro. But in the desktop app the SPA NavBar is hidden, so this dropdown never shows.
Proposed: one row added to the native ExportPopoverContent; click dispatches menuAction("sendToMiro"), which the SPA’s case "sendToMiro" handler (AppLayout.tsx:399) already catches. (Popover is correct here — it’s a transient toolbar menu, not a sheet.)
a · checking connection

Send to Miro board

Checking connection…

Ikea tom
Checking connection…
Both resolve getMiroStatus() then route to Connect or Configure. (If connection moves to Settings ▸ Accounts — see discussion — this transient step disappears entirely: the sheet opens straight on Configure.)
b · connect (first run only) — paste-token

Send to Miro board

Push your quotes onto a new Miro board as sticky notes, grouped by section and theme. Connect once — we never touch your existing boards.

Paste a Miro access token to connect. How do I get a token? ↗

Ikea tom
Send to Miro board

Push your quotes onto a new Miro board as sticky notes, grouped by section and theme. Connect once — we never touch your existing boards.

How do I get a token?

Built: token-only. Finding: the “How do I get a token?” link is unstyled — browser-default blue-underline (no theme rule targets it). A real polish gap, visible left.
Proposed: OAuth button removed — no UX for a feature that doesn’t exist yet. Leading-aligned. Help shown two equivalent ways (you’d pick one): the round “?” Help button (HIG-canonical for a sheet) and an inline Link — both open the same docs URL via NSWorkspace.
c · configure & send — the main screen

Send to Miro board

Connected ✓ ·

Uploads all quotes in this project to Miro — hidden quotes are excluded.

Ikea tom
Send to Miro board

Uploads all quotes in this project to Miro — hidden quotes are excluded.

Built: stacked labels + UA-default <select> for a binary choice; name field shows a placeholder; Disconnect sits inline in the subtitle.
Proposed (your notes applied): binary colour choice → a checkbox; board name prefilled with the project name (editable); the Disconnect affordance is gone (connection belongs in Settings ▸ Accounts); the yellow notice → plain secondary text (no coloured box exists in our Swift UI); right-aligned aligned-form label, as in the Finder/Pixelmator save sheet.
d · pushing

Send to Miro board

Creating board…

Ikea tom
Creating board…

88 of 142 stickies placed

Built: just a “Creating board…” subtitle — no progress, count, or cancel.
Proposed: determinate progress + count + Cancel. But a modal sheet blocks the window 10–30s — the discussion argues this should be a background job on the sidebar row (BN convention), with the sheet dismissable.
e · done — the payoff crossing

Send to Miro board

Board ready — 142 quote stickies placed.

Ikea tom
Board ready
142 quote stickies placed
Built (web): one line — "Board ready — N quote stickies placed." (the boardReady string).
Proposed (native): HIG title+subtitle — green outline checkmark.circle (MessageKind) + title "Board ready" + subtitle "{count} quote stickies placed" + Done / Open in Miro… (HIG: title + informative text + buttons.) Native button is "Open in Miro…" — a trailing ellipsis, not the web's ↗: HIG says a push button that opens another window/view/app takes a trailing ellipsis. → NSWorkspace.shared.open. If the push runs as a background job (see Progress decision), this sheet can be skipped entirely — the row returns to idle and surfaces "Open in Miro".

What changed from the last version (your notes)

Should this be a sheet? (yes — and the modern look is a sheet)

Apple leaned on title-bar-glued sheets for years; since Big Sur the sheet itself changed — it’s now a centred rounded card. So the “modern modal popup” in Pixelmator isn’t a different control; it’s today’s sheet.

“In macOS, a sheet is a cardlike view with rounded corners that floats on top of its parent window. The parent window is dimmed while the sheet is onscreen.” — HIG, Sheets
“Present a sheet in a reasonable default size. People don’t generally expect to resize sheets.” — HIG (→ one size for all steps, not per-step resizing).

And a sheet beats the alternatives by HIG definition for this “scoped task closely related to the current context”:

ContainerHIG saysFit here
Sheet use this“A scoped task… requesting specific information… a simple task they can complete before returning.”Exactly the Send/Configure step. Modern centred card, parent dimmed, constant size.
Alert“Critical information… avoid using an alert merely to provide information.” Centred icon+title+message, ≤3 buttons.Wrong for a form. Right only for an error (partial-board recovery). The centred text idiom is an alert thing — don’t borrow it for the form.
Panel“Supplementary controls/inspector… avoid controls that require typing text.It’s a text-entry form; panel is for sliders/inspectors. No.
Separate window“A self-contained task like editing a document works well in a separate window.”Overkill for a short scoped action.
PopoverTransient, anchored, dismiss-on-outside-click.Fine for the entry menu (it is one), but it dismisses on focus loss — bad for a multi-field form or any OAuth round-trip.
So: the send/configure step is a sheet (modern centred card). The native-idiomatic decomposition still holds for the rest: connect-once → Settings ▸ Accounts (mirrors our shipped LLM-provider pane; means the sheet opens straight on Configure with no connect/disconnect at all); progress → background job on the sidebar row; errors → an alert (the one correct alert use). OAuth gets designed when it’s actually built — likely ASWebAuthenticationSession, but no UI for it now.

Carrying the web UI’s help link into the native world

One outbound help link — miro.howToGetToken. Principle: the help content is single-sourced in the docs site; every surface just opens that URL.

Where this stands (28 Jun 2026)

Shipped & working on desktop today (the "native entry + web panel" path):

So the flow already works — but on desktop the dialog is the web panel in a web overlay, not a native sheet. This doc now specs replacing that rendering with the native Swift sheet (right column). The entry + Keychain layer are reused as-is.

Native Swift build — decisions to approve

DecisionProposal (approve / change)
Data path ✓ APPROVEDNative SwiftUI sheets call the existing Python REST endpoints over localhost — /api/projects/{id}/miro/{status,connect,disconnect,export} with the app's bearer token. Zero backend duplication — Swift is a presentation layer; logic/data/flow stay in Python (the agnostic IR, shared with the future FigJam/Lucid/Mural renderers). Holding this pattern non-dogmatically even where a Swift reimplementation would be simpler.
Token storageReuse what's built: connect → KeychainHelper.set("miro") directly (no bridge needed once native) + overlayMiroToken env-inject. Disconnect → KeychainHelper.delete("miro") — fixes the current "Disconnect doesn't clear the Swift copy" gap for free.
ScreensMirror the shipped flow (right column): checking → connect → configure → creating → done.
Preview — removed (UX only)Drop the "Preview" button from configure (both native and the web panel): the SVG ≠ the real board, and it's a needless decision point. Keep the machinery under it — the agnostic Board IR (miro_board.py) and the SVG renderer (miro_render_svg.py) are the multi-board scaffolding + dev-iteration tool (IR → {SVG, Miro, FigJam, Lucidspark, Mural}), not preview cruft. Only the user-facing Preview affordance goes; the /miro/preview route stays for dev/test.
Done screenGreen checkmark.circle + title "Board ready" + subtitle "{count} quote stickies placed" + Done / Open in Miro. Title+informative-text+buttons is the HIG alert/dialog convention. (Needs a title/subtitle i18n split from the current single boardReady string.)
ContainerModern macOS sheet (centred rounded card, parent dimmed), one constant size, leading-aligned. Per HIG.
Colour controlCheckbox "Colour stickies by sentiment" (binary), not the web <select>.
Board namePrefilled with the project name, editable (vs the web placeholder).
Glyphs / typeOur MessageKind set (success = outline checkmark.circle, green) · SF Pro · native buttons.
Help link"?" Help button or inline LinkNSWorkspace.open(/docs/send-to-miro.html).
Progress ✓ APPROVEDModal sheet — "Creating board…" + Cancel, blocks the window until done. Accepted because the push is fast; stress-test gate: ~3000 stickies — if it exceeds ~30s, revisit and move the push to a dismissable background job on the sidebar row. (The export endpoint is synchronous, so the sheet shows an indeterminate "Creating board…", no live count — same as the web.)
Open in MiroLabel is "Open in Miro…" — a trailing ellipsis, not the web's ↗ (HIG: a push button that opens another window/view/app takes an ellipsis). → NSWorkspace.open(board_url) (minor: opens Miro.app if installed — universal-link routing; documented, parked).
AnonymiseCarry the export popover's global Anonymise toggle into the export request. (Backend honour of anonymise for Miro is a separate small task.)
Scope: ~5 SwiftUI sheet views + a small Swift Miro-API client (localhost calls) + present-sheet wiring from the popover/menu (replacing the menuAction("sendToMiro") → web-panel dispatch). Backend, Keychain, and i18n strings all reused. What's retired: only the web-panel rendering on desktop (the web panel stays for browser/serve mode). Approve the rows above (esp. Data path + Progress) and I'll build it.

Auto-discovered by bristlenose serve --dev (About → Design). Siblings: export-menu-comparison.html, export-popover-lenses.html, miro-flow.html, miro-setup-help.html. HIG quotes pulled from the DocC JSON for Sheets/Alerts/Panels (28 Jun 2026).