Repository navigation
Description
Summary
On a first launch core shows its first-launch setup wizard (components/onboarding/onboarding-wizard.svelte, mounted by components/layout/app-shell.svelte). An extension that opens a dialog from the app-shell-modal chrome slot at the same moment (one extension app does: its sign-in prompt opens on the daemon's first signed-out report, seconds after launch) makes the wizard unusable:
- core's dialog (
$lib/components/ui/dialog, bits-ui 1.8Dialog, which the host API exports asDialogfrom@nodespace/extension-api/ui) is modal: while it is open its scroll lock setspointer-events: noneon<body>and its focus scope holds focus inside the dialog; - the wizard is a hand-rolled overlay (
.onboarding-backdrop,position: fixed,z-index: 200) that sets nopointer-eventsof its own, so it inheritsnonefrom the body and takes no click; - the wizard sits inside
ThemeProvider(.theme-provider { position: relative; z-index: 0 }, its own stacking context), while the dialog is portalled to<body>atz-50, so the dialog and its overlay render on top of the wizard.
Measured in Chromium with core's real ThemeProvider and OnboardingWizard at the release pin and the extension's dialog open:
getComputedStyle(document.body).pointerEvents=none;document.elementFromPointat the centre of the wizard's Next (and of its close button) = the dialog's overlay ([data-slot=dialog-overlay]);- a click on Next lands on the overlay, which bits-ui treats as an outside click: it closes the extension's dialog (as a dismissal) and the wizard does not advance; only a second click reaches the wizard;
- Escape goes to the dialog (its focus scope holds focus), not to the wizard.
The same happens in the other order (the extension's dialog first, the wizard opened over it), and with any other core modal shown while an extension dialog is open.
A second, core-only finding: the wizard does not hold keyboard focus
In core's real built app (Chromium, core's frontend at the release pin built with the Sync extension, none of whose dialogs is open, and a Tauri IPC mock answering core's commands as on a fresh install), document.activeElement while the wizard is open is the journal's textarea.node__content behind it, measured at 0, 0.3, 1 and 2 s after the wizard appears. The wizard's focusTrap focuses its first control on mount, then the daily-journal editor's autofocus takes focus back. Consequences:
- Escape does nothing: the trap's key handler sits on the wizard element, and focus is outside it.
- Tab, and typing, go to the journal behind the modal (text lands in today's note).
- The first real install test shows the same thing: the screenshot of the wizard has the text caret blinking in the empty journal node behind it.
Suggested fix: suspend the editor's autofocus while the onboarding wizard (or any modal) is open, or have focusTrap take focus back on focusin outside the trap, as bits-ui's focus scope does.
Workaround in the extension (shipped there)
The extension's chrome dialogs now take a turn: they open only while no other aria-modal="true" element is on screen, step aside (closing without recording an answer) when one appears, and come back when it is gone. Core gives extensions no signal for its own modals, so this watches the DOM with a MutationObserver while a Sync dialog is waiting or shown.
Suggested core fixes
- Make the wizard robust to a body pointer lock:
pointer-events: autoon.onboarding-backdrop(bits-ui sets the same on its own overlay and content for exactly this reason). The other hand-rolled modals with the same shape (delete-confirmation-modal.svelte,subtree-access-denied-modal.svelte) have the same exposure. - Give extensions a signal: expose "a core modal is open" (or "onboarding is open") through
@nodespace/extension-api, for example a reactiveshell.modalOpengetter or awhen()-usable predicate, so achromecontribution can wait for core's modals without watching the DOM. - Optionally, document in the extension points guide that an
app-shell-modalcontribution shares the screen with core's own modals (the first-launch wizard is not in that slot) and should not open while one is shown.
Reproduction
The extension's browser harness mounts core's real ThemeProvider and OnboardingWizard with the Sync chrome (scripts/ui-browser/wizard-shell.svelte, page ?shell&wizard, bun run test:ui-browser, describe block "next to core's first-launch wizard"). Reverting the extension-side turn-taking makes those journeys fail at the wizard's Next.
Activity
mstomar125 commentedon Oct 20, 2026
Planned for core v0.3.9.
mstomar125 commentedon Oct 20, 2026
Partly fixed by #3855 (merged): the wizard now holds keyboard focus and the hand-rolled modals tolerate a body pointer lock. Still open here: the extension dialog stacking above the wizard (the Sync extension works around it by taking turns), the extension-API modal signal (fix 2) and the docs note (fix 3).
mstomar125 commentedon Oct 22, 2026
Picking up the remaining parts: the extension-API signal for core modals (fix 2) and the extension-points docs note (fix 3).
Expose core-modal-open signal to extensions as shell.modalOpen
Expose core-modal-open signal to extensions as shell.modalOpen (#3865)
mstomar125 commentedon Oct 22, 2026
Done: #3865 (merged, API addition acked by the architect) adds shell.modalOpen to @nodespace/extension-api; core's wizard, delete confirmation and access-denied dialog report through it. Guide text is in nodespace-docs PR 132 for review. The Sync extension can now replace its DOM watcher with the signal. bits-ui dialogs do not register yet (they are user-initiated, not startup).