feat(ui,app): slice 07 modals + palette + spec alignment (#440)#460
Conversation
|
Warning Rate limit exceeded
To continue reviewing without waiting, purchase usage credits in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (22)
📝 WalkthroughWalkthroughThis PR introduces two new modal UI components (CommandPalette and Sheet), adds footer support to the Dialog component, migrates button styling from brand to error tokens, and updates DialogSelectFile to use CommandPalette. It also adds comprehensive stories and tests for the new components and features. ChangesCommandPalette Component
Sheet Component
Dialog Footer Support
Button Styling & Refinements
Sequence Diagram(s)sequenceDiagram
participant User
participant App as App Layer
participant Dialog as Dialog/Modal
participant Content as Content Slot
User->>App: Trigger Dialog/CommandPalette/Sheet
App->>Dialog: Open with state/props
Dialog->>Dialog: Render overlay + container
Dialog->>Content: Render children/content
alt Dialog with Footer
Dialog->>Content: Render body content
Dialog->>Dialog: Render footer slot
Content-->>Dialog: Footer action (Cancel/Save)
else CommandPalette
Dialog->>Content: Render search + list
Content-->>User: Display items
else Sheet
Dialog->>Content: Render side panel content
Content-->>Dialog: Header + body + footer
end
User->>Dialog: Escape / Click overlay
Dialog->>App: Close/onOpenChange
App->>Dialog: Unmount
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces new UI components, including a CommandPalette and a Sheet, while enhancing the existing Dialog component with a dedicated footer slot. It also includes comprehensive E2E and structural tests, updated styling tokens, and refactors several application components to adopt these new patterns. Feedback focuses on maintaining consistent padding across dialogs to align with the new footer standards, adding missing prop support for styling in the CommandPalette, and correcting the implementation of reduced motion preferences to ensure compatibility with component lifecycle events.
806d1b4 to
b483830
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/app/e2e/app/dialog-state-matrix.spec.ts`:
- Around line 41-50: The test is mistakenly capturing a non-serializable DOM
node into `focused` via `page.evaluate(() => document.activeElement)` and then
passing it as `_focused` into `dialog.evaluate`, which is always null and
unused; remove the dead cross-evaluate element tracking by eliminating `focused`
and the `_focused` parameter and instead rely directly on `dialog.evaluate((el)
=> el.contains(document.activeElement))` inside the tab loop (or use a single
`page.evaluate` that queries containment against the dialog element handle
inside the same execution context) so that `page.keyboard.press("Tab")` is
followed by a direct, correct containment assertion using `dialog.evaluate` or a
single page.evaluate call.
In `@packages/ui/src/components/command-palette.tsx`:
- Around line 4-6: The dialog content is rendered without an accessible label;
add an aria-label prop to the CommandPaletteProps (e.g., ariaLabel?: string) and
thread it through the CommandPalette component to pass into the underlying
Kobalte/Dialog.Content (or Dialog.Content) element so that role="dialog" has an
accessible label; also update any call sites like dialog-select-file.tsx to
provide a meaningful ariaLabel and keep existing behavior when not provided
(e.g., default to undefined or a sensible fallback).
In `@packages/ui/src/components/sheet.stories.tsx`:
- Line 1: Remove the file-wide "// `@ts-nocheck`" and convert the stories to use
typed Storybook metadata and story objects: import Meta and StoryObj from
"@storybook/react", import the Sheet component, set the default export as const
meta: Meta<typeof Sheet> = { ... } and export it as default, then define each
story as export const Example: StoryObj<typeof Sheet> = { args: { ... } } so
Storybook and TypeScript validate the Sheet props and catch API drift (ensure
any story args match Sheet's prop names/types).
In `@packages/ui/src/components/sheet.tsx`:
- Around line 32-37: The current Sheet component passes props.class as a key
inside classList (see Kobalte.Content usage), which breaks when props.class
contains spaces; change the JSX to forward class and classList separately: set a
static class={props.class} (or omit if undefined) and pass a merged classList
object from props.classList plus any reactive entries, ensuring the class
attribute appears before classList in the Kobalte.Content props; remove the
dynamic key expression `[props.class ?? ""]` entirely so class names with spaces
are not used as classList tokens.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: c186fc82-7dc0-4af9-bb75-c09be7d2f1d0
📒 Files selected for processing (17)
packages/app/e2e/app/dialog-state-matrix.spec.tspackages/app/src/components/dialog-delete-session.tsxpackages/app/src/components/dialog-edit-project.tsxpackages/app/src/components/dialog-select-file.tsxpackages/ui/src/components/command-palette.csspackages/ui/src/components/command-palette.stories.tsxpackages/ui/src/components/command-palette.tsxpackages/ui/src/components/dialog.csspackages/ui/src/components/dialog.stories.tsxpackages/ui/src/components/dialog.tsxpackages/ui/src/components/keybind.csspackages/ui/src/components/sheet.csspackages/ui/src/components/sheet.stories.tsxpackages/ui/src/components/sheet.tsxpackages/ui/src/styles/theme.csspackages/ui/test/components/dialog-footer.test.tspackages/ui/test/sheet.test.ts
…7, issue #440) - add footer?: JSXElement prop to DialogProps and render conditionally with Show - add [data-slot="dialog-footer"] CSS slot with flex row layout, border-top, and padding - replace hard-coded border-radius: 14px with var(--radius-lg) - replace light-dark() overlay background with var(--scrim-overlay) token - replace bare :focus-visible outline:none with proper focus ring using var(--shadow-xs-border-focus)
) - add WithFooter story showing cancel/confirm footer pattern - add DialogStateMatrix story covering all 6 states: default, with footer, without footer, large, x-large, fit - add dialog-footer.test.ts with 5 structural tests verifying footer prop interface, slot attribute, and conditional Show wrapper
Covers 4 Kobalte behavior requirements from issue #440: - aria-modal attribute on dialog - Escape key closes dialog - Overlay click closes dialog - focus-trap: Tab stays inside dialog Also verifies [data-component="command-palette"] data attribute is set after CommandPalette extraction in this slice.
DialogEditProject was orphaned: showEditProjectDialog had no callers and was not exported. Per updated DESIGN.md (issue #440 §"Modals & overlays"), form-style editing must use Sheet, not Dialog, so this component would need a full rewrite to fit the spec. Deleting now to keep slice 07 spec-aligned without dead code.
The dev branch removed --brand-danger / --brand-danger-hover, leaving button[data-variant="danger"] referencing undefined custom properties (no background, brand-orange focus ring). Map to --error / --error-text so destructive buttons render correctly. Active state also dropped the unrelated --surface-interactive-base (orange tint) in favour of --error-text. Required for slice 07 delete-session migration to secondary + danger button pair (DESIGN.md §"Modals & overlays").
Per updated DESIGN.md §"Modals & overlays": - Drop --scrim-overlay / --kbd-bg / --kbd-border tokens; inline values at consumers (spec: "no token, inline at use site"). - Dialog default width 640 -> 420; large/x-large size variants unchanged. - Dialog overlay scrim is single value rgba(26,22,19,0.32) — no dark variant per spec. - Dialog footer slot becomes MODE 1: sticky bottom + bg-cream + border-weaker top + 12/24 padding (for large/scrolling dialogs). MODE 2 small confirms render footer inline outside the slot. - Sheet width 480 -> 420 (4 sides). - Sheet slide-in keeps --duration-slow (240ms); slide-out switches to --duration-base (120ms) so dismiss feels snappy. - Sheet footer mirrors dialog footer MODE 1 styling. - Keybind chip inlines kbd colors with explicit dark-mode variants (data-color-scheme + prefers-color-scheme). - sheet.test.ts asserts inlined scrim color in dialog.css.
…440) Per DESIGN.md §"Modals & overlays", small destructive confirms use MODE 2: no border, no cream background, footer rendered inline inside the dialog body with `flex justify-end gap-2 px-6 pb-6`. Buttons pair secondary + danger (not ghost + primary). Migrates both copies of DialogDeleteSession to match dialog-rename-session (the MODE 2 reference): - packages/app/src/components/dialog-delete-session.tsx - packages/app/src/pages/session/message-timeline.tsx (inline copy) Width pinned to 420 via `w-full max-w-[420px] mx-auto` so this dialog keeps spec width even if Dialog primitive default changes again.
e149d7b to
744bbee
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
packages/app/e2e/app/dialog-state-matrix.spec.ts (1)
43-48:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove dead cross-context focus payload in the focus-trap loop.
Line 43 captures
document.activeElement, but Line 46 re-queries focus in the dialog context;_focusedis unused and adds misleading cross-evaluate noise.Suggested cleanup
for (let i = 0; i < 5; i++) { await page.keyboard.press("Tab") - const focused = await page.evaluate(() => document.activeElement) - // The dialog element must contain the focused element - const focusedInDialog = await dialog.evaluate( - (el, _focused) => el.contains(document.activeElement), - focused, - ) + const focusedInDialog = await dialog.evaluate((el) => el.contains(document.activeElement)) expect(focusedInDialog).toBe(true) }#!/bin/bash # Verify no dead cross-evaluate activeElement payload remains in this spec rg -n 'page\.evaluate\(\(\) => document\.activeElement\)|_focused' packages/app/e2e/app/dialog-state-matrix.spec.ts # Expected after fix: no matches🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/e2e/app/dialog-state-matrix.spec.ts` around lines 43 - 48, The test captures document.activeElement into the unused focused variable and then passes it as an unused argument to dialog.evaluate, creating dead cross-context payload; remove the const focused = await page.evaluate(() => document.activeElement) and change the dialog.evaluate call (the one that sets focusedInDialog) to only evaluate (el) => el.contains(document.activeElement) without any extra parameters or _focused, and delete any reference to _focused to clean up the focus-trap loop.packages/ui/src/components/sheet.tsx (1)
34-37:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winDon’t inject
props.classintoclassListkeys.Line 36 can turn a space-separated class string into one
classListtoken, which is an invalid DOM token and can throw. ForwardclassandclassListseparately.Proposed fix
<Kobalte.Content data-slot="sheet-content" - classList={{ - ...props.classList, - [props.class ?? ""]: !!props.class, - }} + class={props.class} + classList={props.classList} >#!/bin/bash # Verify the problematic pattern is removed rg -n '\[props\.class \?\? ""\]' packages/ui/src/components/sheet.tsx # Expected after fix: no matches🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/components/sheet.tsx` around lines 34 - 37, The current Sheet component is injecting props.class into the classList object (the snippet with [props.class ?? ""]: !!props.class) which can create an invalid single token when props.class is a space-separated string; remove that dynamic key entirely and stop mutating classList with props.class—forward props.class and props.classList separately (e.g. pass class={props.class} and merge/forward props.classList only as an object), and update any use of classList in the Sheet component to expect a proper tokenized object (identify the code around classList and props.class in the Sheet component in packages/ui/src/components/sheet.tsx to apply this change).
🧹 Nitpick comments (1)
packages/ui/test/sheet.test.ts (1)
94-98: ⚡ Quick winPrefer token-level assertion over hardcoded scrim RGBA.
This check can fail on legitimate token/theming updates; assert
--scrim-overlayusage instead of a fixed color literal.Suggested assertion update
test("dialog-overlay is used for scrim (inlined per DESIGN.md)", () => { // Reuses shared dialog-overlay which inlines the spec scrim color. expect(SHEET_SRC).toContain('data-component="dialog-overlay"') - expect(DIALOG_CSS).toContain("rgba(26, 22, 19, 0.32)") + expect(DIALOG_CSS).toContain("--scrim-overlay") })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/test/sheet.test.ts` around lines 94 - 98, The test "dialog-overlay is used for scrim (inlined per DESIGN.md)" should stop asserting a hardcoded RGBA color; update the assertion on DIALOG_CSS in that test to verify usage of the scrim token (e.g., check for "var(--scrim-overlay)" or "--scrim-overlay") instead of "rgba(26, 22, 19, 0.32)". Locate the test block (test(...) referencing SHEET_SRC and DIALOG_CSS) and replace the literal color expectation with an assertion that DIALOG_CSS contains the scrim CSS variable so theming/token changes won't break the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ui/src/components/dialog.css`:
- Around line 7-8: The dialog overlay currently uses a hard-coded RGBA
(background-color: rgba(26, 22, 19, 0.32)); change that rule in
packages/ui/src/components/dialog.css to use the design scrim token instead
(replace the literal with the theme token, e.g. var(--scrim) or the project’s
overlay/scrim token name), update the background-color declaration for the
dialog overlay selector, and add a sensible fallback value if the token is
unavailable so theme-level scrim changes propagate correctly.
In `@packages/ui/src/components/keybind.css`:
- Around line 9-30: Replace the hardcoded keybind colors and duplicated
color-scheme blocks with the shared CSS tokens: use --kbd-bg for background and
--kbd-border for border (with appropriate fallbacks if desired) in the
[data-component="keybind"] rule instead of literal `#f5f0e9` and rgba(...) values,
and remove the explicit :root[data-color-scheme="dark"] and
`@media`(prefers-color-scheme: dark) color overrides so theme switching is driven
by the token values; update any selector that sets background/border (e.g., the
base [data-component="keybind"] and the dark-scheme selectors) to reference
--kbd-bg and --kbd-border consistently.
---
Duplicate comments:
In `@packages/app/e2e/app/dialog-state-matrix.spec.ts`:
- Around line 43-48: The test captures document.activeElement into the unused
focused variable and then passes it as an unused argument to dialog.evaluate,
creating dead cross-context payload; remove the const focused = await
page.evaluate(() => document.activeElement) and change the dialog.evaluate call
(the one that sets focusedInDialog) to only evaluate (el) =>
el.contains(document.activeElement) without any extra parameters or _focused,
and delete any reference to _focused to clean up the focus-trap loop.
In `@packages/ui/src/components/sheet.tsx`:
- Around line 34-37: The current Sheet component is injecting props.class into
the classList object (the snippet with [props.class ?? ""]: !!props.class) which
can create an invalid single token when props.class is a space-separated string;
remove that dynamic key entirely and stop mutating classList with
props.class—forward props.class and props.classList separately (e.g. pass
class={props.class} and merge/forward props.classList only as an object), and
update any use of classList in the Sheet component to expect a proper tokenized
object (identify the code around classList and props.class in the Sheet
component in packages/ui/src/components/sheet.tsx to apply this change).
---
Nitpick comments:
In `@packages/ui/test/sheet.test.ts`:
- Around line 94-98: The test "dialog-overlay is used for scrim (inlined per
DESIGN.md)" should stop asserting a hardcoded RGBA color; update the assertion
on DIALOG_CSS in that test to verify usage of the scrim token (e.g., check for
"var(--scrim-overlay)" or "--scrim-overlay") instead of "rgba(26, 22, 19,
0.32)". Locate the test block (test(...) referencing SHEET_SRC and DIALOG_CSS)
and replace the literal color expectation with an assertion that DIALOG_CSS
contains the scrim CSS variable so theming/token changes won't break the test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 267d60bf-ae18-41f3-a8a4-1fda418a8fa0
📒 Files selected for processing (19)
packages/app/e2e/app/dialog-state-matrix.spec.tspackages/app/src/components/dialog-edit-project.tsxpackages/app/src/components/dialog-select-file.tsxpackages/app/src/pages/layout.tsxpackages/app/src/pages/session/message-timeline.tsxpackages/ui/src/components/button.csspackages/ui/src/components/command-palette.csspackages/ui/src/components/command-palette.stories.tsxpackages/ui/src/components/command-palette.tsxpackages/ui/src/components/dialog.csspackages/ui/src/components/dialog.stories.tsxpackages/ui/src/components/dialog.tsxpackages/ui/src/components/keybind.csspackages/ui/src/components/sheet.csspackages/ui/src/components/sheet.stories.tsxpackages/ui/src/components/sheet.tsxpackages/ui/test/button-states.test.tspackages/ui/test/components/dialog-footer.test.tspackages/ui/test/sheet.test.ts
💤 Files with no reviewable changes (2)
- packages/app/src/components/dialog-edit-project.tsx
- packages/app/src/pages/layout.tsx
…440) Sidebar's delete-session entry uses this component (the inline copy in message-timeline.tsx covers the in-session entry). Rebase onto dev silently dropped the patch portion that touched this file because dev's slice 04/05 had already moved it to the dialog footer slot, so the secondary/danger button migration only landed on the inline copy. Mirror dialog-rename-session: width 420, footer rendered inline (no slot), secondary + danger button pair per DESIGN.md §"Modals & overlays".
…e 07, #440) Slice 07 introduced packages/ui/src/components/command-palette.{tsx,css} and sheet.{tsx,css} but never added the corresponding @import lines to src/styles/index.css. Browsers loaded the components without their styles, so CommandPalette rendered without background/border/flex layout (making ⌘K appear as a grey overlay with invisible content) and Sheet was equally broken whenever a consumer would land. Add both imports in alphabetic position. No other changes.
The inline DialogDeleteSession (line 953), deleteSession helper (line 887), and navigateAfterSessionRemoval (line 874) had no callers in the file or anywhere else — sidebar deletion goes through @/components/dialog-delete-session via layout.tsx:1153, not through this file. Removing the trio drops 90+ lines of dead code that was misleading me into double-implementing dialog visuals.
Default Spotlight/Raycast-style top-anchored layout (padding-top: 80px, align-items: flex-start) felt too high on smaller screens. Switch to full vertical centering.
) Slice 07 set Dialog default width to 420 per DESIGN.md, but dialog-select-provider renders a multi-column row (icon · name · description · Recommended badge) that wraps and becomes unreadable at that width. Apply size="large" (800px) — same as connect-websearch / custom-provider / release-notes / select-server already use. Other 7 list dialogs (select-model, manage-models, fork, select-mcp, select-directory, select-model-unpaid, connect-provider) were checked and read fine at 420; not migrating preemptively.
Per CodeRabbit #3194493724: passing props.class as a classList key (`[props.class ?? ""]: !!props.class`) breaks at runtime when the prop holds a multi-class Tailwind string — DOMTokenList.toggle rejects tokens containing whitespace. Forward class and classList separately so SolidJS sets the className attribute directly and only uses classList for the reactive map. Sheet has no consumers yet, so no live regression — but the bug would fire on first usage.
#440) Per CodeRabbit #3194493692: page.evaluate(() => document.activeElement) returns null because Playwright cannot serialize DOM nodes across contexts; the resulting `focused` value was always null and never read inside dialog.evaluate. The assertion is carried entirely by el.contains(document.activeElement) re-querying the live DOM. Drop the misleading parameter and the page.evaluate call.
Per CodeRabbit #3194493701: Kobalte.Content always renders role="dialog"; without an accessible label screen readers announce "dialog" with no context (WCAG 4.1.2). Add an optional `label` prop on CommandPalette that forwards to aria-label, and pass "palette.aria.label" / "命令面板" from dialog-select-file.
Per CodeRabbit #3194829417 (partial accept): use design tokens, not hardcoded colors. Reviewer suggested --kbd-bg / --kbd-border which DESIGN.md does not define and dev branch never had. Apply the spec L264 contract instead: --bg-base background, 1px --border-weak ring, 1px hard shadow (physical keycap look), padding 1px/6px, font: var(--type-kbd). Drops the explicit dark-mode blocks because --bg-base / --border-weak already have dark variants in theme.css that cascade automatically.
Mirror the Sheet fix (daa10d0): forward props.class and props.classList as separate Kobalte.Content props instead of merging props.class into the classList object as a key. SolidJS toggleClassKey already trims+splits multi-token keys so the previous form did not crash, but the inline form matches Sheet, removes one layer of indirection, and is the convention flagged by external review. Verified manually: delete-session and rename-session MODE 2 dialogs (both pass class="w-full max-w-[420px] mx-auto") render identically and cancel/submit paths still work.
…ice 07, #440) Earlier draft used --error-text as the hover background. In light theme this is a deeper red and works, but in dark theme --error-text mirrors to a *lighter* red (#f0867a) which combined with white text drops contrast to ~2:1, well below WCAG AA (4.5:1 for 13px). Switch to the same overlay pattern as secondary: keep --error as the base, composite --hover-overlay (5% white in dark / 4% black in light) on hover, --hover-overlay-warm on active. Both themes get a visible state delta without inverting contrast. Tests: update button-states to expect the overlay form; add a new danger-button-contrast test that parses theme.css, composites the overlay, and computes WCAG luminance / contrast vs --fg-on-brand for both themes (rest >= 4.0 light / >= 3.0 dark; hover does not regress below rest). Hand-tested: delete-session danger button hover/active in both light and dark themes.
…(slice 07, #440) CommandPalette had a fixed max-height: 480px and centered layout; on small windows (split-screen, ~400px height, remote desktop) the palette overflowed the viewport top/bottom and content was clipped because the shell sets overflow: hidden. Switch to max-height: min(480px, calc(100dvh - 32px)) so the palette never exceeds the available viewport. Also sync stories: the docs blurb still claimed "Floats 80px from the top of the viewport" (pre-centering); update to centered layout + viewport-clamped max-height. Pass label="Command palette" to all three stories to mirror the app caller and exercise the new aria-label prop.
…440) Sheet declares title? as optional. When callers omit title there is no Kobalte.Title to act as aria-labelledby and Kobalte.Content gets no accessible name (WCAG 4.1.2). Add an optional label?: string prop and forward it to aria-label only when title is absent (avoids double-labelling when both are present). Test: pin the prop in SheetProps and the conditional aria-label expression in source, mirroring the existing structural test pattern in sheet.test.ts. Sheet has no app consumer yet (slice 08 form refactors will plumb this through), but the contract is now safe by default.
…dence (slice 07, #440) Pin the class/classList forwarding contract for both Dialog and Sheet: both must use class={props.class} + classList={props.classList} as separate props, not the legacy classList-key form (classList={{ [props.class ?? ""]: !!props.class }}). The legacy form is not actually a runtime crash because solid-js/web's toggleClassKey trims and splits multi-token keys before delegating to DOM classList.toggle, but the inline form is the codebase convention and removes one layer of indirection. Backstop: a third test reads solid-js/web/dist/web.js directly and asserts the trim+split call is still present. If a future bun + solid version bump removes that runtime invariant, this test fails and forces us to re-evaluate whether the legacy form (anywhere it survives) is still safe. This is the indirect-evidence approach picked over launching DOM render infra (happy-dom + babel-preset-solid + bun loader plugin) for this slice. The DOM render infra story is tracked as a separate follow-up.
The migration from Dialog to CommandPalette dropped Dialog's height: min(calc(100vh - 16px), 512px) clamp. The CSS fix is max-height: min(480px, calc(100dvh - 32px)) on palette-content. This e2e pins the behaviour at two viewport heights: - 400px (below the 480 ceiling): palette stays inside viewport on top and bottom edges (1px tolerance for sub-pixel centering). - 900px (above): palette respects the 480px ceiling rather than growing to fill available height. A future refactor that drops the dvh clamp would fail here instead of at user report time on small/split-screen windows.
Kobalte.Content renders as the role="dialog" element AND carries
data-slot="palette-content" itself. The previous spec did
`dialog.locator('[data-slot="palette-content"]')`, which queries
descendants for the same slot and finds nothing. Result: the spec
either timed out at toBeVisible() or silently passed without testing
the viewport clamp.
Switch to using the dialog locator directly and assert the slot
attribute as a positive contract check before measuring boundingBox.
The backstop test asserted the trim+split call inside
solid-js/web/dist/web.js still exists. Original intent was to alarm if
the runtime ever stopped neutralizing multi-token classList keys, since
the legacy classList-key form would then become a real crash hazard.
But Dialog and Sheet both forward via class={...} now — the legacy
form is gone, the backstop has nothing to back. It also reaches into
solid-js bundle internals (path, file naming, source string) and would
flake on any solid version bump unrelated to our code.
Keep the source-level forwarding contract assertions for both Dialog
and Sheet; drop the runtime grep.
…ice 07, #440) The previous "active state uses --surface-interactive-base across all variants" test was both inaccurate (danger active now overlays --hover-overlay-warm on --error, not the cream surface) and weak (asserted only that the token appears somewhere in the CSS, no per- variant scoping). Replace with one assertion per variant for primary/secondary/ghost that actually scopes to data-variant + :active, plus a negative assertion pinning that danger active does NOT use --surface-interactive-base. The latter guards against a future "convergence" refactor silently re-applying cream tone to a destructive press.
… (slice 07, #440) Mirror dialog.tsx's onOpenAutoFocus handler so [autofocus] elements are explicitly focused on open. dialog-select-file uses `<List search={{ autofocus: true }}>` to focus the textbox; without the explicit handler we relied on Kobalte's first-focusable default, which worked but had no regression guard. Strengthen openPalette e2e helper with toBeFocused() and add a UI-source test pinning the autofocus pattern.
Root cause: - PR #464 fixed the original composer/timeline mismatch by replacing the session timeline medium-width cap with explicit `md:max-w-[800px]`. - The #440 UI slice merge chain later regressed that fix. PR #461 first changed both timeline caps back to `md:max-w-200`, which resolves to about 650px with PawWork's 13px root font. - PR #460 then kept that same `message-timeline.tsx` state while removing dead delete-session code, so #479 surfaced as the composer still appearing wider than the reading column at right-panel-hidden desktop widths. Fix: - Restore both session timeline medium-width caps to `md:max-w-[800px]`. - Keep the existing 2xl timeline cap and composer caps unchanged, preserving the prior #217/#456 intent that composer remains inset at very wide desktop sizes. - Add a focused E2E checkpoint for #479: viewport 1280x900 with `#right-panel[aria-hidden="true"]`, then reuse the existing timeline/composer bounding-box contract. Verification: - Red check: temporarily restored the old `md:max-w-200` cap and confirmed the focused E2E failed at the new 1280px/right-panel-hidden checkpoint with timeline 651px vs composer 720px. - Focused E2E passed: `bun --cwd packages/app test:e2e e2e/app/session.spec.ts -g "session timeline visible content is not narrower than composer shell"`. - Typecheck passed: `bun --cwd packages/app typecheck`. - Diff check passed: `git diff --check`. - PR CI passed, including typecheck, unit-app, unit-opencode, unit-desktop, e2e-artifacts, smoke-macos-arm64, CodeQL, dependency review, commit lint, and PR title lint. Follow-up: - No immediate product follow-up is required for #479. - A separate architecture issue may still be worth opening later for the broader session width contract, because timeline, message item, and composer width values are still split across files and have accumulated breakpoint-specific tests. Closes #479.
Summary
Slice 07 of eleven in #440 (PawWork UI carve-out). Lands the
SheetandCommandPaletteprimitives, adds afooterslot toDialog, migratesdialog-select-filetoCommandPalette, and aligns the dialog/sheet/keybind/destructive-button surface to the updateddocs/DESIGN.md§"Modals & overlays".Why
DESIGN.md §"Modals & overlays" (L193) was updated mid-flight to define a two-mode dialog footer (MODE 1: bg-cream + sticky; MODE 2: inline, no border), pin default dialog/sheet width to 420, inline scrim + kbd colors at the call site, and standardize destructive button pairs as secondary + danger. Slice 07 was reset onto
origin/devto pick up slices 04–06 (button 4-tier rewrite, inputs/picker contract, tag/popover/tooltip rewrite, and the--row-hover-overlaytoken family) so the new primitives land on top of the current design system rather than a stale fork point.Related Issue
#440 (slice 07 of 11)
What landed
New UI primitives (
packages/ui/src/components/):Sheet: four-direction slide-in/out using Kobalte Dialog Root/Portal/Overlay; controlled API; header/body/footer slots; 240ms slide-in / 120ms slide-out per spec.CommandPalette: Kobalte Dialog Content wrapper withdata-component="command-palette"; optionaltransitionandlabel(a11y) props; centered overlay layout.Dialog.footerslot: MODE 1 sticky bg-cream styling moved into the primitive.Spec alignment (DESIGN.md L193 modals + L264 keybind chip):
rgba(26, 22, 19, 0.32)(light/dark identical per spec).--bg-cream+ 1px--border-weakertop + padding 12/24. MODE 2 (small confirm): inline footer, no slot.font: var(--type-kbd),--bg-base, 1px--border-weak, 1px hard shadow, padding 1/6.secondary+danger); button danger variant remapped from--brand-danger/--brand-danger-hoverto--error/--error-text.Hotfixes discovered during hand-test:
command-palette.cssandsheet.csswere never imported inpackages/ui/src/styles/index.css— slice 07 introduced the files but missed the@importlines, so CommandPalette rendered with no background/layout (⌘K appeared as a grey overlay). Added both imports.dialog-select-providerat default 420 broke the multi-column row (icon · name · description · Recommended badge wrapped). Applysize="large"(800px) — same as connect-websearch / custom-provider / release-notes / select-server already use.flex-start+padding-top: 80pxto centered (the Spotlight/Raycast top-anchored layout felt too high on smaller screens).dialog-edit-project.tsx(no callers); inlineDialogDeleteSession+deleteSession+navigateAfterSessionRemovalinmessage-timeline.tsx(no callers — sidebar deletion goes through the standalone component).CodeRabbit review response (6 inline comments, all replied + resolved):
_focusedcross-evaluate parameter dropped from dialog-state-matrix spec (Playwright cannot serialize DOM nodes across contexts);aria-labeladded to CommandPalette via newlabelprop (WCAG 4.1.2); Sheetclass/classListforwarded as separate Kobalte.Content props (DOMTokenList.toggle throws on whitespace tokens).--bg-base/--border-weak), not the reviewer's--kbd-bg/--kbd-borderwhich dev's theme.css does not define.sheet.stories.tsx@ts-nocheckstays — all 53 stories files use it as a codebase convention, single-file change would break consistency. Should be a separate PR if we want to enforce stories typing.Human Review Status
Pending. Hand-tested visible surfaces (delete-session, ⌘K command palette, sidebar hover/selected, Connect provider, keybind chips, light + dark mode). A human should make the final merge decision after reviewing the final diff and CI signal.
Review Focus
keep-oursmerge commit that silently dropped slices 04/05/06 (button 4-tier rewrite, inputs/picker contract, tag/popover/tooltip,--row-hover-overlaytoken family). Reset + cherry-pick produced a cleaner diff againstorigin/dev. Verify no slice 04/05/06 work was lost — the diff againstdevshould show only slice 07 + spec alignment + hotfixes.Risk Notes
size.dialog-select-providerreceivedsize="large"here; the other 7 (select-model, manage-models, fork, select-mcp, select-directory, select-model-unpaid, connect-provider) were hand-checked at 420 and read fine. Spec mandates eventual sheet migration for list-style dialogs but that is out of scope.How To Verify
Screenshots or Recordings
Not attached — visual surfaces verified via dev:desktop hand-test by the PR author.
Checklist