@sableui/react 0.1.1 → 0.2.0

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/{_contract-CYScOdf5.d.ts → _contract-o3tUc8Ig.d.ts} +1 -1
  3. package/dist/{chunk-J23ELQQZ.js → chunk-AOWBYEQ6.js} +4177 -1917
  4. package/dist/{chunk-RWP4XBSK.js → chunk-KFZ43RKU.js} +1 -1
  5. package/dist/{chunk-JI7WJP2L.js → chunk-X276MF2W.js} +5 -0
  6. package/dist/foundations/index.js +1 -1
  7. package/dist/index.d.ts +155 -101
  8. package/dist/index.js +582 -2778
  9. package/dist/ios/index.d.ts +6 -1
  10. package/dist/ios/index.js +4 -4
  11. package/dist/ios/preview.js +1 -1
  12. package/dist/ios/styles.css +20 -6
  13. package/dist/patterns/index.js +1683 -469
  14. package/dist/presets/index.d.ts +1 -1
  15. package/dist/presets/index.js +1 -1
  16. package/dist/styles.css +238 -40
  17. package/dist/{theme-provider-BWcizhAt.d.ts → theme-provider-BE-KqUG-.d.ts} +1 -1
  18. package/dist/themes/atelier/atelier.css +3 -0
  19. package/dist/themes/atelier/index.d.ts +1 -1
  20. package/dist/themes/base/base.css +11 -4
  21. package/dist/themes/base/index.d.ts +1 -1
  22. package/dist/themes/ember/ember.css +3 -0
  23. package/dist/themes/ember/index.d.ts +1 -1
  24. package/dist/themes/index.d.ts +2 -2
  25. package/dist/themes/meridian/index.d.ts +1 -1
  26. package/dist/themes/meridian/meridian.css +10 -5
  27. package/dist/themes/poppy/index.d.ts +1 -1
  28. package/dist/themes/poppy/poppy.css +16 -0
  29. package/dist/themes/prism/index.d.ts +1 -1
  30. package/dist/themes/prism/prism.css +4 -1
  31. package/dist/themes/verdant/index.d.ts +1 -1
  32. package/dist/themes/verdant/verdant.css +7 -2
  33. package/llms/components/bubble.md +5 -5
  34. package/llms/components/checkbox-card.md +1 -1
  35. package/llms/components/context-card.md +2 -2
  36. package/llms/components/dialog.md +1 -1
  37. package/llms/components/empty-state.md +4 -4
  38. package/llms/components/entity.md +1 -1
  39. package/llms/components/fieldset.md +1 -1
  40. package/llms/components/kbd.md +18 -6
  41. package/llms/components/pill.md +1 -1
  42. package/llms/components/side-nav.md +1 -1
  43. package/llms/components/skeleton.md +1 -1
  44. package/llms/components/theme-selector.md +1 -1
  45. package/llms/foundations/shape.md +1 -1
  46. package/llms/patterns/auth-page.md +132 -42
  47. package/llms/patterns/brand-landing-page.md +271 -175
  48. package/llms/patterns/chat-panel.md +120 -39
  49. package/llms/patterns/dashboard-page.md +277 -65
  50. package/llms/patterns/feed-page.md +245 -64
  51. package/llms/patterns/landing-page.md +473 -75
  52. package/llms/patterns/list-detail-page.md +385 -68
  53. package/llms/patterns/settings-page.md +279 -67
  54. package/llms/reference/token-intent.md +4 -2
  55. package/llms/reference/utilities.md +20 -5
  56. package/llms-full.txt +2244 -727
  57. package/llms.txt +14 -15
  58. package/package.json +1 -1
  59. package/registry.json +261 -238
  60. package/targets/ios.json +4 -15
  61. package/llms/components/description.md +0 -96
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-CYScOdf5.js';
1
+ import { T as ThemeManifest } from '../../_contract-o3tUc8Ig.js';
2
2
 
3
3
  /**
4
4
  * Meridian — a Sable theme. Concrete token values live in
@@ -117,15 +117,19 @@
117
117
  ---------------------------------------------------------------------- */
118
118
  --background: oklch(94.6% 0.0083 91.48);
119
119
  --foreground: oklch(26.25% 0.0162 102.18);
120
- --card: oklch(94.6% 0.0083 91.48);
120
+ /* A step above the paper page, as in every preset with a tinted page: a card
121
+ level with it left fields reading as part of the page, not a box to type in. */
122
+ --card: oklch(97.6% 0.006 91.48);
121
123
  --card-foreground: oklch(26.25% 0.0162 102.18);
122
- --popover: oklch(94.6% 0.0083 91.48);
124
+ --popover: oklch(97.6% 0.006 91.48);
123
125
  --popover-foreground: oklch(26.25% 0.0162 102.18);
124
126
  --muted: oklch(91.24% 0.0138 92.99);
125
127
  --muted-foreground: oklch(49.93% 0.0412 93.54);
126
128
  --border: oklch(87.54% 0.0194 93.69);
127
129
  --border-strong: oklch(81.94% 0.0252 94.12);
128
130
  --input: oklch(87.54% 0.0194 93.69);
131
+ /* The resting edge of a control whose outline IS the control (checkbox, radio, an off switch track). Held to WCAG 1.4.11 3:1 on every surface, unlike the quieter --input field outline. */
132
+ --control-border: oklch(58.9% 0.0346 93.54);
129
133
  --ring: oklch(62% 0.2063 32.56);
130
134
  --primary: oklch(26.25% 0.0162 102.18);
131
135
  --primary-foreground: oklch(94.6% 0.0083 91.48);
@@ -167,7 +171,7 @@
167
171
  --sidebar-primary-foreground: oklch(22.46% 0.0094 107.13);
168
172
  --sidebar-accent: oklch(27.1% 0.0143 101.41);
169
173
  --sidebar-accent-foreground: oklch(94.3% 0.0042 91.45);
170
- --sidebar-border: oklch(27.1% 0.0143 101.41);
174
+ --sidebar-border: oklch(34.69% 0.0186 98.75);
171
175
  --sidebar-ring: oklch(65.11% 0.2063 32.56);
172
176
 
173
177
  /* ----------------------------------------------------------------------
@@ -216,8 +220,9 @@
216
220
  * only the edge separates. Caught by the non-text contrast gate.
217
221
  */
218
222
  --border: oklch(34.69% 0.0186 98.75);
219
- --border-strong: oklch(34.69% 0.0186 98.75);
223
+ --border-strong: oklch(44% 0.0225 96); /* one visible step above --border, which was lifted to the old strong value */
220
224
  --input: oklch(34.69% 0.0186 98.75);
225
+ --control-border: oklch(60% 0.002 0);
221
226
  --ring: oklch(65.11% 0.2063 32.56);
222
227
  --primary: oklch(94.3% 0.0042 91.45);
223
228
  --primary-foreground: oklch(22.46% 0.0094 107.13);
@@ -250,6 +255,6 @@
250
255
  --sidebar-primary-foreground: oklch(22.46% 0.0094 107.13);
251
256
  --sidebar-accent: oklch(27.1% 0.0143 101.41);
252
257
  --sidebar-accent-foreground: oklch(94.3% 0.0042 91.45);
253
- --sidebar-border: oklch(27.1% 0.0143 101.41);
258
+ --sidebar-border: oklch(34.69% 0.0186 98.75);
254
259
  --sidebar-ring: oklch(65.11% 0.2063 32.56);
255
260
  }
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-CYScOdf5.js';
1
+ import { T as ThemeManifest } from '../../_contract-o3tUc8Ig.js';
2
2
 
3
3
  /** Original Sable preset; no third-party artwork or reference-site code. */
4
4
  declare const poppyTheme: ThemeManifest;
@@ -69,6 +69,8 @@
69
69
  --border: oklch(69.96908% 0.055090 333.3586);
70
70
  --border-strong: oklch(54.87925% 0.056525 333.9456);
71
71
  --input: oklch(69.96908% 0.055090 333.3586);
72
+ /* The resting edge of a control whose outline IS the control (checkbox, radio, an off switch track). Held to WCAG 1.4.11 3:1 on every surface, unlike the quieter --input field outline. */
73
+ --control-border: oklch(60% 0.0598 321.39);
72
74
  --ring: oklch(40.99289% 0.130336 325.3125);
73
75
  --primary: oklch(38.38082% 0.116390 323.6155);
74
76
  --primary-foreground: oklch(100.00000% 0.000000 89.8756);
@@ -119,14 +121,27 @@
119
121
  --spacing: 0.28rem;
120
122
  --theme-button-radius: 12px;
121
123
  --button-radius: var(--theme-button-radius);
124
+ /* Hard offsets the whole way up. Only sm and md were declared, so the
125
+ default, lg and xl steps fell through to Base's soft blurs and one ladder
126
+ spoke two shadow languages. Offset doubles roughly per step. */
122
127
  --theme-shadow-sm: 3px 3px 0 oklch(32% 0.05 320 / 0.12);
128
+ --theme-shadow: 4px 4px 0 oklch(32% 0.05 320 / 0.13);
123
129
  --theme-shadow-md: 5px 5px 0 oklch(32% 0.05 320 / 0.14);
130
+ --theme-shadow-lg: 8px 8px 0 oklch(32% 0.05 320 / 0.16);
131
+ --theme-shadow-xl: 12px 12px 0 oklch(32% 0.05 320 / 0.18);
124
132
  }
125
133
 
126
134
  .dark[data-sable-theme="poppy"],
127
135
  [data-sable-theme="poppy"].dark,
128
136
  [data-sable-theme="poppy"] .dark,
