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:
committed by
GitHub
parent
3e52be95c5
commit
4506c18f53
@@ -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`)
|
||||
- UI: React, TypeScript, Vite, Tailwind v4
|
||||
- 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`)
|
||||
- Desktop: Tauri v2 (`packages/desktop/src-tauri/`)
|
||||
- VS Code: extension + webview (`packages/vscode/`)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"": {
|
||||
"name": "openchamber-monorepo",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.4.0",
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
@@ -94,7 +95,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@openchamber/desktop",
|
||||
"version": "1.9.5",
|
||||
"version": "1.9.6",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/node": "^24.3.1",
|
||||
@@ -103,8 +104,9 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@openchamber/ui",
|
||||
"version": "1.9.5",
|
||||
"version": "1.9.6",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.4.0",
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
@@ -135,16 +137,6 @@
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@opencode-ai/sdk": "^1.4.6",
|
||||
"@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",
|
||||
"@simplewebauthn/browser": "13.3.0",
|
||||
"@tanstack/react-virtual": "^3.13.18",
|
||||
@@ -211,7 +203,7 @@
|
||||
},
|
||||
"packages/vscode": {
|
||||
"name": "openchamber",
|
||||
"version": "1.9.5",
|
||||
"version": "1.9.6",
|
||||
"dependencies": {
|
||||
"@openchamber/ui": "workspace:*",
|
||||
"@opencode-ai/sdk": "^1.4.6",
|
||||
@@ -234,11 +226,12 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@openchamber/web",
|
||||
"version": "1.9.5",
|
||||
"version": "1.9.6",
|
||||
"bin": {
|
||||
"openchamber": "./bin/cli.js",
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.4.0",
|
||||
"@clack/prompts": "^1.1.0",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@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/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=="],
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -833,7 +832,7 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -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-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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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-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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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/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=="],
|
||||
|
||||
"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/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=="],
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@octokit/rest": "^22.0.1",
|
||||
"@opencode-ai/sdk": "^1.4.6",
|
||||
"@base-ui/react": "^1.4.0",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
|
||||
@@ -41,16 +41,7 @@
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@opencode-ai/sdk": "^1.4.6",
|
||||
"@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",
|
||||
"@base-ui/react": "^1.4.0",
|
||||
"@remixicon/react": "^4.7.0",
|
||||
"@simplewebauthn/browser": "13.3.0",
|
||||
"@tanstack/react-virtual": "^3.13.18",
|
||||
|
||||
@@ -744,7 +744,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
const inputBarOffset = useUIStore((state) => state.inputBarOffset);
|
||||
const isKeyboardOpen = useUIStore((state) => state.isKeyboardOpen);
|
||||
const cornerRadius = useUIStore((state) => state.cornerRadius);
|
||||
const persistChatDraft = useUIStore((state) => state.persistChatDraft);
|
||||
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
|
||||
const isExpandedInput = useUIStore((state) => state.isExpandedInput);
|
||||
@@ -3293,7 +3292,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
>
|
||||
<SelectTrigger
|
||||
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>
|
||||
{renderProjectLabelWithIcon(selectedDraftProject)}
|
||||
@@ -3315,7 +3314,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
>
|
||||
<SelectTrigger
|
||||
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>
|
||||
{selectedDraftBranchLabel ?? 'Branch'}
|
||||
@@ -3496,6 +3495,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
</div>
|
||||
)}
|
||||
<Textarea
|
||||
simple
|
||||
ref={textareaRef}
|
||||
data-chat-input="true"
|
||||
value={message}
|
||||
@@ -3528,7 +3528,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
autoCapitalize={isMobile ? "sentences" : "off"}
|
||||
spellCheck={isMobile || inputSpellcheckEnabled}
|
||||
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(
|
||||
'min-h-[52px] resize-none border-0 px-3 rounded-b-none appearance-none hover:border-transparent bg-transparent relative z-10',
|
||||
isDesktopExpanded
|
||||
@@ -3682,7 +3682,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
</div>
|
||||
|
||||
{/* Mobile Session Status Bar - above input */}
|
||||
{isMobile && <MobileSessionStatusBar cornerRadius={cornerRadius} />}
|
||||
{isMobile && <MobileSessionStatusBar />}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -58,7 +58,6 @@ import { useNotificationStore } from '@/sync/notification-store';
|
||||
|
||||
interface MobileSessionStatusBarProps {
|
||||
onSessionSwitch?: (sessionId: string) => void;
|
||||
cornerRadius?: number;
|
||||
}
|
||||
|
||||
interface SessionWithStatus extends Session {
|
||||
@@ -1161,7 +1160,6 @@ function CollapsedView({
|
||||
currentProjectColor,
|
||||
onToggle,
|
||||
onNewSession,
|
||||
cornerRadius,
|
||||
contextUsage,
|
||||
childIndicators = [],
|
||||
}: {
|
||||
@@ -1175,7 +1173,6 @@ function CollapsedView({
|
||||
currentProjectColor?: string | null;
|
||||
onToggle: () => void;
|
||||
onNewSession: () => void;
|
||||
cornerRadius?: number;
|
||||
contextUsage: SessionContextUsage | null;
|
||||
childIndicators?: Array<{ session: Session; isRunning: boolean }>;
|
||||
}) {
|
||||
@@ -1185,8 +1182,8 @@ function CollapsedView({
|
||||
<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"
|
||||
style={{
|
||||
borderTopLeftRadius: cornerRadius,
|
||||
borderTopRightRadius: cornerRadius,
|
||||
borderTopLeftRadius: 'var(--radius-lg)',
|
||||
borderTopRightRadius: 'var(--radius-lg)',
|
||||
}}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
@@ -1251,7 +1248,6 @@ function ExpandedView({
|
||||
getSessionAgentName,
|
||||
getSessionTitle,
|
||||
needsAttention,
|
||||
cornerRadius,
|
||||
contextUsage,
|
||||
projects,
|
||||
activeProjectId,
|
||||
@@ -1280,7 +1276,6 @@ function ExpandedView({
|
||||
getSessionAgentName: (s: Session) => string;
|
||||
getSessionTitle: (s: Session) => string;
|
||||
needsAttention: (sessionId: string) => boolean;
|
||||
cornerRadius?: number;
|
||||
contextUsage: SessionContextUsage | null;
|
||||
projects: ProjectEntry[];
|
||||
activeProjectId: string | null;
|
||||
@@ -1336,8 +1331,8 @@ function ExpandedView({
|
||||
<div
|
||||
className="w-full border-b border-[var(--interactive-border)] bg-[var(--surface-muted)] order-first overflow-hidden"
|
||||
style={{
|
||||
borderTopLeftRadius: cornerRadius,
|
||||
borderTopRightRadius: cornerRadius,
|
||||
borderTopLeftRadius: 'var(--radius-lg)',
|
||||
borderTopRightRadius: 'var(--radius-lg)',
|
||||
}}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
@@ -1428,7 +1423,6 @@ function ExpandedView({
|
||||
|
||||
export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
onSessionSwitch,
|
||||
cornerRadius,
|
||||
}) => {
|
||||
const { currentTheme } = useThemeSystem();
|
||||
const sessions = useSessions();
|
||||
@@ -1557,7 +1551,6 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
currentProjectColor={currentProjectColor}
|
||||
onToggle={() => setIsMobileSessionStatusBarCollapsed(false)}
|
||||
onNewSession={handleCreateSession}
|
||||
cornerRadius={cornerRadius}
|
||||
contextUsage={contextUsage}
|
||||
childIndicators={currentSessionChildIndicators}
|
||||
/>
|
||||
@@ -1590,7 +1583,6 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
getSessionAgentName={getSessionAgentName}
|
||||
getSessionTitle={getSessionTitle}
|
||||
needsAttention={needsAttention}
|
||||
cornerRadius={cornerRadius}
|
||||
contextUsage={contextUsage}
|
||||
projects={projects}
|
||||
activeProjectId={activeProjectId}
|
||||
|
||||
@@ -136,15 +136,14 @@ export function InlineCommentInput({
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
simple
|
||||
ref={textareaRef}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Add a comment... (Cmd+Enter to save)"
|
||||
className="min-h-[80px] text-sm resize-y"
|
||||
style={{
|
||||
backgroundColor: currentTheme?.colors?.surface?.subtle,
|
||||
}}
|
||||
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)]"
|
||||
className="min-h-[80px] px-3 py-2.5 text-sm resize-y"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 mt-3">
|
||||
|
||||
@@ -131,8 +131,9 @@ export const OpenInAppButton = ({ directory, activeFilePath, className }: OpenIn
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'app-region-no-drag inline-flex h-7 items-center self-center rounded-md border border-[var(--interactive-border)]',
|
||||
'bg-[var(--surface-elevated)] shadow-none overflow-hidden',
|
||||
'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)] overflow-hidden',
|
||||
'border border-border/60',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -132,16 +132,17 @@ export const BottomTerminalDock: React.FC<BottomTerminalDockProps> = ({ isOpen,
|
||||
<section
|
||||
ref={dockRef}
|
||||
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',
|
||||
!isOpen && 'border-t-0'
|
||||
)}
|
||||
style={{
|
||||
style={isFullscreen ? undefined : {
|
||||
height: `${appliedHeight}px`,
|
||||
minHeight: `${appliedHeight}px`,
|
||||
maxHeight: `${appliedHeight}px`,
|
||||
}}
|
||||
aria-hidden={!isOpen || appliedHeight === 0}
|
||||
aria-hidden={!isOpen || (!isFullscreen && appliedHeight === 0)}
|
||||
>
|
||||
{isOpen && !isFullscreen && (
|
||||
<div
|
||||
|
||||
@@ -427,7 +427,7 @@ export const ContextPanel: React.FC = () => {
|
||||
const isFileTabActive = activeTab?.mode === 'file';
|
||||
|
||||
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
|
||||
items={tabItems}
|
||||
activeId={activeTab?.id ?? null}
|
||||
@@ -450,11 +450,9 @@ export const ContextPanel: React.FC = () => {
|
||||
reorderContextPanelTabs(directoryKey, activeTabID, overTabID);
|
||||
}}
|
||||
layoutMode="scrollable"
|
||||
variant="active-pill"
|
||||
activePillLowercase={false}
|
||||
activePillInsetClassName="gap-0.5 pt-0.5 pb-1.5"
|
||||
variant="default"
|
||||
/>
|
||||
<div className="flex items-end gap-1 px-1.5 pb-1.5">
|
||||
<div className="flex items-center gap-1 px-1.5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
|
||||
@@ -846,10 +846,10 @@ export const MainLayout: React.FC = () => {
|
||||
)}
|
||||
style={{
|
||||
left: `${visibleSidebarWidth}px`,
|
||||
width: 'var(--radius-md)',
|
||||
height: 'var(--radius-md)',
|
||||
WebkitMaskImage: '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(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
WebkitMaskImage: 'radial-gradient(circle at 100% 100%, transparent calc(10px - 1px), black 10px)',
|
||||
maskImage: 'radial-gradient(circle at 100% 100%, transparent calc(10px - 1px), black 10px)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
@@ -860,10 +860,10 @@ export const MainLayout: React.FC = () => {
|
||||
)}
|
||||
style={{
|
||||
left: `${visibleSidebarWidth}px`,
|
||||
width: 'var(--radius-md)',
|
||||
height: 'var(--radius-md)',
|
||||
WebkitMaskImage: '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(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
WebkitMaskImage: 'radial-gradient(circle at 100% 0%, transparent calc(10px - 1px), black 10px)',
|
||||
maskImage: 'radial-gradient(circle at 100% 0%, transparent calc(10px - 1px), black 10px)',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -878,10 +878,10 @@ export const MainLayout: React.FC = () => {
|
||||
)}
|
||||
style={{
|
||||
right: `${visibleRightSidebarWidth}px`,
|
||||
width: 'var(--radius-md)',
|
||||
height: 'var(--radius-md)',
|
||||
WebkitMaskImage: '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(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
WebkitMaskImage: 'radial-gradient(circle at 0 100%, transparent calc(10px - 1px), black 10px)',
|
||||
maskImage: 'radial-gradient(circle at 0 100%, transparent calc(10px - 1px), black 10px)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
@@ -892,10 +892,10 @@ export const MainLayout: React.FC = () => {
|
||||
)}
|
||||
style={{
|
||||
right: `${visibleRightSidebarWidth}px`,
|
||||
width: 'var(--radius-md)',
|
||||
height: 'var(--radius-md)',
|
||||
WebkitMaskImage: '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(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
WebkitMaskImage: 'radial-gradient(circle at 0 0, transparent calc(10px - 1px), black 10px)',
|
||||
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(
|
||||
'relative flex flex-1 min-w-0 flex-col overflow-hidden',
|
||||
'bg-sidebar',
|
||||
isSidebarOpen && 'border-l border-border/50 rounded-tl-md rounded-bl-md',
|
||||
isRightSidebarOpen && 'border-r border-border/50 rounded-tr-md rounded-br-md'
|
||||
isSidebarOpen && 'border-l border-border/50 rounded-tl-[10px] rounded-bl-[10px]',
|
||||
isRightSidebarOpen && 'border-r border-border/50 rounded-tr-[10px] rounded-br-[10px]'
|
||||
)}>
|
||||
<Header desktopRightSidebarActionsHost={desktopRightSidebarActionsHost} />
|
||||
<div className={cn(
|
||||
|
||||
@@ -640,7 +640,7 @@ export const ProjectActionsButton = ({
|
||||
<button
|
||||
type="button"
|
||||
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',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
className
|
||||
@@ -657,9 +657,10 @@ export const ProjectActionsButton = ({
|
||||
<button
|
||||
type="button"
|
||||
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',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
'border border-border/60',
|
||||
className
|
||||
)}
|
||||
onClick={openProjectActionsSettings}
|
||||
@@ -690,7 +691,7 @@ export const ProjectActionsButton = ({
|
||||
type="button"
|
||||
disabled={isLoading || isStoppingSelected}
|
||||
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',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
'disabled:cursor-not-allowed',
|
||||
@@ -745,8 +746,9 @@ export const ProjectActionsButton = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'app-region-no-drag inline-flex shrink-0 items-center self-center rounded-md border border-[var(--interactive-border)]',
|
||||
'bg-[var(--surface-elevated)] shadow-none overflow-hidden',
|
||||
'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)] overflow-hidden',
|
||||
'border border-border/60',
|
||||
compact ? 'h-9' : 'h-7',
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -657,7 +657,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
||||
Last updated {formatTime(quotaLastUpdated)}
|
||||
</div>
|
||||
{!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>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
@@ -671,7 +671,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
||||
<DropdownMenuItem
|
||||
key={`${group.providerId}-empty`}
|
||||
className="cursor-default"
|
||||
onSelect={(event) => event.preventDefault()}
|
||||
closeOnClick={false}
|
||||
>
|
||||
<span className="typography-ui-label text-muted-foreground">
|
||||
{group.error ?? 'No rate limits reported.'}
|
||||
@@ -692,7 +692,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
||||
<DropdownMenuItem
|
||||
key={`${group.providerId}-${label}`}
|
||||
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 items-center justify-between gap-3">
|
||||
|
||||
@@ -155,7 +155,7 @@ export const McpDropdownContent: React.FC<McpDropdownContentProps> = ({ active,
|
||||
<Switch
|
||||
checked={isConnected}
|
||||
disabled={isBusy}
|
||||
className="data-[state=checked]:bg-status-info"
|
||||
className="data-[checked]:bg-status-info"
|
||||
onCheckedChange={async (checked) => {
|
||||
setBusyName(serverName);
|
||||
try {
|
||||
@@ -296,7 +296,7 @@ export const McpDropdown: React.FC<McpDropdownProps> = ({ headerIconButtonClass
|
||||
<Switch
|
||||
checked={isConnected}
|
||||
disabled={isBusy}
|
||||
className="data-[state=checked]:bg-status-info"
|
||||
className="data-[checked]:bg-status-info"
|
||||
onCheckedChange={async (checked) => {
|
||||
setBusyName(serverName);
|
||||
try {
|
||||
|
||||
@@ -87,7 +87,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
|
||||
id={id}
|
||||
size="lg"
|
||||
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,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -684,41 +684,29 @@ export const AgentsPage: React.FC = () => {
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={mode === 'primary'}
|
||||
onClick={() => setMode('primary')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
mode === 'primary'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
className="!font-normal"
|
||||
>
|
||||
Primary
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={mode === 'subagent'}
|
||||
onClick={() => setMode('subagent')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
mode === 'subagent'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
className="!font-normal"
|
||||
>
|
||||
Subagent
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={mode === 'all'}
|
||||
onClick={() => setMode('all')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
mode === 'all'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
className="!font-normal"
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
|
||||
@@ -515,7 +515,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<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
|
||||
)}>
|
||||
{providerId ? (
|
||||
|
||||
@@ -352,25 +352,17 @@ export const GitIdentityEditorDialog: React.FC<GitIdentityEditorDialogProps> = (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
aria-pressed={authType === '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
|
||||
</Button>
|
||||
<Button size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
aria-pressed={authType === '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
|
||||
</Button>
|
||||
|
||||
@@ -617,28 +617,20 @@ export const McpPage: React.FC = () => {
|
||||
<span className="typography-ui-label text-foreground">Transport Mode</span>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={mcpType === 'local'}
|
||||
onClick={() => setMcpType('local')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
mcpType === 'local'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
className="!font-normal"
|
||||
>
|
||||
Local · stdio
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={mcpType === 'remote'}
|
||||
onClick={() => setMcpType('remote')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
mcpType === 'remote'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
className="!font-normal"
|
||||
>
|
||||
Remote · SSE
|
||||
</Button>
|
||||
|
||||
@@ -145,6 +145,13 @@ export const DefaultsSettings: React.FC = () => {
|
||||
|
||||
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(
|
||||
async (variant: string) => {
|
||||
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">
|
||||
<Select value={defaultVariant ?? DEFAULT_VARIANT_VALUE} onValueChange={handleVariantChange} disabled={!supportsVariants}>
|
||||
<SelectTrigger className="w-fit min-w-[120px]">
|
||||
<SelectValue placeholder="Thinking" />
|
||||
<SelectValue placeholder="Thinking">
|
||||
{formatVariantLabel(defaultVariant ?? DEFAULT_VARIANT_VALUE)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={DEFAULT_VARIANT_VALUE}>Default</SelectItem>
|
||||
{availableVariants.map((variant) => (
|
||||
<SelectItem key={variant} value={variant}>
|
||||
{variant}
|
||||
{formatVariantLabel(variant)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -520,14 +520,10 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
{THEME_MODE_OPTIONS.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
className={cn(
|
||||
'!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'
|
||||
)}
|
||||
aria-pressed={themeMode === option.value}
|
||||
className="!font-normal"
|
||||
onClick={() => setThemeMode(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
@@ -542,7 +538,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
<span className="typography-ui-label text-foreground shrink-0">Light Theme</span>
|
||||
<Select value={selectedLightTheme?.metadata.id ?? ''} onValueChange={setLightThemePreference}>
|
||||
<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>
|
||||
<SelectContent>
|
||||
{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>
|
||||
<Select value={selectedDarkTheme?.metadata.id ?? ''} onValueChange={setDarkThemePreference}>
|
||||
<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>
|
||||
<SelectContent>
|
||||
{darkThemes.map((theme) => (
|
||||
@@ -969,14 +973,10 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
{MESSAGE_STREAM_TRANSPORT_OPTIONS.map((option) => (
|
||||
<Button
|
||||
key={option.id}
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
className={cn(
|
||||
'!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'
|
||||
)}
|
||||
aria-pressed={messageStreamTransport === option.id}
|
||||
className="!font-normal"
|
||||
onClick={() => handleMessageStreamTransportChange(option.id)}
|
||||
>
|
||||
{option.label}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { toast } from '@/components/ui';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
|
||||
|
||||
@@ -124,14 +123,10 @@ export const SessionRetentionSettings: React.FC = () => {
|
||||
<Button
|
||||
key={option.value}
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
className={cn(
|
||||
'!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'
|
||||
)}
|
||||
aria-pressed={sessionRetentionAction === option.value}
|
||||
className="!font-normal"
|
||||
onClick={() => setSessionRetentionAction(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
|
||||
@@ -1178,14 +1178,10 @@ export const TunnelSettings: React.FC = () => {
|
||||
<Tooltip key={option.value} delayDuration={700}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
className={cn(
|
||||
'!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'
|
||||
)}
|
||||
aria-pressed={tunnelMode === option.value}
|
||||
className="!font-normal"
|
||||
onClick={() => {
|
||||
void handleModeChange(option.value);
|
||||
}}
|
||||
|
||||
@@ -457,55 +457,39 @@ export const VoiceSettings: React.FC = () => {
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={voiceProvider === 'browser'}
|
||||
onClick={() => setVoiceProvider('browser')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
voiceProvider === 'browser'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
className="!font-normal"
|
||||
>
|
||||
Browser
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={voiceProvider === 'openai'}
|
||||
onClick={() => setVoiceProvider('openai')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
voiceProvider === 'openai'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
className="!font-normal"
|
||||
>
|
||||
OpenAI
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={voiceProvider === 'openai-compatible'}
|
||||
onClick={() => setVoiceProvider('openai-compatible')}
|
||||
className={cn(
|
||||
'!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'
|
||||
)}
|
||||
className="!font-normal"
|
||||
>
|
||||
Custom
|
||||
</Button>
|
||||
{isSayAvailable && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={voiceProvider === 'say'}
|
||||
onClick={() => setVoiceProvider('say')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
voiceProvider === 'say'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
className="!font-normal"
|
||||
>
|
||||
<RiAppleLine className="w-3.5 h-3.5 mr-0.5" />
|
||||
Say
|
||||
@@ -756,28 +740,20 @@ export const VoiceSettings: React.FC = () => {
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={sttProvider === 'browser'}
|
||||
onClick={() => setSttProvider('browser')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
sttProvider === 'browser'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
className="!font-normal"
|
||||
>
|
||||
Browser
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={sttProvider === 'server'}
|
||||
onClick={() => setSttProvider('server')}
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
sttProvider === 'server'
|
||||
? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
className="!font-normal"
|
||||
>
|
||||
Server
|
||||
</Button>
|
||||
|
||||
@@ -1587,7 +1587,7 @@ export function NewWorktreeDialog({
|
||||
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</Button>
|
||||
</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}>
|
||||
<CommandInput
|
||||
placeholder="Search branches..."
|
||||
|
||||
@@ -14,11 +14,29 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
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 { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
||||
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import { SETTINGS_PAGE_METADATA, SETTINGS_GROUP_LABELS, type SettingsRuntimeContext } from '@/lib/settings/metadata';
|
||||
import { getSettingsNavIcon } from '@/components/views/SettingsView';
|
||||
|
||||
export const CommandPalette: React.FC = () => {
|
||||
const isCommandPaletteOpen = useUIStore((s) => s.isCommandPaletteOpen);
|
||||
@@ -36,6 +54,8 @@ export const CommandPalette: React.FC = () => {
|
||||
const toggleBottomTerminal = useUIStore((s) => s.toggleBottomTerminal);
|
||||
const setBottomTerminalExpanded = useUIStore((s) => s.setBottomTerminalExpanded);
|
||||
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 openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||
@@ -44,134 +64,84 @@ export const CommandPalette: React.FC = () => {
|
||||
|
||||
const currentDirectory = useDirectoryStore((s) => s.currentDirectory);
|
||||
const { themeMode, setThemeMode } = useThemeSystem();
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
const handleClose = () => {
|
||||
setCommandPaletteOpen(false);
|
||||
};
|
||||
const close = React.useCallback(() => setCommandPaletteOpen(false), [setCommandPaletteOpen]);
|
||||
|
||||
const handleCreateSession = async () => {
|
||||
const run = React.useCallback((fn: () => void | Promise<void>) => async () => {
|
||||
close();
|
||||
await fn();
|
||||
}, [close]);
|
||||
|
||||
const handleCreateSession = run(() => {
|
||||
setActiveMainTab('chat');
|
||||
setSessionSwitcherOpen(false);
|
||||
openNewSessionDraft();
|
||||
handleClose();
|
||||
};
|
||||
});
|
||||
|
||||
const handleOpenSession = (sessionId: string, directoryHint?: string | null) => {
|
||||
setCurrentSession(sessionId, directoryHint ?? null);
|
||||
handleClose();
|
||||
};
|
||||
const handleOpenQuickOpen = run(() => setQuickOpenOpen(true));
|
||||
|
||||
const handleSetThemeMode = (mode: 'light' | 'dark' | 'system') => {
|
||||
setThemeMode(mode);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleShowHelp = () => {
|
||||
setHelpDialogOpen(true);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleOpenQuickOpen = () => {
|
||||
setQuickOpenOpen(true);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleCreateWorktreeSession = () => {
|
||||
handleClose();
|
||||
const handleCreateWorktreeSession = run(() => {
|
||||
createWorktreeSession();
|
||||
};
|
||||
});
|
||||
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
const handleOpenSessionList = () => {
|
||||
const handleOpenSessionList = run(() => {
|
||||
if (isMobile) {
|
||||
const { isSessionSwitcherOpen } = useUIStore.getState();
|
||||
setSessionSwitcherOpen(!isSessionSwitcherOpen);
|
||||
} else {
|
||||
toggleSidebar();
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
});
|
||||
|
||||
const handleOpenDiffPanel = () => {
|
||||
setActiveMainTab('diff');
|
||||
handleClose();
|
||||
};
|
||||
const handleToggleRightSidebar = run(() => toggleRightSidebar());
|
||||
const handleOpenRightSidebarGit = run(() => { setRightSidebarOpen(true); setRightSidebarTab('git'); });
|
||||
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 = () => {
|
||||
setActiveMainTab('git');
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleOpenTerminal = () => {
|
||||
setActiveMainTab('terminal');
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleOpenSettings = () => {
|
||||
setSettingsDialogOpen(true);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleOpenSettingsPage = (slug: string) => {
|
||||
const handleOpenSettingsPage = (slug: string) => run(() => {
|
||||
setSettingsPage(slug);
|
||||
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 isDesktop = isDesktopShell();
|
||||
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
|
||||
.filter((p) => p.slug !== 'home')
|
||||
.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
|
||||
.filter((p) => (p.isAvailable ? p.isAvailable(settingsRuntimeCtx) : true))
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const g = groupLabel(a.group).localeCompare(groupLabel(b.group));
|
||||
if (g !== 0) return g;
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}, [settingsPages]);
|
||||
}, [settingsRuntimeCtx]);
|
||||
|
||||
const handleToggleRightSidebar = () => {
|
||||
toggleRightSidebar();
|
||||
handleClose();
|
||||
};
|
||||
|
||||
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 recentSessions = React.useMemo(() => {
|
||||
return getSessionsByDirectory(currentDirectory ?? '').slice(0, 5);
|
||||
}, [getSessionsByDirectory, currentDirectory]);
|
||||
|
||||
const shortcut = React.useCallback((actionId: string) => {
|
||||
return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides));
|
||||
@@ -183,12 +153,7 @@ export const CommandPalette: React.FC = () => {
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
|
||||
<CommandGroup heading="Actions">
|
||||
<CommandItem onSelect={handleOpenSessionList}>
|
||||
<RiLayoutLeftLine className="mr-2 h-4 w-4" />
|
||||
<span>Open Session List</span>
|
||||
<CommandShortcut>{shortcut('toggle_sidebar')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandGroup heading="Sessions">
|
||||
<CommandItem onSelect={handleOpenQuickOpen}>
|
||||
<RiFileLine className="mr-2 h-4 w-4" />
|
||||
<span>Quick Open</span>
|
||||
@@ -197,17 +162,23 @@ export const CommandPalette: React.FC = () => {
|
||||
<CommandItem onSelect={handleCreateSession}>
|
||||
<RiAddLine className="mr-2 h-4 w-4" />
|
||||
<span>New Session</span>
|
||||
<CommandShortcut>
|
||||
{shortcut('new_chat')}
|
||||
</CommandShortcut>
|
||||
<CommandShortcut>{shortcut('new_chat')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleCreateWorktreeSession}>
|
||||
<RiGitBranchLine className="mr-2 h-4 w-4" />
|
||||
<span>New Worktree Draft</span>
|
||||
<CommandShortcut>
|
||||
{shortcut('new_chat_worktree')}
|
||||
</CommandShortcut>
|
||||
<CommandShortcut>{shortcut('new_chat_worktree')}</CommandShortcut>
|
||||
</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}>
|
||||
<RiLayoutRightLine className="mr-2 h-4 w-4" />
|
||||
<span>Toggle Right Sidebar</span>
|
||||
@@ -215,22 +186,30 @@ export const CommandPalette: React.FC = () => {
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleOpenRightSidebarGit}>
|
||||
<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>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleOpenRightSidebarFiles}>
|
||||
<RiLayoutRightLine className="mr-2 h-4 w-4" />
|
||||
<span>Open Right Sidebar Files</span>
|
||||
<RiFolderLine className="mr-2 h-4 w-4" />
|
||||
<span>Show Files in Right Sidebar</span>
|
||||
<CommandShortcut>{shortcut('open_right_sidebar_files')}</CommandShortcut>
|
||||
</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}>
|
||||
<RiTerminalBoxLine className="mr-2 h-4 w-4" />
|
||||
<span>Toggle Terminal Dock</span>
|
||||
<span>Toggle Terminal</span>
|
||||
<CommandShortcut>{shortcut('toggle_terminal')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleToggleTerminalExpanded}>
|
||||
<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>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleShowHelp}>
|
||||
@@ -238,81 +217,63 @@ export const CommandPalette: React.FC = () => {
|
||||
<span>Keyboard Shortcuts</span>
|
||||
<CommandShortcut>{shortcut('open_help')}</CommandShortcut>
|
||||
</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>
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandGroup heading="Theme">
|
||||
<CommandItem onSelect={() => handleSetThemeMode('light')}>
|
||||
<CommandItem onSelect={() => handleSetThemeMode('light')()}>
|
||||
<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" />}
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => handleSetThemeMode('dark')}>
|
||||
<CommandItem onSelect={() => handleSetThemeMode('dark')()}>
|
||||
<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" />}
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => handleSetThemeMode('system')}>
|
||||
<CommandItem onSelect={() => handleSetThemeMode('system')()}>
|
||||
<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" />}
|
||||
</CommandItem>
|
||||
</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 />
|
||||
<CommandGroup heading="Recent Sessions">
|
||||
{currentSessions.map((session) => (
|
||||
{recentSessions.map((session) => (
|
||||
<CommandItem
|
||||
key={session.id}
|
||||
onSelect={() => handleOpenSession(session.id, currentDirectory ?? null)}
|
||||
onSelect={() => handleOpenSession(session.id, currentDirectory ?? null)()}
|
||||
>
|
||||
<RiChatAi3Line className="mr-2 h-4 w-4" />
|
||||
<span className="truncate">
|
||||
{session.title || 'Untitled Session'}
|
||||
</span>
|
||||
<span className="truncate">{session.title || 'Untitled Session'}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
{}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,8 @@ type ScrollableOverlayProps = React.HTMLAttributes<HTMLElement> & {
|
||||
preventOverscroll?: boolean;
|
||||
useScrollShadow?: boolean;
|
||||
scrollShadowSize?: number;
|
||||
/** Forwarded to the inner element (e.g. textarea). */
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const ScrollableOverlay = React.forwardRef<HTMLElement, ScrollableOverlayProps>(
|
||||
|
||||
@@ -1,32 +1,83 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
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(
|
||||
"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: {
|
||||
variant: {
|
||||
default:
|
||||
"border bg-[var(--primary-base)]/10 text-[var(--primary-base)] shadow-none hover:bg-[var(--primary-base)]/15 hover:text-[var(--primary-base)]",
|
||||
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",
|
||||
default: TINT_PRIMARY,
|
||||
destructive: cn(
|
||||
TINT_DESTRUCTIVE,
|
||||
"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:
|
||||
"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:
|
||||
"bg-interactive-hover text-foreground shadow-none hover:bg-interactive-active",
|
||||
"bg-interactive-hover text-foreground hover:bg-interactive-active",
|
||||
ghost:
|
||||
"text-foreground hover:bg-interactive-hover hover:text-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-lg gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
xs: "h-6 rounded-lg gap-1 px-1.5 typography-micro has-[>svg]:px-1.5",
|
||||
lg: "h-10 rounded-lg px-6 has-[>svg]:px-4",
|
||||
default: "h-9 px-3.5 has-[>svg]:px-3",
|
||||
sm: "h-8 gap-1.5 px-2.5 has-[>svg]:px-2 rounded-[9px] supports-[corner-shape:squircle]:rounded-[50px]",
|
||||
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 px-4 has-[>svg]:px-3.5 rounded-[12px] supports-[corner-shape:squircle]:rounded-[50px]",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,68 +1,72 @@
|
||||
import React from 'react';
|
||||
import { RiCheckboxBlankLine, RiCheckboxLine } from '@remixicon/react';
|
||||
import * as React from 'react';
|
||||
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
|
||||
import { RiCheckLine, RiSubtractLine } from '@remixicon/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface CheckboxProps {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
indeterminate?: boolean;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
/** @deprecated size is fixed; prop retained for backwards compatibility */
|
||||
size?: 'sm' | 'default';
|
||||
}
|
||||
|
||||
|
||||
export const Checkbox = React.memo<CheckboxProps>(function Checkbox({
|
||||
checked,
|
||||
onChange,
|
||||
disabled = false,
|
||||
indeterminate,
|
||||
ariaLabel,
|
||||
className,
|
||||
iconClassName,
|
||||
}) {
|
||||
const handleClick = React.useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
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]
|
||||
);
|
||||
|
||||
const boxSize = 'h-[14px] w-[14px] min-h-[14px] min-w-[14px]';
|
||||
const iconSize = 'h-[10px] w-[10px] min-h-[10px] min-w-[10px]';
|
||||
const isOn = checked || indeterminate;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
<BaseCheckbox.Root
|
||||
checked={checked}
|
||||
onCheckedChange={(next) => onChange(Boolean(next))}
|
||||
disabled={disabled}
|
||||
aria-pressed={checked}
|
||||
indeterminate={indeterminate}
|
||||
aria-label={ariaLabel}
|
||||
className={cn(
|
||||
'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',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
className
|
||||
// AlignUI-style rounded box, no explicit border (rely on inset shadow for unchecked)
|
||||
'group/checkbox relative flex shrink-0 self-center items-center justify-center rounded-[4px] outline-none',
|
||||
boxSize,
|
||||
'transition-[background-color,box-shadow] duration-200 ease-out',
|
||||
// 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 ? (
|
||||
<RiCheckboxLine className={cn('size-4 text-primary', iconClassName)} />
|
||||
) : (
|
||||
<RiCheckboxBlankLine className={cn('size-4', iconClassName)} />
|
||||
)}
|
||||
</button>
|
||||
<BaseCheckbox.Indicator
|
||||
keepMounted
|
||||
className={cn(
|
||||
'flex items-center justify-center text-[var(--primary-base)]',
|
||||
// hide when fully unchecked (no state)
|
||||
'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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,29 +1,45 @@
|
||||
"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";
|
||||
|
||||
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 = ({
|
||||
className,
|
||||
asChild,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) => (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}: TriggerProps) => {
|
||||
const renderProps: AsChildRenderProps = asChild && React.isValidElement(children)
|
||||
? { render: children as React.ReactElement }
|
||||
: { children };
|
||||
return (
|
||||
<BaseCollapsible.Trigger
|
||||
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 = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) => (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
className={cn("overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", className)}
|
||||
}: React.ComponentProps<typeof BaseCollapsible.Panel>) => (
|
||||
<BaseCollapsible.Panel
|
||||
className={cn("overflow-hidden data-[closed]:animate-collapsible-up data-[open]:animate-collapsible-down", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -16,14 +16,18 @@ import {
|
||||
|
||||
function Command({
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
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={{
|
||||
backgroundColor: 'var(--surface-elevated)',
|
||||
color: 'var(--surface-elevated-foreground)',
|
||||
...style,
|
||||
}}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-xl",
|
||||
@@ -41,11 +45,12 @@ function CommandDialog({
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
}: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
|
||||
@@ -1,38 +1,57 @@
|
||||
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 { cn } from "@/lib/utils"
|
||||
|
||||
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({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}: React.ComponentProps<typeof BaseDialog.Root>) {
|
||||
return <BaseDialog.Root {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
asChild,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}: React.ComponentProps<typeof BaseDialog.Trigger> & AsChildProps) {
|
||||
const r = renderFromAsChild(asChild, children);
|
||||
return <BaseDialog.Trigger data-slot="dialog-trigger" {...props} {...r} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}: React.ComponentProps<typeof BaseDialog.Portal>) {
|
||||
return <BaseDialog.Portal {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
asChild,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}: React.ComponentProps<typeof BaseDialog.Close> & AsChildProps) {
|
||||
const r = renderFromAsChild(asChild, children);
|
||||
return <BaseDialog.Close data-slot="dialog-close" {...props} {...r} />
|
||||
}
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof BaseDialog.Backdrop>
|
||||
>(({ className, ...props }, ref) => {
|
||||
React.useEffect(() => {
|
||||
openDialogCount += 1;
|
||||
@@ -46,8 +65,8 @@ const DialogOverlay = React.forwardRef<
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
<BaseDialog.Backdrop
|
||||
ref={ref as React.Ref<HTMLDivElement>}
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"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({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
keyboardAvoid = false,
|
||||
onOpenAutoFocus,
|
||||
onCloseAutoFocus,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
keyboardAvoid?: boolean
|
||||
}) {
|
||||
}: DialogContentProps) {
|
||||
void onOpenAutoFocus
|
||||
void onCloseAutoFocus
|
||||
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogPortal>
|
||||
<DialogOverlay className="rounded-none" />
|
||||
<DialogPrimitive.Content
|
||||
<BaseDialog.Popup
|
||||
data-slot="dialog-content"
|
||||
data-state-slot="dialog"
|
||||
data-keyboard-avoid={keyboardAvoid ? "true" : undefined}
|
||||
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",
|
||||
@@ -81,18 +111,17 @@ function DialogContent({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
<BaseDialog.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/>
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</BaseDialog.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</BaseDialog.Popup>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
@@ -123,9 +152,9 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
}: React.ComponentProps<typeof BaseDialog.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
<BaseDialog.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("typography-markdown leading-none font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
@@ -136,9 +165,9 @@ function DialogTitle({
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
}: React.ComponentProps<typeof BaseDialog.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
<BaseDialog.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground typography-ui-label", className)}
|
||||
{...props}
|
||||
|
||||
@@ -1,87 +1,139 @@
|
||||
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 { 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({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}: React.ComponentProps<typeof BaseMenu.Root>) {
|
||||
return <BaseMenu.Root {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}: React.ComponentProps<typeof BaseMenu.Portal>) {
|
||||
return <BaseMenu.Portal {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
asChild,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
}: React.ComponentProps<typeof BaseMenu.Trigger> & AsChildProps) {
|
||||
const r = renderFromAsChild(asChild, children);
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
<BaseMenu.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...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({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
align,
|
||||
side,
|
||||
alignOffset,
|
||||
style,
|
||||
children,
|
||||
onCloseAutoFocus,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
}: ContentProps) {
|
||||
void onCloseAutoFocus
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
<BaseMenu.Portal>
|
||||
<BaseMenu.Positioner
|
||||
sideOffset={sideOffset}
|
||||
style={{
|
||||
backgroundColor: 'var(--surface-elevated)',
|
||||
color: 'var(--surface-elevated-foreground)',
|
||||
...style,
|
||||
}}
|
||||
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 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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
align={align}
|
||||
side={side}
|
||||
alignOffset={alignOffset}
|
||||
className="z-50"
|
||||
>
|
||||
<BaseMenu.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
style={{
|
||||
backgroundColor: 'var(--surface-elevated)',
|
||||
color: 'var(--surface-elevated-foreground)',
|
||||
...style,
|
||||
}}
|
||||
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({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}: React.ComponentProps<typeof BaseMenu.Group>) {
|
||||
return <BaseMenu.Group data-slot="dropdown-menu-group" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
asChild,
|
||||
children,
|
||||
onSelect,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
}: React.ComponentProps<typeof BaseMenu.Item> & AsChildProps & {
|
||||
inset?: boolean
|
||||
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 (
|
||||
<DropdownMenuPrimitive.Item
|
||||
<BaseMenu.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
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",
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
onClick={handleClick}
|
||||
{...r}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -91,59 +143,54 @@ function DropdownMenuCheckboxItem({
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
}: React.ComponentProps<typeof BaseMenu.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
<BaseMenu.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
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
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<BaseMenu.CheckboxItemIndicator>
|
||||
<RiCheckLine className="size-3"/>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</BaseMenu.CheckboxItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
</BaseMenu.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}: React.ComponentProps<typeof BaseMenu.RadioGroup>) {
|
||||
return <BaseMenu.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
}: React.ComponentProps<typeof BaseMenu.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
<BaseMenu.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<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" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</BaseMenu.RadioItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
</BaseMenu.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -151,11 +198,11 @@ function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
}: React.ComponentProps<"div"> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
<div
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
@@ -170,9 +217,9 @@ function DropdownMenuLabel({
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
}: React.ComponentProps<typeof BaseMenu.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
<BaseMenu.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-0.5 h-px", className)}
|
||||
{...props}
|
||||
@@ -198,8 +245,8 @@ function DropdownMenuShortcut({
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}: React.ComponentProps<typeof BaseMenu.SubmenuRoot>) {
|
||||
return <BaseMenu.SubmenuRoot {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
@@ -207,42 +254,49 @@ function DropdownMenuSubTrigger({
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
}: React.ComponentProps<typeof BaseMenu.SubmenuTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<BaseMenu.SubmenuTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<RiArrowRightSLine className="ml-auto size-3.5" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
</BaseMenu.SubmenuTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
}: React.ComponentProps<typeof BaseMenu.Popup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
style={{
|
||||
backgroundColor: 'var(--surface-elevated)',
|
||||
color: 'var(--surface-elevated-foreground)',
|
||||
}}
|
||||
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 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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<BaseMenu.Portal>
|
||||
<BaseMenu.Positioner className="z-50">
|
||||
<BaseMenu.Popup
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
style={{
|
||||
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 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)]",
|
||||
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 };
|
||||
@@ -8,10 +8,12 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
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",
|
||||
"hover:border-input",
|
||||
"focus:ring-1 focus:ring-primary/50 focus:border-primary/70",
|
||||
"aria-invalid:border-destructive aria-invalid:ring-destructive",
|
||||
"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",
|
||||
// AlignUI-style ring border + transitions
|
||||
"ring-1 ring-inset ring-border/60 transition duration-200 ease-out",
|
||||
"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
|
||||
)}
|
||||
spellCheck={false}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { RiRadioButtonFill, RiRadioButtonLine } from '@remixicon/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface RadioProps {
|
||||
@@ -52,18 +51,26 @@ export const Radio = React.memo<RadioProps>(function Radio({
|
||||
aria-checked={checked}
|
||||
aria-label={ariaLabel}
|
||||
className={cn(
|
||||
'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',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
className
|
||||
'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',
|
||||
'transition-[background-color,box-shadow] duration-200 ease-out',
|
||||
// fill driven from props so first paint is correct
|
||||
checked
|
||||
? '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 ? (
|
||||
<RiRadioButtonFill className={cn('size-4 text-primary', iconClassName)} />
|
||||
) : (
|
||||
<RiRadioButtonLine className={cn('size-4', iconClassName)} />
|
||||
)}
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'block h-[5px] w-[5px] rounded-full bg-white',
|
||||
iconClassName,
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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"
|
||||
|
||||
@@ -7,22 +7,22 @@ function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
}: React.ComponentProps<typeof BaseScrollArea.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
<BaseScrollArea.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
<BaseScrollArea.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"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
</BaseScrollArea.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
<BaseScrollArea.Corner />
|
||||
</BaseScrollArea.Root>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
}: React.ComponentProps<typeof BaseScrollArea.Scrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
<BaseScrollArea.Scrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
@@ -45,11 +45,11 @@ function ScrollBar({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
<BaseScrollArea.Thumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
</BaseScrollArea.Scrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,110 +1,179 @@
|
||||
"use client"
|
||||
|
||||
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 { cn } from "@/lib/utils"
|
||||
import { ScrollableOverlay } from "@/components/ui/ScrollableOverlay";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
type AsChildProps = { asChild?: boolean };
|
||||
type AsChildRenderProps = {
|
||||
render?: React.ReactElement;
|
||||
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({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}: React.ComponentProps<typeof BaseSelect.Group>) {
|
||||
return <BaseSelect.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
type SelectValueProps = Omit<React.ComponentProps<typeof BaseSelect.Value>, "children"> & {
|
||||
placeholder?: React.ReactNode;
|
||||
children?: React.ReactNode | ((value: string | undefined) => React.ReactNode);
|
||||
};
|
||||
|
||||
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({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
asChild,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
}: React.ComponentProps<typeof BaseSelect.Trigger> & AsChildProps & {
|
||||
size?: "sm" | "default" | "lg" | "chip"
|
||||
}) {
|
||||
const asChildRender: AsChildRenderProps | null = asChild && React.isValidElement(children)
|
||||
? { render: children as React.ReactElement }
|
||||
: null;
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
<BaseSelect.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
{...(asChildRender ?? {})}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<RiArrowDownSLine className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
{asChildRender ? undefined : (<>
|
||||
{children}
|
||||
<BaseSelect.Icon>
|
||||
<RiArrowDownSLine className="size-4 opacity-50" />
|
||||
</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({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
fitContent = false,
|
||||
sideOffset,
|
||||
side,
|
||||
align,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content> & {
|
||||
fitContent?: boolean;
|
||||
}) {
|
||||
}: React.ComponentProps<typeof BaseSelect.Popup> & SelectContentExtra) {
|
||||
const alignItemWithTrigger = position === "item-aligned";
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
style={{
|
||||
backgroundColor: 'var(--surface-elevated)',
|
||||
color: 'var(--surface-elevated-foreground)',
|
||||
}}
|
||||
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")
|
||||
)}
|
||||
<BaseSelect.Portal>
|
||||
<BaseSelect.Positioner
|
||||
alignItemWithTrigger={alignItemWithTrigger}
|
||||
sideOffset={sideOffset}
|
||||
side={side}
|
||||
align={align}
|
||||
className="z-50"
|
||||
>
|
||||
{children}
|
||||
</ScrollableOverlay>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
<BaseSelect.Popup
|
||||
data-slot="select-content"
|
||||
style={{
|
||||
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({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
}: React.ComponentProps<typeof BaseSelect.GroupLabel>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
<BaseSelect.GroupLabel
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 typography-meta", className)}
|
||||
{...props}
|
||||
@@ -116,32 +185,32 @@ function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
}: React.ComponentProps<typeof BaseSelect.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
<BaseSelect.Item
|
||||
data-slot="select-item"
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<BaseSelect.ItemIndicator>
|
||||
<RiCheckLine className="size-4"/>
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</BaseSelect.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
<BaseSelect.ItemText>{children}</BaseSelect.ItemText>
|
||||
</BaseSelect.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
}: React.ComponentProps<typeof BaseSelect.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
<BaseSelect.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
@@ -152,9 +221,9 @@ function SelectSeparator({
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
}: React.ComponentProps<typeof BaseSelect.ScrollUpArrow>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
<BaseSelect.ScrollUpArrow
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center justify-center py-1",
|
||||
@@ -163,16 +232,16 @@ function SelectScrollUpButton({
|
||||
{...props}
|
||||
>
|
||||
<RiArrowUpSLine className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
</BaseSelect.ScrollUpArrow>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
}: React.ComponentProps<typeof BaseSelect.ScrollDownArrow>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
<BaseSelect.ScrollDownArrow
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center justify-center py-1",
|
||||
@@ -181,7 +250,7 @@ function SelectScrollDownButton({
|
||||
{...props}
|
||||
>
|
||||
<RiArrowDownSLine className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
</BaseSelect.ScrollDownArrow>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
"use client"
|
||||
|
||||
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"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
}: React.ComponentProps<typeof BaseSeparator>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
<BaseSeparator
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -1,45 +1,131 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import * as React from "react"
|
||||
import { Toaster as Sonner } 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 { theme = "system" } = useTheme()
|
||||
const isDark = useIsDarkTheme()
|
||||
const shadow = isDark ? SHADOW_DARK : SHADOW_LIGHT
|
||||
usePinnedToastStyles(shadow)
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
theme={isDark ? "dark" : "light"}
|
||||
className="toaster group"
|
||||
closeButton={false}
|
||||
toastOptions={{
|
||||
style: {
|
||||
borderRadius: "var(--radius-lg)",
|
||||
},
|
||||
classNames: {
|
||||
toast: "rounded-[var(--radius-lg)]",
|
||||
actionButton: "rounded-[var(--radius-md)]",
|
||||
cancelButton: "rounded-[var(--radius-md)]",
|
||||
closeButton: "rounded-[var(--radius-md)]",
|
||||
toast:
|
||||
"group/toast toast !rounded-[var(--radius-xl)] !border-0 !px-3.5 !py-3 !gap-2.5 !text-foreground",
|
||||
title: "typography-ui-label !font-medium !text-foreground",
|
||||
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={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--error-bg": "var(--popover)",
|
||||
"--error-text": "var(--popover-foreground)",
|
||||
"--error-border": "var(--border)",
|
||||
"--success-bg": "var(--popover)",
|
||||
"--success-text": "var(--popover-foreground)",
|
||||
"--success-border": "var(--border)",
|
||||
"--warning-bg": "var(--popover)",
|
||||
"--warning-text": "var(--popover-foreground)",
|
||||
"--warning-border": "var(--border)",
|
||||
"--info-bg": "var(--popover)",
|
||||
"--info-text": "var(--popover-foreground)",
|
||||
"--info-border": "var(--border)",
|
||||
"--normal-bg": "var(--surface-elevated)",
|
||||
"--normal-text": "var(--foreground)",
|
||||
"--normal-border": "transparent",
|
||||
"--error-bg": "var(--surface-elevated)",
|
||||
"--error-text": "var(--foreground)",
|
||||
"--error-border": "transparent",
|
||||
"--success-bg": "var(--surface-elevated)",
|
||||
"--success-text": "var(--foreground)",
|
||||
"--success-border": "transparent",
|
||||
"--warning-bg": "var(--surface-elevated)",
|
||||
"--warning-text": "var(--foreground)",
|
||||
"--warning-border": "transparent",
|
||||
"--info-bg": "var(--surface-elevated)",
|
||||
"--info-text": "var(--foreground)",
|
||||
"--info-border": "transparent",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...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 itemIDs = React.useMemo(() => items.map((item) => item.id), [items]);
|
||||
const isScrollable = layoutMode === 'scrollable';
|
||||
const isDefaultVariant = variant === 'default';
|
||||
const isActivePillVariant = variant === 'active-pill';
|
||||
const isAnimatedVariant = variant === 'animated';
|
||||
const usesActivePillIndicator = isActivePillVariant || isAnimatedVariant;
|
||||
const useUnderlineIndicator = isDefaultVariant;
|
||||
const usesIndicator = usesActivePillIndicator || useUnderlineIndicator;
|
||||
const useIntrinsicPillSizing = isActivePillVariant && isScrollable;
|
||||
const showPillTrackBackground = isAnimatedVariant;
|
||||
const showPillTrackBackground = usesActivePillIndicator;
|
||||
const shouldAnimateActivePill = animateActivePill ?? isAnimatedVariant;
|
||||
const reorderEnabled = typeof onReorder === 'function';
|
||||
const Wrapper = reorderEnabled ? SortableTabWrapper : StaticTabWrapper;
|
||||
@@ -139,7 +142,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
}, []);
|
||||
|
||||
const updateActivePillRect = React.useCallback(() => {
|
||||
if (!usesActivePillIndicator || !activeId) {
|
||||
if (!usesIndicator || !activeId) {
|
||||
setPillRect((prev) => (prev === null ? prev : null));
|
||||
return;
|
||||
}
|
||||
@@ -172,7 +175,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
};
|
||||
|
||||
setPillRect((prev) => (isSamePillRect(prev, nextRect) ? prev : nextRect));
|
||||
}, [activeId, isSamePillRect, usesActivePillIndicator]);
|
||||
}, [activeId, isSamePillRect, usesIndicator]);
|
||||
|
||||
const updateOverflow = React.useCallback(() => {
|
||||
if (!isScrollable) {
|
||||
@@ -215,7 +218,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
}, [isScrollable, items.length, updateOverflow]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!usesActivePillIndicator) {
|
||||
if (!usesIndicator) {
|
||||
setPillRect(null);
|
||||
return;
|
||||
}
|
||||
@@ -240,7 +243,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [activeId, items.length, updateActivePillRect, usesActivePillIndicator]);
|
||||
}, [activeId, items.length, updateActivePillRect, usesIndicator]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
updateActivePillRect();
|
||||
@@ -319,7 +322,8 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
usesActivePillIndicator && '@container/pill-tabs',
|
||||
usesActivePillIndicator && 'pill-tabs__track',
|
||||
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
|
||||
? 'overflow-x-auto scrollbar-none'
|
||||
: 'overflow-x-hidden',
|
||||
@@ -331,16 +335,29 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
{usesActivePillIndicator && pillRect ? (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute left-0 top-0 z-0 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)]',
|
||||
shouldAnimateActivePill && 'transition-[transform,width,height] duration-200 ease-out'
|
||||
'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)]',
|
||||
'border border-border/60'
|
||||
)}
|
||||
style={{
|
||||
transform: `translate(${pillRect.left}px, ${pillRect.top}px)`,
|
||||
transform: `translate3d(${pillRect.left}px, ${pillRect.top}px, 0)`,
|
||||
width: `${pillRect.width}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}
|
||||
{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) => {
|
||||
const isActive = item.id === activeId;
|
||||
const showInactiveIconOnly = inactiveTabsIconOnly && usesActivePillIndicator && !isActive && Boolean(item.icon);
|
||||
@@ -368,8 +385,8 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
usesActivePillIndicator
|
||||
? 'relative z-10 bg-transparent'
|
||||
: isActive
|
||||
? 'border-r border-border/40 bg-[var(--surface-elevated)] 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-foreground'
|
||||
: 'relative z-10 bg-transparent text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
@@ -380,7 +397,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
onClick={() => onSelect(item.id)}
|
||||
className={cn(
|
||||
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',
|
||||
usesActivePillIndicator && closable && '!flex-none',
|
||||
usesActivePillIndicator && activePillLowercase ? 'lowercase' : null,
|
||||
@@ -396,10 +413,10 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
? 'shrink-0 whitespace-nowrap px-3 text-center'
|
||||
: 'px-3 text-center')
|
||||
: isScrollable
|
||||
? 'max-w-56 justify-start truncate pl-3 pr-2 text-left'
|
||||
: 'w-full justify-center truncate px-2.5 text-center',
|
||||
? 'max-w-56 justify-start truncate px-3 text-left'
|
||||
: 'w-full justify-center truncate px-3 text-center',
|
||||
usesActivePillIndicator
|
||||
? (activePillButtonClassName ?? (isActivePillVariant ? (isMobile ? 'h-[34px]' : 'h-[27px]') : 'h-7'))
|
||||
? (activePillButtonClassName ?? (isActivePillVariant ? (isMobile ? 'h-[38px]' : 'h-[31px]') : 'h-7'))
|
||||
: null,
|
||||
usesActivePillIndicator
|
||||
? 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')}>
|
||||
{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>
|
||||
)}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
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';
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ComponentRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
HTMLButtonElement,
|
||||
React.ComponentPropsWithoutRef<typeof BaseSwitch.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
<BaseSwitch.Root
|
||||
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
|
||||
)}
|
||||
style={{ width: '36px', height: '20px', minWidth: '36px', minHeight: '20px' }}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
<BaseSwitch.Thumb
|
||||
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' }}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
</BaseSwitch.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
Switch.displayName = 'Switch';
|
||||
|
||||
export { Switch };
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { motion, type MotionProps } from "motion/react";
|
||||
import * as Slot from "@radix-ui/react-slot";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Variant = {
|
||||
@@ -219,9 +217,5 @@ export function Text({ variant = "shine", className, ...props }: TextProps) {
|
||||
|
||||
const Component = variantComponent || variants[FALLBACK_INDEX].component;
|
||||
|
||||
return (
|
||||
<Slot.Root>
|
||||
<Component {...props} className={className} />
|
||||
</Slot.Root>
|
||||
);
|
||||
return <Component {...props} className={className} />;
|
||||
}
|
||||
|
||||
@@ -9,41 +9,215 @@ type TextareaProps = React.ComponentProps<"textarea"> & {
|
||||
fillContainer?: boolean;
|
||||
useScrollShadow?: boolean;
|
||||
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>(
|
||||
({ 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 (
|
||||
<ScrollableOverlay
|
||||
as="textarea"
|
||||
ref={ref as React.Ref<HTMLTextAreaElement>}
|
||||
disableHorizontal
|
||||
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}
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
onPointerDown={focusInnerTextarea}
|
||||
style={resizedHeight !== null ? { height: `${resizedHeight}px` } : undefined}
|
||||
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",
|
||||
useScrollShadow ? "border-0" : "border border-border/80 hover:border-input focus:border-primary/70",
|
||||
fillContainer ? "[field-sizing:fixed]" : "field-sizing-content",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
"group/textarea relative flex w-full flex-col rounded-[var(--radius-xl)] bg-[var(--surface-elevated)] pb-2.5",
|
||||
"ring-1 ring-inset ring-border/60 transition duration-200 ease-out",
|
||||
"hover:[&:not(:focus-within)]:bg-[var(--surface-subtle)]",
|
||||
"has-[[disabled]]:pointer-events-none has-[[disabled]]:bg-[var(--surface-subtle)] has-[[disabled]]:ring-transparent",
|
||||
!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"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
<div className="flex h-full min-h-0 flex-1 flex-col gap-2">
|
||||
<textarea
|
||||
ref={ref}
|
||||
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"
|
||||
|
||||
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 }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
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 { cn } from "@/lib/utils"
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
@@ -32,10 +32,10 @@ function Toggle({
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
}: React.ComponentProps<typeof BaseToggle> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
<BaseToggle
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
|
||||
@@ -1,65 +1,94 @@
|
||||
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"
|
||||
|
||||
type AsChildRenderProps = {
|
||||
render?: React.ReactElement;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type ProviderProps = React.ComponentProps<typeof BaseTooltip.Provider> & {
|
||||
delayDuration?: number;
|
||||
skipDelayDuration?: number;
|
||||
};
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
skipDelayDuration,
|
||||
delay,
|
||||
closeDelay,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
}: ProviderProps) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
<BaseTooltip.Provider
|
||||
delay={delay ?? delayDuration}
|
||||
closeDelay={closeDelay ?? skipDelayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type TooltipProps = React.ComponentProps<typeof TooltipPrimitive.Root> & {
|
||||
type TooltipRootProps = React.ComponentProps<typeof BaseTooltip.Root> & {
|
||||
delayDuration?: number
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
delayDuration = 0,
|
||||
delayDuration,
|
||||
...props
|
||||
}: TooltipProps) {
|
||||
return (
|
||||
<TooltipPrimitive.Root data-slot="tooltip" delayDuration={delayDuration} {...props} />
|
||||
)
|
||||
}: TooltipRootProps) {
|
||||
const tooltip = <BaseTooltip.Root {...props} />
|
||||
|
||||
if (delayDuration === undefined) {
|
||||
return tooltip
|
||||
}
|
||||
|
||||
return <TooltipProvider delayDuration={delayDuration}>{tooltip}</TooltipProvider>
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
asChild,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}: React.ComponentProps<typeof BaseTooltip.Trigger> & { asChild?: boolean }) {
|
||||
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({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
side,
|
||||
align,
|
||||
children,
|
||||
style,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
}: ContentProps) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
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",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="fill-muted z-50 size-2" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
<BaseTooltip.Portal>
|
||||
<BaseTooltip.Positioner sideOffset={sideOffset} side={side} align={align} className="z-50">
|
||||
<BaseTooltip.Popup
|
||||
data-slot="tooltip-content"
|
||||
className={cn(
|
||||
"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
|
||||
)}
|
||||
style={{ ...style }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<BaseTooltip.Arrow className="fill-muted z-50 size-2" />
|
||||
</BaseTooltip.Popup>
|
||||
</BaseTooltip.Positioner>
|
||||
</BaseTooltip.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ const FileSelector = React.memo<FileSelectorProps>(({
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<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 ? (
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<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 (
|
||||
<DropdownMenu>
|
||||
<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">
|
||||
{currentOption.label}
|
||||
</span>
|
||||
<RiArrowDownSLine className="size-4 opacity-50" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[220px]">
|
||||
<DropdownMenuContent className="min-w-[140px]">
|
||||
<DropdownMenuRadioGroup
|
||||
value={mode}
|
||||
onValueChange={(value) => onModeChange(value as DiffTabViewMode)}
|
||||
>
|
||||
{DIFF_VIEW_MODE_OPTIONS.map((option) => (
|
||||
<DropdownMenuRadioItem key={option.value} value={option.value}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="typography-meta text-foreground">
|
||||
{option.label}
|
||||
</span>
|
||||
<span className="typography-micro text-muted-foreground">
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
<span className="typography-meta text-foreground">
|
||||
{option.label}
|
||||
</span>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
@@ -582,7 +577,14 @@ const SingleDiffViewer = React.memo<SingleDiffViewerProps>(({
|
||||
}
|
||||
|
||||
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
|
||||
original={diff.original}
|
||||
modified={diff.modified}
|
||||
@@ -590,8 +592,9 @@ const SingleDiffViewer = React.memo<SingleDiffViewerProps>(({
|
||||
fileName={filePath}
|
||||
renderSideBySide={renderSideBySide}
|
||||
wrapLines={wrapLines}
|
||||
layout="inline"
|
||||
/>
|
||||
</div>
|
||||
</ScrollableOverlay>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -508,6 +508,19 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
const [wrapLines, setWrapLines] = React.useState(true);
|
||||
const [isFullscreen, setIsFullscreen] = 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 [mdViewMode, setMdViewMode] = React.useState<'preview' | 'edit'>('edit');
|
||||
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">
|
||||
{selectedFile && !isSearchOpen && (
|
||||
<div className="absolute right-3 top-3 z-30">
|
||||
{renderFloatingFileControls()}
|
||||
<div
|
||||
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>
|
||||
)}
|
||||
<ScrollableOverlay outerClassName="h-full min-w-0" className="h-full min-w-0">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { cn } from '@/lib/utils';
|
||||
import { MultiRunLauncher } from '@/components/multirun';
|
||||
@@ -23,23 +23,22 @@ export const MultiRunWindow: React.FC<MultiRunWindowProps> = ({
|
||||
}
|
||||
|
||||
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 (
|
||||
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay
|
||||
className="fixed inset-0 z-50 bg-black/50 dark:bg-black/75"
|
||||
/>
|
||||
<DialogPrimitive.Content
|
||||
<Dialog.Root
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
if (!next && hasOpenFloatingMenu()) return;
|
||||
onOpenChange(next);
|
||||
}}
|
||||
>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black/50 dark:bg-black/75" />
|
||||
<Dialog.Popup
|
||||
aria-describedby={descriptionId}
|
||||
onInteractOutside={(event) => {
|
||||
if (hasOpenFloatingMenu()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||
'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" />
|
||||
</button>
|
||||
</div>
|
||||
<DialogPrimitive.Description id={descriptionId} className="sr-only">
|
||||
<Dialog.Description id={descriptionId} className="sr-only">
|
||||
OpenChamber Multi-Run window.
|
||||
</DialogPrimitive.Description>
|
||||
</Dialog.Description>
|
||||
<MultiRunLauncher
|
||||
initialPrompt={initialPrompt}
|
||||
onCreated={() => onOpenChange(false)}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
isWindowed
|
||||
/>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
</DialogPrimitive.Root>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -116,7 +116,8 @@ function isPageAvailable(page: SettingsPageMeta, ctx: SettingsRuntimeContext): b
|
||||
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) {
|
||||
case 'projects':
|
||||
return RiFoldersLine;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { SettingsView } from './SettingsView';
|
||||
|
||||
@@ -21,23 +21,22 @@ export const SettingsWindow: React.FC<SettingsWindowProps> = ({ open, onOpenChan
|
||||
}
|
||||
|
||||
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 (
|
||||
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay
|
||||
className="fixed inset-0 z-50 bg-black/50 dark:bg-black/75"
|
||||
/>
|
||||
<DialogPrimitive.Content
|
||||
<Dialog.Root
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
if (!next && hasOpenFloatingMenu()) return;
|
||||
onOpenChange(next);
|
||||
}}
|
||||
>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black/50 dark:bg-black/75" />
|
||||
<Dialog.Popup
|
||||
aria-describedby={descriptionId}
|
||||
onInteractOutside={(event) => {
|
||||
if (hasOpenFloatingMenu()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||
'w-[90vw] max-w-[960px] h-[85vh] max-h-[900px]',
|
||||
@@ -45,12 +44,12 @@ export const SettingsWindow: React.FC<SettingsWindowProps> = ({ open, onOpenChan
|
||||
'bg-background'
|
||||
)}
|
||||
>
|
||||
<DialogPrimitive.Description id={descriptionId} className="sr-only">
|
||||
<Dialog.Description id={descriptionId} className="sr-only">
|
||||
OpenChamber settings window.
|
||||
</DialogPrimitive.Description>
|
||||
</Dialog.Description>
|
||||
<SettingsView onClose={() => onOpenChange(false)} isWindowed />
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
</DialogPrimitive.Root>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -908,13 +908,9 @@ export const TerminalView: React.FC = () => {
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"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
|
||||
)}
|
||||
variant="chip"
|
||||
aria-pressed={activeModifier === 'ctrl'}
|
||||
className="h-6 w-9 p-0"
|
||||
onClick={() => handleModifierToggle('ctrl')}
|
||||
disabled={quickKeysDisabled}
|
||||
>
|
||||
@@ -924,13 +920,9 @@ export const TerminalView: React.FC = () => {
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"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
|
||||
)}
|
||||
variant="chip"
|
||||
aria-pressed={activeModifier === 'cmd'}
|
||||
className="h-6 w-9 p-0"
|
||||
onClick={() => handleModifierToggle('cmd')}
|
||||
disabled={quickKeysDisabled}
|
||||
>
|
||||
|
||||
@@ -202,7 +202,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
|
||||
<RiArrowDownSLine className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-[var(--radix-dropdown-menu-trigger-width)]">
|
||||
<DropdownMenuContent align="start" className="w-[var(--anchor-width)]">
|
||||
{group.sessions.map((session) => (
|
||||
<DropdownMenuItem
|
||||
key={session.id}
|
||||
@@ -249,19 +249,15 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-[220px]">
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
handleRemoveSelectedWorktree();
|
||||
}}
|
||||
onSelect={handleRemoveSelectedWorktree}
|
||||
closeOnClick={false}
|
||||
variant="destructive"
|
||||
>
|
||||
Remove this worktree
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
handleKeepOnlySelectedWorktree();
|
||||
}}
|
||||
onSelect={handleKeepOnlySelectedWorktree}
|
||||
closeOnClick={false}
|
||||
>
|
||||
Leave this one, remove others
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -278,7 +278,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
</div>
|
||||
|
||||
{/* Branch Selection */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
{operation === 'merge' ? `Branch to merge into ${targetBranchLabel}` : 'Branch to rebase onto'}
|
||||
</p>
|
||||
@@ -293,7 +293,8 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
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">
|
||||
<CommandInput
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
RiCheckboxLine,
|
||||
RiCheckboxBlankLine,
|
||||
RiArrowGoBackLine,
|
||||
RiLoader4Line,
|
||||
} from '@remixicon/react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||
import type { GitStatus } from '@/lib/api/types';
|
||||
|
||||
@@ -82,15 +81,6 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
|
||||
[onToggle, onViewDiff]
|
||||
);
|
||||
|
||||
const handleToggleClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onToggle();
|
||||
},
|
||||
[onToggle]
|
||||
);
|
||||
|
||||
const handleRevertClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -109,19 +99,14 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
|
||||
onKeyDown={handleKeyDown}
|
||||
style={indentPx > 0 ? { paddingLeft: `${indentPx}px` } : undefined}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleClick}
|
||||
aria-pressed={checked}
|
||||
aria-label={`Select ${file.path}`}
|
||||
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"
|
||||
>
|
||||
{checked ? (
|
||||
<RiCheckboxLine className="size-4 text-primary" />
|
||||
) : (
|
||||
<RiCheckboxBlankLine className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
<div onClick={(e) => { e.stopPropagation(); }}>
|
||||
<Checkbox
|
||||
size="sm"
|
||||
checked={checked}
|
||||
onChange={() => onToggle()}
|
||||
ariaLabel={`Select ${file.path}`}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className="typography-micro font-semibold w-4 text-center uppercase"
|
||||
style={{ color: descriptor.color }}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { RiArrowDownSLine, RiArrowRightSLine, RiCheckboxBlankLine, RiCheckboxLine, RiSubtractLine } from '@remixicon/react';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -444,26 +445,22 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Changes</h3>
|
||||
{totalCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={areAllSelected ? onClearSelection : onSelectAll}
|
||||
disabled={isRevertingAll}
|
||||
aria-checked={isPartiallySelected ? 'mixed' : hasAnySelected}
|
||||
aria-label={areAllSelected ? 'Clear file selection' : 'Select all files'}
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex h-6 items-center gap-1 rounded px-1.5 text-muted-foreground',
|
||||
'hover:bg-interactive-hover/55 hover:text-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
'inline-flex h-6 items-center gap-1 rounded px-1.5',
|
||||
isRevertingAll && 'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
>
|
||||
{hasAnySelected ? (
|
||||
<RiCheckboxLine className={cn('size-4', isPartiallySelected ? 'text-primary/50' : 'text-primary')} />
|
||||
) : (
|
||||
<RiCheckboxBlankLine className="size-4" />
|
||||
)}
|
||||
<Checkbox
|
||||
size="sm"
|
||||
checked={hasAnySelected}
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pr-1">
|
||||
|
||||
@@ -374,7 +374,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
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">
|
||||
<CommandInput ref={searchInputRef} placeholder="Search branches..." />
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
RiAiGenerate2,
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightSLine,
|
||||
RiCheckboxBlankLine,
|
||||
RiCheckboxLine,
|
||||
RiCloseLine,
|
||||
RiEditLine,
|
||||
RiErrorWarningLine,
|
||||
@@ -20,6 +18,7 @@ import {
|
||||
RiLoader4Line,
|
||||
} from '@remixicon/react';
|
||||
import { toast } from '@/components/ui';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -1698,7 +1697,7 @@ export const PullRequestSection: React.FC<{
|
||||
<Textarea
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
className="min-h-[110px] bg-background/80"
|
||||
className="min-h-[110px]"
|
||||
placeholder="What changed and why"
|
||||
autoCorrect={hasTouchInput ? "on" : "off"}
|
||||
autoCapitalize={hasTouchInput ? "sentences" : "off"}
|
||||
@@ -1719,22 +1718,12 @@ export const PullRequestSection: React.FC<{
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
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>
|
||||
<Checkbox
|
||||
size="sm"
|
||||
checked={draft}
|
||||
onChange={(next) => setDraft(next)}
|
||||
ariaLabel="Toggle draft PR"
|
||||
/>
|
||||
<span className="typography-ui-label text-foreground select-none">Draft</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -63,7 +63,15 @@ textarea[data-chat-input="true"]:focus-visible {
|
||||
}
|
||||
|
||||
.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 */
|
||||
@@ -869,7 +877,7 @@ html:not(.dark) .chat-scroll {
|
||||
}
|
||||
|
||||
.pill-tabs__track {
|
||||
padding-inline: 0.375rem;
|
||||
padding-inline: 0.125rem;
|
||||
}
|
||||
|
||||
@container pill-tabs (max-width: 23rem) {
|
||||
|
||||
@@ -96,11 +96,7 @@ export class CSSVariableGenerator {
|
||||
|
||||
vars.push(` --ring: ${theme.colors.interactive.focusRing} !important;`);
|
||||
|
||||
if (theme.config?.radius?.md) {
|
||||
vars.push(` --radius: ${theme.config.radius.md} !important;`);
|
||||
}
|
||||
|
||||
const sidebarBaseRgb = hexToRgb(theme.colors.surface.muted);
|
||||
const sidebarBaseRgb = hexToRgb(theme.colors.surface.muted);
|
||||
const sidebarAccentRgb = hexToRgb(theme.colors.surface.subtle);
|
||||
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.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.fast) vars.push(` --transition-fast: ${config.transitions.fast};`);
|
||||
if (config.transitions.normal) vars.push(` --transition-normal: ${config.transitions.normal};`);
|
||||
|
||||
@@ -411,14 +411,6 @@
|
||||
"mono": "\"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": {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
|
||||
@@ -411,14 +411,6 @@
|
||||
"mono": "\"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": {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Aura",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": [ "dark", "palette" ]
|
||||
"tags": [
|
||||
"dark",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Aura",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": [ "light", "palette" ]
|
||||
"tags": [
|
||||
"light",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Ayu",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": [ "dark", "palette" ]
|
||||
"tags": [
|
||||
"dark",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Ayu",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": [ "light", "palette" ]
|
||||
"tags": [
|
||||
"light",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Carbonfox",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": [ "dark", "palette" ]
|
||||
"tags": [
|
||||
"dark",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Carbonfox",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": [ "light", "palette" ]
|
||||
"tags": [
|
||||
"light",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Catppuccin",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": [ "dark", "palette" ]
|
||||
"tags": [
|
||||
"dark",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Catppuccin",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": [ "light", "palette" ]
|
||||
"tags": [
|
||||
"light",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -411,14 +411,6 @@
|
||||
"mono": "\"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": {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
|
||||
@@ -411,14 +411,6 @@
|
||||
"mono": "\"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": {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Dracula",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": [ "dark", "palette" ]
|
||||
"tags": [
|
||||
"dark",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Dracula",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": [ "light", "palette" ]
|
||||
"tags": [
|
||||
"light",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
"author": "OpenChamber",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": ["dark", "warm", "earthy", "brand"]
|
||||
"tags": [
|
||||
"dark",
|
||||
"warm",
|
||||
"earthy",
|
||||
"brand"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -127,8 +132,8 @@
|
||||
"markdown": {
|
||||
"heading1": "#c47a3a",
|
||||
"heading2": "#93a56b",
|
||||
"heading3": "#ebe0d1",
|
||||
"heading4": "#ebe0d1",
|
||||
"heading3": "#ebe0d1",
|
||||
"heading4": "#ebe0d1",
|
||||
"link": "#5a6d7a",
|
||||
"linkHover": "#93a56b",
|
||||
"inlineCode": "#93a56b",
|
||||
@@ -167,14 +172,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
"author": "OpenChamber",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": ["light", "warm", "earthy", "brand"]
|
||||
"tags": [
|
||||
"light",
|
||||
"warm",
|
||||
"earthy",
|
||||
"brand"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -167,14 +172,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
"description": "An inky color scheme for prose and code - dark variant",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": ["dark", "warm", "natural", "ink"]
|
||||
"tags": [
|
||||
"dark",
|
||||
"warm",
|
||||
"natural",
|
||||
"ink"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +171,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
"description": "An inky color scheme for prose and code - light variant",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": ["light", "warm", "natural", "paper"]
|
||||
"tags": [
|
||||
"light",
|
||||
"warm",
|
||||
"natural",
|
||||
"paper"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +171,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -411,14 +411,6 @@
|
||||
"mono": "\"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": {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
|
||||
@@ -411,14 +411,6 @@
|
||||
"mono": "\"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": {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Gruvbox",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": [ "dark", "palette" ]
|
||||
"tags": [
|
||||
"dark",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Gruvbox",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": [ "light", "palette" ]
|
||||
"tags": [
|
||||
"light",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
"description": "A color scheme inspired by Japanese art and ink painting - dark variant",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": ["dark", "japanese", "ink", "elegant"]
|
||||
"tags": [
|
||||
"dark",
|
||||
"japanese",
|
||||
"ink",
|
||||
"elegant"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +171,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
"description": "A color scheme inspired by Japanese art and ink painting - light variant (Lotus)",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": ["light", "japanese", "lotus", "elegant"]
|
||||
"tags": [
|
||||
"light",
|
||||
"japanese",
|
||||
"lotus",
|
||||
"elegant"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +171,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -411,14 +411,6 @@
|
||||
"mono": "\"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": {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
|
||||
@@ -411,14 +411,6 @@
|
||||
"mono": "\"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": {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
"description": "A pure monochromatic theme - dark variant with shades of grey",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": ["dark", "monochrome", "minimal", "grayscale"]
|
||||
"tags": [
|
||||
"dark",
|
||||
"monochrome",
|
||||
"minimal",
|
||||
"grayscale"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +171,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
"description": "A pure monochromatic theme - light variant with shades of grey",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": ["light", "monochrome", "minimal", "grayscale"]
|
||||
"tags": [
|
||||
"light",
|
||||
"monochrome",
|
||||
"minimal",
|
||||
"grayscale"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +171,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
"description": "A pure monochromatic theme - dark variant with slight colors added",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": ["dark", "monochrome", "minimal"]
|
||||
"tags": [
|
||||
"dark",
|
||||
"monochrome",
|
||||
"minimal"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -145,14 +149,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
"description": "A pure monochromatic theme - light variant with slight colors added",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": ["light", "monochrome", "minimal"]
|
||||
"tags": [
|
||||
"light",
|
||||
"monochrome",
|
||||
"minimal"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -145,14 +149,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Monokai",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": [ "dark", "palette" ]
|
||||
"tags": [
|
||||
"dark",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Monokai",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": [ "light", "palette" ]
|
||||
"tags": [
|
||||
"light",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Night Owl",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": [ "dark", "palette" ]
|
||||
"tags": [
|
||||
"dark",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Night Owl",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": [ "light", "palette" ]
|
||||
"tags": [
|
||||
"light",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Nord",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": [ "dark", "palette" ]
|
||||
"tags": [
|
||||
"dark",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "Nord",
|
||||
"version": "1.0.0",
|
||||
"variant": "light",
|
||||
"tags": [ "light", "palette" ]
|
||||
"tags": [
|
||||
"light",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
@@ -411,14 +411,6 @@
|
||||
"mono": "\"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": {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
|
||||
@@ -411,14 +411,6 @@
|
||||
"mono": "\"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": {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"description": "One Dark Pro",
|
||||
"version": "1.0.0",
|
||||
"variant": "dark",
|
||||
"tags": [ "dark", "palette" ]
|
||||
"tags": [
|
||||
"dark",
|
||||
"palette"
|
||||
]
|
||||
},
|
||||
"colors": {
|
||||
"primary": {
|
||||
@@ -166,14 +169,6 @@
|
||||
"mono": "\"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": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "250ms ease",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user