Native macOS ↔ web parity checklist
Tracking issue: #4331 Native web-parity dogfood gate.
This checklist compares apps/calendar-web (reference UI) with apps/calendar-macos (native Swift). Status keys:
| Status | Meaning |
|---|---|
| ✅ | Matches web behavior and styling closely enough for dogfood |
| 🟡 | Partial: core behavior works, visual or edge-case gaps remain |
| ❌ | Missing or materially different |
| 🔧 | Shared contract exists; fix is mostly wiring or SwiftUI/AppKit polish |
Sources of truth
- Semantic colors:
apps/calendar-web/src/index.css→bun cli desktop:export→CompassKit/Generated/ThemeTokens.swift(22 roles; guarded bytheme-css.test.tsanddesktop:export --check). - Shortcuts registry:
packages/core/src/shortcuts/shortcuts.registry.ts,KEYMAP,APP_SHORTCUT_BINDINGS→CompassKit/Resources/shortcuts.json+ShortcutIds.swift. - Layout math / fixtures: exported JSON replayed in
CompassKitTests/CompassDataTests. - Manual acceptance: Desktop acceptance, Shortcuts.
1. Design tokens
1.1 Colors (themes, accent, event fills)
| Item | Web | macOS | Status | Notes |
|---|---|---|---|---|
| Light Beach / Dark Abyss palettes | [data-theme] in index.css | ThemeTokens.lightBeach / darkAbyss | ✅ | Generated from same CSS |
| SwiftUI accessor coverage | Tailwind color-* for all 22 roles | NativeWebTheme exposes all roles on ThemeTokenColor+SwiftUI.swift | ✅ | NativeWebTheme exposes all 22 generated palette roles, including raised surfaces, strong borders, accent variants, on-accent text, info and shadow |
| Panel modal backdrop | OverlayPanel / bg-background/85 + backdrop-blur-sm | PanelBackdropView: panelBackdropColor (background @ 85%) over a within-window blur | ✅ | Settings and welcome used the dark overlayBackdrop dim and read as solid gray; that dim stays for menus and confirm dialogs. Welcome centers vertically like web |
| Event default fill hex | theme.util.ts / deck | EventCardView.swift literals #454442, #82A0B2 | 🟡 | Values align; not read from tokens |
| Event focus ring color | --event-focus-color + outline | EventCardView CALayer border from fill-aware helper | 🟡 | Same intent, different paint model |
| Holiday / gradient accents | CSS gradients on chips | Mostly solid fills | 🟡 | Audit per calendar color id |
| Block Party slot colors | Web game CSS | BlockPartyColor.swift hardcoded RGB | 🟡 | Intentional game palette; not semantic tokens |
1.2 Typography
| Item | Web | macOS | Status | Notes |
|---|---|---|---|---|
| Font family | Rubik (index.css @theme) | Resources/Fonts/Rubik-Variable.ttf, .rubik() | ✅ | Fixed XcodeGen packaging: the previous resource declaration omitted every font from the built app, causing system fallback. Font files now appear in the built bundle; fresh local launch inspected. Broader typography and layout acceptance remains partial |
| Base size 14px | text-sm / body | RootView .font(.rubik(14)) | ✅ | |
| Scale (10–20px utilities) | Tailwind --font-size-* | Per-view hardcoded point sizes | 🟡 | No shared typography export yet |
| VT323 / Caveat (special) | Web where used | Bundled fonts | 🟡 | Spot-check Life / onboarding |
1.3 Spacing, radii, borders
| Item | Web | macOS | Status | Notes |
|---|---|---|---|---|
Main content pl-8 | 32px | contentLeadingInset 32 | ✅ | RootView.swift |
| Sidebar width / divider column | SIDEBAR_DIVIDER_WIDTH | sidebarDividerWidth 32 | ✅ | |
| Sidebar leading border full height | 1px border-border, no horizontal rules in body | Rectangle 1px on sidebar leading edge | ✅ | Tyler: avoid extra horizontal dividers inside sidebar |
--radius-default / rounded-xs | index.css @theme | Mixed 4/8/12 in SwiftUI | 🟡 | Palette panel 12; save bar should be rounded-xs (~4) |
| Grid hour lines | border alpha 15% / 12% dark | TimeGridStyleTests | ✅ | |
--border-strong | Strong dividers when needed | Rarely used in SwiftUI | 🟡 |
1.4 Shadows
| Item | Web | macOS | Status | Notes |
|---|---|---|---|---|
--shadow-default on elevated buttons | c-button-elevated | Limited native elevation | 🟡 | Save bar / primary buttons |
| Command palette panel shadow | Floating overlay | Token-colored shadow, 28pt radius and 16pt vertical offset | ✅ | CommandPaletteOverlay.swift; native paint parameters remain platform-specific |
1.5 Focus rings
| Item | Web | macOS | Status | Notes |
|---|---|---|---|---|
| Default interactive focus | c-focus-ring: 1px accent ring + 1px offset on surface-panel | Mixed: AppKit .exterior, custom strokes, NoticeFocusRing 2px no offset | 🟡 | Target: consistent accent border without fill color swap |
Notice focus (p) | Focus moves into notice control | Banner noticeFocusRing on billing/demo/update | 🟡 | Web scans [data-notice]; native NativeNotice enum is smaller |
| Fields | c-field ring on focus | Per-control 1px stroke (e.g. description editor) | 🟡 | |
| Disabled system focus ring | N/A | Quick Add focusRingType = .none | 🟡 | Matches intentional web-like chrome |
Token drift prevention: Prefer extending NativeWebTheme from generated ThemeTokens rather than new literals. Optional follow-up: export radius/typography scales from CSS the same way as colors.
1.6 macOS unified title bar (intentional platform difference)
| Item | Web | macOS | Status | Notes |
|---|---|---|---|---|
| Calendar nav + title + sidebar toggle | In-content header row (CalendarHeader, h-12) | Unified title bar beside traffic lights (MacTitleBarChromeMetrics) | ✅ | Compact 28pt band, 13pt system title and 14pt navigation icons. Local native regression passes, including title/nav alignment with the actual window close button |
| Parity screenshot grid region | Crop includes in-viewport header band | Regional grid crop starts at mainContentTopInset (44px); web crop skips 48px header | ✅ | .github/scripts/compose-desktop-parity-shots.py grid_region_crop() |
2. Surfaces
2.1 Week / day time grid
| Item | Status | Notes |
|---|---|---|
| Deck layout / card frames | ✅ | Shared fixtures, GridLayoutSnapshotTests |
| All-day row above timed scroll | ✅ | TimeGridView.renderAllDayColumns |
| Now line position | ✅ | Shared now math |
| Now line visual (gradient + 1px rim) | 🟡 | macOS: accent gradient + 1px background rim (#4611); web shimmer timing still differs |
Now-cue pulse on t (today) | 🟡 | Web 700ms label shimmer; macOS week header opacity pulse + day view switcher pulse |
| Click grid teaches shortcut | ✅ | Pointer hint overlay |
| Day labels | ✅ | Centered per column; number clamp(18, 2.7cqw, 20.8), weekday clamp(13, 2cqw, 16) |
| Day column header focus | 🟡 | AppKit exterior ring vs web c-focus-ring |
| Shift+C all-day tip strip | 🟡 | Sidebar jump line animates; full web tip stack still partial |
2.2 Event chips
| Item | Status | Notes |
|---|---|---|
| Timed / all-day card shell | ✅ | EventCardView, palette tests |
| Event jump chips (W1, SU2, ...) | ✅ | DayJumpKeys ports web assignDayJumpKeys: day prefix + per-day index on timed and all-day cards, narrowed to the typed day; digits after a day pick focus (400 ms wait for W1 vs W10); bare digits stay quick-time |
| Shift+M weekday column jump | ✅ | handleShiftDayJump; weekend Shift+S→U/A added; digit jump after a day pick wired; bare day letters in jump mode (F toggles off, E / C pass through, other letters swallowed like web); four digits typed within 800 ms after a day pick create on that column (Shift+T then 1230), like web's column time burst |
| Sidebar edit ring | 🟡 | Inset focus layer vs web outline |
| Unsettled outline | ✅ | Dashed text-muted/50 outline on sample events and organized events with awaiting or tentative guests |
2.3 Sidebar event form
| Item | Status | Notes |
|---|---|---|
| Replaces sidebar body while open | ✅ | EventFormView in RootView |
| Date field MM.DD.YY closed display | ✅ | EventFormCompactDateField shows 06.10.26 like web (compare run 39732173380) |
| Datepicker open Enter/Space/ArrowDown | ✅ | Closed field matches web EventFormDateField: ↓, Enter on the committed date, and Space on the shown date open the calendar; ↑ / Shift+↑ / Shift+↓ / Mod+↓ nudge the day (EventFormDateFieldKeys). Open calendar matches web react-datepicker: ← → move a day cursor, ↑ ↓ a week, Enter or Space picks, Esc closes only the calendar (EventFormDateKeysTests drives them). Like web, plain Return in a date field commits or opens instead of saving the form; ⌘ Enter still saves. Hints match web: a ↓ keycap inside the focused field and ↑ ↓ · Enter · Esc in the calendar header |
| Time pickers | 🟡 | The list opens centered on the selected time (or the quarter hour before it) like web. Still differs: macOS shows the list and the date picker as AppKit popovers that can open to the side of the field, while web drops them below it |
| Location / Meet / guests grouping | ✅ | #4607 single EventFormCard + description card (web #4599) |
| Color row keys and hints | ✅ | Like web EventColorPicker: trailing ⌘ 6 keycaps; once color has focus (Mod+6 or E then C) ← → move a focus ring, Space / Enter pick, pick keys 1-9 0 - = choose a swatch and show on each swatch (swatches in web EventColorSlotSchema order, lavender first), and the hint swaps to ← → · Enter. The demo capture also shows a guest row on macOS only: the demo fixture calendar can invite (UI tests need it), the real local calendar cannot |
| Save bar: accent fill, on-accent text, space-between, ⌘Enter hint | 🟡 | macOS save bar matches accent + Mod+Enter keycap (#4610); sidebar shows web save-draft tip (#4614) |
| Mod+digit field jump + hold-Mod chips | ✅ | ModHoldChipsOverlay, layout snapshots; chips sit at the anchor's top-right corner like web DigitHintChipOverlay (top + 2px, flush right) |
| Hold-Mod day header keycaps (⇧SU, ⇧M, ...) | ✅ | Week headers swap the weekday for WeekDayColumnHeaderControl keycaps while Mod is held; bare letters in event-jump mode |
| Hold-Mod sidebar chips (month picker, Up next, calendar list) | ✅ | Anchors merge with nested ones, so the sidebar no longer hides the month picker and Up next chips; the calendar list digit sits on the list, not the sidebar corner. |
| Per-account page-jump targets | ✅ | One Mod+digit target per connected account heading after Up next, like web buildCalendarPageJumpTargets; a collapsed heading expands on jump; one "calendars" slot with no accounts |
| Sticky form actions row (icons + shortcuts) | 🟡 | Header keyboard behavior and shortcut help verified; exact icon/spacing parity still needs visual comparison |
| Close unchanged / edited event | ✅ | 2026-10-23: only changed existing events prompt, like web. Unchanged edits, reverted edits, and create drafts close immediately. Four model regressions pass; native tests verify Close prompts instead of saving edits, Cancel preserves them, and Shift+Escape discards without saving |
| Header actions keyboard focus | ✅ | Mod+0 focuses the first toolbar button after the parent releases title focus, including repeated jumps. Tab / Shift+Tab move through actions, Return / Space activate them, and shortcut help names the keys. Native screenshot inspection at 720×630 confirms a visible Close focus outline; three focused native regressions and full strict verification pass |
| Form duplicate undo / redo | ✅ | Form duplication records its create in undo history. Model and native tests verify duplicate → undo → redo without modifying the original |
| Calendar / recurrence / RSVP shortcut focus | 🟡 | #4783: Actual focus, repeated jumps, keyboard activation and Tab/Shift+Tab traversal pass in both themes after #4870, #4873, #4895 and #4897. The attendee fixture proves scope-dialog callbacks without submitting a real response. Signed-in native RSVP acceptance remains open |
2026-10-22 popover and capture verification
Date and time pickers open below their fields in native user flows at both the normal window size and the canonical 1280×820 capture size. Date keyboard selection/Escape and time selection pass. Fresh exported images also show the popovers below their fields, correcting the earlier placement finding. Sidebar-only crops can cut off the portion of the calendar that extends over the grid.
Found a separate capture reliability defect with multiple installed Compass builds: generic debug URL opening can target another build, causing export timeouts. Capture helpers now activate the test target and route debug URLs to its running bundle path. The existing date/time capture checks assert placement and verify that export preserves the open picker and event form. Signed-in acceptance remains outstanding.
2.4 Mini-calendar (month picker)
| Item | Status | Notes |
|---|---|---|
Keyboard i, arrows, Enter | ✅ | |
| Mod+Alt+J / K / T month jump | ✅ | |
| Selected range / today styling | ✅ | Week mode bolds the selected week row with ink on today; 28.8pt circles; today follows the calendar clock; Go to this month circle hides on the current month |
| Weekday header 10px muted | 🟡 | Rubik 10 in SidebarMonthPicker.swift |
2.5 Up Next / Now panel
| Item | Status | Notes |
|---|---|---|
| Projection + join URL | ✅ | |
N / V shortcuts | ✅ | Registry + router |
| Shortcut hint on card (N/V keycaps) | 🟡 | Hover/focus keycap on native Up Next (#4614) |
| Bottom banner vs web notice stack | 🟡 | Fewer notice types on native |
2.6 Account list / calendars sidebar
| Item | Status | Notes |
|---|---|---|
| Calendar list + digit hide/show | ✅ | |
| Anonymous row | ✅ | |
| Sync status line + shortcut footer | ✅ |
2.7 Settings
| Item | Status | Notes |
|---|---|---|
| Accounts, default calendar, timezone, theme | ✅ | WP-29 |
| Launch at login, global Quick Add | ✅ | macOS-only (expected) |
| Visual match to web settings overlay | 🟡 | Web background tint and 8pt blur verified locally in both themes; section spacing and navigation focus still differ |
2026-10-22: Settings uses the existing background tint at 85% opacity with a 8pt blur on the content behind the panel. Both-theme native tests verify outside-click and Escape dismissal. Native window captures show the blur; view-level parity exports render background compositing differently, so they do not establish full visual equality.
2.8 Onboarding / welcome
| Item | Status | Notes |
|---|---|---|
| Welcome modal steps | ✅ | Both-theme click and keyboard regression passes locally and in CI (#4767) |
| Connect / first-event prompts | 🟡 | |
| Pixel pirate / illustration colors | 🟡 | Hardcoded in PixelPirateView.swift |
2.9 Notices / toasts / banners
| Item | Status | Notes |
|---|---|---|
| Billing past due | ✅ | |
| Demo events, update ready | ✅ | |
| Meeting nudge, connect prompts, first-event | ✅ | Sidebar meeting-page nudge (/), local-mode trial card (S, Esc), first-event card completes on first create |
| Connection + timezone banners | ✅ | Reconnect / Retry / Refresh / Learn more (G); timezone mismatch Switch / Keep; Auto zone follows the Mac each minute |
| Status toasts | 🟡 | StatusToastOverlay vs web toast stack |
Focus latest notice p | ✅ | Connection banner, sample events, meeting nudge, trial card, update, past due |
2.10 Command palette
| Item | Status | Notes |
|---|---|---|
| Placeholder copy | ✅ | Same string |
| Go-to-date from query | ✅ | Shared parsing fixtures |
| Navigation / create / events / recent | ✅ | |
| Fuzzy search + highlight | ✅ | CommandPaletteSearchLogic mirrors filterSections; muted label with bold matches |
| Row icons (Phosphor) | 🟡 | SF Symbol row icons on navigation/more rows (#4613); not Phosphor |
Keycaps (ShortcutKeys) | ✅ | One ShortcutHintChip per key, like web ShortcutKeys |
| Selected row ring inset | ✅ | Accent inset ring + lighter fill (web ring-inset) |
| Missing rows vs web | ✅ | Web section order and rows (Common Actions, Appearance, Settings, More); disabled rows dim and skip; notifications row is web-only (macOS uses system settings) |
| Footer hint | ✅ | Keycap chips for navigate / select / close |
2.11 Command palette (desktop quick-add variant)
| Item | Status | Notes |
|---|---|---|
| Separate placeholder when creating from hotkey | ✅ | Both native Quick Add field implementations match web DESKTOP_QUICK_ADD_PALETTE_PLACEHOLDER: “Add an event, or type a time like 1130” |
2.12 Life view
| Item | Status | Notes |
|---|---|---|
| Grid + sidebar | 🟡 | Founder visual QA noted on #4331 |
| Intro panel theme colors | ✅ | Surface fill and border match web Life intro overrides; primary text and shadows use semantic tokens. Both-theme native screenshots and Enter dismissal verified locally (#4764) |
2.13 Window chrome
| Item | Status | Notes |
|---|---|---|
| Traffic lights + header | ✅ | WindowChromeLayoutTests |
| View switcher keycaps | ✅ | ViewSwitcherView |
3. Keyboard parity
Legend: Mod = ⌘ on macOS. Web bindings from APP_SHORTCUT_BINDINGS / KEYMAP; macOS from shortcuts.json unless noted.
3.1 Navigate
| ID | Binding | Web | macOS | Status |
|---|---|---|---|---|
nav-up-next | N | ✅ | ✅ | ✅ |
nav-join-meeting | V | ✅ | ✅ | ✅ |
nav-previous | J | ✅ | ✅ | ✅ |
nav-next | K | ✅ | ✅ | ✅ |
nav-shift-left | Mod+Shift+← | ✅ | ✅ | ✅ |
nav-shift-right | Mod+Shift+→ | ✅ | ✅ | ✅ |
nav-month-prev | Mod+Alt+J | ✅ | ✅ | ✅ |
nav-month-next | Mod+Alt+K | ✅ | ✅ | ✅ |
nav-month-current | Mod+Alt+T | ✅ | ✅ | ✅ |
nav-picker-step | Arrow keys in picker | ✅ | ✅ | 🟡 telemetry |
nav-today | T (keyup) | ✅ | ✅ | ✅ |
nav-go-to-date | Mod+G | ✅ | ✅ | ✅ |
nav-scroll-* | PgUp/PgDn, Mod+↑/↓ | ✅ | ✅ | ✅ |
nav-day-view | D | ✅ | ✅ | ✅ |
nav-week-view | W | ✅ | ✅ | ✅ |
nav-life-view | L | ✅ | ✅ | ✅ |
| Life prev/next/current | ←/→, H | ✅ | ✅ | ✅ |
3.2 Create
| ID | Binding | Status | Notes |
|---|---|---|---|
create-timed | C (keyup) | ✅ | |
create-allday | Shift+C | ✅ | Tip strip parity 🟡 |
create-place-timed | Shift+arrows | ✅ | |
create-typed-time | digits in draft | ✅ | |
create-place-discard | Esc (secondary TZ) | ✅ |
3.3 Focus
| ID | Binding | Status | Notes |
|---|---|---|---|
focus-sidebar | I | ✅ | Month picker |
focus-event | U | ✅ | focusFirstCalendarEvent() seeds nearest grid event |
focus-shift-hold | F (+ digits) | ✅ | Jump mode: W1 / SU2 chips, bare day letters, digits after a day pick, typed time on the picked column |
focus-week-day | Shift+M… | ✅ | Column select + first event; weekend S→U/A; day-prefix digit targeting and typed-time creation wired |
focus-notice | P | ✅ | Connection, sample events, meeting nudge, trial, update and past-due notices; see §2.9 |
focus-calendar-digit | 1–9 in list | ✅ | |
focus-page-jump | Mod+digit (hold hints) | ✅ | |
focus-find-event | Mod+K |