Skip to content

feat(ui,app): slice 07 modals + palette + spec alignment (#440)#460

Merged
Astro-Han merged 32 commits into
devfrom
claude/i440-slice-07
May 6, 2026
Merged

feat(ui,app): slice 07 modals + palette + spec alignment (#440)#460
Astro-Han merged 32 commits into
devfrom
claude/i440-slice-07

Conversation

@Astro-Han

@Astro-Han Astro-Han commented May 5, 2026

Copy link
Copy Markdown
Owner

Summary

Slice 07 of eleven in #440 (PawWork UI carve-out). Lands the Sheet and CommandPalette primitives, adds a footer slot to Dialog, migrates dialog-select-file to CommandPalette, and aligns the dialog/sheet/keybind/destructive-button surface to the updated docs/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/dev to pick up slices 04–06 (button 4-tier rewrite, inputs/picker contract, tag/popover/tooltip rewrite, and the --row-hover-overlay token 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 with data-component="command-palette"; optional transition and label (a11y) props; centered overlay layout.
  • Dialog.footer slot: MODE 1 sticky bg-cream styling moved into the primitive.

Spec alignment (DESIGN.md L193 modals + L264 keybind chip):

  • Dialog default width 640 → 420; overlay scrim inlined as rgba(26, 22, 19, 0.32) (light/dark identical per spec).
  • Sheet width 480 → 420 (4 sides).
  • Footer MODE 1: sticky bottom + --bg-cream + 1px --border-weaker top + padding 12/24. MODE 2 (small confirm): inline footer, no slot.
  • Keybind chip: physical-keycap visual per L264 — font: var(--type-kbd), --bg-base, 1px --border-weak, 1px hard shadow, padding 1/6.
  • Delete-session migrated to MODE 2 (secondary + danger); button danger variant remapped from --brand-danger/--brand-danger-hover to --error/--error-text.

Hotfixes discovered during hand-test:

  • command-palette.css and sheet.css were never imported in packages/ui/src/styles/index.css — slice 07 introduced the files but missed the @import lines, so CommandPalette rendered with no background/layout (⌘K appeared as a grey overlay). Added both imports.
  • dialog-select-provider at default 420 broke the multi-column row (icon · name · description · Recommended badge wrapped). Apply size="large" (800px) — same as connect-websearch / custom-provider / release-notes / select-server already use.
  • CommandPalette vertical alignment switched from flex-start + padding-top: 80px to centered (the Spotlight/Raycast top-anchored layout felt too high on smaller screens).
  • Dead code dropped: dialog-edit-project.tsx (no callers); inline DialogDeleteSession + deleteSession + navigateAfterSessionRemoval in message-timeline.tsx (no callers — sidebar deletion goes through the standalone component).

CodeRabbit review response (6 inline comments, all replied + resolved):

  • Accepted: _focused cross-evaluate parameter dropped from dialog-state-matrix spec (Playwright cannot serialize DOM nodes across contexts); aria-label added to CommandPalette via new label prop (WCAG 4.1.2); Sheet class/classList forwarded as separate Kobalte.Content props (DOMTokenList.toggle throws on whitespace tokens).
  • Partial: keybind chip uses tokens, but per DESIGN.md L264 (--bg-base / --border-weak), not the reviewer's --kbd-bg / --kbd-border which dev's theme.css does not define.
  • Declined with reasoning: dialog overlay scrim stays inline (L193 explicit: "调用处内联"); sheet.stories.tsx @ts-nocheck stays — 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

  • Reset-onto-dev decision: original branch had a keep-ours merge commit that silently dropped slices 04/05/06 (button 4-tier rewrite, inputs/picker contract, tag/popover/tooltip, --row-hover-overlay token family). Reset + cherry-pick produced a cleaner diff against origin/dev. Verify no slice 04/05/06 work was lost — the diff against dev should show only slice 07 + spec alignment + hotfixes.
  • DESIGN.md spec interpretation for the keybind chip rewrite (L264) and the dialog/sheet token-vs-inline discipline (L193).
  • Sheet primitive has no app consumers yet — landing it now to unblock slice 08 form refactors.

Risk Notes

  • All UI changes; no backend, no migration, no permissions, no platform-specific paths/shells.
  • Default dialog width 640 → 420 affects 8 dialogs that did not override size. dialog-select-provider received size="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.
  • Sheet primitive has zero app consumers; exercised via tests and stories only.

How To Verify

turbo typecheck:                8/8 packages pass
packages/ui bun test:           335/337 pass
  (2 pre-existing failures in apply-patch-file + session-diff, unrelated;
   confirmed same 2 fail on origin/dev baseline)
sheet + button-states + dialog-footer focused tests: 39/39 pass

Hand-test (bun run dev:desktop):
  ⌘K  → centered command palette, list of files/commands/sessions, keybind chips visible
  Sidebar → right-click session → delete → MODE 2 dialog (420 wide, no border, secondary + danger)
  Sidebar → hover/selected session row → --row-hover-overlay / --row-active-overlay applied
  Settings → Providers → Connect provider → 800px wide, no row wrapping
  Keybind chips → physical keycap (white bg, grey border, 1px hard shadow, mono font)
  Light + dark mode both verified

Screenshots or Recordings

Not attached — visual surfaces verified via dev:desktop hand-test by the PR author.

Checklist

  • Human review status is stated above as pending
  • I linked the related issue ([Feature] Rewrite packages/ui + packages/app visual layer against PawWork design system (incremental PR series) #440)
  • This PR has type, primary area, and priority labels (P2 / ui / app / enhancement)
  • I described the review focus and meaningful risks
  • I listed the relevant verification steps and the key result for each
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope
  • I manually checked visible UI changes via dev:desktop (light + dark)
  • I considered macOS and Windows impact — UI-only, no platform-specific paths/shells/permissions
  • I called out the spec alignment scope and CodeRabbit review decisions
  • I reviewed the final diff for unrelated changes and suspicious dependency changes

@Astro-Han Astro-Han added ui Design system and user interface P2 Medium priority labels May 5, 2026
@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@Astro-Han has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 50 minutes and 52 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 349f90a4-f40e-40bb-b0a4-4ebdfeb3e525

📥 Commits

Reviewing files that changed from the base of the PR and between 744bbee and c58e19b.

📒 Files selected for processing (22)
  • packages/app/e2e/actions.ts
  • packages/app/e2e/app/dialog-state-matrix.spec.ts
  • packages/app/e2e/app/palette-viewport.spec.ts
  • packages/app/src/components/dialog-delete-session.tsx
  • packages/app/src/components/dialog-select-file.tsx
  • packages/app/src/components/dialog-select-provider.tsx
  • packages/app/src/i18n/en.ts
  • packages/app/src/i18n/zh.ts
  • packages/app/src/pages/session/message-timeline.tsx
  • packages/ui/src/components/button.css
  • packages/ui/src/components/command-palette.css
  • packages/ui/src/components/command-palette.stories.tsx
  • packages/ui/src/components/command-palette.tsx
  • packages/ui/src/components/dialog.tsx
  • packages/ui/src/components/keybind.css
  • packages/ui/src/components/sheet.tsx
  • packages/ui/src/styles/index.css
  • packages/ui/test/button-states.test.ts
  • packages/ui/test/command-palette.test.ts
  • packages/ui/test/danger-button-contrast.test.ts
  • packages/ui/test/dialog-class-forwarding.test.ts
  • packages/ui/test/sheet.test.ts
📝 Walkthrough

Walkthrough

This 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.

Changes

CommandPalette Component

Layer / File(s) Summary
Component & Props
packages/ui/src/components/command-palette.tsx
New CommandPalette component with optional transition prop; renders a container with data attributes and a Kobalte Content slot for children.
Styling
packages/ui/src/components/command-palette.css
CSS defines fixed overlay, constrained palette container, content layout with borders/shadows, focus styling, and contentHide/contentShow animations based on data-transition and data-expanded.
Integration
packages/app/src/components/dialog-select-file.tsx
DialogSelectFile wrapper changed from Dialog to CommandPalette while preserving all internal logic and interactions.
Stories & E2E Tests
packages/ui/src/components/command-palette.stories.tsx, packages/app/e2e/app/dialog-state-matrix.spec.ts
Storybook stories demonstrate Basic, WithTransition, and WithMockItems variants; e2e tests verify aria-modal, Escape/overlay close, focus-trap, and data-component attribute.

Sheet Component

Layer / File(s) Summary
Types & Component
packages/ui/src/components/sheet.tsx
New SheetSide type and SheetProps interface; Sheet component renders a modal dialog via Kobalte with support for title and footer slots, default-right positioning, and i18n close label.
Styling
packages/ui/src/components/sheet.css
CSS includes keyframes for slide-in/out in four directions, layout containers, header/body/footer slots with borders and spacing, per-side positioning, and reduced-motion safeguards.
Stories & Tests
packages/ui/src/components/sheet.stories.tsx, packages/ui/test/sheet.test.ts
Storybook stories cover RightDefault, LeftSheet, and WithFooter variants; tests verify interface structure, default side, slot usage, Kobalte wiring, and close button accessibility.

Dialog Footer Support

Layer / File(s) Summary
Component Props
packages/ui/src/components/dialog.tsx
DialogProps extended with optional footer?: JSXElement prop; footer is conditionally rendered in a dedicated slot when provided.
Dialog Styling & Overlay
packages/ui/src/components/dialog.css
Added [data-slot="dialog-footer"] block with flex layout, spacing, padding, sticky bottom, and top border; overlay background updated to rgba; border-radius now uses CSS variable; max-width reduced from 640px to 420px.
Integration & Button Updates
packages/app/src/components/dialog-edit-project.tsx, packages/app/src/pages/session/message-timeline.tsx
DialogEditProject moves action controls into Dialog footer; DialogDeleteSession updates to compact centered modal with secondary Cancel and danger-variant Delete buttons.
Stories & Tests
packages/ui/src/components/dialog.stories.tsx, packages/ui/test/components/dialog-footer.test.ts
New WithFooter and DialogStateMatrix stories demonstrate footer patterns; tests verify footer prop, data-slot attribute, Show conditional, and footer content rendering.

Button Styling & Refinements

Layer / File(s) Summary
Button Variant Migration
packages/ui/src/components/button.css, packages/ui/test/button-states.test.ts
Danger button variant styling switched from brand color tokens (--brand-danger, --brand-danger-hover) to error tokens (--error, --error-text); tests updated to assert new token usage.
Keybind Styling
packages/ui/src/components/keybind.css
Background changed from var(--surface-base) to light color (#f5f0e9); border added; box-shadow removed; dark-mode and responsive blocks updated for alignment.
Layout Cleanup
packages/app/src/pages/layout.tsx
Removed duplicated layout.sidebar.toggleWorkspaces() call and unused showEditProjectDialog() function.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • Astro-Han/pawwork#110: Both PRs modify dialog-select-file.tsx at the code level (this PR wraps it in CommandPalette; the other changes tab-opening behavior).
  • Astro-Han/pawwork#448: Both PRs update component CSS styling and design tokens (keybind.css, dialog focus/shadows, button variants).

Suggested labels

enhancement, app

Poem

🐰 A rabbit's ode to modal dreams

CommandPalettes dance in light,
Sheets slide in from left and right,
Dialog footers hold their ground,
Errors shine in red so round,
UI magic, fresh and bright! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main changes: introducing UI primitives (Sheet, CommandPalette), adding a footer slot to Dialog, and aligning specs (slice 07, issue #440).
Description check ✅ Passed The description is comprehensive and well-structured, covering all major template sections: Summary, Why, Related Issue, Human Review Status, Review Focus, Risk Notes, How To Verify, and a detailed Checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/i440-slice-07

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.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/app/src/components/dialog-delete-session.tsx Outdated
Comment thread packages/app/src/components/dialog-edit-project.tsx Outdated
Comment thread packages/ui/src/components/command-palette.tsx
Comment thread packages/ui/src/components/sheet.css
@Astro-Han Astro-Han force-pushed the claude/i440-slice-07 branch from 806d1b4 to b483830 Compare May 6, 2026 09:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 734483c and b483830.

📒 Files selected for processing (17)
  • packages/app/e2e/app/dialog-state-matrix.spec.ts
  • packages/app/src/components/dialog-delete-session.tsx
  • packages/app/src/components/dialog-edit-project.tsx
  • packages/app/src/components/dialog-select-file.tsx
  • packages/ui/src/components/command-palette.css
  • packages/ui/src/components/command-palette.stories.tsx
  • packages/ui/src/components/command-palette.tsx
  • packages/ui/src/components/dialog.css
  • packages/ui/src/components/dialog.stories.tsx
  • packages/ui/src/components/dialog.tsx
  • packages/ui/src/components/keybind.css
  • packages/ui/src/components/sheet.css
  • packages/ui/src/components/sheet.stories.tsx
  • packages/ui/src/components/sheet.tsx
  • packages/ui/src/styles/theme.css
  • packages/ui/test/components/dialog-footer.test.ts
  • packages/ui/test/sheet.test.ts

Comment thread packages/app/e2e/app/dialog-state-matrix.spec.ts
Comment thread packages/ui/src/components/command-palette.tsx
Comment thread packages/ui/src/components/sheet.stories.tsx
Comment thread packages/ui/src/components/sheet.tsx Outdated
Astro-Han added 13 commits May 6, 2026 18:35
…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.
…#440)

Mirror the source change in da2f6f8: danger variant maps to
--error / --error-text per DESIGN.md §"Modals & overlays". Use regex
scoped to the data-variant="danger" block so the assertion can't
accidentally pass on a stray --error reference elsewhere in the file.
@Astro-Han Astro-Han force-pushed the claude/i440-slice-07 branch from e149d7b to 744bbee Compare May 6, 2026 10:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/app/e2e/app/dialog-state-matrix.spec.ts (1)

43-48: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove 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; _focused is 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 win

Don’t inject props.class into classList keys.

Line 36 can turn a space-separated class string into one classList token, which is an invalid DOM token and can throw. Forward class and classList separately.

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 win

Prefer token-level assertion over hardcoded scrim RGBA.

This check can fail on legitimate token/theming updates; assert --scrim-overlay usage 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

📥 Commits

Reviewing files that changed from the base of the PR and between b483830 and 744bbee.

📒 Files selected for processing (19)
  • packages/app/e2e/app/dialog-state-matrix.spec.ts
  • packages/app/src/components/dialog-edit-project.tsx
  • packages/app/src/components/dialog-select-file.tsx
  • packages/app/src/pages/layout.tsx
  • packages/app/src/pages/session/message-timeline.tsx
  • packages/ui/src/components/button.css
  • packages/ui/src/components/command-palette.css
  • packages/ui/src/components/command-palette.stories.tsx
  • packages/ui/src/components/command-palette.tsx
  • packages/ui/src/components/dialog.css
  • packages/ui/src/components/dialog.stories.tsx
  • packages/ui/src/components/dialog.tsx
  • packages/ui/src/components/keybind.css
  • packages/ui/src/components/sheet.css
  • packages/ui/src/components/sheet.stories.tsx
  • packages/ui/src/components/sheet.tsx
  • packages/ui/test/button-states.test.ts
  • packages/ui/test/components/dialog-footer.test.ts
  • packages/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

Comment thread packages/ui/src/components/dialog.css
Comment thread packages/ui/src/components/keybind.css Outdated
Astro-Han added 9 commits May 6, 2026 18:58
…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.
@Astro-Han Astro-Han added app Application behavior and product flows enhancement New feature or request labels May 6, 2026
@Astro-Han Astro-Han changed the title feat(ui): dialog footer slot + sheet primitive + command palette extraction (slice 07, issue #440) feat(ui,app): slice 07 modals + palette + spec alignment (#440) May 6, 2026
Astro-Han added 10 commits May 6, 2026 19:39
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.
@Astro-Han Astro-Han merged commit b79141d into dev May 6, 2026
20 checks passed
@Astro-Han Astro-Han deleted the claude/i440-slice-07 branch May 6, 2026 13:20
Astro-Han added a commit that referenced this pull request May 6, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app Application behavior and product flows enhancement New feature or request P2 Medium priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant