@sableui/react 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +157 -0
- package/CHANGELOG.md +55 -0
- package/CONTRIBUTING.md +221 -0
- package/DESIGN.md +548 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/THIRD_PARTY_NOTICES.md +44 -0
- package/bin/sable-lint.mjs +0 -0
- package/bin/sable.mjs +1223 -0
- package/dist/_contract-CYScOdf5.d.ts +150 -0
- package/dist/chunk-3IWZYBGH.js +398 -0
- package/dist/chunk-5Z5KLOAU.js +167 -0
- package/dist/chunk-7MG4SHYX.js +15 -0
- package/dist/chunk-ADLLDWLW.js +15 -0
- package/dist/chunk-EP55KSYA.js +14 -0
- package/dist/chunk-J23ELQQZ.js +9396 -0
- package/dist/chunk-JI7WJP2L.js +286 -0
- package/dist/chunk-K7RUHUGC.js +14 -0
- package/dist/chunk-KO3NAEBF.js +15 -0
- package/dist/chunk-L4IEIQQA.js +14 -0
- package/dist/chunk-L6Y46C62.js +270 -0
- package/dist/chunk-NEQNXI2G.js +25 -0
- package/dist/chunk-PAOZBZME.js +40 -0
- package/dist/chunk-RWP4XBSK.js +139 -0
- package/dist/chunk-SRQWQUTW.js +63 -0
- package/dist/chunk-YXBOYWBN.js +15 -0
- package/dist/foundations/index.d.ts +88 -0
- package/dist/foundations/index.js +792 -0
- package/dist/icon/index.d.ts +60 -0
- package/dist/icon/index.js +2 -0
- package/dist/icon-sets/brands.d.ts +44 -0
- package/dist/icon-sets/brands.js +147 -0
- package/dist/icon-sets/reicon.d.ts +8 -0
- package/dist/icon-sets/reicon.js +1 -0
- package/dist/index.d.ts +7013 -0
- package/dist/index.js +15462 -0
- package/dist/ios/chrome.css +83 -0
- package/dist/ios/frame.css +5 -0
- package/dist/ios/index.d.ts +70 -0
- package/dist/ios/index.js +313 -0
- package/dist/ios/preview.d.ts +64 -0
- package/dist/ios/preview.js +133 -0
- package/dist/ios/styles.css +77 -0
- package/dist/layout/index.d.ts +280 -0
- package/dist/layout/index.js +2 -0
- package/dist/meta-6dUzgC6M.d.ts +205 -0
- package/dist/patterns/index.d.ts +36 -0
- package/dist/patterns/index.js +934 -0
- package/dist/presets/index.d.ts +37 -0
- package/dist/presets/index.js +117 -0
- package/dist/registry-CyL28OzJ.d.ts +28 -0
- package/dist/styles.css +1893 -0
- package/dist/theme-provider-BWcizhAt.d.ts +105 -0
- package/dist/themes/atelier/atelier.css +213 -0
- package/dist/themes/atelier/index.d.ts +9 -0
- package/dist/themes/atelier/index.js +1 -0
- package/dist/themes/base/base.css +443 -0
- package/dist/themes/base/index.d.ts +9 -0
- package/dist/themes/base/index.js +1 -0
- package/dist/themes/ember/ember.css +270 -0
- package/dist/themes/ember/index.d.ts +9 -0
- package/dist/themes/ember/index.js +1 -0
- package/dist/themes/index.d.ts +10 -0
- package/dist/themes/index.js +8 -0
- package/dist/themes/meridian/index.d.ts +9 -0
- package/dist/themes/meridian/index.js +1 -0
- package/dist/themes/meridian/meridian.css +255 -0
- package/dist/themes/poppy/index.d.ts +6 -0
- package/dist/themes/poppy/index.js +1 -0
- package/dist/themes/poppy/poppy.css +174 -0
- package/dist/themes/prism/index.d.ts +6 -0
- package/dist/themes/prism/index.js +1 -0
- package/dist/themes/prism/prism.css +183 -0
- package/dist/themes/verdant/index.d.ts +9 -0
- package/dist/themes/verdant/index.js +1 -0
- package/dist/themes/verdant/verdant.css +210 -0
- package/dist/themes.css +30 -0
- package/llms/components/accordion.md +93 -0
- package/llms/components/app-header.md +70 -0
- package/llms/components/app-shell.md +117 -0
- package/llms/components/attachment.md +105 -0
- package/llms/components/avatar.md +141 -0
- package/llms/components/badge.md +117 -0
- package/llms/components/banner.md +119 -0
- package/llms/components/bottom-nav.md +80 -0
- package/llms/components/breadcrumbs.md +75 -0
- package/llms/components/browser.md +67 -0
- package/llms/components/bubble.md +119 -0
- package/llms/components/button.md +199 -0
- package/llms/components/calendar.md +75 -0
- package/llms/components/card.md +135 -0
- package/llms/components/carousel.md +87 -0
- package/llms/components/chat-composer.md +126 -0
- package/llms/components/checkbox-card.md +98 -0
- package/llms/components/checkbox.md +90 -0
- package/llms/components/code-block.md +81 -0
- package/llms/components/code.md +62 -0
- package/llms/components/collapsible.md +83 -0
- package/llms/components/color-field.md +100 -0
- package/llms/components/combobox.md +119 -0
- package/llms/components/command-menu.md +158 -0
- package/llms/components/context-card.md +73 -0
- package/llms/components/context-menu.md +79 -0
- package/llms/components/data-table.md +99 -0
- package/llms/components/date-picker.md +97 -0
- package/llms/components/description.md +96 -0
- package/llms/components/destructive-action-modal.md +76 -0
- package/llms/components/dialog.md +127 -0
- package/llms/components/dots-menu.md +64 -0
- package/llms/components/empty-state.md +113 -0
- package/llms/components/entity.md +67 -0
- package/llms/components/feedback.md +63 -0
- package/llms/components/fieldset.md +106 -0
- package/llms/components/file-dropzone.md +103 -0
- package/llms/components/form.md +111 -0
- package/llms/components/gliding-highlight.md +84 -0
- package/llms/components/inline-status.md +62 -0
- package/llms/components/input.md +127 -0
- package/llms/components/kbd.md +57 -0
- package/llms/components/label.md +73 -0
- package/llms/components/marker.md +99 -0
- package/llms/components/marquee.md +66 -0
- package/llms/components/menu.md +122 -0
- package/llms/components/message-scroller.md +110 -0
- package/llms/components/message.md +114 -0
- package/llms/components/meter.md +124 -0
- package/llms/components/middle-truncate.md +62 -0
- package/llms/components/note.md +81 -0
- package/llms/components/notification-badge.md +67 -0
- package/llms/components/number-field.md +70 -0
- package/llms/components/otp-field.md +101 -0
- package/llms/components/page-header.md +76 -0
- package/llms/components/page-loader.md +67 -0
- package/llms/components/pagination.md +92 -0
- package/llms/components/panel-group.md +120 -0
- package/llms/components/panel-section.md +122 -0
- package/llms/components/phone.md +70 -0
- package/llms/components/pill.md +73 -0
- package/llms/components/popover.md +79 -0
- package/llms/components/progress.md +98 -0
- package/llms/components/radio-card.md +90 -0
- package/llms/components/radio.md +86 -0
- package/llms/components/relative-time.md +60 -0
- package/llms/components/scroller.md +88 -0
- package/llms/components/scrub-field.md +97 -0
- package/llms/components/search-input.md +84 -0
- package/llms/components/select.md +106 -0
- package/llms/components/separator.md +70 -0
- package/llms/components/settings-menu.md +139 -0
- package/llms/components/sheet.md +97 -0
- package/llms/components/show-more.md +59 -0
- package/llms/components/side-nav.md +106 -0
- package/llms/components/sidebar.md +87 -0
- package/llms/components/skeleton.md +62 -0
- package/llms/components/slider.md +114 -0
- package/llms/components/spinner.md +87 -0
- package/llms/components/status-bar.md +91 -0
- package/llms/components/status-dot.md +70 -0
- package/llms/components/stepper.md +125 -0
- package/llms/components/streaming-text.md +86 -0
- package/llms/components/switch.md +87 -0
- package/llms/components/table.md +112 -0
- package/llms/components/tabs.md +84 -0
- package/llms/components/textarea.md +88 -0
- package/llms/components/theme-selector.md +95 -0
- package/llms/components/theme-switcher.md +71 -0
- package/llms/components/thinking-orb.md +91 -0
- package/llms/components/toast.md +130 -0
- package/llms/components/toggle.md +95 -0
- package/llms/components/toolbar.md +168 -0
- package/llms/components/tooltip.md +93 -0
- package/llms/components/tree.md +227 -0
- package/llms/components/video.md +66 -0
- package/llms/components/workspace.md +138 -0
- package/llms/foundations/accessibility.md +60 -0
- package/llms/foundations/motion.md +96 -0
- package/llms/foundations/scrolling.md +41 -0
- package/llms/foundations/shape.md +73 -0
- package/llms/foundations/spacing.md +82 -0
- package/llms/foundations/surfaces.md +69 -0
- package/llms/foundations/typography.md +120 -0
- package/llms/layout/aspect-box.md +37 -0
- package/llms/layout/bleed.md +31 -0
- package/llms/layout/center.md +37 -0
- package/llms/layout/columns.md +51 -0
- package/llms/layout/grid-overlay.md +40 -0
- package/llms/layout/grid.md +47 -0
- package/llms/layout/page.md +39 -0
- package/llms/layout/region.md +49 -0
- package/llms/layout/row.md +40 -0
- package/llms/layout/safe-area.md +37 -0
- package/llms/layout/spacer.md +39 -0
- package/llms/layout/split-pane.md +41 -0
- package/llms/layout/stack.md +39 -0
- package/llms/patterns/auth-page.md +68 -0
- package/llms/patterns/brand-landing-page.md +423 -0
- package/llms/patterns/chat-panel.md +210 -0
- package/llms/patterns/dashboard-page.md +170 -0
- package/llms/patterns/feed-page.md +103 -0
- package/llms/patterns/landing-page.md +122 -0
- package/llms/patterns/list-detail-page.md +100 -0
- package/llms/patterns/settings-page.md +121 -0
- package/llms/reference/token-intent.md +324 -0
- package/llms/reference/utilities.md +129 -0
- package/llms/tasks/adding-a-component.md +113 -0
- package/llms/tasks/auditing-an-app.md +216 -0
- package/llms/tasks/building-a-page.md +109 -0
- package/llms/tasks/building-a-tool.md +141 -0
- package/llms/tasks/building-an-ios-prototype.md +105 -0
- package/llms/tasks/building-with-sable.md +73 -0
- package/llms/tasks/choosing-a-component.md +109 -0
- package/llms/tasks/converting-an-app.md +139 -0
- package/llms/tasks/creating-a-preset.md +43 -0
- package/llms/tasks/creating-design-assets.md +96 -0
- package/llms/tasks/designing-a-surface.md +115 -0
- package/llms/tasks/developing-taste.md +157 -0
- package/llms/tasks/getting-started.md +63 -0
- package/llms/tasks/learning-from-builds.md +74 -0
- package/llms/tasks/reviewing-a-design.md +114 -0
- package/llms/tasks/reviewing-a-journey.md +84 -0
- package/llms/tasks/reviewing-motion.md +92 -0
- package/llms/tasks/using-presets.md +87 -0
- package/llms/taste/asset-plan.template.json +53 -0
- package/llms/taste/composition-follows-content.md +20 -0
- package/llms/taste/hierarchy-is-a-relationship.md +22 -0
- package/llms/taste/lessons.json +57 -0
- package/llms/taste/motion-needs-a-reason.md +21 -0
- package/llms/taste/surface-states-travel-together.md +20 -0
- package/llms-full.txt +14906 -0
- package/llms.txt +405 -0
- package/package.json +185 -3
- package/registry.json +18812 -0
- package/scripts/agent-setup.mjs +54 -0
- package/scripts/design-atlas.mjs +221 -0
- package/scripts/design-directions.mjs +123 -0
- package/scripts/design-memory.mjs +166 -0
- package/scripts/design-preferences.mjs +227 -0
- package/scripts/design-presets.mjs +48 -0
- package/scripts/grade.mjs +509 -0
- package/scripts/lint-rules.mjs +1065 -0
- package/scripts/review-evidence.mjs +120 -0
- package/scripts/skeleton.mjs +248 -0
- package/skills/entry/SKILL.md +34 -0
- package/skills/sable/SKILL.md +187 -0
- package/skills/sable/agents/openai.yaml +6 -0
- package/targets/ios.json +982 -0
- package/third-party/reicon-LICENSE.txt +20 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# CodeBlock
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Multi-line code surface. Optional header bar carries a filename and a copy affordance; the body is a horizontally scrollable monospace pre. Syntax highlighting is opt-in: pass a highlighter per block or once via CodeHighlightProvider (the package ships none).
|
|
6
|
+
|
|
7
|
+
**Keywords:** code block, snippet, pre, example
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use CodeBlock for multiline code samples that users may read, inspect, or copy.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Give the snippet a filename, or a sentence of surrounding copy, when it helps readers and agents place it.
|
|
16
|
+
- Do: Keep examples complete enough to teach the intended pattern.
|
|
17
|
+
- Do: Wire highlighting once with CodeHighlightProvider and map token colors to semantic tone tokens (keywords → tone-edition, strings → success, comments → muted-foreground), so code follows the theme instead of a fixed palette.
|
|
18
|
+
- Don't: Do not put short inline identifiers in a CodeBlock; use Code for those.
|
|
19
|
+
- Don't: Do not paste code with the indentation the surrounding file had. A block indented to sit inside your JSX shows the reader leading whitespace that is not part of the example.
|
|
20
|
+
|
|
21
|
+
### Anatomy
|
|
22
|
+
|
|
23
|
+
| Part | Required | Description |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| CodeBlock | yes | The surface: optional filename header, copy affordance, scrollable pre. |
|
|
26
|
+
| CodeHighlightProvider | no | Optional app-level wrapper that supplies one highlighter to every CodeBlock inside it. |
|
|
27
|
+
|
|
28
|
+
## Subcomponents
|
|
29
|
+
|
|
30
|
+
| Name | Type | Description |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| CodeHighlightProvider | { highlight: (code: string) => ReactNode; children: ReactNode } | Supplies one highlighter to every CodeBlock beneath it, so an app wires highlighting once. Map the highlighter's token colors to Sable tone tokens so code follows the theme. |
|
|
33
|
+
|
|
34
|
+
## Props
|
|
35
|
+
|
|
36
|
+
| Name | Type | Default | Description |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| code | string | — | The code to display. Always what the copy button copies, whatever the highlighter renders. |
|
|
39
|
+
| filename | string | — | Optional filename shown in the header bar. |
|
|
40
|
+
| showCopy | boolean | true | Show the copy affordance — in the header bar with a filename, floating in the corner without one. |
|
|
41
|
+
| highlight | ((code: string) => ReactNode) \| false | — | Highlighter for this block; overrides CodeHighlightProvider. false renders plain text inside a provider. |
|
|
42
|
+
| prompt | string | — | A shell prompt ("$", ">"). Makes it a one-line command: the prompt is dimmed and unselectable, the line truncates, and copy takes only code. |
|
|
43
|
+
|
|
44
|
+
## Tokens used
|
|
45
|
+
|
|
46
|
+
`border`, `muted`, `muted-foreground`, `foreground`
|
|
47
|
+
|
|
48
|
+
## Accessibility
|
|
49
|
+
|
|
50
|
+
- Code renders inside a <pre><code> for correct semantics and screen-reader handling.
|
|
51
|
+
- Copy is the ghost icon Button with `useCopy`, which guards clipboard access for SSR safety.
|
|
52
|
+
|
|
53
|
+
## Related
|
|
54
|
+
|
|
55
|
+
- Components: Code
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
### With filename + copy
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { CodeBlock } from "@sableui/react"
|
|
63
|
+
|
|
64
|
+
<CodeBlock filename="cn.ts" code={"import { clsx } from \"clsx\"\nexport const cn = clsx"} />
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### With a highlighter for the whole app
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { CodeBlock, CodeHighlightProvider } from "@sableui/react"
|
|
71
|
+
|
|
72
|
+
<CodeHighlightProvider highlight={(code) => <span dangerouslySetInnerHTML={{ __html: highlightToHtml(code) }} />}><CodeBlock code={"const a = 1"} /></CodeHighlightProvider>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### No header
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { CodeBlock } from "@sableui/react"
|
|
79
|
+
|
|
80
|
+
<CodeBlock showCopy={false} code={"type Size = \"sm\" | \"md\"\nconst size: Size = \"md\""} />
|
|
81
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Code
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Inline code. A styled `<code>` for code spans, keys, and identifiers inside running text — soft muted fill, monospace.
|
|
6
|
+
|
|
7
|
+
**Keywords:** code, inline code, token, monospace
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Code for short inline identifiers, commands, tokens, props, and file names inside prose.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use it for brief technical literals that need to stand apart from body copy.
|
|
16
|
+
- Do: Keep inline code short enough that it does not dominate the paragraph.
|
|
17
|
+
- Don't: Do not use Code for multiline snippets; use CodeBlock instead.
|
|
18
|
+
- Don't: Do not put a value the reader needs to copy inside it. Code has no copy affordance, so they will select it by hand and catch the surrounding punctuation; use CodeBlock with a prompt.
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Name | Type | Default | Description |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| ...props | ComponentPropsWithRef<"code"> | — | All native `<code>` attributes (children, className, ref, …) pass through. |
|
|
25
|
+
|
|
26
|
+
## Tokens used
|
|
27
|
+
|
|
28
|
+
`muted`, `foreground`
|
|
29
|
+
|
|
30
|
+
## Accessibility
|
|
31
|
+
|
|
32
|
+
- Renders a semantic `<code>` element so assistive tech announces it as code.
|
|
33
|
+
- A `<code>` element is not announced distinctly by most screen readers — a code span in running prose is read as ordinary text. Where the exactness matters (a command to type verbatim, a case-sensitive identifier), say so in the sentence rather than relying on the styling to carry it.
|
|
34
|
+
|
|
35
|
+
## Related
|
|
36
|
+
|
|
37
|
+
- Components: CodeBlock, Kbd
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
### Inline
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { Code } from "@sableui/react"
|
|
45
|
+
|
|
46
|
+
Run <Code>pnpm dev</Code> to start the server.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Code vs CodeBlock
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { Code, CodeBlock } from "@sableui/react"
|
|
53
|
+
|
|
54
|
+
// Picking the wrong one is the usual mistake:
|
|
55
|
+
//
|
|
56
|
+
// Code inline, inside a sentence → `useState`
|
|
57
|
+
// CodeBlock multi-line, read-only → an example to study
|
|
58
|
+
// CodeBlock prompt="$" one line, meant to be COPIED → an install command
|
|
59
|
+
|
|
60
|
+
Install with <Code>pnpm add @sableui/react</Code> {/* ✗ they will want to copy it */}
|
|
61
|
+
<CodeBlock prompt="$" code="pnpm add @sableui/react" /> {/* ✓ one line, with a copy button */}
|
|
62
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Collapsible
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Show/hide a panel of content over Base UI Collapsible — open/close state, ARIA expanded wiring, and the animated height transition. The minimal primitive behind Accordion: a trigger that toggles an overflow-clipped panel.
|
|
6
|
+
|
|
7
|
+
**Keywords:** collapsible, disclosure, expand, collapse
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Collapsible for a single piece of optional content that can expand in place.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use it for local detail, advanced settings, or secondary context.
|
|
16
|
+
- Do: Make the trigger label describe the hidden content, not the mechanism.
|
|
17
|
+
- Don't: Do not use Collapsible for multi-section navigation; use Accordion or Tabs instead.
|
|
18
|
+
- Don't: Do not animate it by transitioning height from auto. Use the motion-disclosure recipe, which transitions to the measured panel height — `auto` does not animate and the panel will snap.
|
|
19
|
+
|
|
20
|
+
### Anatomy
|
|
21
|
+
|
|
22
|
+
| Part | Required | Description |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| CollapsibleTrigger | yes | The control that toggles the panel. |
|
|
25
|
+
| CollapsiblePanel | yes | The content that expands or collapses. |
|
|
26
|
+
| CollapsibleIndicator | no | A visual affordance for open or closed state. |
|
|
27
|
+
|
|
28
|
+
## Subcomponents
|
|
29
|
+
|
|
30
|
+
| Name | Type | Description |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| CollapsibleTrigger | Base UI Collapsible.Trigger props + render | The button that toggles the panel. Compose with render={<Button variant="ghost" … />}. |
|
|
33
|
+
| CollapsiblePanel | Base UI Collapsible.Panel props + className (keepMounted, hiddenUntilFound) | The collapsible contents. Overflow-hidden; animates height via the --collapsible-panel-height var Base UI sets. |
|
|
34
|
+
| CollapsibleIndicator | { className? } | Auto-rotating disclosure chevron. Place it inside the trigger content and add the `group/collapsible` class to the trigger so it flips 180° on open. |
|
|
35
|
+
|
|
36
|
+
## Props
|
|
37
|
+
|
|
38
|
+
| Name | Type | Default | Description |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| Collapsible (Root) | Base UI Collapsible.Root props (open, defaultOpen, onOpenChange, disabled) | — | Groups the trigger and panel and owns open state. Uncontrolled by default; pass `open` + `onOpenChange` to control it. |
|
|
41
|
+
| defaultOpen | boolean | false | Whether the panel starts open in the uncontrolled case. |
|
|
42
|
+
| disabled | boolean | false | Disables the trigger and prevents toggling. |
|
|
43
|
+
|
|
44
|
+
## Tokens used
|
|
45
|
+
|
|
46
|
+
`foreground`, `muted-foreground`
|
|
47
|
+
|
|
48
|
+
## Accessibility
|
|
49
|
+
|
|
50
|
+
- Base UI wires aria-expanded on the trigger and aria-controls/id between trigger and panel.
|
|
51
|
+
- Trigger is a real <button>; toggles on click and Enter/Space.
|
|
52
|
+
- Animate open/close via data-[starting-style]/data-[ending-style]; data-[panel-open] marks the open state.
|
|
53
|
+
- Set hiddenUntilFound on the panel to let in-page browser search expand it.
|
|
54
|
+
|
|
55
|
+
## Related
|
|
56
|
+
|
|
57
|
+
- Components: Accordion, ShowMore
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
### Basic
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { Button, Collapsible, CollapsiblePanel, CollapsibleTrigger } from "@sableui/react"
|
|
65
|
+
|
|
66
|
+
<Collapsible><CollapsibleTrigger render={<Button variant="ghost">Details</Button>} /><CollapsiblePanel><p className="pt-3">Hidden content revealed on toggle.</p></CollapsiblePanel></Collapsible>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Open by default
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { Button, Collapsible, CollapsiblePanel, CollapsibleTrigger } from "@sableui/react"
|
|
73
|
+
|
|
74
|
+
<Collapsible defaultOpen><CollapsibleTrigger render={<Button variant="ghost">Summary</Button>} /><CollapsiblePanel><p className="pt-3">Visible on first render.</p></CollapsiblePanel></Collapsible>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### With rotating chevron
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { Button, Collapsible, CollapsibleIndicator, CollapsiblePanel, CollapsibleTrigger } from "@sableui/react"
|
|
81
|
+
|
|
82
|
+
<Collapsible><CollapsibleTrigger className="group/collapsible" render={<Button variant="ghost">Details <CollapsibleIndicator /></Button>} /><CollapsiblePanel><p className="pt-3">Revealed on toggle.</p></CollapsiblePanel></Collapsible>
|
|
83
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ColorField
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A colour swatch that opens a themed picker — saturation/value area, hue and alpha sliders, hex entry, presets, and transparent as a value rather than a separate toggle.
|
|
6
|
+
|
|
7
|
+
**Keywords:** color, colour, color picker, swatch, hex, alpha, transparency, eyedropper, property, workspace
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use ColorField wherever a colour is a property: a canvas background, an effect parameter, a stroke or fill. It is sized to the control ladder so it lines up inside a PropertyRow or PropertyPair. Deliberately not <input type="color">, which opens the OS dialog — that ignores the theme entirely, cannot show a transparency checkerboard, has no alpha channel, hosts no eyedropper or presets, and renders at whatever size the platform picks, so it never aligns in a property grid.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use allowTransparent when "no colour" is a real value. It appears inside the picker as a value; a transparent switch sitting beside a colour control reads as two properties when it is one.
|
|
16
|
+
- Do: Pass swatches when a surface has a house palette. Presets are how most colour choices actually get made, and they cost one row.
|
|
17
|
+
- Do: Decide whether your persistence layer accepts 8-digit hex before enabling allowAlpha. The value format is the contract — see the tokens note.
|
|
18
|
+
- Don't: Put a separate opacity slider next to a ColorField that already has allowAlpha. Two controls writing one channel is how they drift out of sync.
|
|
19
|
+
- Don't: Re-derive hue from the committed value on every render. Grey has no hue, so dragging saturation to zero would snap the hue slider to red with no way back — ColorField keeps hue in its own state for exactly this reason.
|
|
20
|
+
- Don't: Hand-roll a transparency checkerboard. Use the `checkerboard` utility with `--checkerboard-size` (~16px for an artboard, ~8px for a 24px swatch); two hand-rolled copies had already drifted before it existed. Its colours come from --panel / --panel-raised, so it is correct in both modes. It is deliberately not named bg-checkerboard: a custom bg-* utility lands in tailwind-merge's background group and silently evicts, or is evicted by, a real bg-* colour class.
|
|
21
|
+
|
|
22
|
+
### Anatomy
|
|
23
|
+
|
|
24
|
+
| Part | Required | Description |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| Trigger | yes | A swatch at --control-xs with a hairline ring, on a checkerboard so a translucent or transparent value reads as such. |
|
|
27
|
+
| Picker | yes | Saturation/value area over the current hue, a hue slider, an alpha slider when allowAlpha, and a hex field. |
|
|
28
|
+
| Presets | no | An optional swatch row, and the Transparent entry when allowTransparent. |
|
|
29
|
+
|
|
30
|
+
## Props
|
|
31
|
+
|
|
32
|
+
| Name | Type | Default | Description |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| value | string | — | The current value: "transparent", "#rrggbb", or "#rrggbbaa". |
|
|
35
|
+
| onValueChange | (value: string) => void | — | Called with the new value on every commit, including mid-drag. |
|
|
36
|
+
| allowTransparent | boolean | false | Offers "transparent" as a value inside the picker. Colour → transparent → colour restores the colour you had. |
|
|
37
|
+
| allowAlpha | boolean | false | Enables the alpha slider, and therefore 8-digit hex output. Off by default because it widens the value format. |
|
|
38
|
+
| swatches | string[] | — | Optional preset row. |
|
|
39
|
+
| label | string | "Colour" | Accessible name for the trigger; the current value is appended to it. |
|
|
40
|
+
|
|
41
|
+
## Tokens used
|
|
42
|
+
|
|
43
|
+
`panel`, `panel-raised`, `hairline`, `hairline-strong`, `ring`, `popover`
|
|
44
|
+
|
|
45
|
+
## Accessibility
|
|
46
|
+
|
|
47
|
+
- The trigger's accessible name includes the current value, so a screen reader announces the colour rather than just "button" — the swatch itself conveys nothing without sight.
|
|
48
|
+
- The Transparent entry is a toggle with aria-pressed, because it is a state of the value and not a navigation action.
|
|
49
|
+
- The hex field is the keyboard path to any colour. The saturation/value area and the sliders are pointer-driven and are NOT keyboard operable — a known gap, and the reason the hex field is never hidden.
|
|
50
|
+
- The eyedropper is only rendered where the EyeDropper API exists (Chromium), rather than shown and broken elsewhere.
|
|
51
|
+
|
|
52
|
+
## Related
|
|
53
|
+
|
|
54
|
+
- Components: PropertyRow, PropertyPair, ScrubField, Input, Popover
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
### Canvas background
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { ColorField, PropertyRow } from "@sableui/react"
|
|
62
|
+
|
|
63
|
+
<PropertyRow label="Background">
|
|
64
|
+
<ColorField value={background} onValueChange={setBackground} allowTransparent />
|
|
65
|
+
</PropertyRow>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### With alpha and a palette
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { ColorField } from "@sableui/react"
|
|
72
|
+
|
|
73
|
+
<ColorField
|
|
74
|
+
value={tint}
|
|
75
|
+
onValueChange={setTint}
|
|
76
|
+
allowAlpha
|
|
77
|
+
swatches={["#f43f5e", "#facc15", "#22c55e", "#3b82f6", "#a855f7"]}
|
|
78
|
+
label="Tint"
|
|
79
|
+
/>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### The value format is the contract
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { isTransparent, parseHex } from "@sableui/react"
|
|
86
|
+
|
|
87
|
+
// ColorField only ever EMITS these three shapes:
|
|
88
|
+
// "transparent" | "#rrggbb" | "#rrggbbaa"
|
|
89
|
+
// It accepts #rgb and #rgba on paste but never produces them, so a consumer's
|
|
90
|
+
// parser has three cases to handle rather than five.
|
|
91
|
+
//
|
|
92
|
+
// Alpha is omitted when fully opaque — "#3b82f6", never "#3b82f6ff" — so a
|
|
93
|
+
// parser written before alpha support keeps reading opaque colours correctly.
|
|
94
|
+
// Only enable allowAlpha once your parser and your persisted format accept the
|
|
95
|
+
// 8-digit form, or a document saved by a newer build degrades on an older one.
|
|
96
|
+
|
|
97
|
+
import { parseHex, isTransparent } from "@sableui/react"
|
|
98
|
+
|
|
99
|
+
const parsed = isTransparent(value) ? null : parseHex(value) // null = not a colour
|
|
100
|
+
```
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Combobox
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Autocomplete over Base UI Combobox — type-to-filter, roving focus, ARIA combobox/listbox wiring, collision-aware positioning, Esc/outside dismissal. A search input (with chevron trigger) plus a portal-rendered popup of filterable items.
|
|
6
|
+
|
|
7
|
+
**Keywords:** combobox, autocomplete, typeahead, search select, filter, suggestions, picker
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Combobox when users choose from a list that benefits from typing, filtering, or autocomplete — one value, or several with `multiple` and ComboboxChips (tags, collaborators, categories). Use Select for short finite sets, a Checkbox group when every choice should stay visible, and CommandMenu for global actions.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Pass items so internal filtering, empty state, and accessibility wiring have the full option set.
|
|
16
|
+
- Do: Use ComboboxEmpty with helpful copy for zero matches.
|
|
17
|
+
- Do: Wrap in Field when the combobox is part of a form.
|
|
18
|
+
- Do: Reach for ComboboxVirtualList past roughly a thousand rows, and leave filtering to the root. It reads the filtered list through useFilteredItems(), so the rendered rows and the rows Base UI navigates cannot drift apart.
|
|
19
|
+
- Do: Use `multiple` with ComboboxChips for several values from a filterable set, and keep chip labels short so the field does not turn into a paragraph.
|
|
20
|
+
- Don't: Use Combobox for tiny option sets where Select or Radio would be easier to scan.
|
|
21
|
+
- Don't: Use Combobox as a command launcher. Use CommandMenu for actions and navigation.
|
|
22
|
+
- Don't: Take filtering over with filteredItems in order to virtualize. Base UI ignores filteredItems while the untouched field still holds the selected item's label, so the list you render and the list it indexes diverge — and the symptom is clicking one row and getting another.
|
|
23
|
+
|
|
24
|
+
### Anatomy
|
|
25
|
+
|
|
26
|
+
| Part | Required | Description |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| Root | yes | Owns selected value, input value, filtering, and open state. |
|
|
29
|
+
| Input | yes | The searchable text field and trigger. |
|
|
30
|
+
| Chips | no | The field for `multiple`, used in place of Input: one removable chip per selected item plus the text input, in one wrapping shell. |
|
|
31
|
+
| Content | yes | The positioned popup surface. |
|
|
32
|
+
| List | yes | The listbox inside the popup. Takes a render function that receives each filtered item. |
|
|
33
|
+
| Item | yes | A selectable suggestion row. |
|
|
34
|
+
| Empty | no | Fallback shown when filtering returns no items. |
|
|
35
|
+
|
|
36
|
+
## Subcomponents
|
|
37
|
+
|
|
38
|
+
| Name | Type | Description |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| ComboboxInput | Base UI Combobox.Input props (minus the native `size` attribute) + size?: "md" \| "xs" + className | The search field, styled like Input, wrapped with an InputGroup + chevron Trigger that toggles the popup. The chevron flips while the popup is open (data-[popup-open] on the toggle). `size="xs"` is the property-grid rung and mirrors SelectTrigger's `xs` exactly — same height, radius and inset surface — so a combobox and a select in one panel are the same shape holding different content (a font-family picker directly above a font-weight select is the case it exists for). Note the type: the native numeric `size` attribute is omitted rather than shadowed, because intersecting a string union onto it resolves to `never` and breaks every call site. |
|
|
41
|
+
| ComboboxChips | Base UI Combobox.Input props (minus the native `size` attribute) + size?: "md" \| "xs" + chipLabel?: (item) => string + className | The field for a `multiple` combobox, used in place of ComboboxInput. Each selected item renders as a soft `sm` Badge with a ghost `icon-xs` Button that removes it (labelled "Remove {label}"), followed by the text input; the shell wraps and grows as chips are added. `chipLabel` turns an item into its chip text — defaults to the item for strings/numbers and its `label` field for objects. Keyboard is Base UI's: Backspace in an empty field removes the last chip, ←/→ move between chips, Backspace/Delete removes the highlighted one. `size="xs"` mirrors ComboboxInput's property-grid rung. |
|
|
42
|
+
| ComboboxContent | Base UI Popup props + { side?: Side; align?: Align; sideOffset?: number; scroll?: boolean; className?: string } | Portal + Positioner + Popup — it does not render a list, so put a ComboboxList (or ComboboxVirtualList) inside it. Width matches the input via --anchor-width. side is which side of the input the popup renders against (auto-flips on collision), align its alignment along that side, sideOffset the gap in px. Defaults to side='bottom', align='start', sideOffset=6, scroll=true. Pass scroll={false} when a ComboboxVirtualList inside owns the scrolling. |
|
|
43
|
+
| ComboboxList | Base UI Combobox.List props (children: ReactNode \| (item, index) => ReactNode) | The listbox. Required inside ComboboxContent: pass a render function and it is called once per filtered item (or per group when `items` is grouped), so the rendered rows always match Base UI's filtering. |
|
|
44
|
+
| ComboboxVirtualList | { itemHeight: number; groupLabelHeight?: number; overscan?: number; children: (item, { index }) => ReactNode; renderGroupLabel?; className?; itemClassName? } | A windowed list: only the rows on screen exist. Fixed row heights (nothing is measured, so late-arriving content changes how a row looks and never where the rows are). Filtering stays Base UI's — the component reads the filtered list through useFilteredItems() — and groups keep their role=group wrapper and label association. Needs virtualized on the root and scroll={false} on ComboboxContent. |
|
|
45
|
+
| ComboboxItem | Base UI Combobox.Item props + className (value, disabled, index, onClick) | A suggestion row. Highlights via data-[highlighted]; shows a trailing check (ItemIndicator) when it matches the selected value. |
|
|
46
|
+
| ComboboxEmpty | Base UI Combobox.Empty props + className | Fallback shown when the filtered list is empty. Requires the `items` prop on the root. |
|
|
47
|
+
| ComboboxGroup | Base UI Combobox.Group props + className | Groups related items under an accessible label auto-associated with the group. |
|
|
48
|
+
| ComboboxGroupLabel | Base UI Combobox.GroupLabel props + className | The label for a ComboboxGroup. Auto-associated with the group it sits in. |
|
|
49
|
+
|
|
50
|
+
## Props
|
|
51
|
+
|
|
52
|
+
| Name | Type | Default | Description |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| Combobox (Root) | Base UI Combobox.Root props (items, value, defaultValue, onValueChange, inputValue, onInputValueChange, open, onOpenChange, multiple, autoHighlight, filter, disabled) | — | Groups all parts and owns selection + filter state. Pass `items` (flat array or groups) to enable internal filtering and ComboboxEmpty. |
|
|
55
|
+
| items | readonly any[] \| readonly ComboboxItemGroup[] | — | The pool of options, flat or grouped ({ value, items }). Drives type-to-filter and is required for ComboboxEmpty to render its fallback. |
|
|
56
|
+
| multiple | boolean | false | Select several items instead of one. `value` / `defaultValue` / `onValueChange` become arrays, choosing an item toggles its membership, and the popup stays open between picks. Pair it with ComboboxChips in place of ComboboxInput; every other part is unchanged. |
|
|
57
|
+
| virtualized | boolean | false | Tells Base UI the DOM holds only part of the list. Required when using ComboboxVirtualList. |
|
|
58
|
+
|
|
59
|
+
## Tokens used
|
|
60
|
+
|
|
61
|
+
`input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`, `popover`, `popover-foreground`, `border`, `accent`, `accent-foreground`, `primary`, `secondary`, `secondary-foreground`
|
|
62
|
+
|
|
63
|
+
## Accessibility
|
|
64
|
+
|
|
65
|
+
- Base UI provides role=combobox/listbox/option, roving tabindex, arrow-key navigation, and type-to-filter.
|
|
66
|
+
- Esc and outside-press dismiss; focus returns to the input on close.
|
|
67
|
+
- The chevron Trigger carries an aria-label and toggles the suggestions popup.
|
|
68
|
+
- ComboboxEmpty announces filter results politely; keep it mounted (do not conditionally render it).
|
|
69
|
+
- Animate open/close via data-[starting-style]/data-[ending-style] attributes.
|
|
70
|
+
- With `multiple`, the listbox is multi-selectable and each chip's remove button is labelled `Remove {label}`; Backspace in an empty field removes the last chip and ←/→ move between chips, so every chip is reachable without a pointer.
|
|
71
|
+
- ComboboxVirtualList gives every rendered row aria-setsize (the full filtered count) and aria-posinset, so a screen reader is told the size of the list rather than the size of the window.
|
|
72
|
+
- A highlight that lands on a row the window has not rendered is scrolled into view by the root, which forwards it to the list; Base UI's own scrollIntoView only reaches rows that exist.
|
|
73
|
+
|
|
74
|
+
## Related
|
|
75
|
+
|
|
76
|
+
- Components: Select, Checkbox, CommandMenu, Field, SearchInput, Badge
|
|
77
|
+
- Patterns: SettingsPage, ListDetailPage
|
|
78
|
+
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
### Basic
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "@sableui/react"
|
|
85
|
+
|
|
86
|
+
<Combobox items={fruits}><ComboboxInput placeholder="Search fruit…" /><ComboboxContent><ComboboxEmpty>No matches.</ComboboxEmpty><ComboboxList>{(item) => (<ComboboxItem key={item} value={item}>{item}</ComboboxItem>)}</ComboboxList></ComboboxContent></Combobox>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Controlled value
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxList } from "@sableui/react"
|
|
93
|
+
|
|
94
|
+
<Combobox items={fruits} value={value} onValueChange={setValue}><ComboboxInput /><ComboboxContent><ComboboxList>{(item) => (<ComboboxItem key={item} value={item}>{item}</ComboboxItem>)}</ComboboxList></ComboboxContent></Combobox>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Multiple selection
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { Combobox, ComboboxChips, ComboboxContent, ComboboxEmpty, ComboboxItem, ComboboxList } from "@sableui/react"
|
|
101
|
+
|
|
102
|
+
<Combobox multiple items={tags} value={value} onValueChange={setValue}><ComboboxChips placeholder="Add tags…" /><ComboboxContent><ComboboxEmpty>No matching tags.</ComboboxEmpty><ComboboxList>{(item) => (<ComboboxItem key={item} value={item}>{item}</ComboboxItem>)}</ComboboxList></ComboboxContent></Combobox>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Virtualized (long list)
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxVirtualList } from "@sableui/react"
|
|
109
|
+
|
|
110
|
+
<Combobox virtualized items={districts} value={district} onValueChange={setDistrict}><ComboboxInput placeholder="Search districts…" /><ComboboxContent scroll={false}><ComboboxEmpty>No district matches.</ComboboxEmpty><ComboboxVirtualList itemHeight={32}>{(item) => item.label}</ComboboxVirtualList></ComboboxContent></Combobox>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Inside Field
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, Field } from "@sableui/react"
|
|
117
|
+
|
|
118
|
+
<Field label="Creator" description="Search by display name."><Combobox items={creators} value={creator} onValueChange={setCreator}><ComboboxInput placeholder="Search creators" /><ComboboxContent><ComboboxEmpty>No creators found.</ComboboxEmpty><ComboboxList>{(item) => (<ComboboxItem key={item.id} value={item}>{item.name}</ComboboxItem>)}</ComboboxList></ComboboxContent></Combobox></Field>
|
|
119
|
+
```
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# CommandMenu
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A command palette (Cmd+K) built on our Dialog: a top-aligned wide surface with a search input and a scrollable, JS-filtered list of grouped commands. CommandMenuPanel renders the same palette in place, on a card in the page.
|
|
6
|
+
|
|
7
|
+
**Keywords:** command, command menu, palette, launcher, cmd k, search, quick action, spotlight, navigation, search trigger, header search, keyboard shortcut, inline command menu, embedded palette, settings search
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use CommandMenu for global or contextual keyboard-driven actions: navigation, creation, quick settings, or searchable commands. It should feel curated and fast; use Select, Menu, or Combobox for ordinary inline choices.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Open the palette from a header search with CommandMenuTrigger: a button wearing SearchInput's field shell, with the ⌘K / Ctrl K hint and the page-wide shortcut built in. A palette with no visible trigger still takes useCommandMenuShortcut rather than a hand-written keydown listener.
|
|
16
|
+
- Do: Group items by task area, such as Navigation, Create, or Settings, so users can scan before typing.
|
|
17
|
+
- Do: Keep command labels action-oriented and unique enough to search.
|
|
18
|
+
- Do: Provide useful bootstrap items so the menu is valuable before the user types.
|
|
19
|
+
- Do: Pass a filter when the palette searches more than one source. Ranking (exact prefix over substring over subsequence) is the caller's to own, and the returned order is kept verbatim — including group order, which follows each group's first surviving row.
|
|
20
|
+
- Do: List a command the user cannot currently run as disabled with the reason in hint, rather than filtering it away. A palette that hides what you cannot do teaches you nothing about why.
|
|
21
|
+
- Do: Resize the list with listClassName rather than reaching past className into the palette's DOM. className is the dialog surface; listClassName is the Scroller that actually scrolls.
|
|
22
|
+
- Do: Use CommandMenuPanel when the palette lives in the page: a sidebar launcher, a settings search, a demo. It takes the same items, filter, query and children, and keeps the same keyboard model; it is a static card, so it does not trap focus or take it on mount, and Esc clears the query instead of closing anything.
|
|
23
|
+
- Don't: Hand-style a <button> to look like a search field, or put a SearchInput in the header that opens a dialog when focused. The first drifts from the field it imitates; the second is a text box you cannot type in. A header search that opens the palette is CommandMenuTrigger.
|
|
24
|
+
- Don't: Use CommandMenu as a replacement for every dropdown. Simple choices belong in Select, Menu, or Tabs.
|
|
25
|
+
- Don't: Hand-build a palette mock from a search glyph, a row of buttons and a highlight. That is CommandMenuPanel, and the hand-built one has no combobox, no filtering and no keyboard model.
|
|
26
|
+
- Don't: Dump every app route and action into one uncurated list. Keep the palette opinionated.
|
|
27
|
+
- Don't: Rebuild the combobox over Dialog to get consumer-owned ranking. query/onQueryChange and filter are that seam; the keyboard model, activedescendant wiring, and result-count live region come with them.
|
|
28
|
+
|
|
29
|
+
### Anatomy
|
|
30
|
+
|
|
31
|
+
| Part | Required | Description |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| Trigger | no | CommandMenuTrigger — the search-field-shaped button in a header that opens the palette on click or on ⌘K / Ctrl+K. It sits outside CommandMenu; wire its onOpen to the palette's open state. |
|
|
34
|
+
| Dialog surface | yes | The modal shell that traps focus and positions the launcher. CommandMenuPanel replaces it with a static card in the page (border, card fill, no shadow). |
|
|
35
|
+
| Search input | yes | The combobox input that filters commands while focus remains in the field. |
|
|
36
|
+
| Groups | no | Labelled command sections that keep results scannable. |
|
|
37
|
+
| Items | yes | Keyboard-selectable command rows that run onSelect and close the palette. |
|
|
38
|
+
|
|
39
|
+
## Subcomponents
|
|
40
|
+
|
|
41
|
+
| Name | Type | Description |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| CommandMenuTrigger | { onOpen?: () => void; placeholder?: string; shortcut?: string \| false; size?: "sm" \| "md"; disabled?: boolean } | The search field in a header that is really a button. Wears SearchInput's shell (border-input, card surface, leading magnifier, muted placeholder) on the control ladder, with a trailing Kbd: ⌘K on macOS, Ctrl K elsewhere. onOpen runs on click and on the shortcut, which it listens for page-wide (useCommandMenuShortcut). shortcut is the key (default "k"); false drops both the hint and the listener. size is sm in a header beside small buttons, md (default) beside Input and SearchInput. It sits outside CommandMenu, so the Dialog returns focus to it on close. |
|
|
44
|
+
| useCommandMenuShortcut | (onShortcut: (event: KeyboardEvent) => void, key?: string \| false) => void | The page-wide ⌘K / Ctrl+K listener CommandMenuTrigger installs, for a palette with no visible trigger. Ignored while focus is in a text field, textarea, select or contenteditable (Ctrl+K belongs to the field there) except the palette's own input, so the callback may toggle. It calls preventDefault() on a match and skips an event already prevented, so two listeners on one page fire once. key defaults to "k"; false turns it off. Removed on unmount. |
|
|
45
|
+
| CommandMenuPanel | { items?: CommandItem[]; query?: string; onQueryChange?: (query: string) => void; filter?: CommandFilter; listClassName?: string; placeholder?: string; emptyMessage?: string; title?: string; scope?: ReactNode; inputProps?: ComponentProps<'input'>; className?: string; children?: ReactNode } | The palette rendered in place: the same search header, filtered and grouped listbox, keyboard model and result-count live region, on a static card (border-border, bg-card, no shadow) instead of in a Dialog. Takes every CommandMenu prop except open, onOpenChange, finalFocus and description; className lands on the card, title names the listbox. Focus is not trapped and the input does not take focus on mount (inputProps.autoFocus opts in). Esc clears the query, and passes through once it is empty, so a Popover around the panel still closes. A chosen row runs its onSelect and the panel stays as it was. The page-wide shortcut does not fire from its input. |
|
|
46
|
+
| CommandMenuGroup | { label: string; children } | A labelled group of command rows. |
|
|
47
|
+
| CommandMenuItem | { onSelect?: () => void; icon?: string; detail?: ReactNode; hint?: ReactNode; disabled?: boolean; id?: string; children } | A selectable row. Self-filters by its text (unless the root was given a filter), runs onSelect, then closes the palette. detail is secondary text after the label; hint is the end-aligned trailing slot — a shortcut, a count, or a disabled row's reason. A disabled row stays listed, stays announced, and stays reachable by arrow key so its reason can be read, but is inert to Enter and to a click. |
|
|
48
|
+
|
|
49
|
+
## Props
|
|
50
|
+
|
|
51
|
+
| Name | Type | Default | Description |
|
|
52
|
+
| --- | --- | --- | --- |
|
|
53
|
+
| scope | ReactNode | — | A scope chip in the header, between the search glyph and the input — "In: Backdrop". A scope belongs beside the field rather than in its value: put it in the value and the user deletes prose to widen their search, and your filter parses it back out. Pair with inputProps.onKeyDown handling Backspace on an empty query, or the only way out of a scope is the mouse. |
|
|
54
|
+
| inputProps | Omit<ComponentProps<'input'>, 'role' \| 'value' \| 'onChange' \| aria-*> | — | Extra props for the search input. `onKeyDown` runs BEFORE the palette's own handling and opts out of a key by calling preventDefault() — which is what makes a scope chip Backspace-dismissable. Running first is deliberate: Backspace happens not to be a key the palette claims today, so a plain passthrough would work until the palette grows a 'Backspace clears the query' affordance, at which point the consumer's behaviour breaks silently. The combobox wiring (role, aria relationships, controlled value) is not overridable; aria-label and className compose. |
|
|
55
|
+
| open | boolean | — | Controlled open state of the palette. |
|
|
56
|
+
| onOpenChange | (open: boolean) => void | — | Called on open/close (select, Esc, outside click). |
|
|
57
|
+
| items | CommandItem[] | — | Data-driven commands ({ group, label, onSelect, id?, icon?, detail?, hint?, disabled? }). Ignored when children are provided. id is optional identity: filter is handed these same objects, so it is how a caller that ranks over its own richer model maps the array back onto it — and it becomes the row's DOM id (namespaced to the palette) and React key, so two rows may share a label. |
|
|
58
|
+
| query | string | — | Controlled query. Leave undefined to let the palette own it; either way it resets on close (a controlled caller gets onQueryChange('')). |
|
|
59
|
+
| onQueryChange | (query: string) => void | — | Called on every query change. Required to control query, optional otherwise. |
|
|
60
|
+
| filter | ((items: CommandItem[], query: string) => CommandItem[]) \| null | case-insensitive substring match on label | Replaces or disables the built-in match. A function ranks and filters items itself and its order is kept verbatim; null renders items exactly as given. Either also stops declarative CommandMenuItem children self-filtering. The query arrives raw — trimming and case are policy, and this is the prop that hands policy over. |
|
|
61
|
+
| listClassName | string | — | Classes for the scrolling list. className lands on the dialog surface, so this is the seam for the list's own height cap — listClassName="max-h-[60vh]". The list is a Scroller, so the cap lands on the element that scrolls and the edge fades come free. |
|
|
62
|
+
| placeholder | string | "Type a command or search…" | Placeholder for the search input. |
|
|
63
|
+
| emptyMessage | string | "No results found." | Copy shown when the query matches nothing. |
|
|
64
|
+
| title | string | "Command menu" | Accessible name for the palette dialog (visually hidden). |
|
|
65
|
+
| description | string | "Search for a command and press Enter to run it." | Accessible description for the palette dialog (visually hidden). |
|
|
66
|
+
| finalFocus | RefObject<HTMLElement> \| ((closeType) => HTMLElement \| false) \| false | — | Where focus goes when the palette closes. The Dialog default returns it to the trigger — the CommandMenuTrigger, when there is one. A palette opened by the keystroke alone has none, so pass a resolver to restore whatever had focus, or false to restore nothing. |
|
|
67
|
+
| children | ReactNode | — | Declarative commands via CommandMenuGroup / CommandMenuItem. Takes precedence over items. |
|
|
68
|
+
|
|
69
|
+
## Tokens used
|
|
70
|
+
|
|
71
|
+
`card`, `card-foreground`, `border`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`
|
|
72
|
+
|
|
73
|
+
## Accessibility
|
|
74
|
+
|
|
75
|
+
- CommandMenuTrigger is a real <button> named by its visible placeholder, with aria-haspopup=dialog and aria-keyshortcuts (Meta+K on macOS, Control+K elsewhere). Its Kbd hint is aria-hidden, so the shortcut is announced once rather than read as part of the name.
|
|
76
|
+
- The shortcut never fires from inside a text field, textarea, select or contenteditable (Ctrl+K is delete-to-end-of-line in macOS text views), except the palette's own input.
|
|
77
|
+
- CommandMenuPanel is the same combobox and listbox in the page, with no dialog: it does not trap or steal focus, and Esc clears a non-empty query (stopping there) instead of dismissing. With an empty query Esc is left alone for whatever contains the panel.
|
|
78
|
+
- Built on Base UI Dialog, so focus trap, scroll lock, and Esc/outside dismissal come for free; the dialog carries a visually-hidden title + description so it is announced.
|
|
79
|
+
- Follows the ARIA combobox pattern: the search input is role=combobox (aria-autocomplete=list) that owns the role=listbox popup via aria-controls.
|
|
80
|
+
- ArrowDown/ArrowUp/Home/End and PageUp/PageDown move a virtual aria-activedescendant highlight while focus stays in the input; Enter runs the highlighted command. Arrow keys wrap; page jumps clamp.
|
|
81
|
+
- Groups expose role=group labelled by their heading; items are role=option and never enter the tab order.
|
|
82
|
+
- A polite role=status live region announces the result count as the query narrows, so the size of the list is available without reading it.
|
|
83
|
+
- A disabled row carries aria-disabled and is skipped when seeding the highlight, but stays in the arrow-key walk so its reason can be reached and read.
|
|
84
|
+
- The component keeps ownership of aria-activedescendant even when a caller claims a key through inputProps.onKeyDown. Skipping a move is coherent (they took responsibility for that key), but nothing may leave the active id naming a row that is not painted as active — so the opt-out returns before any highlight change rather than after it.
|
|
85
|
+
- Exactly one row is ever painted as active: the pointer takes the highlight over (moving onto a row makes it the aria-activedescendant) rather than adding a hover fill of its own, so what looks highlighted is always what Enter will run.
|
|
86
|
+
|
|
87
|
+
## Related
|
|
88
|
+
|
|
89
|
+
- Components: Dialog, SearchInput, Kbd, Menu, Select, Combobox
|
|
90
|
+
- Patterns: DashboardPage, ListDetailPage
|
|
91
|
+
|
|
92
|
+
## Examples
|
|
93
|
+
|
|
94
|
+
### Data-driven
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { CommandMenu } from "@sableui/react"
|
|
98
|
+
|
|
99
|
+
<CommandMenu open={open} onOpenChange={setOpen} items={[{ group: 'Navigation', label: 'Go to feed', icon: 'home', onSelect: () => {} }, { group: 'Actions', label: 'New post', onSelect: () => {} }]} />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Declarative
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { CommandMenu, CommandMenuGroup, CommandMenuItem } from "@sableui/react"
|
|
106
|
+
|
|
107
|
+
<CommandMenu open={open} onOpenChange={setOpen}><CommandMenuGroup label='Navigation'><CommandMenuItem icon='home' onSelect={() => {}}>Go to feed</CommandMenuItem></CommandMenuGroup></CommandMenu>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Grouped launcher
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { CommandMenu, CommandMenuGroup, CommandMenuItem } from "@sableui/react"
|
|
114
|
+
|
|
115
|
+
<CommandMenu open={open} onOpenChange={setOpen}><CommandMenuGroup label="Create"><CommandMenuItem icon="plus" onSelect={() => createPost()}>New post</CommandMenuItem><CommandMenuItem icon="sparkles" onSelect={() => createEdition()}>New edition</CommandMenuItem></CommandMenuGroup><CommandMenuGroup label="Navigation"><CommandMenuItem icon="home" onSelect={() => navigate("/")}>Home</CommandMenuItem><CommandMenuItem icon="gear" onSelect={() => navigate("/settings")}>Settings</CommandMenuItem></CommandMenuGroup></CommandMenu>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Consumer-owned ranking
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { CommandMenu } from "@sableui/react"
|
|
122
|
+
|
|
123
|
+
<CommandMenu open={open} onOpenChange={setOpen} query={query} onQueryChange={setQuery} filter={(items, q) => rank(items, q)} items={items} />
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Listed but unavailable
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { CommandMenu } from "@sableui/react"
|
|
130
|
+
|
|
131
|
+
<CommandMenu open={open} onOpenChange={setOpen} items={[{ group: 'Layers', label: 'Align left', disabled: true, hint: 'Select another layer to align to', onSelect: () => {} }]} />
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Inline panel
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { CommandMenuPanel, Kbd } from "@sableui/react"
|
|
138
|
+
|
|
139
|
+
<CommandMenuPanel className="w-80" items={[{ group: 'Create', label: 'New project', icon: 'plus', hint: <Kbd>N</Kbd>, onSelect: () => {} }, { group: 'Settings', label: 'Open settings', icon: 'gear', onSelect: () => {} }]} />
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Header search trigger
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import { CommandMenu, CommandMenuTrigger } from "@sableui/react"
|
|
146
|
+
|
|
147
|
+
<CommandMenuTrigger size="sm" placeholder="Search docs" onOpen={() => setOpen(true)} className="w-56" />
|
|
148
|
+
<CommandMenu open={open} onOpenChange={setOpen} items={items} />
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Keyboard shortcut with no visible trigger
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { CommandMenu, useCommandMenuShortcut } from "@sableui/react"
|
|
155
|
+
|
|
156
|
+
useCommandMenuShortcut(() => setOpen((value) => !value))
|
|
157
|
+
<CommandMenu open={open} onOpenChange={setOpen} items={items} />
|
|
158
|
+
```
|