UI refresh: Base UI migration + flat tinted button language + mobile polish (#960)

* deps: bump @opencode-ai/sdk to 1.4.6 and add @base-ui/react

* build: align vite configs with Base UI and packages update

* feat(ui): migrate primitives to Base UI

Replace Radix-based wrappers (button, checkbox, collapsible, command,
dialog, dropdown-menu, input, radio, scroll-area, select, separator,
sonner, switch, textarea, toggle, tooltip) with @base-ui/react
equivalents and a refreshed visual language: flexoki theme tokens,
typography utilities, and a redesigned CommandPalette. Add new
fancy-button variant set and a local slot helper for asChild composition.

* refactor(ui consumers): adopt Base UI primitives across the app

Update chat, layout, git views, settings, multirun, session and agent
management screens to use the new Base UI-backed primitives (anchor
width token rename, dropdown/select/dialog usage adjustments, new
component APIs).

* fix(ScrollableOverlay): forward `disabled` to inner element

Add `disabled` to the type so it propagates via rest spread to the
underlying element (e.g. textarea in simple mode). Resolves a type
error reported after the main merge.

* refactor(dropdown consumers): migrate preventDefault sites to closeOnClick

Base UI's Menu.Item does not check `event.defaultPrevented` before
closing the menu, so the Radix-era `onSelect={(e) => e.preventDefault()}`
pattern no longer keeps the menu open. Replace those sites with the
Base UI `closeOnClick={false}` prop (VSCodeLayout rate-limit info
rows; AgentGroupDetail worktree actions).

* docs(AGENTS): note Base UI as primary source for UI primitives

* fix(ContextPanel): center expand/close buttons in tab header

Header (h-8) had the action row pinned with items-end + pb-1.5, which
placed the h-7 buttons ~2px above the header top and made them look
cropped. Switch to items-center so they sit on the same baseline as
the tabs.

* style(tabs): drop transitions on default variant, soften active pill

Default (underline) variant now switches instantly — no transform/width
transition on the underline indicator, no color transition on tab
buttons. Active pill bg is color-mixed down to 55% of
--interactive-selection so light-theme selection doesn't scream.

* style(button): pill-style elevation and introduce chip variant

Solid variants (default/destructive/neutral/primary/basic) now use the
active-pill shadow stack — hairline navy outer ring + stacked tight/soft
drops — instead of the pure-black inset/border approach. Outline
variant (and FancyButton basic) gets the same elevation without the
white top highlight (no fill to catch light).

Add a new chip variant: flat `border border-border/60` button without
elevation, intended for one-of-N toggle groups where a raised
button would read as the wrong affordance.

* style(header): apply pill-style shadow to OpenIn and Project Actions

Both split-button wrappers previously used a 1px CSS border for
definition. Replace that with the pill elevation stack so the header
buttons match the new Button variants and feel less hard-edged.

* refactor(toggles): migrate one-of-N segmented groups to chip variant

Color Mode, When-sessions-expire, voice/STT providers, auth method,
agent mode, MCP transport, tunnel type and terminal Ctrl/Cmd modifier
toggles all read as flat chip selectors rather than raised buttons.
Switch them from variant="outline" to the new chip variant so the
raised pill-style elevation doesn't fight the selected-state tint.

* fix(elevation): theme-adaptive pill-style shadow for dark mode

The navy (rgba(14,18,27,*)) outer ring was invisible on dark surfaces,
erasing the button/pill edge. Add dark: variants that use a subtle
white outer ring (rgba(255,255,255,0.10)) and darker drop shadows
(rgba(0,0,0,0.30/0.20)) so the elevation reads on both themes.

Applies to Button solid/outline variants, FancyButton equivalents,
the OpenIn and Project Actions header split-buttons, and the active
pill indicator in SortableTabsStrip.

* style(button): flat tinted variants with squircle corner-shape

Replace elevated pill-shadow design with a flat tinted language: pale tinted
fill + hairline tinted border + saturated tinted text for default/destructive.
Add CSS corner-shape: squircle with @supports gate so Chrome 136+ gets the
softened superellipse corners at 50px, Safari falls back to a slightly larger
round radius.

* style(tabs): replace active pill shadow with tinted border

* style(header): flat border and squircle corners for OpenIn / Actions

Drop pill-style shadow stacks in favor of a plain hairline border and apply
the same squircle corner-shape treatment as Button so header controls match
the new visual language.

* style(checkbox): drop filled background for tinted checkmark

Checked / indeterminate states now keep the transparent box, show a
primary-base check/minus glyph, and use a softened primary-tinted inset
border (50% mix) so the control reads as selected without a solid swatch.

* style(layout): widen sidebar-to-chat corner radius to 10px

* style(button): retune tinted variants and unify outline fill

- Bump primary fill toward noticeable but still soft (light 10/16/22,
  dark 16/22/30) and weaken both primary and destructive borders
  (12% light / 20% dark) so the tone reads without shouting.
- Further calm destructive across fill and border so revert-style
  actions match the muted reference.
- Align the outline variant fill with --surface-elevated so git-view
  generate/commit buttons share the header action button surface.

* style(tabs): align pill variant with new button language

Active pill now uses --surface-elevated fill and border-border/60 to
match the outline button. Track and tab/pill geometry adopt the same
squircle treatment as Button: 9-10px fallback with 50px radius under
@supports(corner-shape:squircle). Also tighten track padding (inline
2px, block 2px) and grow active tab height so the indicator fills
the track instead of floating inside it.

* refactor(theme): hardcode radius scale, drop per-theme override

Radius values are now fixed in the design system (flexoki-dark's scale
becomes the app default). Themes can no longer override --radius-* — the
config.radius block is removed from every theme JSON, dropped from the
Theme type, and no longer emitted by the CSS generator.

* chore(ui): lint/type cleanup across Base UI wrappers

* style(tabs): dial pill track background down to near-invisible

* refactor(chip): aria-pressed drives the selected tinted state

The chip variant now carries the tinted primary palette (same as the
default filled button) under aria-pressed, so consumers toggle selection
via a single aria attribute instead of repeating the border/text/bg
override classes. All existing chip consumers migrate to aria-pressed
and drop their hand-rolled selected className.

* chore(defaults): humanize variant label in thinking selector

* fix(diff): render all lines in single-file mode

Single-file mode wrapped PierreDiffViewer in its own layout=fill which
creates a nested virtual-root; combined with the outer CSS containment
on the wrapper, Pierre's virtualizer measured a clamped viewport and
stopped rendering lines past ~150. Mirror the stacked-mode approach:
hoist a ScrollableOverlay as the sole virtual root/content and render
PierreDiffViewer inline, so the shared virtualizer sees the real scroll
viewport and paints every line.

Also drops an unused cn import in SessionRetentionSettings.

* feat(files): collapse floating editor controls behind icon button

The floating file editor toolbar now starts as a single pill icon button
and expands into the full control row on hover or click. Clicking outside
or moving the pointer away collapses it back, keeping the editor surface
unobstructed while the file is being read/edited. Fullscreen overlay
keeps its always-visible toolbar.

* fix(chat): align mobile session status bar radius with chat input

* fix(mobile): keep radio/checkbox/switch at their native size

Mobile CSS forced min 36x36px on every <button>, which swallowed our
Radio and Checkbox primitives (also buttons with role=radio/checkbox)
and ballooned them on touch devices. Exclude role=radio/checkbox/switch
from the touch-target rule and pin the Radio/Checkbox box size with
arbitrary px so padding-scale overrides cannot stretch them either.

* chore(mobile): drop unused cornerRadius prop plumbing

The status bar now hardcodes var(--radius-lg) on both views, so the
cornerRadius prop and the useUIStore selector that sourced it in
ChatInput are dead weight.
This commit is contained in:
Bohdan Triapitsyn
2026-04-19 15:52:54 +03:00
committed by GitHub
parent 3e52be95c5
commit 4506c18f53
126 changed files with 1723 additions and 1448 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ OpenChamber provides UI runtimes (web/desktop/VS Code) for interacting with an O
- Runtime/tooling: Bun (`package.json` `packageManager`), Node >=20 (`package.json` `engines`) - Runtime/tooling: Bun (`package.json` `packageManager`), Node >=20 (`package.json` `engines`)
- UI: React, TypeScript, Vite, Tailwind v4 - UI: React, TypeScript, Vite, Tailwind v4
- State: Zustand (`packages/ui/src/stores/`) - State: Zustand (`packages/ui/src/stores/`)
- UI primitives: Radix UI (`package.json` deps), HeroUI (`package.json` deps), Remixicon (`package.json` deps) - UI primitives: Base UI (`@base-ui/react`, primary source for dropdown/select/dialog/menu/tooltip/etc. — wrappers live in `packages/ui/src/components/ui/`), Radix UI (`package.json` deps, legacy usages being migrated), HeroUI (`package.json` deps), Remixicon (`package.json` deps)
- Server: Express (`packages/web/server/index.js`) - Server: Express (`packages/web/server/index.js`)
- Desktop: Tauri v2 (`packages/desktop/src-tauri/`) - Desktop: Tauri v2 (`packages/desktop/src-tauri/`)
- VS Code: extension + webview (`packages/vscode/`) - VS Code: extension + webview (`packages/vscode/`)
+29 -22
View File
@@ -5,6 +5,7 @@
"": { "": {
"name": "openchamber-monorepo", "name": "openchamber-monorepo",
"dependencies": { "dependencies": {
"@base-ui/react": "^1.4.0",
"@codemirror/autocomplete": "^6.20.0", "@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.1", "@codemirror/commands": "^6.10.1",
"@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-cpp": "^6.0.3",
@@ -94,7 +95,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@openchamber/desktop", "name": "@openchamber/desktop",
"version": "1.9.5", "version": "1.9.6",
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2",
"@types/node": "^24.3.1", "@types/node": "^24.3.1",
@@ -103,8 +104,9 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@openchamber/ui", "name": "@openchamber/ui",
"version": "1.9.5", "version": "1.9.6",
"dependencies": { "dependencies": {
"@base-ui/react": "^1.4.0",
"@codemirror/autocomplete": "^6.20.0", "@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.1", "@codemirror/commands": "^6.10.1",
"@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-cpp": "^6.0.3",
@@ -135,16 +137,6 @@
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "^1.4.6", "@opencode-ai/sdk": "^1.4.6",
"@pierre/diffs": "1.1.0-beta.13", "@pierre/diffs": "1.1.0-beta.13",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.8",
"@remixicon/react": "^4.7.0", "@remixicon/react": "^4.7.0",
"@simplewebauthn/browser": "13.3.0", "@simplewebauthn/browser": "13.3.0",
"@tanstack/react-virtual": "^3.13.18", "@tanstack/react-virtual": "^3.13.18",
@@ -211,7 +203,7 @@
}, },
"packages/vscode": { "packages/vscode": {
"name": "openchamber", "name": "openchamber",
"version": "1.9.5", "version": "1.9.6",
"dependencies": { "dependencies": {
"@openchamber/ui": "workspace:*", "@openchamber/ui": "workspace:*",
"@opencode-ai/sdk": "^1.4.6", "@opencode-ai/sdk": "^1.4.6",
@@ -234,11 +226,12 @@
}, },
"packages/web": { "packages/web": {
"name": "@openchamber/web", "name": "@openchamber/web",
"version": "1.9.5", "version": "1.9.6",
"bin": { "bin": {
"openchamber": "./bin/cli.js", "openchamber": "./bin/cli.js",
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.4.0",
"@clack/prompts": "^1.1.0", "@clack/prompts": "^1.1.0",
"@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-go": "^6.0.1", "@codemirror/lang-go": "^6.0.1",
@@ -531,7 +524,7 @@
"@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="],
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
@@ -539,6 +532,10 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@base-ui/react": ["@base-ui/react@1.4.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.2.7", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-QcqdVbr/+ba2/RAKJIV1PV6S02Q5+r6a4Eym8ndBw+ZbBILkkmQAyRxXCg/pArrHnkrGeU8goe26aw0h6eE8pg=="],
"@base-ui/utils": ["@base-ui/utils@0.2.7", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-nXYKhiL/0JafyJE8PfcflipGftOftlIwKd72rU15iZ1M5yqgg5J9P8NHU71GReDuXco5MJA/eVQqUT5WRqX9sA=="],
"@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="], "@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="],
"@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="], "@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="],
@@ -603,6 +600,8 @@
"@codemirror/view": ["@codemirror/view@6.39.13", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw=="], "@codemirror/view": ["@codemirror/view@6.39.13", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw=="],
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
"@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
@@ -681,13 +680,13 @@
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="], "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.7", "", {}, "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w=="], "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.7", "", {}, "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w=="],
@@ -833,7 +832,7 @@
"@openchamber/web": ["@openchamber/web@workspace:packages/web"], "@openchamber/web": ["@openchamber/web@workspace:packages/web"],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.4.6", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-sQaVfEfQW3m3DeCVlurSTUjgIYdIk+gIfOys51MVFYzJHw+FyjjuAt7EKKA+LeZU5AiWGlpkIRa1rJo5KWMXCw=="], "@opencode-ai/sdk": ["@opencode-ai/sdk@1.4.14", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-X/3Zz61iMh8rwESx6UkxgLted/Fuhld41iqGwTA/+ydVfxbxlsS5iBbUoI/D0fdf1c+EVWpNcF7CHydnvBT3RA=="],
"@peculiar/asn1-android": ["@peculiar/asn1-android@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ=="], "@peculiar/asn1-android": ["@peculiar/asn1-android@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ=="],
@@ -909,8 +908,6 @@
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
@@ -1465,6 +1462,8 @@
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
@@ -2379,6 +2378,8 @@
"requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
@@ -2663,6 +2664,8 @@
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
@@ -2943,6 +2946,8 @@
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"react-syntax-highlighter/@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
"read-pkg/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "read-pkg/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], "read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="],
@@ -2971,6 +2976,8 @@
"whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"workbox-build/@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
"workbox-build/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "workbox-build/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"workbox-build/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "workbox-build/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
+1
View File
@@ -90,6 +90,7 @@
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@octokit/rest": "^22.0.1", "@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.4.6", "@opencode-ai/sdk": "^1.4.6",
"@base-ui/react": "^1.4.0",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
+1 -10
View File
@@ -41,16 +41,7 @@
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "^1.4.6", "@opencode-ai/sdk": "^1.4.6",
"@pierre/diffs": "1.1.0-beta.13", "@pierre/diffs": "1.1.0-beta.13",
"@radix-ui/react-collapsible": "^1.1.12", "@base-ui/react": "^1.4.0",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.8",
"@remixicon/react": "^4.7.0", "@remixicon/react": "^4.7.0",
"@simplewebauthn/browser": "13.3.0", "@simplewebauthn/browser": "13.3.0",
"@tanstack/react-virtual": "^3.13.18", "@tanstack/react-virtual": "^3.13.18",
@@ -744,7 +744,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
const isMobile = useUIStore((state) => state.isMobile); const isMobile = useUIStore((state) => state.isMobile);
const inputBarOffset = useUIStore((state) => state.inputBarOffset); const inputBarOffset = useUIStore((state) => state.inputBarOffset);
const isKeyboardOpen = useUIStore((state) => state.isKeyboardOpen); const isKeyboardOpen = useUIStore((state) => state.isKeyboardOpen);
const cornerRadius = useUIStore((state) => state.cornerRadius);
const persistChatDraft = useUIStore((state) => state.persistChatDraft); const persistChatDraft = useUIStore((state) => state.persistChatDraft);
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled); const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
const isExpandedInput = useUIStore((state) => state.isExpandedInput); const isExpandedInput = useUIStore((state) => state.isExpandedInput);
@@ -3293,7 +3292,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
> >
<SelectTrigger <SelectTrigger
size="sm" size="sm"
className="h-7 min-w-0 w-fit max-w-[42vw] sm:max-w-[18rem] border-transparent bg-transparent px-1.5 hover:bg-transparent data-[state=open]:bg-transparent" className="h-7 min-w-0 w-fit max-w-[42vw] sm:max-w-[18rem] border-transparent bg-transparent px-1.5 hover:bg-transparent data-[popup-open]:bg-transparent"
> >
<SelectValue> <SelectValue>
{renderProjectLabelWithIcon(selectedDraftProject)} {renderProjectLabelWithIcon(selectedDraftProject)}
@@ -3315,7 +3314,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
> >
<SelectTrigger <SelectTrigger
size="sm" size="sm"
className="h-7 min-w-0 w-fit max-w-[48vw] sm:max-w-[20rem] border-transparent bg-transparent px-1.5 hover:bg-transparent data-[state=open]:bg-transparent" className="h-7 min-w-0 w-fit max-w-[48vw] sm:max-w-[20rem] border-transparent bg-transparent px-1.5 hover:bg-transparent data-[popup-open]:bg-transparent"
> >
<SelectValue> <SelectValue>
{selectedDraftBranchLabel ?? 'Branch'} {selectedDraftBranchLabel ?? 'Branch'}
@@ -3496,6 +3495,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
</div> </div>
)} )}
<Textarea <Textarea
simple
ref={textareaRef} ref={textareaRef}
data-chat-input="true" data-chat-input="true"
value={message} value={message}
@@ -3528,7 +3528,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
autoCapitalize={isMobile ? "sentences" : "off"} autoCapitalize={isMobile ? "sentences" : "off"}
spellCheck={isMobile || inputSpellcheckEnabled} spellCheck={isMobile || inputSpellcheckEnabled}
fillContainer={isDesktopExpanded} fillContainer={isDesktopExpanded}
outerClassName={cn('focus-within:ring-0', isDesktopExpanded && 'flex-1 min-h-0')} outerClassName={cn('ring-0 bg-transparent shadow-none hover:bg-transparent focus-within:ring-0', isDesktopExpanded && 'flex-1 min-h-0')}
className={cn( className={cn(
'min-h-[52px] resize-none border-0 px-3 rounded-b-none appearance-none hover:border-transparent bg-transparent relative z-10', 'min-h-[52px] resize-none border-0 px-3 rounded-b-none appearance-none hover:border-transparent bg-transparent relative z-10',
isDesktopExpanded isDesktopExpanded
@@ -3682,7 +3682,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
</div> </div>
{/* Mobile Session Status Bar - above input */} {/* Mobile Session Status Bar - above input */}
{isMobile && <MobileSessionStatusBar cornerRadius={cornerRadius} />} {isMobile && <MobileSessionStatusBar />}
</div> </div>
</div> </div>
</form> </form>
@@ -58,7 +58,6 @@ import { useNotificationStore } from '@/sync/notification-store';
interface MobileSessionStatusBarProps { interface MobileSessionStatusBarProps {
onSessionSwitch?: (sessionId: string) => void; onSessionSwitch?: (sessionId: string) => void;
cornerRadius?: number;
} }
interface SessionWithStatus extends Session { interface SessionWithStatus extends Session {
@@ -1161,7 +1160,6 @@ function CollapsedView({
currentProjectColor, currentProjectColor,
onToggle, onToggle,
onNewSession, onNewSession,
cornerRadius,
contextUsage, contextUsage,
childIndicators = [], childIndicators = [],
}: { }: {
@@ -1175,7 +1173,6 @@ function CollapsedView({
currentProjectColor?: string | null; currentProjectColor?: string | null;
onToggle: () => void; onToggle: () => void;
onNewSession: () => void; onNewSession: () => void;
cornerRadius?: number;
contextUsage: SessionContextUsage | null; contextUsage: SessionContextUsage | null;
childIndicators?: Array<{ session: Session; isRunning: boolean }>; childIndicators?: Array<{ session: Session; isRunning: boolean }>;
}) { }) {
@@ -1185,8 +1182,8 @@ function CollapsedView({
<div <div
className="w-full flex items-center justify-between px-2 py-1 border-b border-[var(--interactive-border)] bg-[var(--surface-muted)] order-first text-left overflow-hidden" className="w-full flex items-center justify-between px-2 py-1 border-b border-[var(--interactive-border)] bg-[var(--surface-muted)] order-first text-left overflow-hidden"
style={{ style={{
borderTopLeftRadius: cornerRadius, borderTopLeftRadius: 'var(--radius-lg)',
borderTopRightRadius: cornerRadius, borderTopRightRadius: 'var(--radius-lg)',
}} }}
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove} onTouchMove={handleTouchMove}
@@ -1251,7 +1248,6 @@ function ExpandedView({
getSessionAgentName, getSessionAgentName,
getSessionTitle, getSessionTitle,
needsAttention, needsAttention,
cornerRadius,
contextUsage, contextUsage,
projects, projects,
activeProjectId, activeProjectId,
@@ -1280,7 +1276,6 @@ function ExpandedView({
getSessionAgentName: (s: Session) => string; getSessionAgentName: (s: Session) => string;
getSessionTitle: (s: Session) => string; getSessionTitle: (s: Session) => string;
needsAttention: (sessionId: string) => boolean; needsAttention: (sessionId: string) => boolean;
cornerRadius?: number;
contextUsage: SessionContextUsage | null; contextUsage: SessionContextUsage | null;
projects: ProjectEntry[]; projects: ProjectEntry[];
activeProjectId: string | null; activeProjectId: string | null;
@@ -1336,8 +1331,8 @@ function ExpandedView({
<div <div
className="w-full border-b border-[var(--interactive-border)] bg-[var(--surface-muted)] order-first overflow-hidden" className="w-full border-b border-[var(--interactive-border)] bg-[var(--surface-muted)] order-first overflow-hidden"
style={{ style={{
borderTopLeftRadius: cornerRadius, borderTopLeftRadius: 'var(--radius-lg)',
borderTopRightRadius: cornerRadius, borderTopRightRadius: 'var(--radius-lg)',
}} }}
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove} onTouchMove={handleTouchMove}
@@ -1428,7 +1423,6 @@ function ExpandedView({
export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
onSessionSwitch, onSessionSwitch,
cornerRadius,
}) => { }) => {
const { currentTheme } = useThemeSystem(); const { currentTheme } = useThemeSystem();
const sessions = useSessions(); const sessions = useSessions();
@@ -1557,7 +1551,6 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
currentProjectColor={currentProjectColor} currentProjectColor={currentProjectColor}
onToggle={() => setIsMobileSessionStatusBarCollapsed(false)} onToggle={() => setIsMobileSessionStatusBarCollapsed(false)}
onNewSession={handleCreateSession} onNewSession={handleCreateSession}
cornerRadius={cornerRadius}
contextUsage={contextUsage} contextUsage={contextUsage}
childIndicators={currentSessionChildIndicators} childIndicators={currentSessionChildIndicators}
/> />
@@ -1590,7 +1583,6 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
getSessionAgentName={getSessionAgentName} getSessionAgentName={getSessionAgentName}
getSessionTitle={getSessionTitle} getSessionTitle={getSessionTitle}
needsAttention={needsAttention} needsAttention={needsAttention}
cornerRadius={cornerRadius}
contextUsage={contextUsage} contextUsage={contextUsage}
projects={projects} projects={projects}
activeProjectId={activeProjectId} activeProjectId={activeProjectId}
@@ -136,15 +136,14 @@ export function InlineCommentInput({
)} )}
<Textarea <Textarea
simple
ref={textareaRef} ref={textareaRef}
value={text} value={text}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Add a comment... (Cmd+Enter to save)" placeholder="Add a comment... (Cmd+Enter to save)"
className="min-h-[80px] text-sm resize-y" outerClassName="rounded-[var(--radius-xl)] bg-[var(--surface-subtle)] ring-1 ring-inset ring-border/60 focus-within:ring-2 focus-within:ring-[var(--interactive-focus-ring)]"
style={{ className="min-h-[80px] px-3 py-2.5 text-sm resize-y"
backgroundColor: currentTheme?.colors?.surface?.subtle,
}}
/> />
<div className="flex items-center justify-end gap-2 mt-3"> <div className="flex items-center justify-end gap-2 mt-3">
@@ -131,8 +131,9 @@ export const OpenInAppButton = ({ directory, activeFilePath, className }: OpenIn
return ( return (
<div <div
className={cn( className={cn(
'app-region-no-drag inline-flex h-7 items-center self-center rounded-md border border-[var(--interactive-border)]', 'app-region-no-drag inline-flex h-7 items-center self-center rounded-[9px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px]',
'bg-[var(--surface-elevated)] shadow-none overflow-hidden', 'bg-[var(--surface-elevated)] overflow-hidden',
'border border-border/60',
className className
)} )}
> >
@@ -132,16 +132,17 @@ export const BottomTerminalDock: React.FC<BottomTerminalDockProps> = ({ isOpen,
<section <section
ref={dockRef} ref={dockRef}
className={cn( className={cn(
'relative flex overflow-hidden border-t border-border bg-sidebar', 'flex overflow-hidden border-t border-border bg-sidebar',
isFullscreen ? 'absolute inset-0 z-40' : 'relative',
isResizing ? 'transition-none' : 'transition-[height] duration-300 ease-in-out', isResizing ? 'transition-none' : 'transition-[height] duration-300 ease-in-out',
!isOpen && 'border-t-0' !isOpen && 'border-t-0'
)} )}
style={{ style={isFullscreen ? undefined : {
height: `${appliedHeight}px`, height: `${appliedHeight}px`,
minHeight: `${appliedHeight}px`, minHeight: `${appliedHeight}px`,
maxHeight: `${appliedHeight}px`, maxHeight: `${appliedHeight}px`,
}} }}
aria-hidden={!isOpen || appliedHeight === 0} aria-hidden={!isOpen || (!isFullscreen && appliedHeight === 0)}
> >
{isOpen && !isFullscreen && ( {isOpen && !isFullscreen && (
<div <div
@@ -427,7 +427,7 @@ export const ContextPanel: React.FC = () => {
const isFileTabActive = activeTab?.mode === 'file'; const isFileTabActive = activeTab?.mode === 'file';
const header = ( const header = (
<header className="flex h-10 items-stretch border-b border-transparent"> <header className="flex h-8 items-stretch border-b border-transparent">
<SortableTabsStrip <SortableTabsStrip
items={tabItems} items={tabItems}
activeId={activeTab?.id ?? null} activeId={activeTab?.id ?? null}
@@ -450,11 +450,9 @@ export const ContextPanel: React.FC = () => {
reorderContextPanelTabs(directoryKey, activeTabID, overTabID); reorderContextPanelTabs(directoryKey, activeTabID, overTabID);
}} }}
layoutMode="scrollable" layoutMode="scrollable"
variant="active-pill" variant="default"
activePillLowercase={false}
activePillInsetClassName="gap-0.5 pt-0.5 pb-1.5"
/> />
<div className="flex items-end gap-1 px-1.5 pb-1.5"> <div className="flex items-center gap-1 px-1.5">
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@@ -846,10 +846,10 @@ export const MainLayout: React.FC = () => {
)} )}
style={{ style={{
left: `${visibleSidebarWidth}px`, left: `${visibleSidebarWidth}px`,
width: 'var(--radius-md)', width: '10px',
height: 'var(--radius-md)', height: '10px',
WebkitMaskImage: 'radial-gradient(circle at 100% 100%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))', WebkitMaskImage: 'radial-gradient(circle at 100% 100%, transparent calc(10px - 1px), black 10px)',
maskImage: 'radial-gradient(circle at 100% 100%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))', maskImage: 'radial-gradient(circle at 100% 100%, transparent calc(10px - 1px), black 10px)',
}} }}
/> />
<div <div
@@ -860,10 +860,10 @@ export const MainLayout: React.FC = () => {
)} )}
style={{ style={{
left: `${visibleSidebarWidth}px`, left: `${visibleSidebarWidth}px`,
width: 'var(--radius-md)', width: '10px',
height: 'var(--radius-md)', height: '10px',
WebkitMaskImage: 'radial-gradient(circle at 100% 0%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))', WebkitMaskImage: 'radial-gradient(circle at 100% 0%, transparent calc(10px - 1px), black 10px)',
maskImage: 'radial-gradient(circle at 100% 0%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))', maskImage: 'radial-gradient(circle at 100% 0%, transparent calc(10px - 1px), black 10px)',
}} }}
/> />
</> </>
@@ -878,10 +878,10 @@ export const MainLayout: React.FC = () => {
)} )}
style={{ style={{
right: `${visibleRightSidebarWidth}px`, right: `${visibleRightSidebarWidth}px`,
width: 'var(--radius-md)', width: '10px',
height: 'var(--radius-md)', height: '10px',
WebkitMaskImage: 'radial-gradient(circle at 0 100%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))', WebkitMaskImage: 'radial-gradient(circle at 0 100%, transparent calc(10px - 1px), black 10px)',
maskImage: 'radial-gradient(circle at 0 100%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))', maskImage: 'radial-gradient(circle at 0 100%, transparent calc(10px - 1px), black 10px)',
}} }}
/> />
<div <div
@@ -892,10 +892,10 @@ export const MainLayout: React.FC = () => {
)} )}
style={{ style={{
right: `${visibleRightSidebarWidth}px`, right: `${visibleRightSidebarWidth}px`,
width: 'var(--radius-md)', width: '10px',
height: 'var(--radius-md)', height: '10px',
WebkitMaskImage: 'radial-gradient(circle at 0 0, transparent calc(var(--radius-md) - 1px), black var(--radius-md))', WebkitMaskImage: 'radial-gradient(circle at 0 0, transparent calc(10px - 1px), black 10px)',
maskImage: 'radial-gradient(circle at 0 0, transparent calc(var(--radius-md) - 1px), black var(--radius-md))', maskImage: 'radial-gradient(circle at 0 0, transparent calc(10px - 1px), black 10px)',
}} }}
/> />
</> </>
@@ -910,8 +910,8 @@ export const MainLayout: React.FC = () => {
<div className={cn( <div className={cn(
'relative flex flex-1 min-w-0 flex-col overflow-hidden', 'relative flex flex-1 min-w-0 flex-col overflow-hidden',
'bg-sidebar', 'bg-sidebar',
isSidebarOpen && 'border-l border-border/50 rounded-tl-md rounded-bl-md', isSidebarOpen && 'border-l border-border/50 rounded-tl-[10px] rounded-bl-[10px]',
isRightSidebarOpen && 'border-r border-border/50 rounded-tr-md rounded-br-md' isRightSidebarOpen && 'border-r border-border/50 rounded-tr-[10px] rounded-br-[10px]'
)}> )}>
<Header desktopRightSidebarActionsHost={desktopRightSidebarActionsHost} /> <Header desktopRightSidebarActionsHost={desktopRightSidebarActionsHost} />
<div className={cn( <div className={cn(
@@ -640,7 +640,7 @@ export const ProjectActionsButton = ({
<button <button
type="button" type="button"
className={cn( className={cn(
'app-region-no-drag inline-flex h-9 w-9 items-center justify-center rounded-md p-2', 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center rounded-[10px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px] p-2',
'typography-ui-label font-medium text-muted-foreground hover:bg-interactive-hover hover:text-foreground transition-colors', 'typography-ui-label font-medium text-muted-foreground hover:bg-interactive-hover hover:text-foreground transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
className className
@@ -657,9 +657,10 @@ export const ProjectActionsButton = ({
<button <button
type="button" type="button"
className={cn( className={cn(
'app-region-no-drag inline-flex h-7 shrink-0 items-center gap-2 self-center rounded-md border border-[var(--interactive-border)]', 'app-region-no-drag inline-flex h-7 shrink-0 items-center gap-2 self-center rounded-[9px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px]',
'bg-[var(--surface-elevated)] px-3 typography-ui-label font-medium text-foreground hover:bg-interactive-hover transition-colors', 'bg-[var(--surface-elevated)] px-3 typography-ui-label font-medium text-foreground hover:bg-interactive-hover transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
'border border-border/60',
className className
)} )}
onClick={openProjectActionsSettings} onClick={openProjectActionsSettings}
@@ -690,7 +691,7 @@ export const ProjectActionsButton = ({
type="button" type="button"
disabled={isLoading || isStoppingSelected} disabled={isLoading || isStoppingSelected}
className={cn( className={cn(
'app-region-no-drag inline-flex h-9 w-9 items-center justify-center rounded-md p-2', 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center rounded-[10px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px] p-2',
'typography-ui-label font-medium text-muted-foreground hover:bg-interactive-hover hover:text-foreground transition-colors', 'typography-ui-label font-medium text-muted-foreground hover:bg-interactive-hover hover:text-foreground transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
@@ -745,8 +746,9 @@ export const ProjectActionsButton = ({
return ( return (
<div <div
className={cn( className={cn(
'app-region-no-drag inline-flex shrink-0 items-center self-center rounded-md border border-[var(--interactive-border)]', 'app-region-no-drag inline-flex shrink-0 items-center self-center rounded-[9px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px]',
'bg-[var(--surface-elevated)] shadow-none overflow-hidden', 'bg-[var(--surface-elevated)] overflow-hidden',
'border border-border/60',
compact ? 'h-9' : 'h-7', compact ? 'h-9' : 'h-7',
className className
)} )}
@@ -657,7 +657,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
Last updated {formatTime(quotaLastUpdated)} Last updated {formatTime(quotaLastUpdated)}
</div> </div>
{!hasRateLimits && ( {!hasRateLimits && (
<DropdownMenuItem className="cursor-default" onSelect={(event) => event.preventDefault()}> <DropdownMenuItem className="cursor-default" closeOnClick={false}>
<span className="typography-ui-label text-muted-foreground">No rate limits available.</span> <span className="typography-ui-label text-muted-foreground">No rate limits available.</span>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
@@ -671,7 +671,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
<DropdownMenuItem <DropdownMenuItem
key={`${group.providerId}-empty`} key={`${group.providerId}-empty`}
className="cursor-default" className="cursor-default"
onSelect={(event) => event.preventDefault()} closeOnClick={false}
> >
<span className="typography-ui-label text-muted-foreground"> <span className="typography-ui-label text-muted-foreground">
{group.error ?? 'No rate limits reported.'} {group.error ?? 'No rate limits reported.'}
@@ -692,7 +692,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
<DropdownMenuItem <DropdownMenuItem
key={`${group.providerId}-${label}`} key={`${group.providerId}-${label}`}
className="cursor-default items-start" className="cursor-default items-start"
onSelect={(event) => event.preventDefault()} closeOnClick={false}
> >
<span className="flex min-w-0 flex-1 flex-col gap-2"> <span className="flex min-w-0 flex-1 flex-col gap-2">
<span className="flex min-w-0 items-center justify-between gap-3"> <span className="flex min-w-0 items-center justify-between gap-3">
@@ -155,7 +155,7 @@ export const McpDropdownContent: React.FC<McpDropdownContentProps> = ({ active,
<Switch <Switch
checked={isConnected} checked={isConnected}
disabled={isBusy} disabled={isBusy}
className="data-[state=checked]:bg-status-info" className="data-[checked]:bg-status-info"
onCheckedChange={async (checked) => { onCheckedChange={async (checked) => {
setBusyName(serverName); setBusyName(serverName);
try { try {
@@ -296,7 +296,7 @@ export const McpDropdown: React.FC<McpDropdownProps> = ({ headerIconButtonClass
<Switch <Switch
checked={isConnected} checked={isConnected}
disabled={isBusy} disabled={isBusy}
className="data-[state=checked]:bg-status-info" className="data-[checked]:bg-status-info"
onCheckedChange={async (checked) => { onCheckedChange={async (checked) => {
setBusyName(serverName); setBusyName(serverName);
try { try {
@@ -87,7 +87,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
id={id} id={id}
size="lg" size="lg"
className={cn( className={cn(
'max-w-full typography-meta text-foreground !border-border/80 !bg-[var(--surface-subtle)] hover:!bg-[var(--interactive-hover)]/70 data-[state=open]:!bg-[var(--interactive-active)]/70', 'max-w-full typography-meta text-foreground !border-border/80 !bg-[var(--surface-subtle)] hover:!bg-[var(--interactive-hover)]/70 data-[popup-open]:!bg-[var(--interactive-active)]/70',
className, className,
)} )}
> >
@@ -684,41 +684,29 @@ export const AgentsPage: React.FC = () => {
</div> </div>
<div className="flex flex-wrap items-center gap-1"> <div className="flex flex-wrap items-center gap-1">
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
aria-pressed={mode === 'primary'}
onClick={() => setMode('primary')} onClick={() => setMode('primary')}
className={cn( className="!font-normal"
'!font-normal',
mode === 'primary'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
Primary Primary
</Button> </Button>
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
aria-pressed={mode === 'subagent'}
onClick={() => setMode('subagent')} onClick={() => setMode('subagent')}
className={cn( className="!font-normal"
'!font-normal',
mode === 'subagent'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
Subagent Subagent
</Button> </Button>
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
aria-pressed={mode === 'all'}
onClick={() => setMode('all')} onClick={() => setMode('all')}
className={cn( className="!font-normal"
'!font-normal',
mode === 'all'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
All All
</Button> </Button>
@@ -515,7 +515,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}> <DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<div className={cn( <div className={cn(
'border-input data-[placeholder]:text-muted-foreground flex items-center justify-between gap-2 rounded-lg border bg-transparent px-2 py-2 typography-ui-label whitespace-nowrap shadow-none outline-none hover:bg-interactive-hover data-[state=open]:bg-interactive-active h-6 w-fit', 'border-input data-[placeholder]:text-muted-foreground flex items-center justify-between gap-2 rounded-lg border bg-transparent px-2 py-2 typography-ui-label whitespace-nowrap shadow-none outline-none hover:bg-interactive-hover data-[popup-open]:bg-interactive-active h-6 w-fit',
className className
)}> )}>
{providerId ? ( {providerId ? (
@@ -352,25 +352,17 @@ export const GitIdentityEditorDialog: React.FC<GitIdentityEditorDialogProps> = (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button size="sm" <Button size="sm"
type="button" type="button"
variant="outline" variant="chip"
aria-pressed={authType === 'ssh'}
onClick={() => setAuthType('ssh')} onClick={() => setAuthType('ssh')}
className={cn(
authType === 'ssh'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
<RiLock2Line className="w-3.5 h-3.5 mr-1" /> SSH <RiLock2Line className="w-3.5 h-3.5 mr-1" /> SSH
</Button> </Button>
<Button size="sm" <Button size="sm"
type="button" type="button"
variant="outline" variant="chip"
aria-pressed={authType === 'token'}
onClick={() => setAuthType('token')} onClick={() => setAuthType('token')}
className={cn(
authType === 'token'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
<RiKeyLine className="w-3.5 h-3.5 mr-1" /> Token <RiKeyLine className="w-3.5 h-3.5 mr-1" /> Token
</Button> </Button>
@@ -617,28 +617,20 @@ export const McpPage: React.FC = () => {
<span className="typography-ui-label text-foreground">Transport Mode</span> <span className="typography-ui-label text-foreground">Transport Mode</span>
<div className="flex flex-wrap items-center gap-1"> <div className="flex flex-wrap items-center gap-1">
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
aria-pressed={mcpType === 'local'}
onClick={() => setMcpType('local')} onClick={() => setMcpType('local')}
className={cn( className="!font-normal"
'!font-normal',
mcpType === 'local'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
Local · stdio Local · stdio
</Button> </Button>
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
aria-pressed={mcpType === 'remote'}
onClick={() => setMcpType('remote')} onClick={() => setMcpType('remote')}
className={cn( className="!font-normal"
'!font-normal',
mcpType === 'remote'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
Remote · SSE Remote · SSE
</Button> </Button>
@@ -145,6 +145,13 @@ export const DefaultsSettings: React.FC = () => {
const DEFAULT_VARIANT_VALUE = '__default__'; const DEFAULT_VARIANT_VALUE = '__default__';
const formatVariantLabel = React.useCallback((variant: string) => {
if (variant === DEFAULT_VARIANT_VALUE) {
return 'Default';
}
return variant.charAt(0).toUpperCase() + variant.slice(1);
}, []);
const handleVariantChange = React.useCallback( const handleVariantChange = React.useCallback(
async (variant: string) => { async (variant: string) => {
const newValue = variant === DEFAULT_VARIANT_VALUE ? undefined : variant || undefined; const newValue = variant === DEFAULT_VARIANT_VALUE ? undefined : variant || undefined;
@@ -251,13 +258,15 @@ export const DefaultsSettings: React.FC = () => {
<div className="flex items-center gap-2 sm:w-fit"> <div className="flex items-center gap-2 sm:w-fit">
<Select value={defaultVariant ?? DEFAULT_VARIANT_VALUE} onValueChange={handleVariantChange} disabled={!supportsVariants}> <Select value={defaultVariant ?? DEFAULT_VARIANT_VALUE} onValueChange={handleVariantChange} disabled={!supportsVariants}>
<SelectTrigger className="w-fit min-w-[120px]"> <SelectTrigger className="w-fit min-w-[120px]">
<SelectValue placeholder="Thinking" /> <SelectValue placeholder="Thinking">
{formatVariantLabel(defaultVariant ?? DEFAULT_VARIANT_VALUE)}
</SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={DEFAULT_VARIANT_VALUE}>Default</SelectItem> <SelectItem value={DEFAULT_VARIANT_VALUE}>Default</SelectItem>
{availableVariants.map((variant) => ( {availableVariants.map((variant) => (
<SelectItem key={variant} value={variant}> <SelectItem key={variant} value={variant}>
{variant} {formatVariantLabel(variant)}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -520,14 +520,10 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
{THEME_MODE_OPTIONS.map((option) => ( {THEME_MODE_OPTIONS.map((option) => (
<Button <Button
key={option.value} key={option.value}
variant="outline" variant="chip"
size="xs" size="xs"
className={cn( aria-pressed={themeMode === option.value}
'!font-normal', className="!font-normal"
themeMode === option.value
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
onClick={() => setThemeMode(option.value)} onClick={() => setThemeMode(option.value)}
> >
{option.label} {option.label}
@@ -542,7 +538,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
<span className="typography-ui-label text-foreground shrink-0">Light Theme</span> <span className="typography-ui-label text-foreground shrink-0">Light Theme</span>
<Select value={selectedLightTheme?.metadata.id ?? ''} onValueChange={setLightThemePreference}> <Select value={selectedLightTheme?.metadata.id ?? ''} onValueChange={setLightThemePreference}>
<SelectTrigger aria-label="Select light theme" className="w-fit"> <SelectTrigger aria-label="Select light theme" className="w-fit">
<SelectValue placeholder="Select theme" /> <SelectValue placeholder="Select theme">
{selectedLightTheme
? formatThemeLabel(selectedLightTheme.metadata.name, 'light')
: undefined}
</SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{lightThemes.map((theme) => ( {lightThemes.map((theme) => (
@@ -557,7 +557,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
<span className="typography-ui-label text-foreground shrink-0">Dark Theme</span> <span className="typography-ui-label text-foreground shrink-0">Dark Theme</span>
<Select value={selectedDarkTheme?.metadata.id ?? ''} onValueChange={setDarkThemePreference}> <Select value={selectedDarkTheme?.metadata.id ?? ''} onValueChange={setDarkThemePreference}>
<SelectTrigger aria-label="Select dark theme" className="w-fit"> <SelectTrigger aria-label="Select dark theme" className="w-fit">
<SelectValue placeholder="Select theme" /> <SelectValue placeholder="Select theme">
{selectedDarkTheme
? formatThemeLabel(selectedDarkTheme.metadata.name, 'dark')
: undefined}
</SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{darkThemes.map((theme) => ( {darkThemes.map((theme) => (
@@ -969,14 +973,10 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
{MESSAGE_STREAM_TRANSPORT_OPTIONS.map((option) => ( {MESSAGE_STREAM_TRANSPORT_OPTIONS.map((option) => (
<Button <Button
key={option.id} key={option.id}
variant="outline" variant="chip"
size="xs" size="xs"
className={cn( aria-pressed={messageStreamTransport === option.id}
'!font-normal', className="!font-normal"
messageStreamTransport === option.id
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
onClick={() => handleMessageStreamTransportChange(option.id)} onClick={() => handleMessageStreamTransportChange(option.id)}
> >
{option.label} {option.label}
@@ -5,7 +5,6 @@ import { toast } from '@/components/ui';
import { NumberInput } from '@/components/ui/number-input'; import { NumberInput } from '@/components/ui/number-input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup'; import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
@@ -124,14 +123,10 @@ export const SessionRetentionSettings: React.FC = () => {
<Button <Button
key={option.value} key={option.value}
type="button" type="button"
variant="outline" variant="chip"
size="xs" size="xs"
className={cn( aria-pressed={sessionRetentionAction === option.value}
'!font-normal', className="!font-normal"
sessionRetentionAction === option.value
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
onClick={() => setSessionRetentionAction(option.value)} onClick={() => setSessionRetentionAction(option.value)}
> >
{option.label} {option.label}
@@ -1178,14 +1178,10 @@ export const TunnelSettings: React.FC = () => {
<Tooltip key={option.value} delayDuration={700}> <Tooltip key={option.value} delayDuration={700}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
className={cn( aria-pressed={tunnelMode === option.value}
'!font-normal', className="!font-normal"
tunnelMode === option.value
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
onClick={() => { onClick={() => {
void handleModeChange(option.value); void handleModeChange(option.value);
}} }}
@@ -457,55 +457,39 @@ export const VoiceSettings: React.FC = () => {
</div> </div>
<div className="flex flex-wrap items-center gap-1"> <div className="flex flex-wrap items-center gap-1">
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
aria-pressed={voiceProvider === 'browser'}
onClick={() => setVoiceProvider('browser')} onClick={() => setVoiceProvider('browser')}
className={cn( className="!font-normal"
'!font-normal',
voiceProvider === 'browser'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
Browser Browser
</Button> </Button>
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
aria-pressed={voiceProvider === 'openai'}
onClick={() => setVoiceProvider('openai')} onClick={() => setVoiceProvider('openai')}
className={cn( className="!font-normal"
'!font-normal',
voiceProvider === 'openai'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
OpenAI OpenAI
</Button> </Button>
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
aria-pressed={voiceProvider === 'openai-compatible'}
onClick={() => setVoiceProvider('openai-compatible')} onClick={() => setVoiceProvider('openai-compatible')}
className={cn( className="!font-normal"
'!font-normal',
voiceProvider === 'openai-compatible'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
Custom Custom
</Button> </Button>
{isSayAvailable && ( {isSayAvailable && (
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
aria-pressed={voiceProvider === 'say'}
onClick={() => setVoiceProvider('say')} onClick={() => setVoiceProvider('say')}
className={cn( className="!font-normal"
'!font-normal',
voiceProvider === 'say'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
<RiAppleLine className="w-3.5 h-3.5 mr-0.5" /> <RiAppleLine className="w-3.5 h-3.5 mr-0.5" />
Say Say
@@ -756,28 +740,20 @@ export const VoiceSettings: React.FC = () => {
</div> </div>
<div className="flex flex-wrap items-center gap-1"> <div className="flex flex-wrap items-center gap-1">
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
aria-pressed={sttProvider === 'browser'}
onClick={() => setSttProvider('browser')} onClick={() => setSttProvider('browser')}
className={cn( className="!font-normal"
'!font-normal',
sttProvider === 'browser'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
Browser Browser
</Button> </Button>
<Button <Button
variant="outline" variant="chip"
size="xs" size="xs"
aria-pressed={sttProvider === 'server'}
onClick={() => setSttProvider('server')} onClick={() => setSttProvider('server')}
className={cn( className="!font-normal"
'!font-normal',
sttProvider === 'server'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: 'text-foreground'
)}
> >
Server Server
</Button> </Button>
@@ -1587,7 +1587,7 @@ export function NewWorktreeDialog({
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" /> <RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[320px] p-0" ref={existingBranchDropdownContentRef}> <DropdownMenuContent align="start" sideOffset={6} className="w-[320px] p-0 max-h-[min(var(--available-height),24rem)] flex flex-col overflow-hidden" ref={existingBranchDropdownContentRef}>
<Command shouldFilter={false}> <Command shouldFilter={false}>
<CommandInput <CommandInput
placeholder="Search branches..." placeholder="Search branches..."
+120 -159
View File
@@ -14,11 +14,29 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useThemeSystem } from '@/contexts/useThemeSystem';
import { useDeviceInfo } from '@/lib/device'; import { useDeviceInfo } from '@/lib/device';
import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiFileLine, RiGitBranchLine, RiLayoutLeftLine, RiLayoutRightLine, RiMoonLine, RiQuestionLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine } from '@remixicon/react'; import {
RiAddLine,
RiChatAi3Line,
RiCheckLine,
RiClipboardLine,
RiComputerLine,
RiFileLine,
RiFolderLine,
RiGitBranchLine,
RiLayoutLeftLine,
RiLayoutRightLine,
RiMoonLine,
RiPieChartLine,
RiQuestionLine,
RiSettings3Line,
RiSunLine,
RiTerminalBoxLine,
} from '@remixicon/react';
import { createWorktreeSession } from '@/lib/worktreeSessionCreator'; import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts'; import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop'; import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
import { SETTINGS_PAGE_METADATA, SETTINGS_GROUP_LABELS, type SettingsRuntimeContext } from '@/lib/settings/metadata'; import { SETTINGS_PAGE_METADATA, SETTINGS_GROUP_LABELS, type SettingsRuntimeContext } from '@/lib/settings/metadata';
import { getSettingsNavIcon } from '@/components/views/SettingsView';
export const CommandPalette: React.FC = () => { export const CommandPalette: React.FC = () => {
const isCommandPaletteOpen = useUIStore((s) => s.isCommandPaletteOpen); const isCommandPaletteOpen = useUIStore((s) => s.isCommandPaletteOpen);
@@ -36,6 +54,8 @@ export const CommandPalette: React.FC = () => {
const toggleBottomTerminal = useUIStore((s) => s.toggleBottomTerminal); const toggleBottomTerminal = useUIStore((s) => s.toggleBottomTerminal);
const setBottomTerminalExpanded = useUIStore((s) => s.setBottomTerminalExpanded); const setBottomTerminalExpanded = useUIStore((s) => s.setBottomTerminalExpanded);
const isBottomTerminalExpanded = useUIStore((s) => s.isBottomTerminalExpanded); const isBottomTerminalExpanded = useUIStore((s) => s.isBottomTerminalExpanded);
const openContextOverview = useUIStore((s) => s.openContextOverview);
const openContextPlan = useUIStore((s) => s.openContextPlan);
const shortcutOverrides = useUIStore((s) => s.shortcutOverrides); const shortcutOverrides = useUIStore((s) => s.shortcutOverrides);
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft); const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
@@ -44,134 +64,84 @@ export const CommandPalette: React.FC = () => {
const currentDirectory = useDirectoryStore((s) => s.currentDirectory); const currentDirectory = useDirectoryStore((s) => s.currentDirectory);
const { themeMode, setThemeMode } = useThemeSystem(); const { themeMode, setThemeMode } = useThemeSystem();
const { isMobile } = useDeviceInfo();
const handleClose = () => { const close = React.useCallback(() => setCommandPaletteOpen(false), [setCommandPaletteOpen]);
setCommandPaletteOpen(false);
};
const handleCreateSession = async () => { const run = React.useCallback((fn: () => void | Promise<void>) => async () => {
close();
await fn();
}, [close]);
const handleCreateSession = run(() => {
setActiveMainTab('chat'); setActiveMainTab('chat');
setSessionSwitcherOpen(false); setSessionSwitcherOpen(false);
openNewSessionDraft(); openNewSessionDraft();
handleClose(); });
};
const handleOpenSession = (sessionId: string, directoryHint?: string | null) => { const handleOpenQuickOpen = run(() => setQuickOpenOpen(true));
setCurrentSession(sessionId, directoryHint ?? null);
handleClose();
};
const handleSetThemeMode = (mode: 'light' | 'dark' | 'system') => { const handleCreateWorktreeSession = run(() => {
setThemeMode(mode);
handleClose();
};
const handleShowHelp = () => {
setHelpDialogOpen(true);
handleClose();
};
const handleOpenQuickOpen = () => {
setQuickOpenOpen(true);
handleClose();
};
const handleCreateWorktreeSession = () => {
handleClose();
createWorktreeSession(); createWorktreeSession();
}; });
const { isMobile } = useDeviceInfo(); const handleOpenSessionList = run(() => {
const handleOpenSessionList = () => {
if (isMobile) { if (isMobile) {
const { isSessionSwitcherOpen } = useUIStore.getState(); const { isSessionSwitcherOpen } = useUIStore.getState();
setSessionSwitcherOpen(!isSessionSwitcherOpen); setSessionSwitcherOpen(!isSessionSwitcherOpen);
} else { } else {
toggleSidebar(); toggleSidebar();
} }
handleClose(); });
};
const handleOpenDiffPanel = () => { const handleToggleRightSidebar = run(() => toggleRightSidebar());
setActiveMainTab('diff'); const handleOpenRightSidebarGit = run(() => { setRightSidebarOpen(true); setRightSidebarTab('git'); });
handleClose(); const handleOpenRightSidebarFiles = run(() => { setRightSidebarOpen(true); setRightSidebarTab('files'); });
}; const handleToggleTerminalDock = run(() => toggleBottomTerminal());
const handleToggleTerminalExpanded = run(() => setBottomTerminalExpanded(!isBottomTerminalExpanded));
const handleShowHelp = run(() => setHelpDialogOpen(true));
const handleOpenSettings = run(() => setSettingsDialogOpen(true));
const handleShowContextUsage = run(() => {
if (currentDirectory) openContextOverview(currentDirectory);
});
const handleShowPlan = run(() => {
if (currentDirectory) openContextPlan(currentDirectory);
});
const handleOpenGitPanel = () => { const handleOpenSettingsPage = (slug: string) => run(() => {
setActiveMainTab('git');
handleClose();
};
const handleOpenTerminal = () => {
setActiveMainTab('terminal');
handleClose();
};
const handleOpenSettings = () => {
setSettingsDialogOpen(true);
handleClose();
};
const handleOpenSettingsPage = (slug: string) => {
setSettingsPage(slug); setSettingsPage(slug);
setSettingsDialogOpen(true); setSettingsDialogOpen(true);
handleClose(); });
};
const handleOpenSession = (sessionId: string, directoryHint?: string | null) => run(() => {
setCurrentSession(sessionId, directoryHint ?? null);
});
const handleSetThemeMode = (mode: 'light' | 'dark' | 'system') => run(() => {
setThemeMode(mode);
});
const settingsRuntimeCtx = React.useMemo<SettingsRuntimeContext>(() => { const settingsRuntimeCtx = React.useMemo<SettingsRuntimeContext>(() => {
const isDesktop = isDesktopShell(); const isDesktop = isDesktopShell();
return { isVSCode: isVSCodeRuntime(), isWeb: !isDesktop && isWebRuntime(), isDesktop }; return { isVSCode: isVSCodeRuntime(), isWeb: !isDesktop && isWebRuntime(), isDesktop };
}, []); }, []);
const settingsPages = React.useMemo(() => { const settingsItems = React.useMemo(() => {
const groupLabel = (g: string) => (SETTINGS_GROUP_LABELS as Record<string, string>)[g] ?? g;
return SETTINGS_PAGE_METADATA return SETTINGS_PAGE_METADATA
.filter((p) => p.slug !== 'home') .filter((p) => p.slug !== 'home')
.filter((p) => (p.isAvailable ? p.isAvailable(settingsRuntimeCtx) : true)); .filter((p) => (p.isAvailable ? p.isAvailable(settingsRuntimeCtx) : true))
}, [settingsRuntimeCtx]);
const settingsItems = React.useMemo(() => {
const groupLabel = (group: string) => (SETTINGS_GROUP_LABELS as Record<string, string>)[group] ?? group;
return settingsPages
.slice() .slice()
.sort((a, b) => { .sort((a, b) => {
const g = groupLabel(a.group).localeCompare(groupLabel(b.group)); const g = groupLabel(a.group).localeCompare(groupLabel(b.group));
if (g !== 0) return g; if (g !== 0) return g;
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title);
}); });
}, [settingsPages]); }, [settingsRuntimeCtx]);
const handleToggleRightSidebar = () => { const recentSessions = React.useMemo(() => {
toggleRightSidebar(); return getSessionsByDirectory(currentDirectory ?? '').slice(0, 5);
handleClose(); }, [getSessionsByDirectory, currentDirectory]);
};
const handleOpenRightSidebarGit = () => {
setRightSidebarOpen(true);
setRightSidebarTab('git');
handleClose();
};
const handleOpenRightSidebarFiles = () => {
setRightSidebarOpen(true);
setRightSidebarTab('files');
handleClose();
};
const handleToggleTerminalDock = () => {
toggleBottomTerminal();
handleClose();
};
const handleToggleTerminalExpanded = () => {
setBottomTerminalExpanded(!isBottomTerminalExpanded);
handleClose();
};
const directorySessions = getSessionsByDirectory(currentDirectory ?? '');
const currentSessions = React.useMemo(() => {
return directorySessions.slice(0, 5);
}, [directorySessions]);
const shortcut = React.useCallback((actionId: string) => { const shortcut = React.useCallback((actionId: string) => {
return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides)); return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides));
@@ -183,12 +153,7 @@ export const CommandPalette: React.FC = () => {
<CommandList> <CommandList>
<CommandEmpty>No results found.</CommandEmpty> <CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Actions"> <CommandGroup heading="Sessions">
<CommandItem onSelect={handleOpenSessionList}>
<RiLayoutLeftLine className="mr-2 h-4 w-4" />
<span>Open Session List</span>
<CommandShortcut>{shortcut('toggle_sidebar')}</CommandShortcut>
</CommandItem>
<CommandItem onSelect={handleOpenQuickOpen}> <CommandItem onSelect={handleOpenQuickOpen}>
<RiFileLine className="mr-2 h-4 w-4" /> <RiFileLine className="mr-2 h-4 w-4" />
<span>Quick Open</span> <span>Quick Open</span>
@@ -197,17 +162,23 @@ export const CommandPalette: React.FC = () => {
<CommandItem onSelect={handleCreateSession}> <CommandItem onSelect={handleCreateSession}>
<RiAddLine className="mr-2 h-4 w-4" /> <RiAddLine className="mr-2 h-4 w-4" />
<span>New Session</span> <span>New Session</span>
<CommandShortcut> <CommandShortcut>{shortcut('new_chat')}</CommandShortcut>
{shortcut('new_chat')}
</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem onSelect={handleCreateWorktreeSession}> <CommandItem onSelect={handleCreateWorktreeSession}>
<RiGitBranchLine className="mr-2 h-4 w-4" /> <RiGitBranchLine className="mr-2 h-4 w-4" />
<span>New Worktree Draft</span> <span>New Worktree Draft</span>
<CommandShortcut> <CommandShortcut>{shortcut('new_chat_worktree')}</CommandShortcut>
{shortcut('new_chat_worktree')}
</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem onSelect={handleOpenSessionList}>
<RiLayoutLeftLine className="mr-2 h-4 w-4" />
<span>{isMobile ? 'Show Session Switcher' : 'Toggle Sidebar'}</span>
<CommandShortcut>{shortcut('toggle_sidebar')}</CommandShortcut>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="View">
<CommandItem onSelect={handleToggleRightSidebar}> <CommandItem onSelect={handleToggleRightSidebar}>
<RiLayoutRightLine className="mr-2 h-4 w-4" /> <RiLayoutRightLine className="mr-2 h-4 w-4" />
<span>Toggle Right Sidebar</span> <span>Toggle Right Sidebar</span>
@@ -215,22 +186,30 @@ export const CommandPalette: React.FC = () => {
</CommandItem> </CommandItem>
<CommandItem onSelect={handleOpenRightSidebarGit}> <CommandItem onSelect={handleOpenRightSidebarGit}>
<RiGitBranchLine className="mr-2 h-4 w-4" /> <RiGitBranchLine className="mr-2 h-4 w-4" />
<span>Open Right Sidebar Git</span> <span>Show Git in Right Sidebar</span>
<CommandShortcut>{shortcut('open_right_sidebar_git')}</CommandShortcut> <CommandShortcut>{shortcut('open_right_sidebar_git')}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem onSelect={handleOpenRightSidebarFiles}> <CommandItem onSelect={handleOpenRightSidebarFiles}>
<RiLayoutRightLine className="mr-2 h-4 w-4" /> <RiFolderLine className="mr-2 h-4 w-4" />
<span>Open Right Sidebar Files</span> <span>Show Files in Right Sidebar</span>
<CommandShortcut>{shortcut('open_right_sidebar_files')}</CommandShortcut> <CommandShortcut>{shortcut('open_right_sidebar_files')}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem onSelect={handleShowContextUsage}>
<RiPieChartLine className="mr-2 h-4 w-4" />
<span>Show Context Usage</span>
</CommandItem>
<CommandItem onSelect={handleShowPlan}>
<RiClipboardLine className="mr-2 h-4 w-4" />
<span>Show Plan</span>
</CommandItem>
<CommandItem onSelect={handleToggleTerminalDock}> <CommandItem onSelect={handleToggleTerminalDock}>
<RiTerminalBoxLine className="mr-2 h-4 w-4" /> <RiTerminalBoxLine className="mr-2 h-4 w-4" />
<span>Toggle Terminal Dock</span> <span>Toggle Terminal</span>
<CommandShortcut>{shortcut('toggle_terminal')}</CommandShortcut> <CommandShortcut>{shortcut('toggle_terminal')}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem onSelect={handleToggleTerminalExpanded}> <CommandItem onSelect={handleToggleTerminalExpanded}>
<RiTerminalBoxLine className="mr-2 h-4 w-4" /> <RiTerminalBoxLine className="mr-2 h-4 w-4" />
<span>Toggle Terminal Expanded</span> <span>{isBottomTerminalExpanded ? 'Collapse Terminal' : 'Expand Terminal'}</span>
<CommandShortcut>{shortcut('toggle_terminal_expanded')}</CommandShortcut> <CommandShortcut>{shortcut('toggle_terminal_expanded')}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem onSelect={handleShowHelp}> <CommandItem onSelect={handleShowHelp}>
@@ -238,81 +217,63 @@ export const CommandPalette: React.FC = () => {
<span>Keyboard Shortcuts</span> <span>Keyboard Shortcuts</span>
<CommandShortcut>{shortcut('open_help')}</CommandShortcut> <CommandShortcut>{shortcut('open_help')}</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem onSelect={handleOpenDiffPanel}>
<RiCodeLine className="mr-2 h-4 w-4" />
<span>Open Diff Panel</span>
<CommandShortcut>{shortcut('open_diff_panel')}</CommandShortcut>
</CommandItem>
<CommandItem onSelect={handleOpenTerminal}>
<RiTerminalBoxLine className="mr-2 h-4 w-4" />
<span>Open Terminal</span>
<CommandShortcut>{shortcut('open_terminal_panel')}</CommandShortcut>
</CommandItem>
<CommandItem onSelect={handleOpenGitPanel}>
<RiGitBranchLine className="mr-2 h-4 w-4" />
<span>Open Git Panel</span>
<CommandShortcut>{shortcut('open_git_panel')}</CommandShortcut>
</CommandItem>
<CommandItem onSelect={handleOpenSettings}>
<RiSettings3Line className="mr-2 h-4 w-4" />
<span>Open Settings</span>
<CommandShortcut>{shortcut('open_settings')}</CommandShortcut>
</CommandItem>
<CommandItem onSelect={() => handleOpenSettingsPage('skills.catalog')}>
<RiSettings3Line className="mr-2 h-4 w-4" />
<span>Open Skills Catalog</span>
</CommandItem>
</CommandGroup>
<CommandGroup heading="Settings">
{settingsItems.map((page) => (
<CommandItem key={page.slug} onSelect={() => handleOpenSettingsPage(page.slug)}>
<RiSettings3Line className="mr-2 h-4 w-4" />
<span>{SETTINGS_GROUP_LABELS[page.group]}: {page.title}</span>
</CommandItem>
))}
</CommandGroup> </CommandGroup>
<CommandSeparator /> <CommandSeparator />
<CommandGroup heading="Theme"> <CommandGroup heading="Theme">
<CommandItem onSelect={() => handleSetThemeMode('light')}> <CommandItem onSelect={() => handleSetThemeMode('light')()}>
<RiSunLine className="mr-2 h-4 w-4" /> <RiSunLine className="mr-2 h-4 w-4" />
<span>Light Theme</span> <span>Light</span>
{themeMode === 'light' && <RiCheckLine className="ml-auto h-4 w-4" />} {themeMode === 'light' && <RiCheckLine className="ml-auto h-4 w-4" />}
</CommandItem> </CommandItem>
<CommandItem onSelect={() => handleSetThemeMode('dark')}> <CommandItem onSelect={() => handleSetThemeMode('dark')()}>
<RiMoonLine className="mr-2 h-4 w-4" /> <RiMoonLine className="mr-2 h-4 w-4" />
<span>Dark Theme</span> <span>Dark</span>
{themeMode === 'dark' && <RiCheckLine className="ml-auto h-4 w-4" />} {themeMode === 'dark' && <RiCheckLine className="ml-auto h-4 w-4" />}
</CommandItem> </CommandItem>
<CommandItem onSelect={() => handleSetThemeMode('system')}> <CommandItem onSelect={() => handleSetThemeMode('system')()}>
<RiComputerLine className="mr-2 h-4 w-4" /> <RiComputerLine className="mr-2 h-4 w-4" />
<span>System Theme</span> <span>System</span>
{themeMode === 'system' && <RiCheckLine className="ml-auto h-4 w-4" />} {themeMode === 'system' && <RiCheckLine className="ml-auto h-4 w-4" />}
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
{currentSessions.length > 0 && ( <CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem onSelect={handleOpenSettings}>
<RiSettings3Line className="mr-2 h-4 w-4" />
<span>Open Settings…</span>
<CommandShortcut>{shortcut('open_settings')}</CommandShortcut>
</CommandItem>
{settingsItems.map((page) => {
const Icon = getSettingsNavIcon(page.slug) ?? RiSettings3Line;
return (
<CommandItem key={page.slug} onSelect={() => handleOpenSettingsPage(page.slug)()}>
<Icon className="mr-2 h-4 w-4" />
<span>{SETTINGS_GROUP_LABELS[page.group]}: {page.title}</span>
</CommandItem>
);
})}
</CommandGroup>
{recentSessions.length > 0 && (
<> <>
<CommandSeparator /> <CommandSeparator />
<CommandGroup heading="Recent Sessions"> <CommandGroup heading="Recent Sessions">
{currentSessions.map((session) => ( {recentSessions.map((session) => (
<CommandItem <CommandItem
key={session.id} key={session.id}
onSelect={() => handleOpenSession(session.id, currentDirectory ?? null)} onSelect={() => handleOpenSession(session.id, currentDirectory ?? null)()}
> >
<RiChatAi3Line className="mr-2 h-4 w-4" /> <RiChatAi3Line className="mr-2 h-4 w-4" />
<span className="truncate"> <span className="truncate">{session.title || 'Untitled Session'}</span>
{session.title || 'Untitled Session'}
</span>
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
</> </>
)} )}
{}
</CommandList> </CommandList>
</CommandDialog> </CommandDialog>
); );
@@ -17,6 +17,8 @@ type ScrollableOverlayProps = React.HTMLAttributes<HTMLElement> & {
preventOverscroll?: boolean; preventOverscroll?: boolean;
useScrollShadow?: boolean; useScrollShadow?: boolean;
scrollShadowSize?: number; scrollShadowSize?: number;
/** Forwarded to the inner element (e.g. textarea). */
disabled?: boolean;
}; };
export const ScrollableOverlay = React.forwardRef<HTMLElement, ScrollableOverlayProps>( export const ScrollableOverlay = React.forwardRef<HTMLElement, ScrollableOverlayProps>(
+64 -13
View File
@@ -1,32 +1,83 @@
import * as React from "react" import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Slot } from "@/components/ui/slot"
// Flat tinted buttons: very pale tinted fill (a desaturated version of the
// border tone) + saturated tinted border + saturated tinted text. No
// elevation. Dark theme mixes into transparent over the surface so the tone
// sits atop the dark background.
const TINT_PRIMARY = [
"bg-[color-mix(in_srgb,var(--primary-base)_10%,var(--background))]",
"text-[var(--primary-base)]",
"border border-[color-mix(in_srgb,var(--primary-base)_12%,transparent)]",
"hover:bg-[color-mix(in_srgb,var(--primary-base)_16%,var(--background))]",
"active:bg-[color-mix(in_srgb,var(--primary-base)_22%,var(--background))]",
"dark:bg-[color-mix(in_srgb,var(--primary-base)_16%,transparent)]",
"dark:border-[color-mix(in_srgb,var(--primary-base)_20%,transparent)]",
"dark:hover:bg-[color-mix(in_srgb,var(--primary-base)_22%,transparent)]",
"dark:active:bg-[color-mix(in_srgb,var(--primary-base)_30%,transparent)]",
].join(" ")
const TINT_DESTRUCTIVE = [
"bg-[color-mix(in_srgb,var(--status-error)_7%,var(--background))]",
"text-[var(--status-error)]",
"border border-[color-mix(in_srgb,var(--status-error)_9%,transparent)]",
"hover:bg-[color-mix(in_srgb,var(--status-error)_11%,var(--background))]",
"active:bg-[color-mix(in_srgb,var(--status-error)_16%,var(--background))]",
"dark:bg-[color-mix(in_srgb,var(--status-error)_9%,transparent)]",
"dark:border-[color-mix(in_srgb,var(--status-error)_14%,transparent)]",
"dark:hover:bg-[color-mix(in_srgb,var(--status-error)_14%,transparent)]",
"dark:active:bg-[color-mix(in_srgb,var(--status-error)_20%,transparent)]",
].join(" ")
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg typography-ui-label font-medium lowercase transition-[background-color,border-color,color,box-shadow,opacity] duration-150 ease-in-out disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", [
"group relative inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[10px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px] typography-ui-label font-medium lowercase tracking-[0.01em] shrink-0",
"transition-[background-color,border-color,color,opacity] duration-150 ease-out outline-none",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"disabled:pointer-events-none disabled:opacity-50",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
],
{ {
variants: { variants: {
variant: { variant: {
default: default: TINT_PRIMARY,
"border bg-[var(--primary-base)]/10 text-[var(--primary-base)] shadow-none hover:bg-[var(--primary-base)]/15 hover:text-[var(--primary-base)]", destructive: cn(
destructive: TINT_DESTRUCTIVE,
"bg-[var(--status-error)]/8 text-[var(--status-error)] shadow-none hover:bg-[var(--status-error)]/12 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", "focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
),
neutral:
"bg-interactive-hover text-foreground border border-border/60 hover:bg-interactive-active",
outline: outline:
"border bg-[var(--surface-elevated)] shadow-none hover:bg-interactive-hover hover:text-foreground", "bg-[var(--surface-elevated)] text-foreground border border-border/60 hover:bg-interactive-hover hover:text-foreground",
// Flat chip for "one-of-N" toggles. Unselected: hairline border + hover
// fill. Selected (aria-pressed): same tinted palette as the default
// button (pale primary fill + primary text + soft primary border).
chip: cn(
"border border-border/60 bg-transparent text-foreground hover:bg-interactive-hover hover:text-foreground",
"aria-pressed:bg-[color-mix(in_srgb,var(--primary-base)_10%,var(--background))]",
"aria-pressed:text-[var(--primary-base)]",
"aria-pressed:border-[color-mix(in_srgb,var(--primary-base)_12%,transparent)]",
"aria-pressed:hover:bg-[color-mix(in_srgb,var(--primary-base)_16%,var(--background))]",
"aria-pressed:hover:text-[var(--primary-base)]",
"dark:aria-pressed:bg-[color-mix(in_srgb,var(--primary-base)_16%,transparent)]",
"dark:aria-pressed:border-[color-mix(in_srgb,var(--primary-base)_20%,transparent)]",
"dark:aria-pressed:hover:bg-[color-mix(in_srgb,var(--primary-base)_22%,transparent)]",
),
secondary: secondary:
"bg-interactive-hover text-foreground shadow-none hover:bg-interactive-active", "bg-interactive-hover text-foreground hover:bg-interactive-active",
ghost: ghost:
"text-foreground hover:bg-interactive-hover hover:text-foreground", "text-foreground hover:bg-interactive-hover hover:text-foreground",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: "h-9 px-3.5 has-[>svg]:px-3",
sm: "h-8 rounded-lg gap-1.5 px-3 has-[>svg]:px-2.5", sm: "h-8 gap-1.5 px-2.5 has-[>svg]:px-2 rounded-[9px] supports-[corner-shape:squircle]:rounded-[50px]",
xs: "h-6 rounded-lg gap-1 px-1.5 typography-micro has-[>svg]:px-1.5", xs: "h-6 gap-1 px-2 typography-micro has-[>svg]:px-1.5 rounded-[7px] supports-[corner-shape:squircle]:rounded-[50px]",
lg: "h-10 rounded-lg px-6 has-[>svg]:px-4", lg: "h-10 px-4 has-[>svg]:px-3.5 rounded-[12px] supports-[corner-shape:squircle]:rounded-[50px]",
icon: "size-9", icon: "size-9",
}, },
}, },
+45 -41
View File
@@ -1,68 +1,72 @@
import React from 'react'; import * as React from 'react';
import { RiCheckboxBlankLine, RiCheckboxLine } from '@remixicon/react'; import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
import { RiCheckLine, RiSubtractLine } from '@remixicon/react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
interface CheckboxProps { interface CheckboxProps {
checked: boolean; checked: boolean;
onChange: (checked: boolean) => void; onChange: (checked: boolean) => void;
disabled?: boolean; disabled?: boolean;
indeterminate?: boolean;
ariaLabel?: string; ariaLabel?: string;
className?: string; className?: string;
iconClassName?: string; iconClassName?: string;
/** @deprecated size is fixed; prop retained for backwards compatibility */
size?: 'sm' | 'default';
} }
export const Checkbox = React.memo<CheckboxProps>(function Checkbox({ export const Checkbox = React.memo<CheckboxProps>(function Checkbox({
checked, checked,
onChange, onChange,
disabled = false, disabled = false,
indeterminate,
ariaLabel, ariaLabel,
className, className,
iconClassName, iconClassName,
}) { }) {
const handleClick = React.useCallback( const boxSize = 'h-[14px] w-[14px] min-h-[14px] min-w-[14px]';
(event: React.MouseEvent) => { const iconSize = 'h-[10px] w-[10px] min-h-[10px] min-w-[10px]';
event.preventDefault(); const isOn = checked || indeterminate;
event.stopPropagation();
if (!disabled) {
onChange(!checked);
}
},
[checked, disabled, onChange]
);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent) => {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
if (!disabled) {
onChange(!checked);
}
}
},
[checked, disabled, onChange]
);
return ( return (
<button <BaseCheckbox.Root
type="button" checked={checked}
onClick={handleClick} onCheckedChange={(next) => onChange(Boolean(next))}
onKeyDown={handleKeyDown}
disabled={disabled} disabled={disabled}
aria-pressed={checked} indeterminate={indeterminate}
aria-label={ariaLabel} aria-label={ariaLabel}
className={cn( className={cn(
'flex size-5 shrink-0 items-center justify-center rounded', // AlignUI-style rounded box, no explicit border (rely on inset shadow for unchecked)
'text-muted-foreground hover:text-foreground', 'group/checkbox relative flex shrink-0 self-center items-center justify-center rounded-[4px] outline-none',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', boxSize,
disabled && 'cursor-not-allowed opacity-50', 'transition-[background-color,box-shadow] duration-200 ease-out',
className // Drive fill directly from React props so the initial paint matches
// the final state without waiting for Base UI to hydrate data attrs.
isOn
? 'bg-transparent shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--primary-base)_50%,transparent)] hover:bg-[var(--interactive-hover)]'
: 'bg-[var(--surface-muted)] shadow-[inset_0_0_0_1px_var(--interactive-border)] hover:bg-[var(--interactive-hover)]',
// focus
'focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)] focus-visible:ring-offset-1 focus-visible:ring-offset-background',
// disabled
'disabled:cursor-not-allowed disabled:opacity-50',
className,
)} )}
> >
{checked ? ( <BaseCheckbox.Indicator
<RiCheckboxLine className={cn('size-4 text-primary', iconClassName)} /> keepMounted
) : ( className={cn(
<RiCheckboxBlankLine className={cn('size-4', iconClassName)} /> 'flex items-center justify-center text-[var(--primary-base)]',
)} // hide when fully unchecked (no state)
</button> 'data-[unchecked]:hidden',
iconClassName,
)}
>
{indeterminate ? (
<RiSubtractLine className={cn(iconSize, 'text-[var(--primary-base)]')} />
) : (
<RiCheckLine className={cn(iconSize, 'text-[var(--primary-base)]')} />
)}
</BaseCheckbox.Indicator>
</BaseCheckbox.Root>
); );
}); });
+30 -14
View File
@@ -1,29 +1,45 @@
"use client" "use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; import * as React from "react";
import { Collapsible as BaseCollapsible } from "@base-ui/react/collapsible";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const Collapsible = CollapsiblePrimitive.Root; const Collapsible = BaseCollapsible.Root;
type AsChildRenderProps = {
render?: React.ReactElement;
children?: React.ReactNode;
};
type TriggerProps = React.ComponentProps<typeof BaseCollapsible.Trigger> & { asChild?: boolean };
const CollapsibleTrigger = ({ const CollapsibleTrigger = ({
className, className,
asChild,
children,
...props ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) => ( }: TriggerProps) => {
<CollapsiblePrimitive.CollapsibleTrigger const renderProps: AsChildRenderProps = asChild && React.isValidElement(children)
className={cn( ? { render: children as React.ReactElement }
"flex w-full items-center justify-between rounded-lg px-2 py-1.5 text-left text-foreground hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", : { children };
className return (
)} <BaseCollapsible.Trigger
{...props} className={cn(
/> "flex w-full items-center justify-between rounded-lg px-2 py-1.5 text-left text-foreground hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
); className
)}
{...props}
{...renderProps}
/>
);
};
const CollapsibleContent = ({ const CollapsibleContent = ({
className, className,
...props ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) => ( }: React.ComponentProps<typeof BaseCollapsible.Panel>) => (
<CollapsiblePrimitive.CollapsibleContent <BaseCollapsible.Panel
className={cn("overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", className)} className={cn("overflow-hidden data-[closed]:animate-collapsible-up data-[open]:animate-collapsible-down", className)}
{...props} {...props}
/> />
); );
+7 -2
View File
@@ -16,14 +16,18 @@ import {
function Command({ function Command({
className, className,
style,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive>) { }: React.ComponentProps<typeof CommandPrimitive>) {
return ( return (
<CommandPrimitive <CommandPrimitive
data-slot="command" data-slot="command"
// Let the parent (DropdownMenuContent, DialogContent, etc.) paint the
// background and provide elevation. Overriding bg here would cover the
// parent's inset shadows (our inner light ring) and flatten the edge.
style={{ style={{
backgroundColor: 'var(--surface-elevated)',
color: 'var(--surface-elevated-foreground)', color: 'var(--surface-elevated-foreground)',
...style,
}} }}
className={cn( className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-xl", "flex h-full w-full flex-col overflow-hidden rounded-xl",
@@ -41,11 +45,12 @@ function CommandDialog({
className, className,
showCloseButton = true, showCloseButton = true,
...props ...props
}: React.ComponentProps<typeof Dialog> & { }: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
title?: string title?: string
description?: string description?: string
className?: string className?: string
showCloseButton?: boolean showCloseButton?: boolean
children?: React.ReactNode
}) { }) {
return ( return (
<Dialog {...props}> <Dialog {...props}>
+58 -29
View File
@@ -1,38 +1,57 @@
import * as React from "react" import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog" import { Dialog as BaseDialog } from "@base-ui/react/dialog"
import { RiCloseLine } from '@remixicon/react'; import { RiCloseLine } from '@remixicon/react';
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
let openDialogCount = 0; let openDialogCount = 0;
type AsChildProps = { asChild?: boolean };
type AsChildRenderProps = {
render?: React.ReactElement;
children?: React.ReactNode;
};
function renderFromAsChild(asChild: boolean | undefined, children: React.ReactNode) {
if (asChild && React.isValidElement(children)) {
return { render: children as React.ReactElement } satisfies AsChildRenderProps;
}
return { children };
}
function Dialog({ function Dialog({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) { }: React.ComponentProps<typeof BaseDialog.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} /> return <BaseDialog.Root {...props} />
} }
function DialogTrigger({ function DialogTrigger({
asChild,
children,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) { }: React.ComponentProps<typeof BaseDialog.Trigger> & AsChildProps) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> const r = renderFromAsChild(asChild, children);
return <BaseDialog.Trigger data-slot="dialog-trigger" {...props} {...r} />
} }
function DialogPortal({ function DialogPortal({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) { }: React.ComponentProps<typeof BaseDialog.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> return <BaseDialog.Portal {...props} />
} }
function DialogClose({ function DialogClose({
asChild,
children,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) { }: React.ComponentProps<typeof BaseDialog.Close> & AsChildProps) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> const r = renderFromAsChild(asChild, children);
return <BaseDialog.Close data-slot="dialog-close" {...props} {...r} />
} }
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, HTMLDivElement,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof BaseDialog.Backdrop>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
React.useEffect(() => { React.useEffect(() => {
openDialogCount += 1; openDialogCount += 1;
@@ -46,8 +65,8 @@ const DialogOverlay = React.forwardRef<
}, []); }, []);
return ( return (
<DialogPrimitive.Overlay <BaseDialog.Backdrop
ref={ref} ref={ref as React.Ref<HTMLDivElement>}
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/50 dark:bg-black/75", "fixed inset-0 z-50 bg-black/50 dark:bg-black/75",
@@ -57,23 +76,34 @@ const DialogOverlay = React.forwardRef<
/> />
) )
}); });
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; DialogOverlay.displayName = "DialogOverlay";
type DialogContentProps = Omit<React.ComponentProps<typeof BaseDialog.Popup>, "children"> & {
showCloseButton?: boolean
keyboardAvoid?: boolean
children?: React.ReactNode
onOpenAutoFocus?: (event: Event) => void
onCloseAutoFocus?: (event: Event) => void
}
function DialogContent({ function DialogContent({
className, className,
children, children,
showCloseButton = true, showCloseButton = true,
keyboardAvoid = false, keyboardAvoid = false,
onOpenAutoFocus,
onCloseAutoFocus,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & { }: DialogContentProps) {
showCloseButton?: boolean void onOpenAutoFocus
keyboardAvoid?: boolean void onCloseAutoFocus
}) {
return ( return (
<DialogPortal data-slot="dialog-portal"> <DialogPortal>
<DialogOverlay className="rounded-none" /> <DialogOverlay className="rounded-none" />
<DialogPrimitive.Content <BaseDialog.Popup
data-slot="dialog-content" data-slot="dialog-content"
data-state-slot="dialog"
data-keyboard-avoid={keyboardAvoid ? "true" : undefined} data-keyboard-avoid={keyboardAvoid ? "true" : undefined}
className={cn( className={cn(
"bg-background text-foreground fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-6 shadow-none overflow-hidden pwa-dialog-content", "bg-background text-foreground fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-6 shadow-none overflow-hidden pwa-dialog-content",
@@ -81,18 +111,17 @@ function DialogContent({
)} )}
{...props} {...props}
> >
{children} {children}
{showCloseButton && ( {showCloseButton && (
<DialogPrimitive.Close <BaseDialog.Close
data-slot="dialog-close" data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-interactive-active data-[state=open]:text-foreground absolute top-2 right-2 rounded-lg opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none text-muted-foreground hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" className="ring-offset-background focus:ring-ring data-[open]:bg-interactive-active data-[open]:text-foreground absolute top-2 right-2 rounded-lg opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none text-muted-foreground hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
> >
<RiCloseLine/> <RiCloseLine/>
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </BaseDialog.Close>
)} )}
</DialogPrimitive.Content> </BaseDialog.Popup>
</DialogPortal> </DialogPortal>
) )
} }
@@ -123,9 +152,9 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
function DialogTitle({ function DialogTitle({
className, className,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) { }: React.ComponentProps<typeof BaseDialog.Title>) {
return ( return (
<DialogPrimitive.Title <BaseDialog.Title
data-slot="dialog-title" data-slot="dialog-title"
className={cn("typography-markdown leading-none font-semibold text-foreground", className)} className={cn("typography-markdown leading-none font-semibold text-foreground", className)}
{...props} {...props}
@@ -136,9 +165,9 @@ function DialogTitle({
function DialogDescription({ function DialogDescription({
className, className,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) { }: React.ComponentProps<typeof BaseDialog.Description>) {
return ( return (
<DialogPrimitive.Description <BaseDialog.Description
data-slot="dialog-description" data-slot="dialog-description"
className={cn("text-muted-foreground typography-ui-label", className)} className={cn("text-muted-foreground typography-ui-label", className)}
{...props} {...props}
+136 -82
View File
@@ -1,87 +1,139 @@
import * as React from "react" import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { Menu as BaseMenu } from "@base-ui/react/menu"
import { RiArrowRightSLine, RiCheckLine } from '@remixicon/react'; import { RiArrowRightSLine, RiCheckLine } from '@remixicon/react';
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
type AsChildProps = { asChild?: boolean };
type AsChildRenderProps = {
render?: React.ReactElement;
children?: React.ReactNode;
};
function renderFromAsChild(asChild: boolean | undefined, children: React.ReactNode) {
if (asChild && React.isValidElement(children)) {
return { render: children as React.ReactElement } satisfies AsChildRenderProps;
}
return { children };
}
function DropdownMenu({ function DropdownMenu({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { }: React.ComponentProps<typeof BaseMenu.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> return <BaseMenu.Root {...props} />
} }
function DropdownMenuPortal({ function DropdownMenuPortal({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { }: React.ComponentProps<typeof BaseMenu.Portal>) {
return ( return <BaseMenu.Portal {...props} />
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
} }
function DropdownMenuTrigger({ function DropdownMenuTrigger({
asChild,
children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof BaseMenu.Trigger> & AsChildProps) {
const r = renderFromAsChild(asChild, children);
return ( return (
<DropdownMenuPrimitive.Trigger <BaseMenu.Trigger
data-slot="dropdown-menu-trigger" data-slot="dropdown-menu-trigger"
{...props} {...props}
{...r}
/> />
) )
} }
type ContentProps = {
sideOffset?: number;
align?: "start" | "center" | "end";
side?: "top" | "right" | "bottom" | "left";
alignOffset?: number;
style?: React.CSSProperties;
className?: string;
children?: React.ReactNode;
onCloseAutoFocus?: (event: Event) => void;
} & Omit<React.ComponentProps<typeof BaseMenu.Popup>, "style" | "className" | "children">
function DropdownMenuContent({ function DropdownMenuContent({
className, className,
sideOffset = 4, sideOffset = 4,
align,
side,
alignOffset,
style, style,
children,
onCloseAutoFocus,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { }: ContentProps) {
void onCloseAutoFocus
return ( return (
<DropdownMenuPrimitive.Portal> <BaseMenu.Portal>
<DropdownMenuPrimitive.Content <BaseMenu.Positioner
data-slot="dropdown-menu-content"
sideOffset={sideOffset} sideOffset={sideOffset}
style={{ align={align}
backgroundColor: 'var(--surface-elevated)', side={side}
color: 'var(--surface-elevated-foreground)', alignOffset={alignOffset}
...style, className="z-50"
}} >
className={cn( <BaseMenu.Popup
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-visible rounded-xl border-2 border-border/60 p-1 shadow-none", data-slot="dropdown-menu-content"
className style={{
)} backgroundColor: 'var(--surface-elevated)',
{...props} color: 'var(--surface-elevated-foreground)',
/> ...style,
</DropdownMenuPrimitive.Portal> }}
className={cn(
"data-[open]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[open]:fade-in-0 data-[closed]:zoom-out-95 data-[open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--available-height)] min-w-[8rem] origin-[var(--transform-origin)] overflow-visible rounded-xl p-1 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]",
className
)}
{...props}
>
{children}
</BaseMenu.Popup>
</BaseMenu.Positioner>
</BaseMenu.Portal>
) )
} }
function DropdownMenuGroup({ function DropdownMenuGroup({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { }: React.ComponentProps<typeof BaseMenu.Group>) {
return ( return <BaseMenu.Group data-slot="dropdown-menu-group" {...props} />
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
} }
function DropdownMenuItem({ function DropdownMenuItem({
className, className,
inset, inset,
variant = "default", variant = "default",
asChild,
children,
onSelect,
onClick,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { }: React.ComponentProps<typeof BaseMenu.Item> & AsChildProps & {
inset?: boolean inset?: boolean
variant?: "default" | "destructive" variant?: "default" | "destructive"
onSelect?: React.ComponentProps<typeof BaseMenu.Item>["onClick"]
}) { }) {
const r = renderFromAsChild(asChild, children);
const handleClick: NonNullable<React.ComponentProps<typeof BaseMenu.Item>["onClick"]> = (event) => {
onClick?.(event);
if (!event.defaultPrevented) onSelect?.(event);
};
return ( return (
<DropdownMenuPrimitive.Item <BaseMenu.Item
data-slot="dropdown-menu-item" data-slot="dropdown-menu-item"
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[variant=destructive]:text-destructive data-[variant=destructive]:hover:bg-destructive/10 dark:data-[variant=destructive]:hover:bg-destructive/20 data-[variant=destructive]:hover:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[variant=destructive]:text-destructive data-[variant=destructive]:hover:bg-destructive/10 dark:data-[variant=destructive]:hover:bg-destructive/20 data-[variant=destructive]:hover:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className className
)} )}
{...props} {...props}
onClick={handleClick}
{...r}
/> />
) )
} }
@@ -91,59 +143,54 @@ function DropdownMenuCheckboxItem({
children, children,
checked, checked,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { }: React.ComponentProps<typeof BaseMenu.CheckboxItem>) {
return ( return (
<DropdownMenuPrimitive.CheckboxItem <BaseMenu.CheckboxItem
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
className={cn( className={cn(
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[state=checked]:bg-interactive-selection data-[state=checked]:text-interactive-selection-foreground relative flex cursor-pointer items-center gap-2 rounded-lg py-1 px-2 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[checked]:bg-interactive-selection data-[checked]:text-interactive-selection-foreground relative flex cursor-pointer items-center gap-2 rounded-lg py-1 px-2 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className className
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="pointer-events-none absolute right-2 flex size-3.5 items-center justify-center"> <span className="pointer-events-none absolute right-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <BaseMenu.CheckboxItemIndicator>
<RiCheckLine className="size-3"/> <RiCheckLine className="size-3"/>
</DropdownMenuPrimitive.ItemIndicator> </BaseMenu.CheckboxItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </BaseMenu.CheckboxItem>
) )
} }
function DropdownMenuRadioGroup({ function DropdownMenuRadioGroup({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { }: React.ComponentProps<typeof BaseMenu.RadioGroup>) {
return ( return <BaseMenu.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
} }
function DropdownMenuRadioItem({ function DropdownMenuRadioItem({
className, className,
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { }: React.ComponentProps<typeof BaseMenu.RadioItem>) {
return ( return (
<DropdownMenuPrimitive.RadioItem <BaseMenu.RadioItem
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
className={cn( className={cn(
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[state=checked]:bg-interactive-selection data-[state=checked]:text-interactive-selection-foreground relative flex cursor-pointer items-start gap-2 rounded-lg py-1 pl-2 pr-8 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[checked]:bg-interactive-selection data-[checked]:text-interactive-selection-foreground relative flex cursor-pointer items-start gap-2 rounded-lg py-1 pl-2 pr-8 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className className
)} )}
{...props} {...props}
> >
<span className="pointer-events-none absolute right-2 flex size-3.5 items-center justify-center text-primary"> <span className="pointer-events-none absolute right-2 flex size-3.5 items-center justify-center text-primary">
<DropdownMenuPrimitive.ItemIndicator> <BaseMenu.RadioItemIndicator>
<RiCheckLine className="size-3" /> <RiCheckLine className="size-3" />
</DropdownMenuPrimitive.ItemIndicator> </BaseMenu.RadioItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </BaseMenu.RadioItem>
) )
} }
@@ -151,11 +198,11 @@ function DropdownMenuLabel({
className, className,
inset, inset,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { }: React.ComponentProps<"div"> & {
inset?: boolean inset?: boolean
}) { }) {
return ( return (
<DropdownMenuPrimitive.Label <div
data-slot="dropdown-menu-label" data-slot="dropdown-menu-label"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
@@ -170,9 +217,9 @@ function DropdownMenuLabel({
function DropdownMenuSeparator({ function DropdownMenuSeparator({
className, className,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { }: React.ComponentProps<typeof BaseMenu.Separator>) {
return ( return (
<DropdownMenuPrimitive.Separator <BaseMenu.Separator
data-slot="dropdown-menu-separator" data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-0.5 h-px", className)} className={cn("bg-border -mx-1 my-0.5 h-px", className)}
{...props} {...props}
@@ -198,8 +245,8 @@ function DropdownMenuShortcut({
function DropdownMenuSub({ function DropdownMenuSub({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { }: React.ComponentProps<typeof BaseMenu.SubmenuRoot>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> return <BaseMenu.SubmenuRoot {...props} />
} }
function DropdownMenuSubTrigger({ function DropdownMenuSubTrigger({
@@ -207,42 +254,49 @@ function DropdownMenuSubTrigger({
inset, inset,
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof BaseMenu.SubmenuTrigger> & {
inset?: boolean inset?: boolean
}) { }) {
return ( return (
<DropdownMenuPrimitive.SubTrigger <BaseMenu.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger" data-slot="dropdown-menu-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<RiArrowRightSLine className="ml-auto size-3.5" /> <RiArrowRightSLine className="ml-auto size-3.5" />
</DropdownMenuPrimitive.SubTrigger> </BaseMenu.SubmenuTrigger>
) )
} }
function DropdownMenuSubContent({ function DropdownMenuSubContent({
className, className,
children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { }: React.ComponentProps<typeof BaseMenu.Popup>) {
return ( return (
<DropdownMenuPrimitive.SubContent <BaseMenu.Portal>
data-slot="dropdown-menu-sub-content" <BaseMenu.Positioner className="z-50">
style={{ <BaseMenu.Popup
backgroundColor: 'var(--surface-elevated)', data-slot="dropdown-menu-sub-content"
color: 'var(--surface-elevated-foreground)', style={{
}} backgroundColor: 'var(--surface-elevated)',
className={cn( color: 'var(--surface-elevated-foreground)',
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-visible rounded-xl border-2 border-border/60 p-1 shadow-none", }}
className className={cn(
)} "data-[open]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[open]:fade-in-0 data-[closed]:zoom-out-95 data-[open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[var(--transform-origin)] overflow-visible rounded-xl p-1 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]",
{...props} className
/> )}
{...props}
>
{children}
</BaseMenu.Popup>
</BaseMenu.Positioner>
</BaseMenu.Portal>
) )
} }
@@ -0,0 +1,134 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Slot } from "@/components/ui/slot";
// Flat tinted fancy-button: pale tinted fill + saturated tinted border +
// saturated tinted text. Matches the Button primitive design language.
const TINT_PRIMARY = [
"bg-[color-mix(in_srgb,var(--primary-base)_10%,var(--background))]",
"text-[var(--primary-base)]",
"border border-[color-mix(in_srgb,var(--primary-base)_12%,transparent)]",
"hover:bg-[color-mix(in_srgb,var(--primary-base)_16%,var(--background))]",
"active:bg-[color-mix(in_srgb,var(--primary-base)_22%,var(--background))]",
"dark:bg-[color-mix(in_srgb,var(--primary-base)_16%,transparent)]",
"dark:border-[color-mix(in_srgb,var(--primary-base)_20%,transparent)]",
"dark:hover:bg-[color-mix(in_srgb,var(--primary-base)_22%,transparent)]",
"dark:active:bg-[color-mix(in_srgb,var(--primary-base)_30%,transparent)]",
].join(" ");
const TINT_DESTRUCTIVE = [
"bg-[color-mix(in_srgb,var(--status-error)_7%,var(--background))]",
"text-[var(--status-error)]",
"border border-[color-mix(in_srgb,var(--status-error)_9%,transparent)]",
"hover:bg-[color-mix(in_srgb,var(--status-error)_11%,var(--background))]",
"active:bg-[color-mix(in_srgb,var(--status-error)_16%,var(--background))]",
"dark:bg-[color-mix(in_srgb,var(--status-error)_9%,transparent)]",
"dark:border-[color-mix(in_srgb,var(--status-error)_14%,transparent)]",
"dark:hover:bg-[color-mix(in_srgb,var(--status-error)_14%,transparent)]",
"dark:active:bg-[color-mix(in_srgb,var(--status-error)_20%,transparent)]",
].join(" ");
const fancyButtonRoot = cva(
[
"group relative inline-flex items-center justify-center whitespace-nowrap [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px] typography-ui-label outline-none",
"transition-[background-color,border-color,color,opacity] duration-150 ease-out",
"focus:outline-none",
"disabled:pointer-events-none disabled:text-muted-foreground/60",
"disabled:bg-interactive-hover",
],
{
variants: {
variant: {
neutral:
"bg-interactive-hover text-foreground border border-border/60 hover:bg-interactive-active",
primary: TINT_PRIMARY,
destructive: TINT_DESTRUCTIVE,
basic:
"bg-background text-foreground border border-border/60 hover:bg-interactive-hover hover:text-foreground",
},
size: {
medium: "h-10 gap-3 rounded-[var(--radius-xl)] px-3.5",
small: "h-9 gap-3 rounded-[var(--radius-lg)] px-3",
xsmall: "h-8 gap-2 rounded-[var(--radius-lg)] px-2.5",
},
},
defaultVariants: {
variant: "neutral",
size: "medium",
},
},
);
const fancyButtonIcon = cva("relative z-10 size-5 shrink-0", {
variants: {
size: {
medium: "-mx-1",
small: "-mx-1",
xsmall: "-mx-1",
},
},
defaultVariants: {
size: "medium",
},
});
type FancyButtonVariants = VariantProps<typeof fancyButtonRoot>;
type FancyButtonContextValue = Pick<FancyButtonVariants, "variant" | "size">;
const FancyButtonContext = React.createContext<FancyButtonContextValue>({});
type RootProps = FancyButtonVariants &
React.ButtonHTMLAttributes<HTMLButtonElement> & {
asChild?: boolean;
};
const FancyButtonRoot = React.forwardRef<HTMLButtonElement, RootProps>(
({ asChild, children, variant, size, className, ...rest }, ref) => {
const Component = (asChild ? Slot : "button") as React.ElementType;
const ctx = React.useMemo<FancyButtonContextValue>(
() => ({ variant, size }),
[variant, size],
);
return (
<FancyButtonContext.Provider value={ctx}>
<Component
ref={ref}
className={cn(fancyButtonRoot({ variant, size }), className)}
{...rest}
>
{children}
</Component>
</FancyButtonContext.Provider>
);
},
);
FancyButtonRoot.displayName = "FancyButton.Root";
type IconProps<T extends React.ElementType = "div"> = {
as?: T;
className?: string;
} & Omit<React.ComponentPropsWithoutRef<T>, "as" | "className">;
function FancyButtonIcon<T extends React.ElementType = "div">({
as,
className,
...rest
}: IconProps<T>) {
const { size } = React.useContext(FancyButtonContext);
const Component = (as ?? "div") as React.ElementType;
return (
<Component
className={cn(fancyButtonIcon({ size }), className)}
{...rest}
/>
);
}
FancyButtonIcon.displayName = "FancyButton.Icon";
export { FancyButtonRoot as Root, FancyButtonIcon as Icon };
// eslint-disable-next-line react-refresh/only-export-components
export { fancyButtonRoot as fancyButtonVariants };
+6 -4
View File
@@ -8,10 +8,12 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"text-foreground border border-border/80 file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 appearance-none flex h-9 w-full min-w-0 rounded-lg bg-transparent px-3 py-1 typography-markdown outline-none focus-visible:outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:typography-ui-label file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:typography-ui-label", "text-foreground file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-[var(--surface-elevated)] appearance-none flex h-9 w-full min-w-0 rounded-lg px-3 py-1 typography-markdown outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:typography-ui-label file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:typography-ui-label",
"hover:border-input", // AlignUI-style ring border + transitions
"focus:ring-1 focus:ring-primary/50 focus:border-primary/70", "ring-1 ring-inset ring-border/60 transition duration-200 ease-out",
"aria-invalid:border-destructive aria-invalid:ring-destructive", "hover:[&:not(:focus)]:bg-[var(--surface-subtle)] hover:[&:not(:focus)]:ring-transparent",
"focus:ring-2 focus:ring-[var(--interactive-focus-ring)] focus-visible:outline-none",
"aria-invalid:ring-[var(--status-error)] aria-invalid:focus:ring-[var(--status-error)]",
className className
)} )}
spellCheck={false} spellCheck={false}
+17 -10
View File
@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { RiRadioButtonFill, RiRadioButtonLine } from '@remixicon/react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
interface RadioProps { interface RadioProps {
@@ -52,18 +51,26 @@ export const Radio = React.memo<RadioProps>(function Radio({
aria-checked={checked} aria-checked={checked}
aria-label={ariaLabel} aria-label={ariaLabel}
className={cn( className={cn(
'flex size-5 shrink-0 items-center justify-center rounded', 'group/radio relative flex h-[14px] w-[14px] min-h-[14px] min-w-[14px] shrink-0 self-center items-center justify-center rounded-full outline-none',
'text-muted-foreground hover:text-foreground', 'transition-[background-color,box-shadow] duration-200 ease-out',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', // fill driven from props so first paint is correct
disabled && 'cursor-not-allowed opacity-50', checked
className ? 'bg-[color-mix(in_srgb,var(--primary-base)_80%,transparent)] shadow-none hover:bg-[var(--primary-base)]'
: 'bg-[var(--surface-muted)] shadow-[inset_0_0_0_1px_var(--interactive-border)] hover:bg-[var(--interactive-hover)]',
'focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)] focus-visible:ring-offset-1 focus-visible:ring-offset-background',
'disabled:cursor-not-allowed disabled:opacity-50',
className,
)} )}
> >
{checked ? ( {checked ? (
<RiRadioButtonFill className={cn('size-4 text-primary', iconClassName)} /> <span
) : ( aria-hidden
<RiRadioButtonLine className={cn('size-4', iconClassName)} /> className={cn(
)} 'block h-[5px] w-[5px] rounded-full bg-white',
iconClassName,
)}
/>
) : null}
</button> </button>
); );
}); });
+11 -11
View File
@@ -1,5 +1,5 @@
import * as React from "react" import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" import { ScrollArea as BaseScrollArea } from "@base-ui/react/scroll-area"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -7,22 +7,22 @@ function ScrollArea({
className, className,
children, children,
...props ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) { }: React.ComponentProps<typeof BaseScrollArea.Root>) {
return ( return (
<ScrollAreaPrimitive.Root <BaseScrollArea.Root
data-slot="scroll-area" data-slot="scroll-area"
className={cn("relative", className)} className={cn("relative", className)}
{...props} {...props}
> >
<ScrollAreaPrimitive.Viewport <BaseScrollArea.Viewport
data-slot="scroll-area-viewport" data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:outline-none focus-visible:ring-[3px] will-change-scroll" className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:outline-none focus-visible:ring-[3px] will-change-scroll"
> >
{children} {children}
</ScrollAreaPrimitive.Viewport> </BaseScrollArea.Viewport>
<ScrollBar /> <ScrollBar />
<ScrollAreaPrimitive.Corner /> <BaseScrollArea.Corner />
</ScrollAreaPrimitive.Root> </BaseScrollArea.Root>
) )
} }
@@ -30,9 +30,9 @@ function ScrollBar({
className, className,
orientation = "vertical", orientation = "vertical",
...props ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) { }: React.ComponentProps<typeof BaseScrollArea.Scrollbar>) {
return ( return (
<ScrollAreaPrimitive.ScrollAreaScrollbar <BaseScrollArea.Scrollbar
data-slot="scroll-area-scrollbar" data-slot="scroll-area-scrollbar"
orientation={orientation} orientation={orientation}
className={cn( className={cn(
@@ -45,11 +45,11 @@ function ScrollBar({
)} )}
{...props} {...props}
> >
<ScrollAreaPrimitive.ScrollAreaThumb <BaseScrollArea.Thumb
data-slot="scroll-area-thumb" data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full" className="bg-border relative flex-1 rounded-full"
/> />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </BaseScrollArea.Scrollbar>
) )
} }
+142 -73
View File
@@ -1,110 +1,179 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select" import { Select as BaseSelect } from "@base-ui/react/select"
import type { SelectRootChangeEventDetails } from "@base-ui/react/select";
import { RiArrowDownSLine, RiArrowUpSLine, RiCheckLine } from '@remixicon/react'; import { RiArrowDownSLine, RiArrowUpSLine, RiCheckLine } from '@remixicon/react';
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ScrollableOverlay } from "@/components/ui/ScrollableOverlay"; import { ScrollableOverlay } from "@/components/ui/ScrollableOverlay";
function Select({ type AsChildProps = { asChild?: boolean };
...props type AsChildRenderProps = {
}: React.ComponentProps<typeof SelectPrimitive.Root>) { render?: React.ReactElement;
return <SelectPrimitive.Root data-slot="select" {...props} /> children?: React.ReactNode;
};
type SelectRootProps<Value extends string = string> = Omit<
React.ComponentProps<typeof BaseSelect.Root>,
"value" | "defaultValue" | "onValueChange"
> & {
value?: Value;
defaultValue?: Value;
onValueChange?: (value: Value, eventDetails: SelectRootChangeEventDetails) => void;
};
function Select<Value extends string = string>({ onValueChange, ...props }: SelectRootProps<Value>) {
const handleValueChange = React.useCallback(
(value: unknown, eventDetails: SelectRootChangeEventDetails) => {
if (typeof value === "string") {
onValueChange?.(value as Value, eventDetails);
}
},
[onValueChange]
);
return <BaseSelect.Root {...props} onValueChange={handleValueChange} />
} }
function SelectGroup({ function SelectGroup({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) { }: React.ComponentProps<typeof BaseSelect.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} /> return <BaseSelect.Group data-slot="select-group" {...props} />
} }
function SelectValue({ type SelectValueProps = Omit<React.ComponentProps<typeof BaseSelect.Value>, "children"> & {
...props placeholder?: React.ReactNode;
}: React.ComponentProps<typeof SelectPrimitive.Value>) { children?: React.ReactNode | ((value: string | undefined) => React.ReactNode);
return <SelectPrimitive.Value data-slot="select-value" {...props} /> };
function SelectValue({ placeholder, children, ...props }: SelectValueProps) {
return (
<BaseSelect.Value data-slot="select-value" {...props}>
{(value: unknown) => {
const resolvedValue =
typeof value === "string" || value === undefined
? value
: value === null
? undefined
: String(value);
if (typeof children === "function") {
return children(resolvedValue);
}
if (children !== undefined && children !== null) return children;
if (resolvedValue === undefined || resolvedValue === "") {
return placeholder as React.ReactNode;
}
return resolvedValue;
}}
</BaseSelect.Value>
)
} }
function SelectTrigger({ function SelectTrigger({
className, className,
size = "default", size = "default",
children, children,
asChild,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { }: React.ComponentProps<typeof BaseSelect.Trigger> & AsChildProps & {
size?: "sm" | "default" | "lg" | "chip" size?: "sm" | "default" | "lg" | "chip"
}) { }) {
const asChildRender: AsChildRenderProps | null = asChild && React.isValidElement(children)
? { render: children as React.ReactElement }
: null;
return ( return (
<SelectPrimitive.Trigger <BaseSelect.Trigger
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-lg border bg-transparent px-2 py-2 typography-ui-label whitespace-nowrap shadow-none outline-none focus-visible:outline-none hover:bg-interactive-hover data-[state=open]:bg-interactive-active focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-6 data-[size=sm]:h-6 data-[size=lg]:h-8 data-[size=lg]:py-1.5 data-[size=chip]:h-7 data-[size=chip]:py-1 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-lg border bg-transparent px-2 py-2 typography-ui-label whitespace-nowrap shadow-none outline-none focus-visible:outline-none hover:bg-interactive-hover data-[popup-open]:bg-interactive-active focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-6 data-[size=sm]:h-6 data-[size=lg]:h-8 data-[size=lg]:py-1.5 data-[size=chip]:h-7 data-[size=chip]:py-1 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
{...(asChildRender ?? {})}
> >
{children} {asChildRender ? undefined : (<>
<SelectPrimitive.Icon asChild> {children}
<RiArrowDownSLine className="size-4 opacity-50" /> <BaseSelect.Icon>
</SelectPrimitive.Icon> <RiArrowDownSLine className="size-4 opacity-50" />
</SelectPrimitive.Trigger> </BaseSelect.Icon>
</>)}
</BaseSelect.Trigger>
) )
} }
type SelectContentExtra = {
position?: "popper" | "item-aligned";
fitContent?: boolean;
sideOffset?: number;
side?: "top" | "right" | "bottom" | "left";
align?: "start" | "center" | "end";
};
function SelectContent({ function SelectContent({
className, className,
children, children,
position = "popper", position = "popper",
fitContent = false, fitContent = false,
sideOffset,
side,
align,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Content> & { }: React.ComponentProps<typeof BaseSelect.Popup> & SelectContentExtra) {
fitContent?: boolean; const alignItemWithTrigger = position === "item-aligned";
}) {
return ( return (
<SelectPrimitive.Portal> <BaseSelect.Portal>
<SelectPrimitive.Content <BaseSelect.Positioner
data-slot="select-content" alignItemWithTrigger={alignItemWithTrigger}
style={{ sideOffset={sideOffset}
backgroundColor: 'var(--surface-elevated)', side={side}
color: 'var(--surface-elevated-foreground)', align={align}
}} className="z-50"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden rounded-xl border-2 border-border/60 shadow-none transform-gpu will-change-transform",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
fitContent && "w-max min-w-0",
className
)}
position={position}
{...props}
>
<ScrollableOverlay
as={SelectPrimitive.Viewport}
outerClassName={cn(
"max-h-[var(--radix-select-content-available-height)]",
fitContent ? "w-max" : "w-full"
)}
className={cn(
"p-1",
position === "popper" &&
(fitContent
? "h-[var(--radix-select-trigger-height)] w-max min-w-0 scroll-my-1"
: "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1")
)}
> >
{children} <BaseSelect.Popup
</ScrollableOverlay> data-slot="select-content"
</SelectPrimitive.Content> style={{
</SelectPrimitive.Portal> backgroundColor: 'var(--surface-elevated)',
color: 'var(--surface-elevated-foreground)',
}}
className={cn(
"data-[open]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[open]:fade-in-0 data-[closed]:zoom-out-95 data-[open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[var(--available-height)] min-w-[8rem] origin-[var(--transform-origin)] overflow-x-hidden rounded-xl transform-gpu will-change-transform shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]",
!alignItemWithTrigger &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
fitContent && "w-max min-w-0",
className
)}
{...props}
>
<ScrollableOverlay
outerClassName={cn(
"max-h-[var(--available-height)]",
fitContent ? "w-max" : "w-full"
)}
className={cn(
"p-1",
!alignItemWithTrigger &&
(fitContent
? "w-max min-w-0 scroll-my-1"
: "w-full min-w-[var(--anchor-width)] scroll-my-1")
)}
>
{children}
</ScrollableOverlay>
</BaseSelect.Popup>
</BaseSelect.Positioner>
</BaseSelect.Portal>
) )
} }
function SelectLabel({ function SelectLabel({
className, className,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) { }: React.ComponentProps<typeof BaseSelect.GroupLabel>) {
return ( return (
<SelectPrimitive.Label <BaseSelect.GroupLabel
data-slot="select-label" data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 typography-meta", className)} className={cn("text-muted-foreground px-2 py-1.5 typography-meta", className)}
{...props} {...props}
@@ -116,32 +185,32 @@ function SelectItem({
className, className,
children, children,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) { }: React.ComponentProps<typeof BaseSelect.Item>) {
return ( return (
<SelectPrimitive.Item <BaseSelect.Item
data-slot="select-item" data-slot="select-item"
className={cn( className={cn(
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[state=checked]:bg-interactive-selection data-[state=checked]:text-interactive-selection-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 typography-ui-label outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[selected]:bg-interactive-selection data-[selected]:text-interactive-selection-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 typography-ui-label outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className className
)} )}
{...props} {...props}
> >
<span className="absolute right-2 flex size-3.5 items-center justify-center"> <span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator> <BaseSelect.ItemIndicator>
<RiCheckLine className="size-4"/> <RiCheckLine className="size-4"/>
</SelectPrimitive.ItemIndicator> </BaseSelect.ItemIndicator>
</span> </span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <BaseSelect.ItemText>{children}</BaseSelect.ItemText>
</SelectPrimitive.Item> </BaseSelect.Item>
) )
} }
function SelectSeparator({ function SelectSeparator({
className, className,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) { }: React.ComponentProps<typeof BaseSelect.Separator>) {
return ( return (
<SelectPrimitive.Separator <BaseSelect.Separator
data-slot="select-separator" data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)} className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props} {...props}
@@ -152,9 +221,9 @@ function SelectSeparator({
function SelectScrollUpButton({ function SelectScrollUpButton({
className, className,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { }: React.ComponentProps<typeof BaseSelect.ScrollUpArrow>) {
return ( return (
<SelectPrimitive.ScrollUpButton <BaseSelect.ScrollUpArrow
data-slot="select-scroll-up-button" data-slot="select-scroll-up-button"
className={cn( className={cn(
"flex cursor-pointer items-center justify-center py-1", "flex cursor-pointer items-center justify-center py-1",
@@ -163,16 +232,16 @@ function SelectScrollUpButton({
{...props} {...props}
> >
<RiArrowUpSLine className="size-4" /> <RiArrowUpSLine className="size-4" />
</SelectPrimitive.ScrollUpButton> </BaseSelect.ScrollUpArrow>
) )
} }
function SelectScrollDownButton({ function SelectScrollDownButton({
className, className,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) { }: React.ComponentProps<typeof BaseSelect.ScrollDownArrow>) {
return ( return (
<SelectPrimitive.ScrollDownButton <BaseSelect.ScrollDownArrow
data-slot="select-scroll-down-button" data-slot="select-scroll-down-button"
className={cn( className={cn(
"flex cursor-pointer items-center justify-center py-1", "flex cursor-pointer items-center justify-center py-1",
@@ -181,7 +250,7 @@ function SelectScrollDownButton({
{...props} {...props}
> >
<RiArrowDownSLine className="size-4" /> <RiArrowDownSLine className="size-4" />
</SelectPrimitive.ScrollDownButton> </BaseSelect.ScrollDownArrow>
) )
} }
+3 -5
View File
@@ -1,20 +1,18 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator" import { Separator as BaseSeparator } from "@base-ui/react/separator"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Separator({ function Separator({
className, className,
orientation = "horizontal", orientation = "horizontal",
decorative = true,
...props ...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { }: React.ComponentProps<typeof BaseSeparator>) {
return ( return (
<SeparatorPrimitive.Root <BaseSeparator
data-slot="separator" data-slot="separator"
decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
+50
View File
@@ -0,0 +1,50 @@
import * as React from "react";
type AnyProps = Record<string, unknown>;
function mergeRefs<T>(...refs: Array<React.Ref<T> | undefined>) {
return (value: T) => {
for (const ref of refs) {
if (!ref) continue;
if (typeof ref === "function") ref(value);
else (ref as React.MutableRefObject<T | null>).current = value;
}
};
}
function mergeProps(childProps: AnyProps, slotProps: AnyProps): AnyProps {
const merged: AnyProps = { ...slotProps };
for (const key in childProps) {
const slotValue = slotProps[key];
const childValue = childProps[key];
if (/^on[A-Z]/.test(key) && typeof slotValue === "function" && typeof childValue === "function") {
merged[key] = (...args: unknown[]) => {
(childValue as (...a: unknown[]) => unknown)(...args);
(slotValue as (...a: unknown[]) => unknown)(...args);
};
} else if (key === "className" && typeof slotValue === "string" && typeof childValue === "string") {
merged[key] = `${childValue} ${slotValue}`;
} else if (key === "style" && typeof slotValue === "object" && typeof childValue === "object") {
merged[key] = { ...(childValue as object), ...(slotValue as object) };
} else {
merged[key] = childValue;
}
}
return merged;
}
export interface SlotProps extends React.HTMLAttributes<HTMLElement> {
children?: React.ReactNode;
}
export const Slot = React.forwardRef<HTMLElement, SlotProps>(function Slot(
{ children, ...slotProps },
ref,
) {
if (!React.isValidElement(children)) return null;
const child = children as React.ReactElement<AnyProps & { ref?: React.Ref<unknown> }>;
return React.cloneElement(child, {
...mergeProps(child.props as AnyProps, slotProps as AnyProps),
ref: mergeRefs(ref as React.Ref<unknown>, (child as unknown as { ref?: React.Ref<unknown> }).ref),
} as AnyProps);
});
+111 -25
View File
@@ -1,45 +1,131 @@
"use client" "use client"
import { useTheme } from "next-themes" import * as React from "react"
import { Toaster as Sonner } from "sonner" import { Toaster as Sonner } from "sonner"
import type { ToasterProps } from "sonner" import type { ToasterProps } from "sonner"
const SHADOW_DARK =
"inset 0 1px 0 0 rgba(255,255,255,0.12), inset 0 0 0 1px rgba(255,255,255,0.08), 0 0 0 1px rgba(0,0,0,0.36), 0 1px 1px -0.5px rgba(0,0,0,0.22), 0 3px 3px -1.5px rgba(0,0,0,0.20), 0 6px 6px -3px rgba(0,0,0,0.16)"
const SHADOW_LIGHT =
"inset 0 1px 0 0 rgba(255,255,255,0.8), inset 0 0 0 1px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.10), 0 1px 2px -0.5px rgba(0,0,0,0.08), 0 4px 8px -2px rgba(0,0,0,0.08), 0 12px 20px -4px rgba(0,0,0,0.08)"
function useIsDarkTheme() {
const getIsDark = () =>
typeof document !== "undefined" && document.documentElement.classList.contains("dark")
const [isDark, setIsDark] = React.useState(getIsDark)
React.useEffect(() => {
const update = () => setIsDark(getIsDark())
const observer = new MutationObserver(update)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
})
update()
return () => observer.disconnect()
}, [])
return isDark
}
// Sonner makes each toast focusable (tabIndex=0) and defines a `:focus-visible`
// box-shadow that erases our custom elevation on click. We pin every toast's
// box-shadow as an !important inline style and strip tabIndex so the element
// can't receive focus-induced style swaps.
function usePinnedToastStyles(shadow: string) {
React.useEffect(() => {
if (typeof document === "undefined") return
const apply = (el: HTMLElement) => {
el.style.setProperty("box-shadow", shadow, "important")
el.style.setProperty("outline", "none", "important")
if (el.getAttribute("tabindex") === "0") el.setAttribute("tabindex", "-1")
}
const applyToAll = () => {
document
.querySelectorAll<HTMLElement>("[data-sonner-toast]")
.forEach(apply)
}
applyToAll()
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
m.addedNodes.forEach((node) => {
if (!(node instanceof HTMLElement)) return
if (node.matches?.("[data-sonner-toast]")) apply(node)
node
.querySelectorAll?.<HTMLElement>("[data-sonner-toast]")
.forEach(apply)
})
}
// Re-pin in case sonner mutates style.cssText on interactions.
applyToAll()
})
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "tabindex", "data-expanded", "data-swiping"],
})
return () => observer.disconnect()
}, [shadow])
}
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme() const isDark = useIsDarkTheme()
const shadow = isDark ? SHADOW_DARK : SHADOW_LIGHT
usePinnedToastStyles(shadow)
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={isDark ? "dark" : "light"}
className="toaster group" className="toaster group"
closeButton={false} closeButton={false}
toastOptions={{ toastOptions={{
style: {
borderRadius: "var(--radius-lg)",
},
classNames: { classNames: {
toast: "rounded-[var(--radius-lg)]", toast:
actionButton: "rounded-[var(--radius-md)]", "group/toast toast !rounded-[var(--radius-xl)] !border-0 !px-3.5 !py-3 !gap-2.5 !text-foreground",
cancelButton: "rounded-[var(--radius-md)]", title: "typography-ui-label !font-medium !text-foreground",
closeButton: "rounded-[var(--radius-md)]", description: "typography-meta !text-muted-foreground !mt-0.5",
actionButton:
"!rounded-[var(--radius-md)] !bg-[var(--primary-base)] !text-white hover:!opacity-85 !px-2 !py-1 typography-meta !font-medium transition-opacity",
cancelButton:
"!rounded-[var(--radius-md)] !bg-[var(--interactive-hover)] !text-foreground hover:!bg-[var(--interactive-active)] !px-2 !py-1 typography-meta !font-medium transition-colors",
closeButton:
"!rounded-[var(--radius-md)] !bg-[var(--interactive-hover)] !text-foreground hover:!bg-[var(--interactive-active)]",
icon: "!text-muted-foreground",
success: "[&_[data-icon]]:!text-[var(--status-success)]",
error: "[&_[data-icon]]:!text-[var(--status-error)]",
warning: "[&_[data-icon]]:!text-[var(--status-warning)]",
info: "[&_[data-icon]]:!text-[var(--status-info)]",
},
style: {
borderRadius: "var(--radius-xl)",
backgroundColor: "var(--surface-elevated)",
}, },
}} }}
style={ style={
{ {
"--normal-bg": "var(--popover)", "--normal-bg": "var(--surface-elevated)",
"--normal-text": "var(--popover-foreground)", "--normal-text": "var(--foreground)",
"--normal-border": "var(--border)", "--normal-border": "transparent",
"--error-bg": "var(--popover)", "--error-bg": "var(--surface-elevated)",
"--error-text": "var(--popover-foreground)", "--error-text": "var(--foreground)",
"--error-border": "var(--border)", "--error-border": "transparent",
"--success-bg": "var(--popover)", "--success-bg": "var(--surface-elevated)",
"--success-text": "var(--popover-foreground)", "--success-text": "var(--foreground)",
"--success-border": "var(--border)", "--success-border": "transparent",
"--warning-bg": "var(--popover)", "--warning-bg": "var(--surface-elevated)",
"--warning-text": "var(--popover-foreground)", "--warning-text": "var(--foreground)",
"--warning-border": "var(--border)", "--warning-border": "transparent",
"--info-bg": "var(--popover)", "--info-bg": "var(--surface-elevated)",
"--info-text": "var(--popover-foreground)", "--info-text": "var(--foreground)",
"--info-border": "var(--border)", "--info-border": "transparent",
} as React.CSSProperties } as React.CSSProperties
} }
{...props} {...props}
@@ -101,11 +101,14 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
const [overflow, setOverflow] = React.useState<{ left: boolean; right: boolean }>({ left: false, right: false }); const [overflow, setOverflow] = React.useState<{ left: boolean; right: boolean }>({ left: false, right: false });
const itemIDs = React.useMemo(() => items.map((item) => item.id), [items]); const itemIDs = React.useMemo(() => items.map((item) => item.id), [items]);
const isScrollable = layoutMode === 'scrollable'; const isScrollable = layoutMode === 'scrollable';
const isDefaultVariant = variant === 'default';
const isActivePillVariant = variant === 'active-pill'; const isActivePillVariant = variant === 'active-pill';
const isAnimatedVariant = variant === 'animated'; const isAnimatedVariant = variant === 'animated';
const usesActivePillIndicator = isActivePillVariant || isAnimatedVariant; const usesActivePillIndicator = isActivePillVariant || isAnimatedVariant;
const useUnderlineIndicator = isDefaultVariant;
const usesIndicator = usesActivePillIndicator || useUnderlineIndicator;
const useIntrinsicPillSizing = isActivePillVariant && isScrollable; const useIntrinsicPillSizing = isActivePillVariant && isScrollable;
const showPillTrackBackground = isAnimatedVariant; const showPillTrackBackground = usesActivePillIndicator;
const shouldAnimateActivePill = animateActivePill ?? isAnimatedVariant; const shouldAnimateActivePill = animateActivePill ?? isAnimatedVariant;
const reorderEnabled = typeof onReorder === 'function'; const reorderEnabled = typeof onReorder === 'function';
const Wrapper = reorderEnabled ? SortableTabWrapper : StaticTabWrapper; const Wrapper = reorderEnabled ? SortableTabWrapper : StaticTabWrapper;
@@ -139,7 +142,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
}, []); }, []);
const updateActivePillRect = React.useCallback(() => { const updateActivePillRect = React.useCallback(() => {
if (!usesActivePillIndicator || !activeId) { if (!usesIndicator || !activeId) {
setPillRect((prev) => (prev === null ? prev : null)); setPillRect((prev) => (prev === null ? prev : null));
return; return;
} }
@@ -172,7 +175,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
}; };
setPillRect((prev) => (isSamePillRect(prev, nextRect) ? prev : nextRect)); setPillRect((prev) => (isSamePillRect(prev, nextRect) ? prev : nextRect));
}, [activeId, isSamePillRect, usesActivePillIndicator]); }, [activeId, isSamePillRect, usesIndicator]);
const updateOverflow = React.useCallback(() => { const updateOverflow = React.useCallback(() => {
if (!isScrollable) { if (!isScrollable) {
@@ -215,7 +218,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
}, [isScrollable, items.length, updateOverflow]); }, [isScrollable, items.length, updateOverflow]);
React.useEffect(() => { React.useEffect(() => {
if (!usesActivePillIndicator) { if (!usesIndicator) {
setPillRect(null); setPillRect(null);
return; return;
} }
@@ -240,7 +243,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
return () => { return () => {
observer.disconnect(); observer.disconnect();
}; };
}, [activeId, items.length, updateActivePillRect, usesActivePillIndicator]); }, [activeId, items.length, updateActivePillRect, usesIndicator]);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
updateActivePillRect(); updateActivePillRect();
@@ -319,7 +322,8 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
usesActivePillIndicator && '@container/pill-tabs', usesActivePillIndicator && '@container/pill-tabs',
usesActivePillIndicator && 'pill-tabs__track', usesActivePillIndicator && 'pill-tabs__track',
usesActivePillIndicator && (activePillInsetClassName ?? 'gap-0.5 py-0.5'), usesActivePillIndicator && (activePillInsetClassName ?? 'gap-0.5 py-0.5'),
showPillTrackBackground && 'rounded-lg bg-[var(--surface-muted)]/50', useUnderlineIndicator && 'items-center overflow-y-hidden',
showPillTrackBackground && 'rounded-[10px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px] bg-[color-mix(in_srgb,var(--foreground)_2%,transparent)] p-0.5 gap-0.5',
isScrollable isScrollable
? 'overflow-x-auto scrollbar-none' ? 'overflow-x-auto scrollbar-none'
: 'overflow-x-hidden', : 'overflow-x-hidden',
@@ -331,16 +335,29 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
{usesActivePillIndicator && pillRect ? ( {usesActivePillIndicator && pillRect ? (
<div <div
className={cn( className={cn(
'pointer-events-none absolute left-0 top-0 z-0 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)]', 'pointer-events-none absolute left-0 top-0 z-0 rounded-[9px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px] bg-[var(--surface-elevated)]',
shouldAnimateActivePill && 'transition-[transform,width,height] duration-200 ease-out' 'border border-border/60'
)} )}
style={{ style={{
transform: `translate(${pillRect.left}px, ${pillRect.top}px)`, transform: `translate3d(${pillRect.left}px, ${pillRect.top}px, 0)`,
width: `${pillRect.width}px`, width: `${pillRect.width}px`,
height: `${pillRect.height}px`, height: `${pillRect.height}px`,
transition: shouldAnimateActivePill
? 'transform 300ms cubic-bezier(0.65, 0, 0.35, 1), width 300ms cubic-bezier(0.65, 0, 0.35, 1), height 300ms cubic-bezier(0.65, 0, 0.35, 1)'
: undefined,
}} }}
/> />
) : null} ) : null}
{useUnderlineIndicator && pillRect ? (
<div
className="pointer-events-none absolute left-0 -bottom-px z-10 h-[3px] rounded-t-[2px] bg-[var(--primary-base)]"
style={{
transform: `translate3d(${pillRect.left}px, 0, 0)`,
width: `${pillRect.width}px`,
}}
aria-hidden
/>
) : null}
{items.map((item) => { {items.map((item) => {
const isActive = item.id === activeId; const isActive = item.id === activeId;
const showInactiveIconOnly = inactiveTabsIconOnly && usesActivePillIndicator && !isActive && Boolean(item.icon); const showInactiveIconOnly = inactiveTabsIconOnly && usesActivePillIndicator && !isActive && Boolean(item.icon);
@@ -368,8 +385,8 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
usesActivePillIndicator usesActivePillIndicator
? 'relative z-10 bg-transparent' ? 'relative z-10 bg-transparent'
: isActive : isActive
? 'border-r border-border/40 bg-[var(--surface-elevated)] text-foreground' ? 'relative z-10 bg-transparent text-foreground'
: 'border-r border-border/40 bg-[var(--surface-elevated)]/25 text-muted-foreground hover:bg-[var(--surface-elevated)]/65 hover:text-foreground' : 'relative z-10 bg-transparent text-muted-foreground hover:text-foreground'
)} )}
> >
<button <button
@@ -380,7 +397,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
onClick={() => onSelect(item.id)} onClick={() => onSelect(item.id)}
className={cn( className={cn(
usesActivePillIndicator usesActivePillIndicator
? 'animated-tabs__button pill-tabs__button relative z-10 flex flex-1 items-center justify-center rounded-lg text-sm font-medium transition-colors duration-150 !min-h-0' ? 'animated-tabs__button pill-tabs__button relative z-10 flex flex-1 items-center justify-center rounded-[9px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px] text-sm font-medium transition-colors duration-150 !min-h-0'
: 'flex h-full min-w-0 items-center typography-micro', : 'flex h-full min-w-0 items-center typography-micro',
usesActivePillIndicator && closable && '!flex-none', usesActivePillIndicator && closable && '!flex-none',
usesActivePillIndicator && activePillLowercase ? 'lowercase' : null, usesActivePillIndicator && activePillLowercase ? 'lowercase' : null,
@@ -396,10 +413,10 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
? 'shrink-0 whitespace-nowrap px-3 text-center' ? 'shrink-0 whitespace-nowrap px-3 text-center'
: 'px-3 text-center') : 'px-3 text-center')
: isScrollable : isScrollable
? 'max-w-56 justify-start truncate pl-3 pr-2 text-left' ? 'max-w-56 justify-start truncate px-3 text-left'
: 'w-full justify-center truncate px-2.5 text-center', : 'w-full justify-center truncate px-3 text-center',
usesActivePillIndicator usesActivePillIndicator
? (activePillButtonClassName ?? (isActivePillVariant ? (isMobile ? 'h-[34px]' : 'h-[27px]') : 'h-7')) ? (activePillButtonClassName ?? (isActivePillVariant ? (isMobile ? 'h-[38px]' : 'h-[31px]') : 'h-7'))
: null, : null,
usesActivePillIndicator usesActivePillIndicator
? isActive ? isActive
@@ -419,7 +436,16 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
</> </>
) : ( ) : (
<span className={cn('flex min-w-0 items-center gap-1.5', !isScrollable && 'justify-center')}> <span className={cn('flex min-w-0 items-center gap-1.5', !isScrollable && 'justify-center')}>
{item.icon ? <span className="flex shrink-0 items-center justify-center">{item.icon}</span> : null} {item.icon ? (
<span
className={cn(
'flex shrink-0 items-center justify-center transition-colors duration-200 ease-out',
isActive ? 'text-[var(--primary-base)]' : 'text-muted-foreground'
)}
>
{item.icon}
</span>
) : null}
<span className="truncate leading-[1.2]">{item.label}</span> <span className="truncate leading-[1.2]">{item.label}</span>
</span> </span>
)} )}
+9 -9
View File
@@ -1,29 +1,29 @@
import * as React from 'react'; import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch'; import { Switch as BaseSwitch } from '@base-ui/react/switch';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const Switch = React.forwardRef< const Switch = React.forwardRef<
React.ComponentRef<typeof SwitchPrimitives.Root>, HTMLButtonElement,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> React.ComponentPropsWithoutRef<typeof BaseSwitch.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SwitchPrimitives.Root <BaseSwitch.Root
className={cn( className={cn(
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-[var(--interactive-border)]', 'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[checked]:bg-primary data-[unchecked]:bg-[var(--interactive-border)]',
className className
)} )}
style={{ width: '36px', height: '20px', minWidth: '36px', minHeight: '20px' }} style={{ width: '36px', height: '20px', minWidth: '36px', minHeight: '20px' }}
{...props} {...props}
ref={ref} ref={ref}
> >
<SwitchPrimitives.Thumb <BaseSwitch.Thumb
className={cn( className={cn(
'pointer-events-none block rounded-full bg-background shadow-none ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0' 'pointer-events-none block rounded-full bg-background shadow-none ring-0 transition-transform data-[checked]:translate-x-4 data-[unchecked]:translate-x-0'
)} )}
style={{ width: '16px', height: '16px', minWidth: '16px', minHeight: '16px' }} style={{ width: '16px', height: '16px', minWidth: '16px', minHeight: '16px' }}
/> />
</SwitchPrimitives.Root> </BaseSwitch.Root>
)); ));
Switch.displayName = SwitchPrimitives.Root.displayName; Switch.displayName = 'Switch';
export { Switch }; export { Switch };
+1 -7
View File
@@ -1,7 +1,5 @@
import * as React from "react"; import * as React from "react";
import { motion, type MotionProps } from "motion/react"; import { motion, type MotionProps } from "motion/react";
import * as Slot from "@radix-ui/react-slot";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
type Variant = { type Variant = {
@@ -219,9 +217,5 @@ export function Text({ variant = "shine", className, ...props }: TextProps) {
const Component = variantComponent || variants[FALLBACK_INDEX].component; const Component = variantComponent || variants[FALLBACK_INDEX].component;
return ( return <Component {...props} className={className} />;
<Slot.Root>
<Component {...props} className={className} />
</Slot.Root>
);
} }
+200 -26
View File
@@ -9,41 +9,215 @@ type TextareaProps = React.ComponentProps<"textarea"> & {
fillContainer?: boolean; fillContainer?: boolean;
useScrollShadow?: boolean; useScrollShadow?: boolean;
scrollShadowSize?: number; scrollShadowSize?: number;
hasError?: boolean;
/**
* AlignUI "simple" mode: render a bare textarea (no compound wrapper).
* Used for chat composer or anywhere the textarea is embedded inside an
* existing styled container.
*/
simple?: boolean;
/**
* Slot rendered in the bottom-right (next to the resize handle).
* Typically a `<TextareaCharCounter />`.
*/
endSlot?: React.ReactNode;
}; };
function ResizeHandle({ onResizeStart }: { onResizeStart: (event: React.PointerEvent<HTMLDivElement>) => void }) {
return (
<div
role="separator"
aria-orientation="horizontal"
aria-label="Resize textarea"
onPointerDown={onResizeStart}
// generous hit area; SVG renders centered in the bottom-right corner
className="pointer-events-auto -m-2 flex size-7 cursor-ns-resize items-end justify-end p-2 touch-none select-none"
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<path
d="M9.11111 2L2 9.11111M10 6.44444L6.44444 10"
className="stroke-muted-foreground"
strokeLinecap="round"
/>
</svg>
</div>
);
}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, outerClassName, scrollbarClassName, fillContainer = false, useScrollShadow = false, scrollShadowSize, ...props }, ref) => { (
{
className,
outerClassName,
scrollbarClassName,
fillContainer = false,
useScrollShadow = false,
scrollShadowSize,
hasError,
disabled,
simple,
endSlot,
...props
},
ref,
) => {
const wrapperRef = React.useRef<HTMLDivElement>(null);
const dragStateRef = React.useRef<{ startY: number; startHeight: number } | null>(null);
const [resizedHeight, setResizedHeight] = React.useState<number | null>(null);
const handleResizeStart = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
const wrapper = wrapperRef.current;
if (!wrapper) return;
dragStateRef.current = {
startY: event.clientY,
startHeight: wrapper.getBoundingClientRect().height,
};
const target = event.currentTarget;
target.setPointerCapture(event.pointerId);
const onMove = (moveEvent: PointerEvent) => {
const state = dragStateRef.current;
if (!state) return;
const next = state.startHeight + (moveEvent.clientY - state.startY);
setResizedHeight(Math.max(82, next));
};
const onUp = () => {
dragStateRef.current = null;
target.removeEventListener('pointermove', onMove);
target.removeEventListener('pointerup', onUp);
target.removeEventListener('pointercancel', onUp);
};
target.addEventListener('pointermove', onMove);
target.addEventListener('pointerup', onUp);
target.addEventListener('pointercancel', onUp);
event.preventDefault();
}, []);
const focusInnerTextarea = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
// Clicking the wrapper chrome (below/around the textarea) should focus it.
const target = event.target as HTMLElement;
if (target.closest('textarea, button, [role="separator"]')) return;
const textarea = wrapperRef.current?.querySelector('textarea');
if (textarea && document.activeElement !== textarea) {
event.preventDefault();
textarea.focus();
}
}, []);
// Simple mode: bare textarea, caller owns the wrapper.
if (simple) {
return (
<ScrollableOverlay
as="textarea"
ref={ref as React.Ref<HTMLTextAreaElement>}
disableHorizontal
fillContainer={fillContainer}
useScrollShadow={useScrollShadow}
scrollShadowSize={scrollShadowSize}
outerClassName={outerClassName}
scrollbarClassName={scrollbarClassName}
className={cn(
"block w-full appearance-none resize-none bg-transparent text-foreground typography-markdown outline-none",
"px-3 py-2 md:typography-ui-label",
"placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
fillContainer ? "[field-sizing:fixed]" : "field-sizing-content",
className,
)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
disabled={disabled}
{...props}
/>
);
}
return ( return (
<ScrollableOverlay <div
as="textarea" ref={wrapperRef}
ref={ref as React.Ref<HTMLTextAreaElement>} onPointerDown={focusInnerTextarea}
disableHorizontal style={resizedHeight !== null ? { height: `${resizedHeight}px` } : undefined}
fillContainer={fillContainer}
useScrollShadow={useScrollShadow}
scrollShadowSize={scrollShadowSize}
outerClassName={cn(
"w-full rounded-lg focus-within:ring-1 focus-within:ring-inset focus-within:ring-primary/50",
useScrollShadow && "border border-border/80 hover:border-input",
outerClassName
)}
scrollbarClassName={scrollbarClassName}
className={cn( className={cn(
"text-foreground placeholder:text-muted-foreground appearance-none dark:bg-input/30 flex min-h-16 w-full rounded-lg bg-transparent px-3 py-2 typography-markdown outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:typography-ui-label", "group/textarea relative flex w-full flex-col rounded-[var(--radius-xl)] bg-[var(--surface-elevated)] pb-2.5",
useScrollShadow ? "border-0" : "border border-border/80 hover:border-input focus:border-primary/70", "ring-1 ring-inset ring-border/60 transition duration-200 ease-out",
fillContainer ? "[field-sizing:fixed]" : "field-sizing-content", "hover:[&:not(:focus-within)]:bg-[var(--surface-subtle)]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "has-[[disabled]]:pointer-events-none has-[[disabled]]:bg-[var(--surface-subtle)] has-[[disabled]]:ring-transparent",
className !hasError && [
"hover:[&:not(:focus-within)]:ring-transparent",
"focus-within:ring-2 focus-within:ring-[var(--interactive-focus-ring)]",
],
hasError && [
"ring-[var(--status-error)]",
"focus-within:ring-2 focus-within:ring-[var(--status-error)]",
],
outerClassName,
)} )}
spellCheck={false} >
autoComplete="off" <div className="flex h-full min-h-0 flex-1 flex-col gap-2">
autoCorrect="off" <textarea
autoCapitalize="off" ref={ref}
{...props} className={cn(
/> "block w-full flex-1 min-h-0 appearance-none resize-none bg-transparent text-foreground typography-markdown outline-none",
"min-h-[82px] pl-3 pr-2.5 pt-2.5 md:typography-ui-label",
"focus-visible:outline-none disabled:cursor-not-allowed",
!disabled && [
"placeholder:select-none placeholder:text-muted-foreground placeholder:transition placeholder:duration-200 placeholder:ease-out",
"group-hover/textarea:placeholder:text-foreground/80",
"focus:placeholder:text-foreground/80",
],
disabled && "text-muted-foreground/60 placeholder:text-muted-foreground/60",
className,
)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
disabled={disabled}
{...props}
/>
<div className="flex items-center justify-end gap-1.5 pl-3 pr-2.5">
{endSlot}
<ResizeHandle onResizeStart={handleResizeStart} />
</div>
</div>
</div>
) )
} }
) )
Textarea.displayName = "Textarea" Textarea.displayName = "Textarea"
export { Textarea } function TextareaCharCounter({
current,
max,
className,
}: {
current?: number;
max?: number;
className?: string;
}) {
if (current === undefined || max === undefined) return null;
const isError = current > max;
return (
<span
className={cn(
"typography-meta text-muted-foreground",
"group-has-[[disabled]]/textarea:text-muted-foreground/60",
isError && "text-[var(--status-error)]",
className,
)}
>
{current}/{max}
</span>
);
}
export { Textarea, TextareaCharCounter }
+4 -4
View File
@@ -1,12 +1,12 @@
import * as React from "react" import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle" import { Toggle as BaseToggle } from "@base-ui/react/toggle"
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const toggleVariants = cva( const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-xl typography-ui-label font-medium hover:bg-interactive-hover hover:text-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-interactive-selection data-[state=on]:text-interactive-selection-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", "inline-flex items-center justify-center gap-2 rounded-xl typography-ui-label font-medium hover:bg-interactive-hover hover:text-foreground disabled:pointer-events-none disabled:opacity-50 data-[pressed]:bg-interactive-selection data-[pressed]:text-interactive-selection-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{ {
variants: { variants: {
variant: { variant: {
@@ -32,10 +32,10 @@ function Toggle({
variant, variant,
size, size,
...props ...props
}: React.ComponentProps<typeof TogglePrimitive.Root> & }: React.ComponentProps<typeof BaseToggle> &
VariantProps<typeof toggleVariants>) { VariantProps<typeof toggleVariants>) {
return ( return (
<TogglePrimitive.Root <BaseToggle
data-slot="toggle" data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))} className={cn(toggleVariants({ variant, size, className }))}
{...props} {...props}
+60 -31
View File
@@ -1,65 +1,94 @@
import * as React from "react" import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
type AsChildRenderProps = {
render?: React.ReactElement;
children?: React.ReactNode;
};
type ProviderProps = React.ComponentProps<typeof BaseTooltip.Provider> & {
delayDuration?: number;
skipDelayDuration?: number;
};
function TooltipProvider({ function TooltipProvider({
delayDuration = 0, delayDuration = 0,
skipDelayDuration,
delay,
closeDelay,
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { }: ProviderProps) {
return ( return (
<TooltipPrimitive.Provider <BaseTooltip.Provider
data-slot="tooltip-provider" delay={delay ?? delayDuration}
delayDuration={delayDuration} closeDelay={closeDelay ?? skipDelayDuration}
{...props} {...props}
/> />
) )
} }
type TooltipProps = React.ComponentProps<typeof TooltipPrimitive.Root> & { type TooltipRootProps = React.ComponentProps<typeof BaseTooltip.Root> & {
delayDuration?: number delayDuration?: number
} }
function Tooltip({ function Tooltip({
delayDuration = 0, delayDuration,
...props ...props
}: TooltipProps) { }: TooltipRootProps) {
return ( const tooltip = <BaseTooltip.Root {...props} />
<TooltipPrimitive.Root data-slot="tooltip" delayDuration={delayDuration} {...props} />
) if (delayDuration === undefined) {
return tooltip
}
return <TooltipProvider delayDuration={delayDuration}>{tooltip}</TooltipProvider>
} }
function TooltipTrigger({ function TooltipTrigger({
asChild,
children,
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { }: React.ComponentProps<typeof BaseTooltip.Trigger> & { asChild?: boolean }) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} /> const renderProps: AsChildRenderProps = asChild && React.isValidElement(children)
? { render: children as React.ReactElement }
: { children };
return <BaseTooltip.Trigger data-slot="tooltip-trigger" {...props} {...renderProps} />
} }
type ContentProps = React.ComponentProps<typeof BaseTooltip.Popup> & {
sideOffset?: number;
side?: "top" | "right" | "bottom" | "left";
align?: "start" | "center" | "end";
};
function TooltipContent({ function TooltipContent({
className, className,
sideOffset = 0, sideOffset = 0,
side,
align,
children, children,
style, style,
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) { }: ContentProps) {
return ( return (
<TooltipPrimitive.Portal> <BaseTooltip.Portal>
<TooltipPrimitive.Content <BaseTooltip.Positioner sideOffset={sideOffset} side={side} align={align} className="z-50">
data-slot="tooltip-content" <BaseTooltip.Popup
sideOffset={sideOffset} data-slot="tooltip-content"
className={cn( className={cn(
"bg-muted text-muted-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-xl px-3 py-1.5 typography-meta text-balance overflow-hidden transform-gpu will-change-transform", "bg-muted text-muted-foreground animate-in fade-in-0 zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-[var(--transform-origin)] rounded-xl px-3 py-1.5 typography-meta text-balance overflow-hidden transform-gpu will-change-transform",
className className
)} )}
style={{ style={{ ...style }}
...style, {...props}
}} >
{...props} {children}
> <BaseTooltip.Arrow className="fill-muted z-50 size-2" />
{children} </BaseTooltip.Popup>
<TooltipPrimitive.Arrow className="fill-muted z-50 size-2" /> </BaseTooltip.Positioner>
</TooltipPrimitive.Content> </BaseTooltip.Portal>
</TooltipPrimitive.Portal>
) )
} }
+16 -13
View File
@@ -216,7 +216,7 @@ const FileSelector = React.memo<FileSelectorProps>(({
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className="flex h-8 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-interactive-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"> <button className="flex h-7 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-interactive-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring">
{selectedFileEntry ? ( {selectedFileEntry ? (
<div className="flex min-w-0 items-center gap-3"> <div className="flex min-w-0 items-center gap-3">
<FileTypeIcon filePath={selectedFileEntry.path} className="h-3.5 w-3.5 flex-shrink-0" /> <FileTypeIcon filePath={selectedFileEntry.path} className="h-3.5 w-3.5 flex-shrink-0" />
@@ -288,28 +288,23 @@ const DiffViewModeSelector = React.memo<DiffViewModeSelectorProps>(({ mode, onMo
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className="flex h-8 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-interactive-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"> <button className="flex h-7 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-interactive-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring">
<span className="min-w-0 truncate typography-meta"> <span className="min-w-0 truncate typography-meta">
{currentOption.label} {currentOption.label}
</span> </span>
<RiArrowDownSLine className="size-4 opacity-50" /> <RiArrowDownSLine className="size-4 opacity-50" />
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[220px]"> <DropdownMenuContent className="min-w-[140px]">
<DropdownMenuRadioGroup <DropdownMenuRadioGroup
value={mode} value={mode}
onValueChange={(value) => onModeChange(value as DiffTabViewMode)} onValueChange={(value) => onModeChange(value as DiffTabViewMode)}
> >
{DIFF_VIEW_MODE_OPTIONS.map((option) => ( {DIFF_VIEW_MODE_OPTIONS.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value}> <DropdownMenuRadioItem key={option.value} value={option.value}>
<div className="flex flex-col gap-0.5"> <span className="typography-meta text-foreground">
<span className="typography-meta text-foreground"> {option.label}
{option.label} </span>
</span>
<span className="typography-micro text-muted-foreground">
{option.description}
</span>
</div>
</DropdownMenuRadioItem> </DropdownMenuRadioItem>
))} ))}
</DropdownMenuRadioGroup> </DropdownMenuRadioGroup>
@@ -582,7 +577,14 @@ const SingleDiffViewer = React.memo<SingleDiffViewerProps>(({
} }
return ( return (
<div className="absolute inset-0" style={{ contain: 'size layout' }}> <ScrollableOverlay
outerClassName="absolute inset-0"
disableHorizontal={false}
observeMutations={false}
preventOverscroll
data-diff-virtual-root
data-diff-virtual-content
>
<PierreDiffViewer <PierreDiffViewer
original={diff.original} original={diff.original}
modified={diff.modified} modified={diff.modified}
@@ -590,8 +592,9 @@ const SingleDiffViewer = React.memo<SingleDiffViewerProps>(({
fileName={filePath} fileName={filePath}
renderSideBySide={renderSideBySide} renderSideBySide={renderSideBySide}
wrapLines={wrapLines} wrapLines={wrapLines}
layout="inline"
/> />
</div> </ScrollableOverlay>
); );
}); });
+33 -2
View File
@@ -508,6 +508,19 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const [wrapLines, setWrapLines] = React.useState(true); const [wrapLines, setWrapLines] = React.useState(true);
const [isFullscreen, setIsFullscreen] = React.useState(false); const [isFullscreen, setIsFullscreen] = React.useState(false);
const [isSearchOpen, setIsSearchOpen] = React.useState(false); const [isSearchOpen, setIsSearchOpen] = React.useState(false);
const [isFloatingToolbarOpen, setIsFloatingToolbarOpen] = React.useState(false);
const floatingToolbarRef = React.useRef<HTMLDivElement | null>(null);
React.useEffect(() => {
if (!isFloatingToolbarOpen) return;
const handler = (event: MouseEvent) => {
if (floatingToolbarRef.current && !floatingToolbarRef.current.contains(event.target as Node)) {
setIsFloatingToolbarOpen(false);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [isFloatingToolbarOpen]);
const [textViewMode, setTextViewMode] = React.useState<'view' | 'edit'>('edit'); const [textViewMode, setTextViewMode] = React.useState<'view' | 'edit'>('edit');
const [mdViewMode, setMdViewMode] = React.useState<'preview' | 'edit'>('edit'); const [mdViewMode, setMdViewMode] = React.useState<'preview' | 'edit'>('edit');
const [jsonViewMode, setJsonViewMode] = React.useState<'tree' | 'text'>('tree'); const [jsonViewMode, setJsonViewMode] = React.useState<'tree' | 'text'>('tree');
@@ -2741,8 +2754,26 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<div className="flex-1 min-h-0 min-w-0 relative"> <div className="flex-1 min-h-0 min-w-0 relative">
{selectedFile && !isSearchOpen && ( {selectedFile && !isSearchOpen && (
<div className="absolute right-3 top-3 z-30"> <div
{renderFloatingFileControls()} ref={floatingToolbarRef}
className="absolute right-3 top-3 z-30"
onMouseEnter={() => setIsFloatingToolbarOpen(true)}
onMouseLeave={() => setIsFloatingToolbarOpen(false)}
>
{isFloatingToolbarOpen ? (
renderFloatingFileControls()
) : (
<Button
variant="ghost"
size="sm"
onClick={() => setIsFloatingToolbarOpen(true)}
className="h-8 w-8 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-0 text-muted-foreground shadow-sm hover:text-foreground"
aria-label="Show editor controls"
title="Editor controls"
>
<RiMore2Fill className="h-4 w-4" />
</Button>
)}
</div> </div>
)} )}
<ScrollableOverlay outerClassName="h-full min-w-0" className="h-full min-w-0"> <ScrollableOverlay outerClassName="h-full min-w-0" className="h-full min-w-0">
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog'; import { Dialog } from '@base-ui/react/dialog';
import { RiCloseLine } from '@remixicon/react'; import { RiCloseLine } from '@remixicon/react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { MultiRunLauncher } from '@/components/multirun'; import { MultiRunLauncher } from '@/components/multirun';
@@ -23,23 +23,22 @@ export const MultiRunWindow: React.FC<MultiRunWindowProps> = ({
} }
return Boolean( return Boolean(
document.querySelector('[data-slot="dropdown-menu-content"][data-state="open"], [data-slot="select-content"][data-state="open"]') document.querySelector('[data-slot="dropdown-menu-content"][data-open], [data-slot="select-content"][data-open]')
); );
}, []); }, []);
return ( return (
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}> <Dialog.Root
<DialogPrimitive.Portal> open={open}
<DialogPrimitive.Overlay onOpenChange={(next) => {
className="fixed inset-0 z-50 bg-black/50 dark:bg-black/75" if (!next && hasOpenFloatingMenu()) return;
/> onOpenChange(next);
<DialogPrimitive.Content }}
>
<Dialog.Portal>
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black/50 dark:bg-black/75" />
<Dialog.Popup
aria-describedby={descriptionId} aria-describedby={descriptionId}
onInteractOutside={(event) => {
if (hasOpenFloatingMenu()) {
event.preventDefault();
}
}}
className={cn( className={cn(
'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]', 'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
'w-[90vw] max-w-[720px] h-[680px] max-h-[85vh]', 'w-[90vw] max-w-[720px] h-[680px] max-h-[85vh]',
@@ -57,17 +56,17 @@ export const MultiRunWindow: React.FC<MultiRunWindowProps> = ({
<RiCloseLine className="h-5 w-5" /> <RiCloseLine className="h-5 w-5" />
</button> </button>
</div> </div>
<DialogPrimitive.Description id={descriptionId} className="sr-only"> <Dialog.Description id={descriptionId} className="sr-only">
OpenChamber Multi-Run window. OpenChamber Multi-Run window.
</DialogPrimitive.Description> </Dialog.Description>
<MultiRunLauncher <MultiRunLauncher
initialPrompt={initialPrompt} initialPrompt={initialPrompt}
onCreated={() => onOpenChange(false)} onCreated={() => onOpenChange(false)}
onCancel={() => onOpenChange(false)} onCancel={() => onOpenChange(false)}
isWindowed isWindowed
/> />
</DialogPrimitive.Content> </Dialog.Popup>
</DialogPrimitive.Portal> </Dialog.Portal>
</DialogPrimitive.Root> </Dialog.Root>
); );
}; };
@@ -116,7 +116,8 @@ function isPageAvailable(page: SettingsPageMeta, ctx: SettingsRuntimeContext): b
return page.isAvailable(ctx); return page.isAvailable(ctx);
} }
function getSettingsNavIcon(slug: SettingsPageSlug): React.ComponentType<{ className?: string }> | null { // eslint-disable-next-line react-refresh/only-export-components
export function getSettingsNavIcon(slug: SettingsPageSlug): React.ComponentType<{ className?: string }> | null {
switch (slug) { switch (slug) {
case 'projects': case 'projects':
return RiFoldersLine; return RiFoldersLine;
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog'; import { Dialog } from '@base-ui/react/dialog';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { SettingsView } from './SettingsView'; import { SettingsView } from './SettingsView';
@@ -21,23 +21,22 @@ export const SettingsWindow: React.FC<SettingsWindowProps> = ({ open, onOpenChan
} }
return Boolean( return Boolean(
document.querySelector('[data-slot="dropdown-menu-content"][data-state="open"], [data-slot="select-content"][data-state="open"]') document.querySelector('[data-slot="dropdown-menu-content"][data-open], [data-slot="select-content"][data-open]')
); );
}, []); }, []);
return ( return (
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}> <Dialog.Root
<DialogPrimitive.Portal> open={open}
<DialogPrimitive.Overlay onOpenChange={(next) => {
className="fixed inset-0 z-50 bg-black/50 dark:bg-black/75" if (!next && hasOpenFloatingMenu()) return;
/> onOpenChange(next);
<DialogPrimitive.Content }}
>
<Dialog.Portal>
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black/50 dark:bg-black/75" />
<Dialog.Popup
aria-describedby={descriptionId} aria-describedby={descriptionId}
onInteractOutside={(event) => {
if (hasOpenFloatingMenu()) {
event.preventDefault();
}
}}
className={cn( className={cn(
'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]', 'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
'w-[90vw] max-w-[960px] h-[85vh] max-h-[900px]', 'w-[90vw] max-w-[960px] h-[85vh] max-h-[900px]',
@@ -45,12 +44,12 @@ export const SettingsWindow: React.FC<SettingsWindowProps> = ({ open, onOpenChan
'bg-background' 'bg-background'
)} )}
> >
<DialogPrimitive.Description id={descriptionId} className="sr-only"> <Dialog.Description id={descriptionId} className="sr-only">
OpenChamber settings window. OpenChamber settings window.
</DialogPrimitive.Description> </Dialog.Description>
<SettingsView onClose={() => onOpenChange(false)} isWindowed /> <SettingsView onClose={() => onOpenChange(false)} isWindowed />
</DialogPrimitive.Content> </Dialog.Popup>
</DialogPrimitive.Portal> </Dialog.Portal>
</DialogPrimitive.Root> </Dialog.Root>
); );
}; };
@@ -908,13 +908,9 @@ export const TerminalView: React.FC = () => {
<Button <Button
type="button" type="button"
size="sm" size="sm"
variant="outline" variant="chip"
className={cn( aria-pressed={activeModifier === 'ctrl'}
"h-6 w-9 p-0", className="h-6 w-9 p-0"
activeModifier === 'ctrl'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: undefined
)}
onClick={() => handleModifierToggle('ctrl')} onClick={() => handleModifierToggle('ctrl')}
disabled={quickKeysDisabled} disabled={quickKeysDisabled}
> >
@@ -924,13 +920,9 @@ export const TerminalView: React.FC = () => {
<Button <Button
type="button" type="button"
size="sm" size="sm"
variant="outline" variant="chip"
className={cn( aria-pressed={activeModifier === 'cmd'}
"h-6 w-9 p-0", className="h-6 w-9 p-0"
activeModifier === 'cmd'
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
: undefined
)}
onClick={() => handleModifierToggle('cmd')} onClick={() => handleModifierToggle('cmd')}
disabled={quickKeysDisabled} disabled={quickKeysDisabled}
> >
@@ -202,7 +202,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
<RiArrowDownSLine className="h-4 w-4 flex-shrink-0 text-muted-foreground" /> <RiArrowDownSLine className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[var(--radix-dropdown-menu-trigger-width)]"> <DropdownMenuContent align="start" className="w-[var(--anchor-width)]">
{group.sessions.map((session) => ( {group.sessions.map((session) => (
<DropdownMenuItem <DropdownMenuItem
key={session.id} key={session.id}
@@ -249,19 +249,15 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[220px]"> <DropdownMenuContent align="end" className="min-w-[220px]">
<DropdownMenuItem <DropdownMenuItem
onSelect={(e) => { onSelect={handleRemoveSelectedWorktree}
e.preventDefault(); closeOnClick={false}
handleRemoveSelectedWorktree();
}}
variant="destructive" variant="destructive"
> >
Remove this worktree Remove this worktree
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onSelect={(e) => { onSelect={handleKeepOnlySelectedWorktree}
e.preventDefault(); closeOnClick={false}
handleKeepOnlySelectedWorktree();
}}
> >
Leave this one, remove others Leave this one, remove others
</DropdownMenuItem> </DropdownMenuItem>
@@ -278,7 +278,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
</div> </div>
{/* Branch Selection */} {/* Branch Selection */}
<div className="space-y-3"> <div className="flex flex-col gap-3">
<p className="typography-meta text-muted-foreground"> <p className="typography-meta text-muted-foreground">
{operation === 'merge' ? `Branch to merge into ${targetBranchLabel}` : 'Branch to rebase onto'} {operation === 'merge' ? `Branch to merge into ${targetBranchLabel}` : 'Branch to rebase onto'}
</p> </p>
@@ -293,7 +293,8 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="start" align="start"
className="w-[--radix-dropdown-menu-trigger-width] p-0 max-h-(--radix-dropdown-menu-content-available-height) flex flex-col overflow-hidden" sideOffset={6}
className="w-[var(--anchor-width)] p-0 max-h-[min(var(--available-height),24rem)] flex flex-col overflow-hidden"
> >
<Command className="h-full min-h-0"> <Command className="h-full min-h-0">
<CommandInput <CommandInput
@@ -1,11 +1,10 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { import {
RiCheckboxLine,
RiCheckboxBlankLine,
RiArrowGoBackLine, RiArrowGoBackLine,
RiLoader4Line, RiLoader4Line,
} from '@remixicon/react'; } from '@remixicon/react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Checkbox } from '@/components/ui/checkbox';
import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import type { GitStatus } from '@/lib/api/types'; import type { GitStatus } from '@/lib/api/types';
@@ -82,15 +81,6 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
[onToggle, onViewDiff] [onToggle, onViewDiff]
); );
const handleToggleClick = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
onToggle();
},
[onToggle]
);
const handleRevertClick = useCallback( const handleRevertClick = useCallback(
(event: React.MouseEvent) => { (event: React.MouseEvent) => {
event.preventDefault(); event.preventDefault();
@@ -109,19 +99,14 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
style={indentPx > 0 ? { paddingLeft: `${indentPx}px` } : undefined} style={indentPx > 0 ? { paddingLeft: `${indentPx}px` } : undefined}
> >
<button <div onClick={(e) => { e.stopPropagation(); }}>
type="button" <Checkbox
onClick={handleToggleClick} size="sm"
aria-pressed={checked} checked={checked}
aria-label={`Select ${file.path}`} onChange={() => onToggle()}
className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" ariaLabel={`Select ${file.path}`}
> />
{checked ? ( </div>
<RiCheckboxLine className="size-4 text-primary" />
) : (
<RiCheckboxBlankLine className="size-4" />
)}
</button>
<span <span
className="typography-micro font-semibold w-4 text-center uppercase" className="typography-micro font-semibold w-4 text-center uppercase"
style={{ color: descriptor.color }} style={{ color: descriptor.color }}
@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { RiArrowDownSLine, RiArrowRightSLine, RiCheckboxBlankLine, RiCheckboxLine, RiSubtractLine } from '@remixicon/react'; import { RiArrowDownSLine, RiArrowRightSLine, RiCheckboxBlankLine, RiCheckboxLine, RiSubtractLine } from '@remixicon/react';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
@@ -444,26 +445,22 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<h3 className="typography-ui-header font-semibold text-foreground">Changes</h3> <h3 className="typography-ui-header font-semibold text-foreground">Changes</h3>
{totalCount > 0 ? ( {totalCount > 0 ? (
<button <div
type="button"
onClick={areAllSelected ? onClearSelection : onSelectAll}
disabled={isRevertingAll}
aria-checked={isPartiallySelected ? 'mixed' : hasAnySelected}
aria-label={areAllSelected ? 'Clear file selection' : 'Select all files'}
className={cn( className={cn(
'inline-flex h-6 items-center gap-1 rounded px-1.5 text-muted-foreground', 'inline-flex h-6 items-center gap-1 rounded px-1.5',
'hover:bg-interactive-hover/55 hover:text-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
isRevertingAll && 'cursor-not-allowed opacity-50' isRevertingAll && 'cursor-not-allowed opacity-50'
)} )}
> >
{hasAnySelected ? ( <Checkbox
<RiCheckboxLine className={cn('size-4', isPartiallySelected ? 'text-primary/50' : 'text-primary')} /> size="sm"
) : ( checked={hasAnySelected}
<RiCheckboxBlankLine className="size-4" /> indeterminate={isPartiallySelected}
)} disabled={isRevertingAll}
onChange={() => (areAllSelected ? onClearSelection() : onSelectAll())}
ariaLabel={areAllSelected ? 'Clear file selection' : 'Select all files'}
/>
<span className="typography-meta text-muted-foreground">{selectedCount}/{totalCount}</span> <span className="typography-meta text-muted-foreground">{selectedCount}/{totalCount}</span>
</button> </div>
) : null} ) : null}
</div> </div>
<div className="flex items-center gap-2 pr-1"> <div className="flex items-center gap-2 pr-1">
@@ -374,7 +374,7 @@ export const IntegrateCommitsSection: React.FC<{
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
className="w-72 p-0 max-h-(--radix-dropdown-menu-content-available-height) flex flex-col overflow-hidden" className="w-72 p-0 max-h-[var(--available-height)] flex flex-col overflow-hidden"
> >
<Command className="h-full min-h-0"> <Command className="h-full min-h-0">
<CommandInput ref={searchInputRef} placeholder="Search branches..." /> <CommandInput ref={searchInputRef} placeholder="Search branches..." />
@@ -6,8 +6,6 @@ import {
RiAiGenerate2, RiAiGenerate2,
RiArrowDownSLine, RiArrowDownSLine,
RiArrowRightSLine, RiArrowRightSLine,
RiCheckboxBlankLine,
RiCheckboxLine,
RiCloseLine, RiCloseLine,
RiEditLine, RiEditLine,
RiErrorWarningLine, RiErrorWarningLine,
@@ -20,6 +18,7 @@ import {
RiLoader4Line, RiLoader4Line,
} from '@remixicon/react'; } from '@remixicon/react';
import { toast } from '@/components/ui'; import { toast } from '@/components/ui';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
@@ -1698,7 +1697,7 @@ export const PullRequestSection: React.FC<{
<Textarea <Textarea
value={body} value={body}
onChange={(e) => setBody(e.target.value)} onChange={(e) => setBody(e.target.value)}
className="min-h-[110px] bg-background/80" className="min-h-[110px]"
placeholder="What changed and why" placeholder="What changed and why"
autoCorrect={hasTouchInput ? "on" : "off"} autoCorrect={hasTouchInput ? "on" : "off"}
autoCapitalize={hasTouchInput ? "sentences" : "off"} autoCapitalize={hasTouchInput ? "sentences" : "off"}
@@ -1719,22 +1718,12 @@ export const PullRequestSection: React.FC<{
} }
}} }}
> >
<button <Checkbox
type="button" size="sm"
onClick={(e) => { checked={draft}
e.preventDefault(); onChange={(next) => setDraft(next)}
e.stopPropagation(); ariaLabel="Toggle draft PR"
setDraft((v) => !v); />
}}
aria-label="Toggle draft PR"
className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
{draft ? (
<RiCheckboxLine className="size-4 text-primary" />
) : (
<RiCheckboxBlankLine className="size-4" />
)}
</button>
<span className="typography-ui-label text-foreground select-none">Draft</span> <span className="typography-ui-label text-foreground select-none">Draft</span>
</div> </div>
+10 -2
View File
@@ -63,7 +63,15 @@ textarea[data-chat-input="true"]:focus-visible {
} }
.oc-cm-selected-line { .oc-cm-selected-line {
background: color-mix(in srgb, var(--accent) 70%, transparent); background: color-mix(in srgb, var(--primary-base) 22%, transparent);
}
.pierre-diff-wrapper {
--diffs-bg-selection-override: color-mix(in srgb, var(--primary-base) 22%, transparent);
--diffs-bg-selection-background-override: color-mix(in srgb, var(--primary-base) 14%, transparent);
--diffs-bg-selection-number-override: color-mix(in srgb, var(--primary-base) 28%, transparent);
--diffs-bg-selection-number-background-override: color-mix(in srgb, var(--primary-base) 18%, transparent);
--diffs-selection-color-override: var(--primary-base);
} }
/* Codemirror syntax fallback for classHighlighter tokens */ /* Codemirror syntax fallback for classHighlighter tokens */
@@ -869,7 +877,7 @@ html:not(.dark) .chat-scroll {
} }
.pill-tabs__track { .pill-tabs__track {
padding-inline: 0.375rem; padding-inline: 0.125rem;
} }
@container pill-tabs (max-width: 23rem) { @container pill-tabs (max-width: 23rem) {
+1 -14
View File
@@ -96,11 +96,7 @@ export class CSSVariableGenerator {
vars.push(` --ring: ${theme.colors.interactive.focusRing} !important;`); vars.push(` --ring: ${theme.colors.interactive.focusRing} !important;`);
if (theme.config?.radius?.md) { const sidebarBaseRgb = hexToRgb(theme.colors.surface.muted);
vars.push(` --radius: ${theme.config.radius.md} !important;`);
}
const sidebarBaseRgb = hexToRgb(theme.colors.surface.muted);
const sidebarAccentRgb = hexToRgb(theme.colors.surface.subtle); const sidebarAccentRgb = hexToRgb(theme.colors.surface.subtle);
const sidebarBorderRgb = hexToRgb(theme.colors.interactive.border); const sidebarBorderRgb = hexToRgb(theme.colors.interactive.border);
@@ -518,15 +514,6 @@ export class CSSVariableGenerator {
if (config.fonts.heading) vars.push(` --font-heading: ${config.fonts.heading};`); if (config.fonts.heading) vars.push(` --font-heading: ${config.fonts.heading};`);
} }
if (config.radius) {
if (config.radius.none) vars.push(` --radius-none: ${config.radius.none};`);
if (config.radius.sm) vars.push(` --radius-sm: ${config.radius.sm};`);
if (config.radius.md) vars.push(` --radius-md: ${config.radius.md};`);
if (config.radius.lg) vars.push(` --radius-lg: ${config.radius.lg};`);
if (config.radius.xl) vars.push(` --radius-xl: ${config.radius.xl};`);
if (config.radius.full) vars.push(` --radius-full: ${config.radius.full};`);
}
if (config.transitions) { if (config.transitions) {
if (config.transitions.fast) vars.push(` --transition-fast: ${config.transitions.fast};`); if (config.transitions.fast) vars.push(` --transition-fast: ${config.transitions.fast};`);
if (config.transitions.normal) vars.push(` --transition-normal: ${config.transitions.normal};`); if (config.transitions.normal) vars.push(` --transition-normal: ${config.transitions.normal};`);
@@ -411,14 +411,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"spacing": { "spacing": {
"xs": "0.25rem", "xs": "0.25rem",
"sm": "0.5rem", "sm": "0.5rem",
@@ -411,14 +411,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"spacing": { "spacing": {
"xs": "0.25rem", "xs": "0.25rem",
"sm": "0.5rem", "sm": "0.5rem",
@@ -5,7 +5,10 @@
"description": "Aura", "description": "Aura",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": [ "dark", "palette" ] "tags": [
"dark",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Aura", "description": "Aura",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": [ "light", "palette" ] "tags": [
"light",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Ayu", "description": "Ayu",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": [ "dark", "palette" ] "tags": [
"dark",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Ayu", "description": "Ayu",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": [ "light", "palette" ] "tags": [
"light",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Carbonfox", "description": "Carbonfox",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": [ "dark", "palette" ] "tags": [
"dark",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Carbonfox", "description": "Carbonfox",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": [ "light", "palette" ] "tags": [
"light",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Catppuccin", "description": "Catppuccin",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": [ "dark", "palette" ] "tags": [
"dark",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Catppuccin", "description": "Catppuccin",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": [ "light", "palette" ] "tags": [
"light",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -411,14 +411,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"spacing": { "spacing": {
"xs": "0.25rem", "xs": "0.25rem",
"sm": "0.5rem", "sm": "0.5rem",
@@ -411,14 +411,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"spacing": { "spacing": {
"xs": "0.25rem", "xs": "0.25rem",
"sm": "0.5rem", "sm": "0.5rem",
@@ -5,7 +5,10 @@
"description": "Dracula", "description": "Dracula",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": [ "dark", "palette" ] "tags": [
"dark",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Dracula", "description": "Dracula",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": [ "light", "palette" ] "tags": [
"light",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -6,7 +6,12 @@
"author": "OpenChamber", "author": "OpenChamber",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": ["dark", "warm", "earthy", "brand"] "tags": [
"dark",
"warm",
"earthy",
"brand"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -127,8 +132,8 @@
"markdown": { "markdown": {
"heading1": "#c47a3a", "heading1": "#c47a3a",
"heading2": "#93a56b", "heading2": "#93a56b",
"heading3": "#ebe0d1", "heading3": "#ebe0d1",
"heading4": "#ebe0d1", "heading4": "#ebe0d1",
"link": "#5a6d7a", "link": "#5a6d7a",
"linkHover": "#93a56b", "linkHover": "#93a56b",
"inlineCode": "#93a56b", "inlineCode": "#93a56b",
@@ -167,14 +172,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -6,7 +6,12 @@
"author": "OpenChamber", "author": "OpenChamber",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": ["light", "warm", "earthy", "brand"] "tags": [
"light",
"warm",
"earthy",
"brand"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -167,14 +172,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,12 @@
"description": "An inky color scheme for prose and code - dark variant", "description": "An inky color scheme for prose and code - dark variant",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": ["dark", "warm", "natural", "ink"] "tags": [
"dark",
"warm",
"natural",
"ink"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +171,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,12 @@
"description": "An inky color scheme for prose and code - light variant", "description": "An inky color scheme for prose and code - light variant",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": ["light", "warm", "natural", "paper"] "tags": [
"light",
"warm",
"natural",
"paper"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +171,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -411,14 +411,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"spacing": { "spacing": {
"xs": "0.25rem", "xs": "0.25rem",
"sm": "0.5rem", "sm": "0.5rem",
@@ -411,14 +411,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"spacing": { "spacing": {
"xs": "0.25rem", "xs": "0.25rem",
"sm": "0.5rem", "sm": "0.5rem",
@@ -5,7 +5,10 @@
"description": "Gruvbox", "description": "Gruvbox",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": [ "dark", "palette" ] "tags": [
"dark",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Gruvbox", "description": "Gruvbox",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": [ "light", "palette" ] "tags": [
"light",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,12 @@
"description": "A color scheme inspired by Japanese art and ink painting - dark variant", "description": "A color scheme inspired by Japanese art and ink painting - dark variant",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": ["dark", "japanese", "ink", "elegant"] "tags": [
"dark",
"japanese",
"ink",
"elegant"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +171,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,12 @@
"description": "A color scheme inspired by Japanese art and ink painting - light variant (Lotus)", "description": "A color scheme inspired by Japanese art and ink painting - light variant (Lotus)",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": ["light", "japanese", "lotus", "elegant"] "tags": [
"light",
"japanese",
"lotus",
"elegant"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +171,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -411,14 +411,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"spacing": { "spacing": {
"xs": "0.25rem", "xs": "0.25rem",
"sm": "0.5rem", "sm": "0.5rem",
@@ -411,14 +411,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"spacing": { "spacing": {
"xs": "0.25rem", "xs": "0.25rem",
"sm": "0.5rem", "sm": "0.5rem",
@@ -5,7 +5,12 @@
"description": "A pure monochromatic theme - dark variant with shades of grey", "description": "A pure monochromatic theme - dark variant with shades of grey",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": ["dark", "monochrome", "minimal", "grayscale"] "tags": [
"dark",
"monochrome",
"minimal",
"grayscale"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +171,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,12 @@
"description": "A pure monochromatic theme - light variant with shades of grey", "description": "A pure monochromatic theme - light variant with shades of grey",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": ["light", "monochrome", "minimal", "grayscale"] "tags": [
"light",
"monochrome",
"minimal",
"grayscale"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +171,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,11 @@
"description": "A pure monochromatic theme - dark variant with slight colors added", "description": "A pure monochromatic theme - dark variant with slight colors added",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": ["dark", "monochrome", "minimal"] "tags": [
"dark",
"monochrome",
"minimal"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -145,14 +149,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,11 @@
"description": "A pure monochromatic theme - light variant with slight colors added", "description": "A pure monochromatic theme - light variant with slight colors added",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": ["light", "monochrome", "minimal"] "tags": [
"light",
"monochrome",
"minimal"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -145,14 +149,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Monokai", "description": "Monokai",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": [ "dark", "palette" ] "tags": [
"dark",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Monokai", "description": "Monokai",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": [ "light", "palette" ] "tags": [
"light",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Night Owl", "description": "Night Owl",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": [ "dark", "palette" ] "tags": [
"dark",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Night Owl", "description": "Night Owl",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": [ "light", "palette" ] "tags": [
"light",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Nord", "description": "Nord",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": [ "dark", "palette" ] "tags": [
"dark",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -5,7 +5,10 @@
"description": "Nord", "description": "Nord",
"version": "1.0.0", "version": "1.0.0",
"variant": "light", "variant": "light",
"tags": [ "light", "palette" ] "tags": [
"light",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",
@@ -411,14 +411,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"spacing": { "spacing": {
"xs": "0.25rem", "xs": "0.25rem",
"sm": "0.5rem", "sm": "0.5rem",
@@ -411,14 +411,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"spacing": { "spacing": {
"xs": "0.25rem", "xs": "0.25rem",
"sm": "0.5rem", "sm": "0.5rem",
@@ -5,7 +5,10 @@
"description": "One Dark Pro", "description": "One Dark Pro",
"version": "1.0.0", "version": "1.0.0",
"variant": "dark", "variant": "dark",
"tags": [ "dark", "palette" ] "tags": [
"dark",
"palette"
]
}, },
"colors": { "colors": {
"primary": { "primary": {
@@ -166,14 +169,6 @@
"mono": "\"IBM Plex Mono\", monospace", "mono": "\"IBM Plex Mono\", monospace",
"heading": "\"IBM Plex Mono\", monospace" "heading": "\"IBM Plex Mono\", monospace"
}, },
"radius": {
"none": "0",
"sm": "0.325rem",
"md": "0.75rem",
"lg": "1.125rem",
"xl": "1.5rem",
"full": "9999px"
},
"transitions": { "transitions": {
"fast": "150ms ease", "fast": "150ms ease",
"normal": "250ms ease", "normal": "250ms ease",

Some files were not shown because too many files have changed in this diff Show More