refactor(staged): migrate UI to shadcn-svelte + Tailwind v4#765
Conversation
Land the shadcn-svelte spike: adopt Tailwind v4, bridge design tokens to the Staged theme, and migrate bespoke chrome to shadcn-svelte primitives. - Add shadcn-svelte UI components (Button, Input, Select, Checkbox, Label, Badge, Dialog, AlertDialog, DropdownMenu, Popover, Alert, Tooltip, Context Menu, etc.) - Replace native inputs, bespoke menus, modals, banners, and theme selector with shadcn equivalents; delete the bespoke chrome they replace - Migrate toasts/alerts to svelte-sonner - Sweep Button and Tooltip usage across settings, branches, projects, sidebar, timeline, sessions, diff, notes, and actions - Consolidate icon imports onto @lucide/svelte and drop lucide-svelte Signed-off-by: Matt Toohey <contact@matttoohey.com>
The app chrome and diff viewer shared a single theme axis: whatever Shiki syntax theme the user picked drove both the diff highlighting AND every chrome CSS variable via createAdaptiveTheme(). Split this into two independent axes. App chrome — fixed light/dark mode: - Add a persisted `mode: light | dark | system` preference (store key `app-mode`, default `system`). `system` resolves via `prefers-color-scheme` and re-applies on its `change` event. - Add fixed DARK_BASE (laserwave-derived, matching the prior :root defaults) and LIGHT_BASE (github-light-derived) color sets. - applyChromeTheme() now feeds those fixed bases into createAdaptiveTheme() instead of getTheme(), so chrome no longer derives from the diff theme. --theme-is-dark (read by the darkMode store) reflects the resolved mode. Diff viewer — keeps its full set of selectable themes, scoped to the diff: - Rename the `syntaxTheme` preference to `diffTheme` (store key `diff-theme`, migrating from the legacy `syntax-theme` key). The full SYNTAX_THEMES list and setSyntaxTheme() are retained. - selectDiffTheme() no longer writes app chrome variables. It only re-highlights the diff (via a bumped version prop) and recomputes a diff-scoped var map from a second createAdaptiveTheme() run. - DiffModal applies that var map onto the diff-viewer container element so the diff area fully re-themes (bg/text/tints) while surrounding chrome stays on the fixed mode, and passes the version as syntaxThemeVersion to trigger re-highlighting. Settings: split the single "Theme" control into an "Appearance" toggle group (Light / Dark / System) bound to `mode` and a "Diff theme" swatch picker bound to `diffTheme`. Deviation from the note's minimal sketch: because diff lines consume the full chrome var set (--bg-primary, --text-primary, …), the diff container receives the entire adaptive var map — not just the --diff-* subset — so a dark diff theme renders correctly on light chrome and vice versa (option i, true re-theming). Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn-svelte migration translated the old hand-set footer button radii (6px compact / 8px enlarged) into token-backed radius utilities driven by a `--radius: 4px` base, silently roughly halving them (md=3.2px, lg=4px). Since the shared Button derives its corner radius entirely from the global `--radius` token, bump that base to 8px so the derived scale (md≈6.4px, lg=8px) restores parity with the pre-migration values. This uniformly softens all shadcn surfaces, which matches the intent. Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn migration's LIGHT_BASE chrome documents itself as derived from the github-light palette, and its bg/fg/comment values do match plain `github-light` (#ffffff / #24292e). But its gitColors were sourced from a different theme — `github-light-default` — whose accents are darker and more muted, darkening the added green from #28a745 to #1a7f37 (and shifting deleted/modified off the plain github-light values, including modified from blue #2188ff to brown #9a6700). Align gitColors with the github-light theme the base actually derives from so the light-mode added/commit greens (and deleted/modified accents) match the pre-migration appearance. Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shared shadcn Button outline variant baked in shadow-xs, which rendered an unwanted drop shadow on RHS footer buttons (e.g. branch cards). Remove shadow-xs from the outline variant so these buttons sit flat against their surfaces. Signed-off-by: Matt Toohey <contact@matttoohey.com>
Light-mode dialogs, popovers, dropdown/context menus, select content, and shadcn cards rendered on a dirty #ebebeb gray instead of a clean panel. createAdaptiveTheme() in src/lib/theme.ts computes floating surfaces with elevate(0.08), where elevate(amount) = adjust(primaryBg, dir * amount) and dir = isDark ? 1 : -1. The algorithm is dark-oriented: it lightens surfaces to make them read as lifted. In light mode dir = -1, so the full 0.08 step darkened floating surfaces all the way to #ebebeb against the white page — the shadcn primitives now read --bg-elevated for these surfaces, so the darkening became visible as a regression. Gate the light branch on isDark and halve the step: dark themes keep elevate(0.08) (lighten), light themes use adjust(primaryBg, -0.04) for a subtle ~#f5f5f5 off-white surface that reads as a distinct, lifted panel without going gray. The ring-1 ring-foreground/10 + shadow-md the shadcn primitives already apply carry the rest of the elevation. The hover line is unchanged — a slight darken on hover is correct. Verified with `pnpm check` (svelte-check + tsc): 0 errors, 0 warnings. Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn-svelte migration changed SessionModal from a conditionally
mounted component ({#if openSessionId}) into a persistently mounted one
controlled by an `open` prop (the shadcn Dialog pattern). Its initial
data load lived in onMount, which now fires exactly once when the parent
card mounts — at which point sessionId is still the empty-string
fallback (sessionId={openSessionId ?? ''}). getSession('') returns null,
so loadSession() sets error = 'Session not found', and because onMount
never runs again the modal is stuck on that error for every subsequent
open.
Move the session load out of onMount into a reactive $effect keyed on
`open` + `sessionId`, so the modal (re)loads whenever it is opened or the
target session changes, and stops polling when it closes. onMount now
only registers the search shortcut target. This restores correct
behaviour for the always-mounted callers (BranchCard, ProjectSection)
and is a no-op for the keyed {#each} caller (SessionLauncher), which
already remounts per session id.
Verified with `pnpm check` (svelte-check + tsc): 0 errors, 0 warnings.
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…face The chat messages area (.modal-content) in SessionModal inherited --bg-primary while the composer/input area (.input-area) paints --bg-chrome, leaving the two regions visually identical apart from the input's top border. Set the messages-area background to a 50/50 color-mix of the two endpoints so it reads as a distinct surface that sits halfway toward the composer: background: color-mix(in srgb, var(--bg-primary) 50%, var(--bg-chrome) 50%); Using color-mix over the existing theme variables keeps the blend adaptive across light/dark modes rather than hardcoding a hex. The composer's --bg-chrome background is left untouched. Signed-off-by: Matt Toohey <contact@matttoohey.com>
… surface The note content area (.modal-content in NoteModal.svelte) set no background of its own, so it inherited the shadcn Dialog.Content surface (bg-popover -> --bg-elevated). That left it visually identical to the dialog's surrounding chrome. Set the note content background to a 50/50 color-mix between --bg-elevated (its current inherited surface) and --bg-chrome (the same endpoint the chat messages area blends toward), so it reads as a distinct surface that sits halfway toward the chrome: background: color-mix(in srgb, var(--bg-elevated) 50%, var(--bg-chrome) 50%); Using color-mix over the theme variables keeps the blend adaptive across light/dark modes, mirroring the chat dialog change (7b84b25). The dialog header, footer/next-steps action bar, and input fields are left untouched. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn dialog and alert-dialog overlay primitives hardcoded a bg-black/10 backdrop, ignoring the mode-aware --shadow-overlay token. In light mode this rendered a near-white scrim (only 10% black over the white page) instead of the pre-migration overlay. Replace bg-black/10 with the token-backed bg-[var(--shadow-overlay)] in both overlay primitives. --shadow-overlay already encodes the full rgba with alpha (0.4 light / 0.6 dark), is live on the document root via applyChromeTheme(), and has a static fallback in app.css, so the arbitrary-value utility applies it directly with no opacity modifier. This restores the pre-migration scrim in light mode while leaving dark mode essentially unchanged. The supports-backdrop-filter:backdrop-blur-xs class and the theme.ts token are untouched. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Opening the chat session (SessionModal) or note detail (NoteModal)
dialog auto-popped the tooltip on the first header button.
When a bits-ui Dialog opens it auto-focuses the first focusable
element inside the content — here that is the first header
Tooltip.Trigger button. bits-ui tooltips open on focus (not just
hover), so the focused trigger's tooltip appeared immediately and
unprompted every time the dialog opened.
Pass onOpenAutoFocus={(e) => e.preventDefault()} to both
Dialog.Content elements (the prop is spread through to
DialogPrimitive.Content) so the dialog no longer forces focus onto
the first header button on open, leaving its tooltip closed until
the user actually hovers or focuses it. Esc-to-close and focus
trapping (driven by bits-ui's document-level handlers) are
unaffected.
Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The prior color-mix (9ba88cb) blended --bg-elevated with --bg-chrome, but those collapse to essentially the same color in light mode (~#f5f5f5), so the 50/50 mix equaled the original surface and the tint was invisible; the endpoints only diverged in dark mode. Switch the second endpoint to --bg-primary, which differs from --bg-elevated in both modes, so the content surface is now perceptibly distinct from the dialog chrome in light (~#fafafa) and dark (~#2f2b36). Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Signed-off-by: Matt Toohey <contact@matttoohey.com>
The base dialog-title.svelte applied leading-none (line-height: 1), so the title's line box equaled the font's em size exactly and descenders (g, y, p, q, j) hung below it. On its own that's fine, but the SessionModal, NoteModal, ActionOutputModal, and ImageViewerModal titles add overflow-hidden for ellipsis truncation, which clipped everything below the line box — including those descenders. Change the base title's leading-none to leading-tight (line-height: 1.25) so descenders sit inside the line box and survive overflow-hidden. This fixes all four truncating titles uniformly and is a negligible layout shift for the non-truncating dialog titles (headers center their content via flex). Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Signed-off-by: Matt Toohey <contact@matttoohey.com>
The main app background and project detail background both paint --bg-chrome, which createAdaptiveTheme() derives by darkening the base background via calculateChromeColors(). On a near-white light base (luminance 1.0) the logFloor step darkened chrome to ~0.916 luminance, a noticeably gray surface, because darkening away from white reads heavier than the equivalent lightening does on a dark base. Add a LIGHT_CHROME_CONTRAST_SCALE (0.6) applied to the chrome luminance difference for light themes (bgLum >= 0.5), so the derived chrome sits closer to the page background — a lighter gray. Deepest stays at 2x the (now reduced) diff, so it lightens proportionally and the chrome/deepest relationship is preserved. Dark themes are unaffected (scale = 1). Because this changes the derivation in theme.ts rather than any single component, every surface that reads --bg-chrome lightens uniformly. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The previous LIGHT_CHROME_CONTRAST_SCALE of 0.6 took chrome from ~0.916 luminance to ~0.949 — still a noticeable gray against the white page. Halve the scale again to 0.3 (chrome luminance ~0.975) so the main app and project chrome read as a much lighter, off-white surface that sits clearly closer to the page background. Deepest still scales proportionally (2x the reduced diff), and dark themes remain unaffected. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The 0.3 scale overshot — chrome went too pale. Pull it most of the way back to the original (scale = 1.0) while keeping a small lightening, by setting LIGHT_CHROME_CONTRAST_SCALE = 0.85. The darker chrome / deepest surfaces in the light theme are now nearly as dark as pre-change but sit a touch lighter against the page. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The repo search dropdown (Add Repo / New Project) rendered far down and to the right of the modal instead of attached to the repo field. RepoSearchInput positioned the dropdown with position:fixed and viewport coords from getBoundingClientRect(). The shadcn Dialog.Content applies -translate-x-1/2 -translate-y-1/2, and a CSS transform on an ancestor makes that ancestor the containing block for position:fixed descendants. So inside the dialog the dropdown's viewport-space top/left were re-interpreted relative to the centered, translated dialog box — placing it far from the input. Drop the JS-driven fixed positioning entirely. Make .repo-search-wrapper position:relative and anchor .repo-dropdown with position:absolute; top:calc(100% + 4px); left:0; right:0 so the dropdown naturally sits flush under the search field and matches its width, independent of any transformed ancestor. Removes updateDropdownPosition, dropdownStyle, and the window resize handler; as a side benefit, the dropdown now scrolls with the field instead of detaching. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn-svelte migration replaced the bespoke FormToggle (which rendered full-width Local/Remote option cards) with ToggleGroup.Root + ToggleGroup.Items. The form passed `class="flex flex-col gap-2"` on the root, but the base ToggleGroup.Root class already bakes in `w-fit flex-row items-center` — the explicit `flex flex-col` didn't override the cross-axis `items-center` (so items no longer stretched), and `w-fit` shrunk the container to its content's intrinsic width. The two option cards collapsed to the icon+label width instead of spanning the dialog. Switch the root to the proper API: set `orientation="vertical"` to activate the base's `data-vertical:flex-col data-vertical:items-stretch`, and pass `w-full` to override the baked-in `w-fit`. The items already have `w-full` and stretch naturally now. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Signed-off-by: Matt Toohey <contact@matttoohey.com>
Drop the 8px gap between the Local and Remote option cards in the New Project form so they sit flush against each other. Signed-off-by: Matt Toohey <contact@matttoohey.com>
The Name and Repository fields (and the Subpath / PR-or-Branch fields revealed after a repo is picked) rendered on the dialog's gray surface instead of white in light mode. The shadcn-svelte Input is bg-transparent (only dark:bg-input/30 paints a background), and RepoSearchInput's .search-input wrapper set background: transparent. So in the New Project dialog all these fields showed through to Dialog.Content's surface — --popover → --bg-elevated (~#f5f5f5 off-white) — rather than reading as distinct white fields. Paint the fields with the primary background: - Add bg-background (--background → --bg-primary, white in light mode) to the shadcn Input class on the Name, PR-or-Branch placeholder, Subpath, and BranchPicker inputs. dark:bg-input/30 still wins under .dark, so dark mode is unchanged. - Change RepoSearchInput's .search-input wrapper from transparent to var(--bg-primary) so the repo field matches. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Signed-off-by: Matt Toohey <contact@matttoohey.com>
The Local/Remote location toggles in the New Project form set no background for the unselected (data-[state=off]) state, so the option cards showed through to the dialog's gray Dialog.Content surface (--popover -> --bg-elevated) instead of the page surface. Only the selected card painted a background (data-[state=on]:bg-foreground). Add bg-background to the item class (and switch the hover from bg-transparent to bg-background) so the unselected card reads as white in light mode and the dark base surface in dark mode, matching the sibling form fields. The selected state's data-[state=on]:bg-foreground still overrides, so the active card stays black (light) / white (dark). Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Signed-off-by: Matt Toohey <contact@matttoohey.com>
shadcn's Button (and other migrated primitives) size text with Tailwind's rem-based text-* utilities, which the migration never re-anchored to the app's 13px root. Since 1rem = 13px here, text-sm (0.875rem) rendered at ~11.4px and text-xs (0.75rem) at ~9.75px — smaller than the pre-shadcn UI, which sized text off the Staged --size-* tokens (e.g. FormButton's var(--size-sm) ≈ 12px). The hand-added text-xs overrides on the branch-card Diff/PR buttons and the note "Start note"/"Start commit" buttons compounded this to ~19% small. Option B (systemic): map the Tailwind text-* namespace to the Staged --size-* tokens in the @theme inline block — text-xs→--size-xs, text-sm→--size-sm, text-base→--size-md, text-lg→--size-lg, text-xl→--size-xl. This realigns every migrated text-* utility to the old px scale in one place, fixing the root cause rather than patching individual buttons. Line-heights keep Tailwind's paired defaults. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…light to blue The generic app accent (--ui-accent -> shadcn --primary) was sourced from the git "added" color (gitColors.added), so primary buttons, focus rings, active filter chips, running/cloud indicators, and status pills all rendered in the same green that marks added files and commit dots. The two roles — git-added semantic vs. generic UI highlight — shared one color and couldn't be recolored independently. Introduce a dedicated accentPrimary in createAdaptiveTheme(), sourced from fallbackBlue (#58a6ff dark / #0969da light) so it stays blue in both modes — unlike accentBlue (gitColors.modified), which is gold/orange in dark mode and thus unsuitable as the accent. Point ui.accent / ui.accentHover at accentPrimary instead of accentGreen. status.added, timeline.commitColor/commitBg, and diff.addedBg are left on accentGreen, so git semantics stay correct (added = green, commits = green dots) while every --ui-accent consumer flips to blue. --primary-foreground (--bg-deepest) tracks the resolved mode and keeps adequate contrast on both the light- and dark-mode blue. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The decoupled accent used fallbackBlue (#0969da in light mode), which read noticeably darker and heavier than the #28a745 green accent it replaced. Give accentPrimary its own blue pair instead of reusing fallbackBlue: light mode is now the brighter #2188ff, sitting at roughly the same luminance/ energy as the old green, while dark mode keeps #58a6ff (already close to the old #3fb950). fallbackBlue is left untouched so accentBlue's fallback path and other consumers are unaffected. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The branch-card footer holds two button groups in one align-items:center row: the left "New note / New commit / New code review" add buttons (in BranchTimeline) and the right Diff / PR buttons (in BranchCard's footerActions snippet + BranchCardPrButton). Both groups already render at text-xs, so their text sizes matched. But their heights did not: the RHS buttons use the shared Button's size="sm" (a fixed h-8 = 32px), while the LHS add buttons set h-auto + py-1, making their height content-driven (~26-27px from the text-xs line box plus 8px vertical padding and the 1px dashed border). Side by side in the centered footer row, the LHS dashed buttons rendered visibly shorter than the Diff/PR buttons. Replace the non-enlarged LHS class's `py-1 h-auto` with `h-8` on all three add buttons so they pin to the same 32px height as the RHS size="sm" buttons. px-2.5 and rounded-md already align with sm's padding/radius (rounded-md => --radius-md = 6px = min(--radius-md, 10px)), and the enlarged variant is untouched. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 62cf25ab3a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| <ActionOutputModal | ||
| open={actionOutputModal !== null} | ||
| executionId={actionOutputModal?.executionId ?? ''} |
There was a problem hiding this comment.
Mount the action output modal only when it has an execution
When no action output is open, this still mounts ActionOutputModal with executionId=''. That component's reactive effect does not check open, so every branch card immediately calls getActionOutputBuffer('') and installs output/status listeners for a closed modal; on pages with many branches this creates unnecessary backend calls/listeners and can surface spurious load errors. Please keep the previous conditional mount or make the modal's effect bail out unless open && executionId.
Useful? React with 👍 / 👎.
| <SessionModal | ||
| open={sessionMgr.openSessionId !== null} | ||
| sessionId={sessionMgr.openSessionId ?? ''} |
There was a problem hiding this comment.
Don't let closed session modals capture search shortcuts
This unconditionally mounts a SessionModal for every branch even when openSessionId is null. SessionModal registers a global search shortcut target in onMount, and runSearchShortcut() always dispatches to the last registered target, so a hidden per-branch modal can consume Cmd/Ctrl+F and search-next/previous even when no session modal is open (or when a different modal is visible). Please either mount it only while open, as before, or register/unregister the search target based on the open prop.
Useful? React with 👍 / 👎.
…cons The branch-card footer buttons rendered their resting label (and, for the LHS add buttons, their icons) in de-emphasized colors, so they read dimmer than the sibling Diff button — which uses the shared outline Button's default full-contrast foreground. - PR button (BranchCardPrButton): drop the baked-in text-muted-foreground so its label inherits text-foreground like the Diff button. The state-specific overrides (destructive on error, status-added icon on MERGED) are untouched. - LHS add buttons (New note / New commit / New code review in BranchTimeline): drop the resting text-[var(--text-muted)] from both the enlarged and compact class branches, and drop the always-on [&_svg]:!text-[var(--note|commit|review-color)] icon tint so the icons follow the label's foreground color at rest instead of painting their semantic accent. The hover states (border/bg/text shifting to the note/commit/review accent) and the [&_svg] transitions are kept, so the buttons still light up to their accent on hover — they just sit at full contrast at rest. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The shadcn-svelte migration left ActionOutputModal mounted persistently —
BranchCardActionsBar renders it with `open={actionOutputModal !== null}`
and `executionId={actionOutputModal?.executionId ?? ''}` rather than
conditionally mounting it. But the modal's load/listen $effect keyed off
executionId alone, so every branch card's closed instance immediately
called getActionOutputBuffer('') and installed output/status listeners for
a closed modal. On pages with many branches that is a burst of needless
backend calls and listeners, and the empty-id fetch surfaces a spurious
"not found" load error.
Guard the effect: bail out (running cleanup) unless `open && executionId`,
so a closed or execution-less instance does no work. Reading `open` also
subscribes the effect to it, so the load/listen path runs exactly when the
modal opens onto a real execution and tears down when it closes.
Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… open SessionModal is mounted persistently for every branch card (the `open` prop toggles visibility, per the shadcn Dialog pattern), and it registered its global search-shortcut target in onMount. Since runSearchShortcut() always dispatches to the last-registered target, every closed, off-screen per-branch modal also held a target — so a hidden modal could capture Cmd/Ctrl+F and search-next/previous even when no session modal was open, or when a different modal was visible. Move the registration out of onMount into a $effect gated on `open`: the target is registered when the modal opens and torn down (via the effect's cleanup) when it closes, so only a visible modal can claim the search shortcut. onMount is no longer used and is dropped from the import; onDestroy still unregisters as a final safety. Verified with pnpm check (svelte-check + tsc): 0 errors, 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Summary
Migrates the Staged app UI from its bespoke Svelte components to shadcn-svelte on Tailwind v4.
src/lib/components/ui/*): alert, alert-dialog, badge, button, checkbox, context-menu, dialog, dropdown-menu, input, label, popover, select, separator, sonner, toggle, toggle-group, tooltip.AlertCard,ConfirmDialog,DropdownMenu,FormButton,FormInput,FormToggle,ToastHost,alerts,backdropDismiss,menu/*).app.csswith the design tokens and wires upcomponents.json, Tailwind v4, and the updated Vite/TS config.Follow-up fixes
A series of light/dark-mode polish commits on top of the migration:
Test plan
staged-ci,differ-ci, and the Rust checks pass on push.