129
137
  .dark [data-sable-theme="poppy"]:not(.light) {
138
+ /* The light ladder's ink is 32% lightness — LIGHTER than this 20% page, so
139
+ in dark mode it drew a faint glow, not a shadow. Same offsets, near-black. */
140
+ --theme-shadow-sm: 3px 3px 0 oklch(8% 0.03 320 / 0.5);
141
+ --theme-shadow: 4px 4px 0 oklch(8% 0.03 320 / 0.52);
142
+ --theme-shadow-md: 5px 5px 0 oklch(8% 0.03 320 / 0.55);
143
+ --theme-shadow-lg: 8px 8px 0 oklch(8% 0.03 320 / 0.6);
144
+ --theme-shadow-xl: 12px 12px 0 oklch(8% 0.03 320 / 0.65);
130
145
  --background: oklch(20.33265% 0.047438 308.8247);
131
146
  --foreground: oklch(96.99788% 0.031195 84.5922);
132
147
  --card: oklch(28.82474% 0.059607 315.2029);
@@ -138,6 +153,7 @@
138
153
  --border: oklch(62.48980% 0.070560 325.5308);
139
154
  --border-strong: oklch(72.17143% 0.060424 328.9840);
140
155
  --input: oklch(62.48980% 0.070560 325.5308);
156
+ --control-border: oklch(67.3% 0.0412 332.63);
141
157
  --ring: oklch(86.64637% 0.118528 86.6791);
142
158
  --primary: oklch(86.64637% 0.118528 86.6791);
143
159
  --primary-foreground: oklch(27.91553% 0.070219 323.8141);
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-CYScOdf5.js';
1
+ import { T as ThemeManifest } from '../../_contract-o3tUc8Ig.js';
2
2
 
3
3
  /** Original Sable preset; no third-party artwork or reference-site code. */
4
4
  declare const prismTheme: ThemeManifest;
@@ -12,7 +12,7 @@
12
12
  --region-ultrawide: 1800px;
13
13
  --theme-radius-none: 0px;
14
14
  --theme-radius-xs: 2px;
15
- --theme-radius-sm: 4px;
15
+ --theme-radius-sm: 3px; /* a step of its own — sm and md were both 4px, which erased a rung of the ladder */
16
16
  --theme-radius-md: 4px;
17
17
  --theme-radius-lg: 6px;
18
18
  --theme-radius-xl: 8px;
@@ -69,6 +69,8 @@
69
69
  --border: oklch(79.84882% 0.029246 251.5080);
70
70
  --border-strong: oklch(64.27577% 0.042824 253.1390);
71
71
  --input: oklch(79.84882% 0.029246 251.5080);
72
+ /* The resting edge of a control whose outline IS the control (checkbox, radio, an off switch track). Held to WCAG 1.4.11 3:1 on every surface, unlike the quieter --input field outline. */
73
+ --control-border: oklch(60% 0.0298 252.77);
72
74
  --ring: oklch(50.45463% 0.203215 263.5596);
73
75
  --primary: oklch(50.45463% 0.203215 263.5596);
74
76
  --primary-foreground: oklch(100.00000% 0.000000 89.8756);
@@ -147,6 +149,7 @@
147
149
  --border: oklch(58.06940% 0.052170 251.5856);
148
150
  --border-strong: oklch(69.19219% 0.042050 253.1082);
149
151
  --input: oklch(58.06940% 0.052170 251.5856);
152
+ --control-border: oklch(67.7% 0.0275 255.1);
150
153
  --ring: oklch(82.28684% 0.088371 261.1846);
151
154
  --primary: oklch(82.28684% 0.088371 261.1846);
152
155
  --primary-foreground: oklch(25.20350% 0.047984 259.9119);
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-CYScOdf5.js';
1
+ import { T as ThemeManifest } from '../../_contract-o3tUc8Ig.js';
2
2
 
3
3
  /**
4
4
  * Verdant — a Sable theme. Concrete token values live in
@@ -90,15 +90,19 @@
90
90
  ---------------------------------------------------------------------- */
91
91
  --background: oklch(98.6% 0.004 106);
92
92
  --foreground: oklch(24% 0.035 175);
93
- --card: oklch(96.1% 0.005 106);
93
+ /* Cards, fields and popovers sit at or above the page, never recessed into it:
94
+ a card darker than the page made every field read as a filled grey box. */
95
+ --card: oklch(99.7% 0.002 106);
94
96
  --card-foreground: oklch(24% 0.035 175);
95
- --popover: oklch(98.6% 0.004 106);
97
+ --popover: oklch(99.7% 0.002 106);
96
98
  --popover-foreground: oklch(24% 0.035 175);
97
99
  --muted: oklch(93% 0.007 106);
98
100
  --muted-foreground: oklch(49% 0.018 175);
99
101
  --border: oklch(88.5% 0.005 106);
100
102
  --border-strong: oklch(81% 0.008 130);
101
103
  --input: oklch(88.5% 0.005 106);
104
+ /* The resting edge of a control whose outline IS the control (checkbox, radio, an off switch track). Held to WCAG 1.4.11 3:1 on every surface, unlike the quieter --input field outline. */
105
+ --control-border: oklch(59.9% 0.0149 175);
102
106
  --ring: oklch(24% 0.035 175);
103
107
  --primary: oklch(24% 0.035 175);
104
108
  --primary-foreground: oklch(98.6% 0.004 106);
@@ -174,6 +178,7 @@
174
178
  --border: oklch(36% 0.03 175);
175
179
  --border-strong: oklch(40% 0.028 175);
176
180
  --input: oklch(36% 0.03 175);
181
+ --control-border: oklch(58.7% 0.0158 172);
177
182
  --ring: oklch(85% 0.015 170);
178
183
  --primary: oklch(98.6% 0.004 106);
179
184
  --primary-foreground: oklch(24% 0.035 175);
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- The surface a message's content sits on: four treatments, an align-aware tail corner, a readable measure cap, and an optional row of reaction pills.
5
+ The surface a message's content sits on: four treatments, an align-aware tail, a readable measure cap, and an optional row of reaction pills.
6
6
 
7
7
  **Keywords:** chat bubble, message bubble, speech bubble, reactions, conversation, messaging, ai chat
8
8
 
@@ -14,7 +14,7 @@ The tinted (or untinted) surface a turn's content sits on. Kept separate from Me
14
14
 
15
15
  - Do: Use `muted` for the other party and `primary` for the reader's own messages. Two treatments is the whole vocabulary a conversation needs to say who is speaking.
16
16
  - Do: Reach for `variant="plain"` for long-form assistant output — rendered markdown, code blocks, tables. A tinted box around three paragraphs reads as a quote block rather than as a turn, and `plain` drops the measure cap with the surface.
17
- - Do: Pass the same `align` you gave the enclosing Message, so the flattened corner lands on the speaker's side.
17
+ - Do: Pass the same `align` you gave the enclosing Message, so the tail lands on the speaker's side.
18
18
  - Do: Set `tail={false}` on the continuation bubbles in a grouped run — the tail marks where a speaker starts, and repeating it on every line makes a block of one person's messages look like four separate turns.
19
19
  - Do: Let the register size it. Inside `<Density value="compact">` a Bubble sets itself in the workspace prose tier automatically — a docked agent panel needs no per-call-site sizing, and hardcoding one defeats the mechanism.
20
20
  - Don't: Do not remove the measure cap to fill a wide window. ~42rem is a reading measure; a bubble spanning a 1600px viewport is a paragraph at 140 characters per line.
@@ -26,7 +26,7 @@ The tinted (or untinted) surface a turn's content sits on. Kept separate from Me
26
26
 
27
27
  | Part | Required | Description |
28
28
  | --- | --- | --- |
29
- | Bubble | yes | The surface itself. Holds the variant, the side, and the flattened tail corner. |
29
+ | Bubble | yes | The surface itself. Holds the variant, the side, and the tail. |
30
30
  | BubbleReactions | no | The row of reaction pills, just under the bubble at the message column's small gap, so it reads as belonging to that message rather than the next row. |
31
31
  | BubbleReaction | no | One reaction pill — emoji, count (hidden at 1), and pressed state. |
32
32
 
@@ -42,8 +42,8 @@ The tinted (or untinted) surface a turn's content sits on. Kept separate from Me
42
42
  | Name | Type | Default | Description |
43
43
  | --- | --- | --- | --- |
44
44
  | variant | "muted" \| "primary" \| "outline" \| "plain" | "muted" | Surface treatment. `muted` for the other party, `primary` for the reader's own messages, `outline` for a quieter own-side, `plain` for long-form output that should not sit in a box (also drops the measure cap and padding). |
45
- | align | "start" \| "end" | "start" | Which side of the conversation the bubble is on — flattens the corner nearest the speaker. Match it to the enclosing Message. |
46
- | tail | boolean | true | Flatten the speaker-side corner. A flattened corner rather than a drawn arrow, so it holds up in every theme's radius language (flat through fully pilled) with no per-theme geometry. Turn it off for the continuation bubbles in a grouped run. |
45
+ | align | "start" \| "end" | "start" | Which side of the conversation the bubble is on — puts the tail on the speaker's side. Match it to the enclosing Message. |
46
+ | tail | boolean | true | Draw the tail: a small ear off the speaker-side top corner, which goes square to meet it. The same 5px in every theme's radius language, flat through fully pilled. The bubble keeps the ear's space either way, so turning it off for the continuation bubbles in a grouped run keeps the run aligned. |
47
47
  | ...div props | ComponentPropsWithRef<"div"> | — | Spread onto the surface element. |
48
48
 
49
49
  ## Variants
@@ -39,7 +39,7 @@ Use CheckboxCard when an option needs more visual weight, supporting text, or ic
39
39
  | onCheckedChange | (checked: boolean, eventDetails) => void | — | Fires with the next checked state (Base UI passes eventDetails second). Pair with `checked` for a controlled card; omit both to let it manage its own state. |
40
40
  | title | ReactNode | — | The card's primary label (text-title-sm). |
41
41
  | description | ReactNode | — | Optional supporting copy beneath the title (text-body-sm, muted). |
42
- | icon | ReactNode | — | Optional leading glyph (a solid <Icon>); takes the tone colour. |
42
+ | icon | ReactNode | — | Optional glyph (a solid <Icon>) in the tone colour. Its presence switches to the two-dimensional card RadioCard uses — icon top-left, checkbox top-right, copy below — so the two cards match in one form. |
43
43
  | tone | "neutral" \| "standard" \| "collectible" \| "edition" \| "info" \| "warning" | "neutral" | Semantic colour the checked state carries across the border, checkbox box, and icon. |
44
44
  | value | string | — | Names the card inside a group-owned CheckboxGroup: the card is checked when the group's value array includes it. Also the value submitted with a form. Inside a CheckboxGroup, `name` stands in for `value` when `value` is omitted. |
45
45
  | checked / onCheckedChange / defaultChecked / disabled | Base UI Checkbox.Root props | — | Standard checkbox state. Without a group-owned value, each card is independent — drive checked/onCheckedChange per item for multi-select. |
@@ -67,7 +67,7 @@ import { Avatar, ContextCard, ContextCardContent, ContextCardTrigger, Entity } f
67
67
  ### With stat row
68
68
 
69
69
  ```tsx
70
- import { Avatar, ContextCard, ContextCardContent, ContextCardTrigger, Description, DescriptionItem, Entity } from "@sableui/react"
70
+ import { Avatar, ContextCard, ContextCardContent, ContextCardTrigger, Entity } from "@sableui/react"
71
71
 
72
- <ContextCard><ContextCardTrigger render={<a href='/u/nyx' className='text-title-sm underline-offset-2 hover:underline'>@nyx</a>} /><ContextCardContent><Entity className='[--entity-px:0] [--entity-py:0]' leading={<Avatar fallback='NY' size='lg' />} title='Nyx' subtitle='@nyx' /><Description cols='2' className='mt-4 border-t border-border pt-3'><DescriptionItem term='Posts' detail='128' /><DescriptionItem term='Followers' detail='4.2k' /></Description></ContextCardContent></ContextCard>
72
+ <ContextCard><ContextCardTrigger render={<a href='/u/nyx' className='text-title-sm underline-offset-2 hover:underline'>@nyx</a>} /><ContextCardContent><Entity className='[--entity-px:0] [--entity-py:0]' leading={<Avatar fallback='NY' size='lg' />} title='Nyx' subtitle='@nyx' /><div className='@container mt-4 border-t border-border pt-3'><dl className='sable-list-dl sable-list-dl-2'><dt>Posts</dt><dd>128</dd><dt>Followers</dt><dd>4.2k</dd></dl></div></ContextCardContent></ContextCard>
73
73
  ```
@@ -38,7 +38,7 @@ Use Dialog for focused decisions, short modal forms, confirmations, and content
38
38
  | Name | Type | Description |
39
39
  | --- | --- | --- |
40
40
  | DialogContent | Base UI Popup props + size?: "default" \| "fullscreen" + showCloseButton?: boolean + scroll?: boolean + className | Backdrop + popup; portals to <body>. Composes DialogOverlay by default. size="fullscreen" is the media viewer: a near-opaque scrim (bg-overlay/95), an edge-to-edge popup that centres its children with no card chrome, motion-fade instead of motion-pop, and a safe-area-aware floating close button (Button variant="floating"); at that size showCloseButton defaults to true and scroll to false, and the media fits itself with max-h-full max-w-full object-contain. At the default size, showCloseButton (defaults to false) renders a built-in close (X) button in the top-end corner, and the header leaves room for it (Icon name="xmark"). The popup caps at the viewport height less a 1rem margin and wraps its children in a scrolling region, so a tall dialog scrolls instead of overflowing a viewport whose own scroll is locked; pass scroll={false} when the dialog owns its internal layout (a pinned header over a scrolling body) and supplies its own min-h-0 scroll region. |
41
- | DialogOverlay | Base UI Backdrop props + size?: "default" \| "fullscreen" + className | The styleable scrim/backdrop — bg-overlay/50 by default, near-opaque bg-overlay/95 at size="fullscreen". Composed into DialogContent (at the matching size) by default; exported for manual styling. |
41
+ | DialogOverlay | Base UI Backdrop props + size?: "default" \| "fullscreen" + className | The styleable scrim/backdrop — bg-overlay/(--scrim-alpha) by default (50% light, 72% dark, so it dims a near-black page as visibly as a white one), near-opaque bg-overlay/95 at size="fullscreen". Composed into DialogContent (at the matching size) by default; exported for manual styling. |
42
42
  | DialogTrigger | Base UI Dialog.Trigger props (render) | Element that opens the dialog. Compose via render prop. |
43
43
  | DialogClose | Base UI Dialog.Close props (render) | Element that dismisses the dialog. Compose via render prop. |
44
44
  | DialogHeader | ComponentProps<"div"> | Top section holding DialogTitle/DialogDescription. |
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- The centered empty / error block: optional icon disc or error code, title, description and an action. tone="destructive" makes it an announced error, size="lg" a page-level state, surface="card" a self-contained card.
5
+ The centered empty / error block: optional icon or error code, title, description and an action. tone="destructive" makes it an announced error, size="lg" a page-level state, surface="card" a self-contained card.
6
6
 
7
7
  **Keywords:** empty, zero state, blank slate, no results, placeholder, onboarding, no data
8
8
 
@@ -33,12 +33,12 @@ Use EmptyState when a content area has no items, no search results, or no first-
33
33
 
34
34
  | Name | Type | Default | Description |
35
35
  | --- | --- | --- | --- |
36
- | icon | ReactNode | — | Optional leading glyph — pass an `<Icon />`. Rendered in a soft muted disc. |
36
+ | icon | ReactNode | — | Optional glyph above the title — an `<Icon />` takes the muted ink at 3xl; an image or illustration renders as given. |
37
37
  | title | ReactNode | — | The headline. Short and direct. |
38
38
  | description | ReactNode | — | Optional supporting line beneath the title. |
39
39
  | action | ReactNode | — | Optional action below the copy — typically a `<Button />`. |
40
40
  | code | ReactNode | — | A large figure above the title, such as an error code (404). |
41
- | tone | "neutral" \| "destructive" | "neutral" | destructive is an error state: a destructive icon disc (a warning by default) and role="alert" so it is announced. |
41
+ | tone | "neutral" \| "destructive" | "neutral" | destructive is an error state: a destructive icon (a warning by default) and role="alert" so it is announced. |
42
42
  | size | "md" \| "lg" | "md" | lg is page-level: the title is an h2 and the block takes more room. md sits in a panel, list or card. |
43
43
  | surface | "none" \| "card" | "none" | card draws it as a Card, for an error or empty block standing on its own in a page. |
44
44
 
@@ -54,7 +54,7 @@ Use EmptyState when a content area has no items, no search results, or no first-
54
54
 
55
55
  ## Accessibility
56
56
 
57
- - Decorative icon discs should not carry meaning the title doesn't already convey.
57
+ - Decorative icons should not carry meaning the title doesn't already convey.
58
58
  - Pair with an actionable next step so the zero-state is a path forward, not a dead end.
59
59
 
60
60
  ## Related
@@ -46,7 +46,7 @@ Use Entity to show a compact identity block for a person, organization, file, pr
46
46
 
47
47
  ## Related
48
48
 
49
- - Components: Avatar, Badge, Description
49
+ - Components: Avatar, Badge
50
50
 
51
51
  ## Examples
52
52
 
@@ -56,7 +56,7 @@ Use Fieldset to group related controls under a shared label or explanation.
56
56
 
57
57
  ## Related
58
58
 
59
- - Components: Form, Label, Description
59
+ - Components: Form, Label
60
60
 
61
61
  ## Examples
62
62
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- A keyboard key indicator. Renders a styled <kbd> key cap in monospace; compose several with a + between to show a shortcut combo. `mod` renders the platform modifier (⌘ or Ctrl).
5
+ A keyboard key indicator. Renders a raised <kbd> key cap sized from the surrounding text, so one Kbd fits body copy, menus and tooltips; KbdGroup sets one cap per key for a combo. `mod` renders the platform modifier (⌘ or Ctrl); `variant="ghost"` is the bare label for menu rows and tooltips.
6
6
 
7
7
  **Keywords:** keyboard, shortcut, key, kbd
8
8
 
@@ -13,8 +13,10 @@ Use Kbd to display keyboard keys or shortcuts in help text, menus, and docs.
13
13
  ### Best practices
14
14
 
15
15
  - Do: Use it only for literal keyboard input such as Cmd, Enter, or Esc.
16
- - Do: Keep multi-key shortcuts readable by separating individual keys.
16
+ - Do: Show a combo as one cap per key inside KbdGroup — `<KbdGroup><Kbd mod /><Kbd>K</Kbd></KbdGroup>` — not several keys typed into one cap.
17
+ - Do: Use `variant="ghost"` where the shortcut sits on a surface of its own — a menu row's trailing slot, a tooltip — so it reads as a hint, not a second control.
17
18
  - Don't: Do not use Kbd as a decorative badge or button replacement.
19
+ - Don't: Do not set a text size on a Kbd. It sizes itself from the text around it.
18
20
  - Don't: Do not hardcode ⌘ or Ctrl. The modifier differs by platform; use `<Kbd mod />`, which renders ⌘ on macOS/iOS and Ctrl elsewhere, or Windows users read Mac keys.
19
21
 
20
22
  ## Props
@@ -22,17 +24,19 @@ Use Kbd to display keyboard keys or shortcuts in help text, menus, and docs.
22
24
  | Name | Type | Default | Description |
23
25
  | --- | --- | --- | --- |
24
26
  | mod | boolean | false | Render the platform's shortcut modifier in place of children: ⌘ on macOS and iOS, Ctrl everywhere else. Server-rendered as Ctrl and corrected on hydration. |
27
+ | variant | "key" \| "ghost" | "key" | `key`: a raised cap on the surface it sits on, with a heavier bottom edge as the key's lip. `ghost`: the bare label, for menu rows, tooltips and other surfaces that are already a frame. |
25
28
  | className | string | — | Merge additional classes onto the key cap. |
26
29
  | ...props | ComponentPropsWithRef<"kbd"> | — | All native <kbd> attributes (children, ref, etc.). |
27
30
 
28
31
  ## Tokens used
29
32
 
30
- `border`, `muted`, `muted-foreground`
33
+ `border`, `border-strong`, `background`, `muted-foreground`
31
34
 
32
35
  ## Accessibility
33
36
 
34
37
  - Uses the semantic <kbd> element so assistive tech announces keyboard input.
35
- - Monospace key text keeps single keys and symbols legible at small sizes.
38
+ - KbdGroup is itself a <kbd> around the per-key caps — the HTML spelling of one key combination — so the combo is announced as a single piece of input.
39
+ - Sized in em from the surrounding text, so it scales with the user's text size and never shrinks below 12px.
36
40
 
37
41
  ## Related
38
42
 
@@ -51,7 +55,15 @@ import { Kbd } from "@sableui/react"
51
55
  ### Combo
52
56
 
53
57
  ```tsx
54
- import { Kbd, Row } from "@sableui/react"
58
+ import { Kbd, KbdGroup } from "@sableui/react"
55
59
 
56
- <Row gap={1}><Kbd mod /><span>+</span><Kbd>K</Kbd></Row>
60
+ <KbdGroup><Kbd mod /><Kbd>K</Kbd></KbdGroup>
61
+ ```
62
+
63
+ ### In a menu row
64
+
65
+ ```tsx
66
+ import { Kbd, MenuItem } from "@sableui/react"
67
+
68
+ <MenuItem>Search<KbdGroup className="ms-auto"><Kbd variant="ghost" mod /><Kbd variant="ghost">K</Kbd></KbdGroup></MenuItem>
57
69
  ```
@@ -24,7 +24,7 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
24
24
  | --- | --- | --- | --- |
25
25
  | selected | boolean | false | Marks the pill as chosen. Solid lifts onto the primary surface; outline takes a primary tint and soft primary edge. Sets aria-pressed. |
26
26
  | appearance | "solid" \| "outline" | "solid" | solid for a filter row; outline for quiet chips that annotate other content (a reaction under a message, a tag on a card), where a filled chip would compete with what it annotates. |
27
- | size | "md" \| "sm" | "md" | sm sits on the density ladder's xs control height, for chips under or inside other content. |
27
+ | size | "md" \| "sm" | "md" | md sits on the density ladder's xs control height (28px); sm is 24px, for chips under or inside other content. |
28
28
  | icon | ReactNode | — | Optional leading glyph (e.g. <Icon name="lock" />), rendered before the children with the base gap. |
29
29
 
30
30
  ## Variants
@@ -15,7 +15,7 @@ Use SideNav for vertical navigation inside a settings page, documentation area,
15
15
  - Do: Use aria-label on SideNav when a page contains more than one nav landmark.
16
16
  - Do: Group related items with SideNavGroup when the rail has more than one conceptual area.
17
17
  - Do: Use size='lg' and activeVariant='sidebar' when the nav is part of persistent app chrome.
18
- - Do: Inside a rounded, padded frame, give each item rounded-concentric-<outer>/<inset> (e.g. rounded-concentric-lg/2 in a rounded-lg p-2 box). The rows' own radius is fixed per size, and a same-radius row inset into a frame reads as a second system.
18
+ - Do: Rows are rounded-sm, which nests in the usual rounded-lg p-2 frame. In any other rounded, padded frame give each item rounded-concentric-<outer>/<inset> — a same-radius row inset into a frame reads as a second system.
19
19
  - Don't: Use SideNav for bottom mobile navigation; use BottomNav in AppShell's bottomNav slot.
20
20
  - Don't: Mix content and sidebar active variants in the same rail unless individual items intentionally override the group.
21
21
 
@@ -32,7 +32,7 @@ Use Skeleton to reserve space and hint at the shape of content while data is loa
32
32
  - **arrival** · Root · _no motion_ — A skeleton stands in for content that has not arrived; fading the placeholder in adds a wait before the wait.
33
33
  - **response** · Root · _no motion_ — Nothing here is interactive. A skeleton that reacted to the pointer would invite a click on a thing that cannot be clicked.
34
34
  - **transition** · Root · _no motion_ — The skeleton has one state. Swapping it for real content is the parent's job, and the parent decides whether that swap is animated.
35
- - **activity** · Root · `motion-shimmer` — The sweep is the entire point of the component: it is what separates 'still loading' from 'loaded, and empty'. A static grey block is indistinguishable from a broken layout, which is why users reload the page.
35
+ - **activity** · Root · `motion-shimmer` — The sweep is the entire point of the component: it is what separates 'still loading' from 'loaded, and empty'. A static grey block is indistinguishable from a broken layout, which is why users reload the page. It is one band of light against the viewport, so every skeleton in a loading layout lights in sequence as a single wave rather than each block flashing at its own speed; each pass eases in, glides and settles, then rests off-screen before the next.
36
36
 
37
37
  ## Accessibility
38
38
 
@@ -12,7 +12,7 @@ Use ThemeSelector wherever a person picks a theme. The default `cards` layout is
12
12
 
13
13
  ### Best practices
14
14
 
15
- - Do: Use layout="list" in a popover, a menu, or a docked panel. Rows sit on the --control-sm ladder and still show each theme's swatch strip, so the choice stays visible rather than described. Rows take `sable-frame-inner`, so host the list in a sable-frame (a Popover, or `sable-frame sable-surface sable-surface-popover sable-frame-popover`) and their corners nest concentrically.
15
+ - Do: Use layout="list" in a popover, a menu, or a docked panel. Rows sit on the --control-sm ladder and still show each theme's swatch strip, so the choice stays visible rather than described. Rows take `sable-frame-inner`, so host the list in a sable-frame (`sable-frame sable-surface sable-surface-popover sable-frame-popover` — PopoverContent is not one; give it that class list, or its p-4 leaves the rows unnested) and their corners nest concentrically.
16
16
  - Do: Pass mode={useSableTheme().mode} so every preview renders in the colour mode actually in use, rather than in each theme's manifest default.
17
17
  - Do: Load each theme's fonts in the host app. Every card carries a type specimen in that theme's own face so people can compare typography as well as color. A theme whose fonts are not loaded falls back to system-ui, and its specimen stops telling the truth.
18
18
  - Don't: Add a viewport breakpoint to force a column count — `className="sm:grid-cols-1"` or similar. `sm:` keys off the VIEWPORT, not this component's container, so on a wide screen it does nothing for a narrow parent. The cards grid already reflows on its own width via auto-fit.
@@ -30,7 +30,7 @@ Each step is 4px apart, so common insets land on a real step. Themes and the cus
30
30
  ## Rules
31
31
 
32
32
  1. **Nested corners are concentric: inner radius = outer radius − inset. Write it as rounded-concentric-<outer>/<inset>, not a hardcoded step.** A 12px popover with 4px padding takes 8px items. The utility computes the subtraction from the live tokens, so the pair stays concentric when a theme or the customizer rescales the radius ladder; a hardcoded rounded-sm drifts.
33
- 2. **A list of rows inside a rounded popup uses popup-inset on the popup, rounded-popup-item on the rows and popup-bleed on a separator.** All three derive from the popup's radius: the inset grows to a third of it (never below --spacing(1), so density moves it too), the rows take what is left, and a separator bleeds by exactly the inset. A rounder look gets more room instead of rows that round into pills, and the default ladder still lands on the familiar 4px inset and 8px rows. *Enforced by sable-lint: popup-separator-bleed.*
33
+ 2. **A list of rows inside a rounded popup uses popup-inset on the popup, rounded-popup-item on the rows and popup-bleed on a separator.** All three derive from the popup's radius: the inset grows to a third of it (never below --spacing(1), so density moves it too), the rows take what is left after the inset and the 1px border, and a separator bleeds by exactly the inset. A rounder look gets more room instead of rows that round into pills; the default ladder lands on a 4px inset and 7px rows. *Enforced by sable-lint: popup-separator-bleed.*
34
34
  3. **A Button, Pill or Badge gets its look from a variant, never from override classes.** border, bg, rounded, ring and shadow classes on a component fight the variant's own hover, selected and focus states, and give one call site a look no other site can ask for. Use the variant that exists (Button variant="floating" over content, "on-color" on a fill; Pill appearance="outline"; Badge appearance and tone) or add one to the component. *Enforced by sable-lint: component-restyle.*
35
35
  4. **The outer radius is always at least the inner radius.** If the inset is larger than the outer radius, square the inner element or step the container up.
36
36
  5. **Never mix radii within one component.** A component reads as one object when its corners agree.