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."
ExportDropdown already has the Send to Miro… row → onSendToMiro. But in the desktop app the SPA NavBar is hidden, so this dropdown never shows.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.)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.)Push your quotes onto a new Miro board as sticky notes, grouped by section and theme. Connect once — we never touch your existing boards.
Link — both open the same docs URL via NSWorkspace.Uploads all quotes in this project to Miro — hidden quotes are excluded.
<select> for a binary choice; name field shows a placeholder; Disconnect sits inline in the subtitle.88 of 142 stickies placed
boardReady string).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".checkmark.circle, green, regular weight (our MessageKind convention).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”:
| Container | HIG says | Fit 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. |
| Popover | Transient, 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. |
ASWebAuthenticationSession, but no UI for it now.
One outbound help link — miro.howToGetToken. Principle: the help content is single-sourced
in the docs site; every surface just opens that URL.
<a href>. In the desktop WKWebView the nav policy (security rule 1) routes the off-host URL to NSWorkspace.shared.open() — already opens in the system browser. (But it’s unstyled — worth fixing regardless of platform.)NSWorkspace.shared.open(docsURL). Never copy the help text into Swift — only the link + destination./docs/how-to/miro-setup.html → /docs/send-to-miro.html) — affects web and native equally.Shipped & working on desktop today (the "native entry + web panel" path):
ContentView.swift) + Quotes-menu item (MenuCommands.swift) → open the React panel (left column) in the WKWebView.store-miro-token → KeychainHelper.set (iCloud/synchronizable); overlayMiroToken env-injects on launch. Verified end-to-end 28 Jun (board built, token survives relaunch, syncs across Macs).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.
| Decision | Proposal (approve / change) |
|---|---|
| Data path ✓ APPROVED | Native 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 storage | Reuse 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. |
| Screens | Mirror 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 screen | Green 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.) |
| Container | Modern macOS sheet (centred rounded card, parent dimmed), one constant size, leading-aligned. Per HIG. |
| Colour control | Checkbox "Colour stickies by sentiment" (binary), not the web <select>. |
| Board name | Prefilled with the project name, editable (vs the web placeholder). |
| Glyphs / type | Our MessageKind set (success = outline checkmark.circle, green) · SF Pro · native buttons. |
| Help link | "?" Help button or inline Link → NSWorkspace.open(/docs/send-to-miro.html). |
| Progress ✓ APPROVED | Modal 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 Miro | Label 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). |
| Anonymise | Carry the export popover's global Anonymise toggle into the export request. (Backend honour of anonymise for Miro is a separate small task.) |
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).