@sableui/react 0.14.0 → 0.16.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 (163) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/DESIGN.md +3 -1
  3. package/bin/sable-lint.mjs +5 -1
  4. package/dist/{_contract-CkqXDVww.d.ts → _contract-CDa9-NdB.d.ts} +47 -1
  5. package/dist/blocks/index.d.ts +12 -1
  6. package/dist/blocks/index.js +4 -0
  7. package/dist/blocks/message-reactions-block.js +87 -0
  8. package/dist/blocks/message-reactions-block.meta.js +23 -0
  9. package/dist/blocks/wallet-block.js +2 -2
  10. package/dist/components/app-shell/app-shell-frame.js +16 -0
  11. package/dist/components/app-shell/app-shell-main.js +12 -0
  12. package/dist/components/app-shell/app-shell.js +28 -4
  13. package/dist/components/app-shell/app-shell.meta.js +88 -4
  14. package/dist/components/area-chart/area-chart.meta.js +6 -1
  15. package/dist/components/attachment/attachment.meta.js +2 -1
  16. package/dist/components/attachment/attachment.variants.js +1 -1
  17. package/dist/components/badge/badge.meta.js +8 -1
  18. package/dist/components/badge/badge.variants.js +7 -7
  19. package/dist/components/banner/banner.meta.js +6 -1
  20. package/dist/components/banner/banner.variants.js +5 -5
  21. package/dist/components/bar-chart/bar-chart.meta.js +6 -1
  22. package/dist/components/bubble/bubble.js +105 -5
  23. package/dist/components/bubble/bubble.meta.js +42 -6
  24. package/dist/components/bubble/bubble.variants.js +1 -1
  25. package/dist/components/bubble/index.js +2 -0
  26. package/dist/components/caret-menu/caret-menu.js +5 -6
  27. package/dist/components/chat-composer/chat-composer.meta.js +2 -2
  28. package/dist/components/chat-composer/chat-composer.variants.js +1 -1
  29. package/dist/components/color-field/color-field.js +6 -8
  30. package/dist/components/combobox/combobox.js +27 -5
  31. package/dist/components/combobox/combobox.meta.js +10 -2
  32. package/dist/components/command-menu/command-menu.js +14 -3
  33. package/dist/components/command-menu/command-menu.meta.js +9 -1
  34. package/dist/components/date-picker/date-picker.meta.js +2 -2
  35. package/dist/components/feedback/feedback.js +9 -2
  36. package/dist/components/feedback/feedback.meta.js +8 -1
  37. package/dist/components/gradient-art/gradient-art.js +8 -5
  38. package/dist/components/gradient-art/gradient-art.meta.js +35 -2
  39. package/dist/components/input/input.meta.js +11 -3
  40. package/dist/components/input-group/input-group.js +1 -1
  41. package/dist/components/input-group/input-group.meta.js +1 -1
  42. package/dist/components/like-button/index.js +7 -0
  43. package/dist/components/like-button/like-button.js +89 -0
  44. package/dist/components/like-button/like-button.meta.js +132 -0
  45. package/dist/components/line-chart/line-chart.meta.js +6 -1
  46. package/dist/components/menu/menu.js +1 -1
  47. package/dist/components/meter/meter.js +53 -7
  48. package/dist/components/meter/meter.meta.js +51 -4
  49. package/dist/components/meter/meter.variants.js +43 -7
  50. package/dist/components/note/note.meta.js +5 -1
  51. package/dist/components/note/note.variants.js +4 -4
  52. package/dist/components/number-field/number-field.meta.js +2 -2
  53. package/dist/components/otp-field/otp-field.meta.js +2 -2
  54. package/dist/components/panel-group/panel-group.js +5 -8
  55. package/dist/components/pie-chart/pie-chart.meta.js +6 -1
  56. package/dist/components/radar-chart/radar-chart.meta.js +6 -1
  57. package/dist/components/radial-chart/radial-chart.meta.js +6 -1
  58. package/dist/components/reaction-picker/index.js +13 -0
  59. package/dist/components/reaction-picker/reaction-picker.js +104 -0
  60. package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
  61. package/dist/components/search-input/search-input.meta.js +11 -3
  62. package/dist/components/segmented-bar/segmented-bar.meta.js +8 -1
  63. package/dist/components/segmented-bar/segmented-bar.variants.js +7 -7
  64. package/dist/components/select/select.js +1 -1
  65. package/dist/components/select/select.meta.js +2 -2
  66. package/dist/components/sheet/sheet.js +5 -4
  67. package/dist/components/sidebar/sidebar.js +16 -1
  68. package/dist/components/sidebar/sidebar.meta.js +7 -2
  69. package/dist/components/sparkline/sparkline.meta.js +8 -1
  70. package/dist/components/status-dot/status-dot.meta.js +8 -1
  71. package/dist/components/status-dot/status-dot.variants.js +7 -7
  72. package/dist/components/stepper/stepper.meta.js +2 -1
  73. package/dist/components/stepper/stepper.variants.js +1 -1
  74. package/dist/components/table/table.meta.js +4 -4
  75. package/dist/components/textarea/textarea.js +5 -0
  76. package/dist/components/textarea/textarea.meta.js +3 -3
  77. package/dist/components/toast/toast.js +25 -13
  78. package/dist/components/toast/toast.meta.js +7 -1
  79. package/dist/file-hashes.json +112 -100
  80. package/dist/foundations/accessibility.js +5 -0
  81. package/dist/foundations/motion.js +21 -1
  82. package/dist/icon-sets/reicon.js +11 -1
  83. package/dist/index.d.ts +425 -84
  84. package/dist/index.js +11 -1
  85. package/dist/lib/chart/types.js +8 -8
  86. package/dist/lib/chart/use-arrival.js +5 -4
  87. package/dist/lib/field-shell.js +1 -1
  88. package/dist/lib/highlight-glide.js +6 -6
  89. package/dist/lib/scrub-gesture.js +6 -6
  90. package/dist/lib/taxonomy.js +2 -0
  91. package/dist/lib/use-caret-trigger.js +11 -18
  92. package/dist/lib/use-match-highlight.js +69 -0
  93. package/dist/lib/use-smooth-wheel.js +10 -8
  94. package/dist/motion/Burst.js +79 -0
  95. package/dist/motion/index.js +2 -0
  96. package/dist/presets/index.d.ts +13 -2
  97. package/dist/presets/index.js +17 -1
  98. package/dist/styles.css +241 -0
  99. package/dist/{theme-provider-CdSicAad.d.ts → theme-provider-BydfM5Z0.d.ts} +1 -1
  100. package/dist/themes/_contract.js +14 -0
  101. package/dist/themes/atelier/atelier.css +18 -0
  102. package/dist/themes/atelier/index.d.ts +1 -1
  103. package/dist/themes/contrast.js +48 -1
  104. package/dist/themes/ember/ember.css +18 -0
  105. package/dist/themes/ember/index.d.ts +1 -1
  106. package/dist/themes/index.d.ts +2 -2
  107. package/dist/themes/meridian/index.d.ts +1 -1
  108. package/dist/themes/meridian/meridian.css +18 -0
  109. package/dist/themes/poppy/index.d.ts +1 -1
  110. package/dist/themes/poppy/poppy.css +18 -0
  111. package/dist/themes/prism/index.d.ts +1 -1
  112. package/dist/themes/prism/prism.css +18 -0
  113. package/dist/themes/sable/index.d.ts +1 -1
  114. package/dist/themes/sable/sable.css +18 -0
  115. package/dist/themes/verdant/index.d.ts +1 -1
  116. package/dist/themes/verdant/verdant.css +18 -0
  117. package/llms/blocks/message-reactions-block.md +116 -0
  118. package/llms/blocks/wallet-block.md +5 -4
  119. package/llms/components/app-shell.md +72 -3
  120. package/llms/components/area-chart.md +1 -1
  121. package/llms/components/attachment.md +1 -1
  122. package/llms/components/badge.md +1 -1
  123. package/llms/components/banner.md +1 -1
  124. package/llms/components/bar-chart.md +1 -1
  125. package/llms/components/bubble.md +17 -7
  126. package/llms/components/chat-composer.md +1 -1
  127. package/llms/components/combobox.md +2 -1
  128. package/llms/components/command-menu.md +3 -2
  129. package/llms/components/date-picker.md +1 -1
  130. package/llms/components/feedback.md +2 -1
  131. package/llms/components/gradient-art.md +28 -1
  132. package/llms/components/input-group.md +1 -1
  133. package/llms/components/input.md +4 -2
  134. package/llms/components/like-button.md +91 -0
  135. package/llms/components/line-chart.md +1 -1
  136. package/llms/components/meter.md +33 -4
  137. package/llms/components/note.md +1 -1
  138. package/llms/components/number-field.md +1 -1
  139. package/llms/components/otp-field.md +1 -1
  140. package/llms/components/pie-chart.md +1 -1
  141. package/llms/components/radar-chart.md +1 -1
  142. package/llms/components/radial-chart.md +1 -1
  143. package/llms/components/reaction-picker.md +115 -0
  144. package/llms/components/search-input.md +20 -2
  145. package/llms/components/segmented-bar.md +1 -1
  146. package/llms/components/select.md +1 -1
  147. package/llms/components/sidebar.md +3 -2
  148. package/llms/components/sparkline.md +1 -1
  149. package/llms/components/status-dot.md +1 -1
  150. package/llms/components/stepper.md +1 -1
  151. package/llms/components/table.md +2 -2
  152. package/llms/components/textarea.md +2 -2
  153. package/llms/components/toast.md +2 -1
  154. package/llms/foundations/accessibility.md +1 -0
  155. package/llms/foundations/motion.md +18 -2
  156. package/llms/reference/token-intent.md +22 -2
  157. package/llms/reference/utilities.md +9 -3
  158. package/llms-full.txt +596 -61
  159. package/llms.txt +10 -7
  160. package/package.json +1 -1
  161. package/registry.json +968 -68
  162. package/scripts/lint-rules.mjs +6 -3
  163. package/targets/ios.json +18 -0
package/registry.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://sable.design/registry.schema.json",
3
3
  "name": "@sableui/react",
4
- "version": "0.14.0",
4
+ "version": "0.16.0",
5
5
  "description": "Themeable React components, semantic tokens, layout patterns, presets, and package-owned agent skills. Default theme: Sable.",
6
6
  "engine": {
7
7
  "behavior": "@base-ui/react",
@@ -349,7 +349,7 @@
349
349
  {
350
350
  "name": "input",
351
351
  "intent": "The outline of a form control specifically.",
352
- "whenToUse": "Text inputs, textareas and selects. Not the edge of a checkbox, radio or switch — that is `control-border`.",
352
+ "whenToUse": "Text inputs, textareas and selects (through `field-border`, which derives from it). Not the edge of a checkbox, radio or switch — that is `control-border`.",
353
353
  "insteadOf": "`border`. They are often the same value, but a theme is entitled to make form controls read differently from other chrome, and only this token gives it the hook."
354
354
  },
355
355
  {
@@ -954,6 +954,7 @@
954
954
  "motion-tick",
955
955
  "motion-appear",
956
956
  "motion-arrive",
957
+ "motion-bloom",
957
958
  "motion-size",
958
959
  "motion-reveal",
959
960
  "motion-draw",
@@ -1412,6 +1413,103 @@
1412
1413
  "intent": "The edge of a floating surface. Fainter than `border`: the shadow carries the elevation.",
1413
1414
  "whenToUse": "Every floating surface — popover, menu, select, tooltip, dialog, sheet, toast, a floating panel — as `border-border-floating` or `ring-border-floating`, together with `bg-popover` and a `shadow-md`+ shadow. Derived for every theme (a faint alpha of the foreground); a theme may tune it with `--theme-border-floating`.",
1414
1415
  "insteadOf": "`border`. A full hairline plus a shadow stacks into a heavy outline around anything that floats. `border` is for resting edges: inputs, cards, badges."
1416
+ },
1417
+ {
1418
+ "name": "field",
1419
+ "intent": "The fill of a text field: Input, Textarea, Select, Combobox, SearchInput, InputGroup, ChatComposer.",
1420
+ "whenToUse": "Read by every field through its shell, so you rarely name it. Derived for every theme (`card`, an outlined field on the card); a theme fills its fields instead with `--theme-field: var(--muted)` and `--theme-field-border: transparent`, and every field changes at once. Name it only for a field-shaped surface of your own that must match.",
1421
+ "insteadOf": "`card` or `muted` on a hand-built field. A field that paints `card` keeps its outline when the theme fills the rest, and one that paints `muted` stays filled when the theme outlines them.",
1422
+ "pairsWith": [
1423
+ "field-border"
1424
+ ]
1425
+ },
1426
+ {
1427
+ "name": "field-border",
1428
+ "intent": "The outline of a text field. Derived from `input`; `transparent` in a theme with filled fields.",
1429
+ "whenToUse": "With `field`, on the same field-shaped surfaces. Tune it with `--theme-field-border`.",
1430
+ "insteadOf": "`input` on a text field. `input` stays the theme's outline colour for form chrome; this is the field's outline, which a filled theme turns off.",
1431
+ "pairsWith": [
1432
+ "field"
1433
+ ]
1434
+ },
1435
+ {
1436
+ "name": "shell-ground",
1437
+ "intent": "The ground of an inset app frame: the rail and the gap around the raised main panel.",
1438
+ "whenToUse": "Painted by `<AppShell frame=\"inset\">` and the Sidebar in its slot; you rarely name it. Derived for every theme as one step off `sidebar` toward its ink, so the panel reads as lifted even where the rail is the page colour. Tune it with `--theme-shell-ground`.",
1439
+ "insteadOf": "`sidebar` or `canvas` behind an inset panel. `sidebar` is the page colour in most themes, so the panel would sit on itself; `canvas` is a workspace's artboard, not app chrome.",
1440
+ "pairsWith": [
1441
+ "sidebar"
1442
+ ]
1443
+ },
1444
+ {
1445
+ "name": "destructive-mark",
1446
+ "intent": "`destructive` at graphic strength: the same hue lifted and saturated, for a mark that carries no text.",
1447
+ "whenToUse": "A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-destructive-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-destructive-mark` or gets one derived from `destructive`.",
1448
+ "insteadOf": "`bg-destructive` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-destructive` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.",
1449
+ "pairsWith": [
1450
+ "background",
1451
+ "card"
1452
+ ]
1453
+ },
1454
+ {
1455
+ "name": "success-mark",
1456
+ "intent": "`success` at graphic strength: the same hue lifted and saturated, for a mark that carries no text.",
1457
+ "whenToUse": "A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-success-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-success-mark` or gets one derived from `success`.",
1458
+ "insteadOf": "`bg-success` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-success` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.",
1459
+ "pairsWith": [
1460
+ "background",
1461
+ "card"
1462
+ ]
1463
+ },
1464
+ {
1465
+ "name": "tone-warning-mark",
1466
+ "intent": "`tone-warning` at graphic strength: the same hue lifted and saturated, for a mark that carries no text.",
1467
+ "whenToUse": "A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-tone-warning-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-tone-warning-mark` or gets one derived from `tone-warning`.",
1468
+ "insteadOf": "`bg-tone-warning` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-tone-warning` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.",
1469
+ "pairsWith": [
1470
+ "background",
1471
+ "card"
1472
+ ]
1473
+ },
1474
+ {
1475
+ "name": "tone-info-mark",
1476
+ "intent": "`tone-info` at graphic strength: the same hue lifted and saturated, for a mark that carries no text.",
1477
+ "whenToUse": "A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-tone-info-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-tone-info-mark` or gets one derived from `tone-info`.",
1478
+ "insteadOf": "`bg-tone-info` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-tone-info` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.",
1479
+ "pairsWith": [
1480
+ "background",
1481
+ "card"
1482
+ ]
1483
+ },
1484
+ {
1485
+ "name": "tone-1-mark",
1486
+ "intent": "`tone-1` at graphic strength: the same hue lifted and saturated, for a mark that carries no text.",
1487
+ "whenToUse": "A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-tone-1-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-tone-1-mark` or gets one derived from `tone-1`.",
1488
+ "insteadOf": "`bg-tone-1` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-tone-1` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.",
1489
+ "pairsWith": [
1490
+ "background",
1491
+ "card"
1492
+ ]
1493
+ },
1494
+ {
1495
+ "name": "tone-2-mark",
1496
+ "intent": "`tone-2` at graphic strength: the same hue lifted and saturated, for a mark that carries no text.",
1497
+ "whenToUse": "A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-tone-2-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-tone-2-mark` or gets one derived from `tone-2`.",
1498
+ "insteadOf": "`bg-tone-2` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-tone-2` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.",
1499
+ "pairsWith": [
1500
+ "background",
1501
+ "card"
1502
+ ]
1503
+ },
1504
+ {
1505
+ "name": "tone-3-mark",
1506
+ "intent": "`tone-3` at graphic strength: the same hue lifted and saturated, for a mark that carries no text.",
1507
+ "whenToUse": "A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-tone-3-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-tone-3-mark` or gets one derived from `tone-3`.",
1508
+ "insteadOf": "`bg-tone-3` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-tone-3` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.",
1509
+ "pairsWith": [
1510
+ "background",
1511
+ "card"
1512
+ ]
1415
1513
  }
1416
1514
  ],
1417
1515
  "palette": {
@@ -1785,7 +1883,10 @@
1785
1883
  "paperclip",
1786
1884
  "chart",
1787
1885
  "flag",
1788
- "at-sign"
1886
+ "at-sign",
1887
+ "replay",
1888
+ "motion",
1889
+ "emoji"
1789
1890
  ]
1790
1891
  },
1791
1892
  "layoutPrimitives": {
@@ -2955,7 +3056,12 @@
2955
3056
  "dock",
2956
3057
  "player bar",
2957
3058
  "now playing",
2958
- "persistent"
3059
+ "persistent",
3060
+ "inset",
3061
+ "inset frame",
3062
+ "floating panel",
3063
+ "rounded content",
3064
+ "scroll container"
2959
3065
  ],
2960
3066
  "usage": {
2961
3067
  "description": "Use AppShell as the outer product frame for authenticated or app-like experiences. It coordinates responsive chrome slots and guarantees a skip link plus main landmark; put route content inside the children and let AppHeader, Sidebar, BottomNav, and Banner own their own semantics.",
@@ -2976,10 +3082,25 @@
2976
3082
  "guidance": "do",
2977
3083
  "description": "Wrap EVERY return of a route in its own Page or Region — including early returns for loading, empty and error states. AppShell's <main> deliberately carries no padding (it is a frame, and padding belongs to the region that chose the width), so a route that returns a bare spinner or error renders flush against the chrome. This is the most common conversion bug: the happy path gets a Page, the three early returns do not."
2978
3084
  },
3085
+ {
3086
+ "guidance": "do",
3087
+ "description": "Use frame=\"inset\" for dense, multi-pane product apps — a CRM, an issue tracker, an AI workspace — where framing the content as one raised panel on the rail's ground helps it read as the workspace and the rail as chrome. Keep the Sidebar on surface=\"rail\" so its tint is the ground. Inset moves the page scroll from the window to <main id=\"main-content\"> on desktop: point scroll listeners, scroll restoration and scroll-to-top at that element, not window."
3088
+ },
2979
3089
  {
2980
3090
  "guidance": "dont",
2981
3091
  "description": "Nest AppShell inside route cards or panels. It is page chrome, not local content."
2982
3092
  },
3093
+ {
3094
+ "guidance": "dont",
3095
+ "description": "Wrap a route in its own h-full overflow-y-auto box inside an inset shell. main is already the page's scroll container; a second one traps the scrollbar inside a smaller box and breaks sticky headers and the skip link's scroll target. Scroll a genuinely separate pane (a list beside a detail view) with Scroller.",
3096
+ "instead": [
3097
+ "Scroller"
3098
+ ]
3099
+ },
3100
+ {
3101
+ "guidance": "dont",
3102
+ "description": "Combine frame=\"inset\" with a full-width header bar above the rail. The inset ground is the rail running the full height of the window; a bar across the top cuts it off. Use the flush frame with Sidebar surface=\"page\" for that layout."
3103
+ },
2983
3104
  {
2984
3105
  "guidance": "dont",
2985
3106
  "description": "Put desktop-only navigation in the mobile header slot or mobile-only controls in the sidebar slot."
@@ -3004,7 +3125,7 @@
3004
3125
  {
3005
3126
  "name": "Main",
3006
3127
  "required": true,
3007
- "description": "The route content landmark with id main-content."
3128
+ "description": "The route content landmark with id main-content. With frame=\"inset\", from lg it is a rounded, bordered panel that scrolls inside itself."
3008
3129
  },
3009
3130
  {
3010
3131
  "name": "Bottom nav",
@@ -3062,6 +3183,12 @@
3062
3183
  "name": "overlays",
3063
3184
  "type": "ReactNode",
3064
3185
  "description": "Floating overlays — a message launcher, FAB, or modal mount points."
3186
+ },
3187
+ {
3188
+ "name": "frame",
3189
+ "type": "\"flush\" | \"inset\"",
3190
+ "default": "\"flush\"",
3191
+ "description": "How the desktop frame (lg and up) holds the content; below lg both are the same mobile layout with page scroll. \"flush\": main sits on the page beside the rail and the document scrolls. \"inset\": the shell fills the viewport and paints the shell-ground token (one step off the rail's surface, tunable with --theme-shell-ground) behind the rail and around the panel; main becomes a rounded-lg panel with a hairline border and shadow-sm, inset by a spacing step from the window edges, that scrolls inside itself (scrollbar-quiet) while the rail stays put. The Sidebar in the sidebar slot drops its right border and fills the row. main stays the #main-content target (tabIndex -1, so the skip link lands focus in the scroller) and the inline-size query container; sticky children stick to the panel's top; banners and the dock stay in flow, so the panel's bottom gap sits above a dock. With Sidebar surface=\"page\" the ground is the page colour and the panel's border alone lifts it."
3065
3192
  }
3066
3193
  ],
3067
3194
  "subcomponents": [
@@ -3075,13 +3202,17 @@
3075
3202
  "background",
3076
3203
  "foreground",
3077
3204
  "primary",
3078
- "primary-foreground"
3205
+ "primary-foreground",
3206
+ "sidebar",
3207
+ "shell-ground",
3208
+ "border"
3079
3209
  ],
3080
3210
  "a11y": [
3081
3211
  "Renders a skip link to #main-content as the first focusable element.",
3082
3212
  "Wraps content in a <main id=\"main-content\"> landmark.",
3083
3213
  "Slot components own their own ARIA (nav landmarks, labels).",
3084
- "The dock follows <main> in DOM order, so keyboard and screen-reader users reach the player after the page content, not before it."
3214
+ "The dock follows <main> in DOM order, so keyboard and screen-reader users reach the player after the page content, not before it.",
3215
+ "With frame=\"inset\", <main> is the desktop scroll container and takes tabIndex -1: the skip link moves focus into it, so PageDown and the arrow keys scroll the content, without adding a tab stop."
3085
3216
  ],
3086
3217
  "examples": [
3087
3218
  {
@@ -3089,6 +3220,11 @@
3089
3220
  "code": "<AppShell\n banners={<Banner tone=\"warning\">Sync is delayed — changes may be slow to appear.</Banner>}\n header={<AppHeader title=\"Studio\" />}\n sidebar={\n <Sidebar>\n <SidebarNav>\n <SidebarItem icon={<Icon name=\"home\" />} label=\"Home\" active render={<Link to=\"/\" />} />\n <SidebarItem icon={<Icon name=\"bell\" />} label=\"Notifications\" render={<Link to=\"/notifications\" />} />\n <SidebarItem icon={<Icon name=\"gear\" />} label=\"Settings\" render={<Link to=\"/settings\" />} />\n </SidebarNav>\n </Sidebar>\n }\n bottomNav={\n <BottomNav>\n <BottomNavItem icon={<Icon name=\"home\" />} label=\"Home\" active render={<Link to=\"/\" />} />\n <BottomNavItem icon={<Icon name=\"bell\" />} label=\"Alerts\" render={<Link to=\"/notifications\" />} />\n </BottomNav>\n }\n>\n <Region max=\"feed\">\n <PageHeader title=\"Home\" description=\"Latest posts from people you follow.\" />\n </Region>\n</AppShell>",
3090
3221
  "imports": "import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader, Region, Sidebar, SidebarItem, SidebarNav } from \"@sableui/react\"\n// <Link> = your router's link (react-router, next/link, @tanstack/router, …)"
3091
3222
  },
3223
+ {
3224
+ "label": "Inset frame",
3225
+ "code": "<AppShell\n frame=\"inset\"\n header={<AppHeader title=\"Pipeline\" />}\n sidebar={\n <Sidebar>\n <SidebarHeader>\n <SidebarBrand mark={<Icon name=\"gem\" variant=\"solid\" />} label=\"Relay\" render={<Link to=\"/\" />} />\n </SidebarHeader>\n <SidebarNav>\n <SidebarItem icon={<Icon name=\"inbox\" />} label=\"Inbox\" render={<Link to=\"/inbox\" />} />\n <SidebarItem icon={<Icon name=\"user\" />} label=\"People\" active render={<Link to=\"/people\" />} />\n <SidebarItem icon={<Icon name=\"folder\" />} label=\"Companies\" render={<Link to=\"/companies\" />} />\n </SidebarNav>\n </Sidebar>\n }\n bottomNav={\n <BottomNav>\n <BottomNavItem icon={<Icon name=\"inbox\" />} label=\"Inbox\" render={<Link to=\"/inbox\" />} />\n <BottomNavItem icon={<Icon name=\"user\" />} label=\"People\" active render={<Link to=\"/people\" />} />\n <BottomNavItem icon={<Icon name=\"folder\" />} label=\"Companies\" render={<Link to=\"/companies\" />} />\n </BottomNav>\n }\n>\n <Page max=\"wide\" gap={4} className=\"py-6\">\n <PageHeader\n title=\"People\"\n description=\"Everyone in your pipeline, newest first.\"\n actions={<Button><Icon name=\"plus\" />Add person</Button>}\n />\n <Card>\n <CardContent flush>\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead>Company</TableHead>\n <TableHead numeric>Deals</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n <TableRow>\n <TableCell>Ada Lovelace</TableCell>\n <TableCell>Northwind</TableCell>\n <TableCell numeric>4</TableCell>\n </TableRow>\n <TableRow>\n <TableCell>Grace Hopper</TableCell>\n <TableCell>Harbor Labs</TableCell>\n <TableCell numeric>2</TableCell>\n </TableRow>\n </TableBody>\n </Table>\n </CardContent>\n </Card>\n </Page>\n</AppShell>",
3226
+ "imports": "import { AppHeader, AppShell, BottomNav, BottomNavItem, Button, Card, CardContent, Icon, Page, PageHeader, Sidebar, SidebarBrand, SidebarHeader, SidebarItem, SidebarNav, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@sableui/react\"\n// <Link> = your router's link (react-router, next/link, @tanstack/router, …)"
3227
+ },
3092
3228
  {
3093
3229
  "label": "SkipLink in a layout of your own",
3094
3230
  "code": "<>\n <SkipLink />\n <header className=\"flex items-center justify-between px-(--page-inset) py-4\">\n <Link to=\"/\">Alder & Lune</Link>\n <Button variant=\"ghost\" size=\"icon\" aria-label=\"Search\"><Icon name=\"search\" /></Button>\n </header>\n <main id=\"main-content\" tabIndex={-1}>\n <Region max=\"wide\">\n <PageHeader title=\"Lighting\" />\n </Region>\n </main>\n</>",
@@ -3286,7 +3422,12 @@
3286
3422
  "popover",
3287
3423
  "popover-foreground",
3288
3424
  "foreground",
3289
- "background"
3425
+ "background",
3426
+ "tone-warning-mark",
3427
+ "tone-info-mark",
3428
+ "tone-1-mark",
3429
+ "tone-2-mark",
3430
+ "tone-3-mark"
3290
3431
  ],
3291
3432
  "motion": [
3292
3433
  {
@@ -3494,7 +3635,8 @@
3494
3635
  "destructive",
3495
3636
  "overlay",
3496
3637
  "overlay-foreground",
3497
- "ring"
3638
+ "ring",
3639
+ "destructive-mark"
3498
3640
  ],
3499
3641
  "motion": [
3500
3642
  {
@@ -4012,7 +4154,14 @@
4012
4154
  "tone-info-foreground",
4013
4155
  "success-foreground",
4014
4156
  "tone-warning-foreground",
4015
- "destructive-foreground"
4157
+ "destructive-foreground",
4158
+ "destructive-mark",
4159
+ "success-mark",
4160
+ "tone-warning-mark",
4161
+ "tone-info-mark",
4162
+ "tone-1-mark",
4163
+ "tone-2-mark",
4164
+ "tone-3-mark"
4016
4165
  ],
4017
4166
  "a11y": [
4018
4167
  "Badges are static text by default; do not rely on color alone for meaning.",
@@ -4220,7 +4369,12 @@
4220
4369
  "tone-1",
4221
4370
  "tone-1-foreground",
4222
4371
  "destructive",
4223
- "destructive-foreground"
4372
+ "destructive-foreground",
4373
+ "destructive-mark",
4374
+ "success-mark",
4375
+ "tone-warning-mark",
4376
+ "tone-info-mark",
4377
+ "tone-1-mark"
4224
4378
  ],
4225
4379
  "a11y": [
4226
4380
  "Static banners render as a labelled region (role=region); set `live` to announce dynamic status bars (role=status / role=alert) when they appear or change.",
@@ -4468,7 +4622,12 @@
4468
4622
  "popover",
4469
4623
  "popover-foreground",
4470
4624
  "foreground",
4471
- "background"
4625
+ "background",
4626
+ "tone-warning-mark",
4627
+ "tone-info-mark",
4628
+ "tone-1-mark",
4629
+ "tone-2-mark",
4630
+ "tone-3-mark"
4472
4631
  ],
4473
4632
  "motion": [
4474
4633
  {
@@ -4924,7 +5083,12 @@
4924
5083
  {
4925
5084
  "name": "BubbleReaction",
4926
5085
  "required": false,
4927
- "description": "One reaction pill — emoji, count (hidden at 1), and pressed state."
5086
+ "description": "One reaction pill — emoji, count (hidden at 1), and pressed state. Celebrates when it becomes the reader's own."
5087
+ },
5088
+ {
5089
+ "name": "BubbleReactionAdd",
5090
+ "required": false,
5091
+ "description": "The add-reaction pill at the end of the row: opens a ReactionPicker above it."
4928
5092
  }
4929
5093
  ]
4930
5094
  },
@@ -4933,7 +5097,8 @@
4933
5097
  "Message",
4934
5098
  "MessageScroller",
4935
5099
  "Marker",
4936
- "Attachment"
5100
+ "Attachment",
5101
+ "ReactionPicker"
4937
5102
  ]
4938
5103
  },
4939
5104
  "props": [
@@ -4969,8 +5134,13 @@
4969
5134
  },
4970
5135
  {
4971
5136
  "name": "BubbleReaction",
4972
- "type": "{ emoji: ReactNode; count?: number; active?: boolean } & ComponentPropsWithRef<\"button\">",
4973
- "description": "One reaction pill. A toggle, not a label: `active` drives `aria-pressed` as well as the styling, and `count` is hidden at 1 because the pill already says \"one\"."
5137
+ "type": "{ emoji: ReactNode; label?: string; count?: number; active?: boolean; motion?: MotionLevel } & ComponentPropsWithRef<\"button\">",
5138
+ "description": "One reaction pill. A toggle, not a label: `active` drives `aria-pressed` as well as the styling, `label` names it (\"Party, 3\"), and `count` is hidden at 1 because the pill already says \"one\". When it turns on — pressed, or newly added already on — the emoji blooms, copies of it burst out and the count rolls; pills the page loads with stay still."
5139
+ },
5140
+ {
5141
+ "name": "BubbleReactionAdd",
5142
+ "type": "{ reactions?: Reaction[]; selected?: string[]; onSelect?: (emoji: string) => void; label?: string; motion?: MotionLevel } & ComponentPropsWithRef<\"button\">",
5143
+ "description": "The add-reaction pill: a smiley that opens a ReactionPicker above it. Choosing an emoji closes the picker and calls `onSelect`; add the emoji to the row and its new pill celebrates on arrival."
4974
5144
  }
4975
5145
  ],
4976
5146
  "variants": {
@@ -5002,9 +5172,35 @@
5002
5172
  "muted-foreground",
5003
5173
  "ring"
5004
5174
  ],
5175
+ "motion": [
5176
+ {
5177
+ "slot": "arrival",
5178
+ "recipe": "none",
5179
+ "part": "Bubble",
5180
+ "why": "A bubble arrives with its Message row, which owns how a new message enters the transcript. A reaction pill that the reader just added arrives with its own celebration (below)."
5181
+ },
5182
+ {
5183
+ "slot": "response",
5184
+ "recipe": "motion-bloom",
5185
+ "part": "BubbleReaction",
5186
+ "why": "Reacting is the reader's choice, so it is answered like a like: the emoji grows from nothing past its size and settles, while copies of it spray outward and fade (Burst with the emoji as its particle, the way chat apps answer a reaction). Plays when a pill turns on and when a pill mounts already on into a row that has rendered — never for the pills a page loads with. Pills inherit Pill's motion-interactive and motion-press."
5187
+ },
5188
+ {
5189
+ "slot": "transition",
5190
+ "recipe": "custom",
5191
+ "part": "BubbleReaction",
5192
+ "why": "The count rolls with NumberTicker as people react. Turning a reaction off is quiet: the tint eases out and the count rolls back, with no burst. Under `subtle` or reduced motion the bloom and the spray are dropped and the tint and count alone answer."
5193
+ },
5194
+ {
5195
+ "slot": "activity",
5196
+ "recipe": "none",
5197
+ "part": "Bubble",
5198
+ "why": "A bubble is static content; a message still being written is StreamingText or a Marker, not a state of the surface."
5199
+ }
5200
+ ],
5005
5201
  "a11y": [
5006
5202
  "The surface is a plain container with no imposed role — a bubble is presentation, and announcing one as a region would put a landmark around every line of a conversation.",
5007
- "Reaction pills are real buttons reporting `aria-pressed`, so the reader's own reaction is state rather than colour alone; the emoji is hidden from AT and the count is left readable.",
5203
+ "Reaction pills are real buttons reporting `aria-pressed`, so the reader's own reaction is state rather than colour alone. Give each a `label`: it is named \"Party, 3\", with the emoji and its burst hidden from AT. The add-reaction pill is named \"Add reaction\" and opens a toolbar picker.",
5008
5204
  "`whitespace-pre-wrap` and `break-words` keep pasted logs, long URLs, and hard-wrapped text inside the surface instead of overflowing the conversation.",
5009
5205
  "Every variant meets AA against its own surface, including `primary`, which pairs the primary fill with its paired foreground token rather than a hardcoded white."
5010
5206
  ],
@@ -5021,8 +5217,8 @@
5021
5217
  },
5022
5218
  {
5023
5219
  "label": "With reactions",
5024
- "code": "<Message messageId=\"m3\">\n\t<Bubble>Shipped 0.45.0.</Bubble>\n\t<BubbleReactions>\n\t\t<BubbleReaction emoji=\"🎉\" count={3} active onClick={() => toggle(\"🎉\")} />\n\t\t<BubbleReaction emoji=\"👀\" count={1} onClick={() => toggle(\"👀\")} />\n\t</BubbleReactions>\n</Message>",
5025
- "imports": "import { Bubble, BubbleReaction, BubbleReactions, Message } from \"@sableui/react\""
5220
+ "code": "<Message messageId=\"m3\">\n\t<Bubble>Shipped 0.45.0.</Bubble>\n\t<BubbleReactions>\n\t\t<BubbleReaction emoji=\"🎉\" label=\"Party\" count={3} active onClick={() => toggle(\"🎉\")} />\n\t\t<BubbleReaction emoji=\"👀\" label=\"Eyes\" count={1} onClick={() => toggle(\"👀\")} />\n\t\t<BubbleReactionAdd selected={mine} onSelect={toggle} />\n\t</BubbleReactions>\n</Message>",
5221
+ "imports": "import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions, Message } from \"@sableui/react\""
5026
5222
  },
5027
5223
  {
5028
5224
  "label": "A grouped run drops the repeated tail",
@@ -6457,8 +6653,8 @@
6457
6653
  ]
6458
6654
  },
6459
6655
  "tokensUsed": [
6460
- "card",
6461
- "input",
6656
+ "field",
6657
+ "field-border",
6462
6658
  "ring",
6463
6659
  "destructive",
6464
6660
  "foreground",
@@ -7585,6 +7781,12 @@
7585
7781
  "default": "Base UI's locale-aware contains",
7586
7782
  "description": "How items match the query. `null` turns matching off, for items a server already filtered: pair it with `onInputValueChange` to fetch."
7587
7783
  },
7784
+ {
7785
+ "name": "highlightMatches",
7786
+ "type": "boolean",
7787
+ "default": "true",
7788
+ "description": "Paints the typed text wherever it appears in a row's label, on the `highlight` / `highlight-foreground` pair, through the CSS Custom Highlight API — no `<mark>` in the row, and the accessible name is unchanged. Only typed text counts: a field still holding the selected item's label lists everything and lights nothing. A literal, case-insensitive match within the label; a `filter` that matches differently leaves rows it kept unpainted. `false` turns it off."
7789
+ },
7588
7790
  {
7589
7791
  "name": "onInputValueChange",
7590
7792
  "type": "(inputValue: string, eventDetails) => void",
@@ -7652,8 +7854,10 @@
7652
7854
  }
7653
7855
  ],
7654
7856
  "tokensUsed": [
7655
- "input",
7656
- "card",
7857
+ "highlight",
7858
+ "highlight-foreground",
7859
+ "field-border",
7860
+ "field",
7657
7861
  "foreground",
7658
7862
  "muted-foreground",
7659
7863
  "ring",
@@ -7929,6 +8133,12 @@
7929
8133
  "default": "case-insensitive substring match on label",
7930
8134
  "description": "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."
7931
8135
  },
8136
+ {
8137
+ "name": "highlightMatches",
8138
+ "type": "boolean",
8139
+ "default": "true",
8140
+ "description": "Paints the query wherever it appears in a row's label (not its detail or hint), on the `highlight` / `highlight-foreground` pair, through the CSS Custom Highlight API — no `<mark>` in the row, and the accessible name is unchanged. A literal, case-insensitive match, so under a fuzzy `filter` a row that matched as a subsequence keeps an unpainted label. `false` turns it off. Applies to CommandMenuPanel too."
8141
+ },
7932
8142
  {
7933
8143
  "name": "listClassName",
7934
8144
  "type": "string",
@@ -7973,7 +8183,7 @@
7973
8183
  {
7974
8184
  "name": "CommandMenuTrigger",
7975
8185
  "type": "{ onOpen?: () => void; placeholder?: string; shortcut?: string | false; size?: \"sm\" | \"md\"; disabled?: boolean }",
7976
- "description": "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."
8186
+ "description": "The search field in a header that is really a button. Wears SearchInput's shell (the field fill and outline, 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."
7977
8187
  },
7978
8188
  {
7979
8189
  "name": "useCommandMenuShortcut",
@@ -7997,6 +8207,8 @@
7997
8207
  }
7998
8208
  ],
7999
8209
  "tokensUsed": [
8210
+ "highlight",
8211
+ "highlight-foreground",
8000
8212
  "card",
8001
8213
  "card-foreground",
8002
8214
  "border",
@@ -8919,8 +9131,8 @@
8919
9131
  "popover",
8920
9132
  "popover-foreground",
8921
9133
  "border",
8922
- "input",
8923
- "card",
9134
+ "field-border",
9135
+ "field",
8924
9136
  "ring"
8925
9137
  ],
8926
9138
  "a11y": [
@@ -10161,7 +10373,8 @@
10161
10373
  "accent",
10162
10374
  "accent-foreground",
10163
10375
  "ring",
10164
- "success"
10376
+ "success",
10377
+ "success-mark"
10165
10378
  ],
10166
10379
  "motion": [
10167
10380
  {
@@ -10176,6 +10389,12 @@
10176
10389
  "part": "Rating toggles",
10177
10390
  "why": "The rating row is a choice among a few similar marks. The eased selected fill is what makes the pick read as made, not just hovered."
10178
10391
  },
10392
+ {
10393
+ "slot": "response",
10394
+ "recipe": "motion-bloom",
10395
+ "part": "Chosen rating mark",
10396
+ "why": "The mark itself answers the pick: it blooms past its size and settles, so choosing reads as a small decision landing rather than a fill changing. Only the chosen mark, only on choosing — nothing blooms when the form opens or a pick is let go. Expressive: under subtle the fill alone answers."
10397
+ },
10179
10398
  {
10180
10399
  "slot": "transition",
10181
10400
  "recipe": "motion-tick",
@@ -11105,7 +11324,7 @@
11105
11324
  "generative"
11106
11325
  ],
11107
11326
  "usage": {
11108
- "description": "Use GradientArt where a real image belongs but none exists yet, or where the image is decoration: a product cover before upload, a profile banner, an album tile, an avatar with no photo. It reads the theme's own tokens, so it follows every preset, mode and live edit; `seed` fixes each item's composition so a list of covers or avatars stays individually recognisable.",
11327
+ "description": "Use GradientArt where a real image belongs but none exists yet, or where the image is decoration: a product cover before upload, a profile banner, an album tile, an avatar with no photo. It reads the theme's own tokens, so it follows every preset, mode and live edit; `seed` fixes each item's composition so a list of covers or avatars stays individually recognisable. `intensity=\"soft\"` paints the same art as a pastel wash — the iridescent aura AI products use as a card or modal header band, and as a stage for floating chips and logo tiles.",
11109
11328
  "bestPractices": [
11110
11329
  {
11111
11330
  "guidance": "do",
@@ -11119,6 +11338,14 @@
11119
11338
  "guidance": "do",
11120
11339
  "description": "Keep the default `mesh` for covers, tiles and avatars. Reach for `grain` where a printed, tactile surface suits — a banner, a hero card — and keep one variant per surface type so art reads as a system."
11121
11340
  },
11341
+ {
11342
+ "guidance": "do",
11343
+ "description": "Use `intensity=\"soft\"` as a stage: a card or modal header band, the art on an integration or pricing card, a field behind floating prompt chips or rounded logo tiles. Make the tiles and chips floating surfaces (`bg-popover` with `border-border-floating` and a shadow, or a `floating` Button) so they lift off the wash; soft stays light in dark mode too, so the same composition holds in both."
11344
+ },
11345
+ {
11346
+ "guidance": "dont",
11347
+ "description": "Do not put text straight on soft art with `text-overlay-foreground` ink. Soft is a light ground in every mode, so overlay ink washes out on it; put the content on a Card or a chip over the art, or use the default vivid intensity for a short overlay label."
11348
+ },
11122
11349
  {
11123
11350
  "guidance": "dont",
11124
11351
  "description": "Do not put body text over it. The art spans the palette's lightest and most saturated tones, so no single ink is guaranteed to clear contrast across it; reserve overlays for a short label on a scrim or an avatar's initials."
@@ -11157,6 +11384,12 @@
11157
11384
  "type": "string | number",
11158
11385
  "description": "Fixes the composition: the same seed paints the same art in every theme. Pass a stable id."
11159
11386
  },
11387
+ {
11388
+ "name": "intensity",
11389
+ "type": "\"vivid\" | \"soft\"",
11390
+ "default": "\"vivid\"",
11391
+ "description": "vivid: the preset's art at full strength. soft: every colour and the ground lifted to a light pastel of itself (high lightness, half chroma) — a wash for header bands and for staging chips and logo tiles, light in both modes."
11392
+ },
11160
11393
  {
11161
11394
  "name": "children",
11162
11395
  "type": "ReactNode",
@@ -11172,6 +11405,10 @@
11172
11405
  "variant": [
11173
11406
  "mesh",
11174
11407
  "grain"
11408
+ ],
11409
+ "intensity": [
11410
+ "vivid",
11411
+ "soft"
11175
11412
  ]
11176
11413
  },
11177
11414
  "tokensUsed": [
@@ -11237,6 +11474,11 @@
11237
11474
  "label": "Avatar with initials",
11238
11475
  "code": "<GradientArt seed=\"Mara Okafor\" className=\"grid size-10 place-items-center rounded-full text-label-md text-overlay-foreground\">MO</GradientArt>",
11239
11476
  "imports": "import { GradientArt } from \"@sableui/react\""
11477
+ },
11478
+ {
11479
+ "label": "Soft header band with logo tiles",
11480
+ "code": "<Card className=\"w-full max-w-sm\">\n\t<GradientArt intensity=\"soft\" seed=\"Integrations\" className=\"flex h-32 items-center justify-center gap-3 rounded-t-lg\">\n\t\t{[\"mail\", \"calendar\", \"chart\"].map((name) => (\n\t\t\t<span key={name} className=\"grid size-12 place-items-center rounded-lg border border-border-floating bg-popover text-popover-foreground shadow-md\">\n\t\t\t\t<Icon name={name} className=\"text-xl\" />\n\t\t\t</span>\n\t\t))}\n\t</GradientArt>\n\t<CardHeader>\n\t\t<CardTitle>Connect your tools</CardTitle>\n\t\t<CardDescription>Bring mail, calendar and analytics into one place.</CardDescription>\n\t</CardHeader>\n\t<CardFooter>\n\t\t<Button>Connect</Button>\n\t</CardFooter>\n</Card>",
11481
+ "imports": "import { Button, Card, CardDescription, CardFooter, CardHeader, CardTitle, GradientArt, Icon } from \"@sableui/react\""
11240
11482
  }
11241
11483
  ]
11242
11484
  },
@@ -11347,7 +11589,7 @@
11347
11589
  "group": "Forms",
11348
11590
  "register": "either",
11349
11591
  "path": "components/input",
11350
- "summary": "Styled native text input. Soft-square (rounded-sm), card surface, 2px focus ring; aria-invalid raises the destructive error treatment.",
11592
+ "summary": "Styled native text input. Soft-square (rounded-sm), the theme's field surface (outlined or filled), 2px focus ring; aria-invalid raises the destructive error treatment.",
11351
11593
  "keywords": [
11352
11594
  "input",
11353
11595
  "text input",
@@ -11378,6 +11620,14 @@
11378
11620
  "guidance": "do",
11379
11621
  "description": "Keep a focusable field at 16px or larger on mobile. iOS Safari zooms the whole page when a focused input computes under 16px and never zooms back out. Input handles this itself — below the sm breakpoint it renders text-body-lg (17px) and returns to text-body-md at sm and up. Build a custom field the same way: a second paired tier under a max-sm: variant, never a hand-set font-size."
11380
11622
  },
11623
+ {
11624
+ "guidance": "do",
11625
+ "description": "Choose outlined or filled fields once, in the theme: `--theme-field: var(--muted)` with `--theme-field-border: transparent` turns every Input, Textarea, Select, Combobox, SearchInput, InputGroup and ChatComposer into a borderless filled well; unset, they are outlined on the card. Every field reads the `field` / `field-border` pair, so they change together."
11626
+ },
11627
+ {
11628
+ "guidance": "dont",
11629
+ "description": "Restyle one Input as a filled chip with `bg-muted border-transparent`. It drifts from every other field on the page, and the theme can no longer move them together; set the field tokens instead."
11630
+ },
11381
11631
  {
11382
11632
  "guidance": "dont",
11383
11633
  "description": "Override an Input's type with a page tier below 16px (text-body-md, text-body-sm, text-label-*) without a >=16px mobile arm — that reintroduces the iOS focus zoom."
@@ -11464,8 +11714,8 @@
11464
11714
  ]
11465
11715
  },
11466
11716
  "tokensUsed": [
11467
- "input",
11468
- "card",
11717
+ "field-border",
11718
+ "field",
11469
11719
  "foreground",
11470
11720
  "muted-foreground",
11471
11721
  "ring",
@@ -11607,8 +11857,8 @@
11607
11857
  }
11608
11858
  ],
11609
11859
  "tokensUsed": [
11610
- "input",
11611
- "card",
11860
+ "field-border",
11861
+ "field",
11612
11862
  "ring",
11613
11863
  "destructive",
11614
11864
  "muted-foreground",
@@ -11879,6 +12129,217 @@
11879
12129
  }
11880
12130
  ]
11881
12131
  },
12132
+ {
12133
+ "name": "LikeButton",
12134
+ "status": "stable",
12135
+ "group": "Actions",
12136
+ "register": "either",
12137
+ "path": "components/like-button",
12138
+ "summary": "A pressed-or-not toggle for a like, a star or a save, with its count. Pressing it on fills the glyph, which blooms while a ring and a spray of dots burst from it and the count rolls up; pressing it off is quiet.",
12139
+ "keywords": [
12140
+ "like",
12141
+ "heart",
12142
+ "favorite",
12143
+ "favourite",
12144
+ "love",
12145
+ "star",
12146
+ "bookmark",
12147
+ "save",
12148
+ "upvote",
12149
+ "reaction",
12150
+ "count",
12151
+ "toggle"
12152
+ ],
12153
+ "usage": {
12154
+ "description": "Use LikeButton for a personal, reversible endorsement that other people can see the tally of: liking a post, starring a repository, saving a recipe. It is the one place in Sable that celebrates, because the person chose it and it costs nothing to undo.",
12155
+ "bestPractices": [
12156
+ {
12157
+ "guidance": "do",
12158
+ "description": "Pass the server's total as `count` and update it when the save lands. The button shows the viewer's own like at once, before the save, and goes back to `count` exactly whenever it changes, so a failed save that leaves `count` alone can also turn `pressed` back off."
12159
+ },
12160
+ {
12161
+ "guidance": "do",
12162
+ "description": "Match the glyph, family and label to the meaning: `icon=\"star\" tone=\"yellow\" label=\"Star\"` for a star. The label is the accessible name, so it must say what pressing does."
12163
+ },
12164
+ {
12165
+ "guidance": "do",
12166
+ "description": "Use `format={{ notation: \"compact\" }}` once counts pass a few thousand, so a feed of buttons keeps one width."
12167
+ },
12168
+ {
12169
+ "guidance": "dont",
12170
+ "description": "Do not use it for a setting or a view switch (bold, mute, grid or list). Those are tools, not endorsements, and a burst on every press of a formatting control is noise; use Toggle.",
12171
+ "instead": [
12172
+ "Toggle"
12173
+ ]
12174
+ },
12175
+ {
12176
+ "guidance": "dont",
12177
+ "description": "Do not use it for a rating or a verdict on an answer (helpful, not helpful). That is a question with two sides; use Feedback.",
12178
+ "instead": [
12179
+ "Feedback"
12180
+ ]
12181
+ },
12182
+ {
12183
+ "guidance": "dont",
12184
+ "description": "Do not press it for the user. A burst is a reply to a person's choice — liking something on their behalf, or on load, turns delight into a notification."
12185
+ }
12186
+ ]
12187
+ },
12188
+ "related": {
12189
+ "components": [
12190
+ "Toggle",
12191
+ "Feedback",
12192
+ "NumberTicker",
12193
+ "Button"
12194
+ ]
12195
+ },
12196
+ "props": [
12197
+ {
12198
+ "name": "count",
12199
+ "type": "number",
12200
+ "description": "The total, rolled when it changes. Pressing adds or removes the viewer's like at once; the button shows `count` exactly again whenever `count` changes. Omit it for a bare glyph (an icon-size button)."
12201
+ },
12202
+ {
12203
+ "name": "pressed",
12204
+ "type": "boolean",
12205
+ "description": "Controlled pressed state."
12206
+ },
12207
+ {
12208
+ "name": "defaultPressed",
12209
+ "type": "boolean",
12210
+ "default": "false",
12211
+ "description": "Initial pressed state when uncontrolled."
12212
+ },
12213
+ {
12214
+ "name": "onPressedChange",
12215
+ "type": "(pressed: boolean, event) => void",
12216
+ "description": "Called when the user presses it, with the new state."
12217
+ },
12218
+ {
12219
+ "name": "icon",
12220
+ "type": "string",
12221
+ "default": "\"heart\"",
12222
+ "description": "The glyph, by icon name. Its `solid` variant is shown while pressed."
12223
+ },
12224
+ {
12225
+ "name": "tone",
12226
+ "type": "PaletteColor",
12227
+ "default": "\"red\"",
12228
+ "description": "The palette family the glyph fills with while pressed, and the burst blooms from. A family, so it follows the theme."
12229
+ },
12230
+ {
12231
+ "name": "label",
12232
+ "type": "string",
12233
+ "default": "\"Like\"",
12234
+ "description": "The accessible name, read before the count."
12235
+ },
12236
+ {
12237
+ "name": "format",
12238
+ "type": "Intl.NumberFormatOptions",
12239
+ "description": "How to write the count, e.g. `{ notation: \"compact\" }`."
12240
+ },
12241
+ {
12242
+ "name": "variant",
12243
+ "type": "\"ghost\" | \"outline\" | \"secondary\"",
12244
+ "default": "\"ghost\"",
12245
+ "description": "The Button surface."
12246
+ },
12247
+ {
12248
+ "name": "size",
12249
+ "type": "\"xs\" | \"sm\" | \"md\"",
12250
+ "default": "\"sm\"",
12251
+ "description": "A Button size; without a count, the matching icon size."
12252
+ },
12253
+ {
12254
+ "name": "disabled",
12255
+ "type": "boolean",
12256
+ "default": "false",
12257
+ "description": "Disables it."
12258
+ },
12259
+ {
12260
+ "name": "motion",
12261
+ "type": "\"full\" | \"subtle\" | \"off\"",
12262
+ "default": "\"full\"",
12263
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The bloom and the burst are the flourish; the fill and the count changing stay."
12264
+ }
12265
+ ],
12266
+ "variants": {
12267
+ "variant": [
12268
+ "ghost",
12269
+ "outline",
12270
+ "secondary"
12271
+ ],
12272
+ "size": [
12273
+ "xs",
12274
+ "sm",
12275
+ "md"
12276
+ ],
12277
+ "tone": [
12278
+ "red",
12279
+ "orange",
12280
+ "yellow",
12281
+ "green",
12282
+ "teal",
12283
+ "blue",
12284
+ "purple",
12285
+ "pink"
12286
+ ]
12287
+ },
12288
+ "tokensUsed": [
12289
+ "palette-dot",
12290
+ "accent",
12291
+ "ring"
12292
+ ],
12293
+ "motion": [
12294
+ {
12295
+ "slot": "arrival",
12296
+ "recipe": "none",
12297
+ "part": "Root",
12298
+ "why": "It arrives with the post or row it belongs to. A page that loads with likes already given must be still: nothing blooms on mount."
12299
+ },
12300
+ {
12301
+ "slot": "response",
12302
+ "recipe": "motion-bloom",
12303
+ "part": "Glyph",
12304
+ "why": "Pressing it on is the moment. The glyph is 16px, so it needs real amplitude to read as an event: it grows from nothing past its size and settles, after a 90ms beat in which the ring leads. Inherits Button's motion-press for the press itself. Pressing off has no bloom — taking a like back is not a celebration."
12305
+ },
12306
+ {
12307
+ "slot": "transition",
12308
+ "recipe": "custom",
12309
+ "part": "Burst",
12310
+ "why": "The follow-through: <Burst> draws a disc that hollows into a ring and shifts colour, then dots thrown outward in the theme's palette families. Expressive only — under `subtle` or reduced motion it renders nothing and the fill alone answers the press. The count rolls with NumberTicker."
12311
+ },
12312
+ {
12313
+ "slot": "activity",
12314
+ "recipe": "none",
12315
+ "part": "Root",
12316
+ "why": "There is no in-between state to show: the like is shown at once, before it is saved."
12317
+ }
12318
+ ],
12319
+ "a11y": [
12320
+ "A native <button> through Button, with `aria-pressed`. Enter and Space press it.",
12321
+ "The accessible name is the label then the count (\"Like 1,301\"); the burst is aria-hidden and the rolling digits are hidden behind the count's real text.",
12322
+ "The pressed state is carried by the filled glyph and `aria-pressed`, never by colour alone.",
12323
+ "Under reduced motion the glyph fills and the count changes at once; the bloom and the burst are dropped."
12324
+ ],
12325
+ "examples": [
12326
+ {
12327
+ "label": "A post's likes",
12328
+ "code": "<LikeButton count={post.likes} defaultPressed={post.liked} onPressedChange={(on) => api.like(post.id, on)} />",
12329
+ "imports": "import { LikeButton } from \"@sableui/react\""
12330
+ },
12331
+ {
12332
+ "label": "Star a repository",
12333
+ "code": "<LikeButton icon=\"star\" tone=\"yellow\" label=\"Star\" variant=\"outline\" count={repo.stars} format={{ notation: \"compact\" }} />",
12334
+ "imports": "import { LikeButton } from \"@sableui/react\""
12335
+ },
12336
+ {
12337
+ "label": "Bare glyph",
12338
+ "code": "<LikeButton label=\"Save recipe\" />",
12339
+ "imports": "import { LikeButton } from \"@sableui/react\""
12340
+ }
12341
+ ]
12342
+ },
11882
12343
  {
11883
12344
  "name": "LineChart",
11884
12345
  "status": "beta",
@@ -12063,7 +12524,12 @@
12063
12524
  "popover",
12064
12525
  "popover-foreground",
12065
12526
  "foreground",
12066
- "background"
12527
+ "background",
12528
+ "tone-warning-mark",
12529
+ "tone-info-mark",
12530
+ "tone-1-mark",
12531
+ "tone-2-mark",
12532
+ "tone-3-mark"
12067
12533
  ],
12068
12534
  "motion": [
12069
12535
  {
@@ -13523,7 +13989,12 @@
13523
13989
  "capacity",
13524
13990
  "limit",
13525
13991
  "password strength",
13526
- "level"
13992
+ "level",
13993
+ "segmented",
13994
+ "tick bar",
13995
+ "score",
13996
+ "credits",
13997
+ "spend limit"
13527
13998
  ],
13528
13999
  "usage": {
13529
14000
  "description": "Use Meter to show a static measurement against a known maximum — storage used, seats taken, API quota, password strength. It is announced as a meter, not a progress bar, so a screen-reader user hears a level rather than a task underway.",
@@ -13550,6 +14021,17 @@
13550
14021
  {
13551
14022
  "guidance": "do",
13552
14023
  "description": "Use shape=\"ring\" when the reading is a score that belongs in a small, glanceable spot on a card. It is the same measurement drawn as a circular gauge, not a different component."
14024
+ },
14025
+ {
14026
+ "guidance": "do",
14027
+ "description": "Use `segments` (a tick bar, ▮▮▮▮▮▮▯▯▯▯) for a discrete budget, score or quota read at a glance — a spend limit, credits left, an ICP lead score, months of data coverage. Pick a count that matches the unit when there is one (10 for a score out of 10, 12 for months) and otherwise size it to the track: a tick reads as a tick when it is taller than it is wide, about one per 8px of track (40 across a card row, 20 in a narrow table cell). Any reading above min shows at least one tick."
14028
+ },
14029
+ {
14030
+ "guidance": "dont",
14031
+ "description": "Do not segment a fine-grained continuous value where the exact proportion matters (disk at 73.4%) — ticks round it to the nearest mark; use the continuous bar. And do not use a tick bar for a task in flight or steps completed: that is Progress, or a Stepper for named steps.",
14032
+ "instead": [
14033
+ "Progress"
14034
+ ]
13553
14035
  }
13554
14036
  ],
13555
14037
  "anatomy": [
@@ -13561,7 +14043,7 @@
13561
14043
  {
13562
14044
  "name": "MeterIndicator",
13563
14045
  "required": true,
13564
- "description": "The fill, sized from value and coloured by tone."
14046
+ "description": "The fill, sized from value and coloured by tone. Not used with `segments`: the track draws its ticks instead."
13565
14047
  },
13566
14048
  {
13567
14049
  "name": "MeterLabel",
@@ -13622,6 +14104,11 @@
13622
14104
  "type": "boolean",
13623
14105
  "description": "Print the value in the ring's centre (formatted like MeterValue)."
13624
14106
  },
14107
+ {
14108
+ "name": "segments",
14109
+ "type": "number",
14110
+ "description": "Draw the bar as this many separate ticks instead of a continuous fill (a quota, score or spend limit). Filled ticks = round(share × N), at least one for any reading above min, none at min. Ticks take the tone; empty ones sit on the rail colour. The segmented track has its own, taller sm / md / lg ladder. Bar only: ignored for shape=\"ring\"."
14111
+ },
13625
14112
  {
13626
14113
  "name": "strokeWidth",
13627
14114
  "type": "number",
@@ -13648,7 +14135,7 @@
13648
14135
  {
13649
14136
  "name": "MeterTrack",
13650
14137
  "type": "Base UI Track props + className + size",
13651
- "description": "The rail. Rendered automatically when Meter has no children; renders a MeterIndicator by default."
14138
+ "description": "The rail. Rendered automatically when Meter has no children; renders a MeterIndicator by default — or, when the Meter has `segments`, its row of ticks (children are not rendered then)."
13652
14139
  },
13653
14140
  {
13654
14141
  "name": "MeterIndicator",
@@ -13687,11 +14174,16 @@
13687
14174
  "tokensUsed": [
13688
14175
  "secondary",
13689
14176
  "primary",
14177
+ "tone-3",
13690
14178
  "success",
13691
14179
  "tone-warning",
13692
14180
  "destructive",
13693
14181
  "foreground",
13694
- "muted-foreground"
14182
+ "muted-foreground",
14183
+ "destructive-mark",
14184
+ "success-mark",
14185
+ "tone-warning-mark",
14186
+ "tone-3-mark"
13695
14187
  ],
13696
14188
  "motion": [
13697
14189
  {
@@ -13712,6 +14204,12 @@
13712
14204
  "part": "MeterIndicator",
13713
14205
  "why": "Width and colour ease on the motion tokens (no recipe animates width) when the reading changes — freeing space after a delete visibly shrinks the bar, so the user sees the cause and effect rather than a different bar."
13714
14206
  },
14207
+ {
14208
+ "slot": "transition",
14209
+ "recipe": "custom",
14210
+ "part": "MeterTrack (segments)",
14211
+ "why": "With segments, the fill runs through the ticks from the edge it is leaving — left to right as it grows, right to left as it shrinks — each tick easing its colour on the fast token after a stagger that is a share of the slow token (a full sweep is one slow duration) × --motion-expressive. Every tick switching at once would read as a different bar, not the same budget moving; `subtle` lands them together and off is instant."
14212
+ },
13715
14213
  {
13716
14214
  "slot": "activity",
13717
14215
  "recipe": "none",
@@ -13740,6 +14238,16 @@
13740
14238
  "code": "<Meter value={7.2} max={10} tone=\"warning\" getAriaValueText={(_, v) => `${v} of 10 gigabytes used`}>\n\t<div className=\"mb-2 flex flex-wrap items-center justify-between gap-x-3 gap-y-1\">\n\t\t<MeterLabel>Storage</MeterLabel>\n\t\t<MeterValue>{(_, value) => `${value} of 10 GB`}</MeterValue>\n\t</div>\n\t<MeterTrack />\n</Meter>",
13741
14239
  "imports": "import { Meter, MeterLabel, MeterTrack, MeterValue } from \"@sableui/react\""
13742
14240
  },
14241
+ {
14242
+ "label": "Segmented spend limit",
14243
+ "code": "<Meter value={1240} max={2000} segments={40} getAriaValueText={() => \"€1,240 of €2,000 spent this month\"}>\n\t<div className=\"mb-2 flex flex-wrap items-center justify-between gap-x-3 gap-y-1\">\n\t\t<MeterLabel>Monthly spend limit</MeterLabel>\n\t\t<MeterValue>{() => \"€1,240 / €2,000\"}</MeterValue>\n\t</div>\n\t<MeterTrack />\n</Meter>",
14244
+ "imports": "import { Meter, MeterLabel, MeterTrack, MeterValue } from \"@sableui/react\""
14245
+ },
14246
+ {
14247
+ "label": "Segmented score cell",
14248
+ "code": "<div className=\"flex items-center gap-3\">\n\t<Meter value={8} max={10} segments={10} size=\"sm\" tone=\"success\" className=\"w-24\" aria-label=\"ICP score\" getAriaValueText={(_, v) => `${v} out of 10`} />\n\t<span className=\"text-mono-sm text-foreground\">8/10</span>\n</div>",
14249
+ "imports": "import { Meter } from \"@sableui/react\""
14250
+ },
13743
14251
  {
13744
14252
  "label": "Tones",
13745
14253
  "code": "<div className=\"flex flex-col gap-3\">\n\t<Meter value={30} tone=\"success\" aria-label=\"Seats used\" />\n\t<Meter value={82} tone=\"warning\" aria-label=\"API quota used\" />\n\t<Meter value={100} tone=\"destructive\" aria-label=\"Storage used\" />\n</div>",
@@ -14118,7 +14626,11 @@
14118
14626
  "tone-info",
14119
14627
  "tone-warning",
14120
14628
  "success",
14121
- "destructive"
14629
+ "destructive",
14630
+ "destructive-mark",
14631
+ "success-mark",
14632
+ "tone-warning-mark",
14633
+ "tone-info-mark"
14122
14634
  ],
14123
14635
  "motion": [
14124
14636
  {
@@ -14418,8 +14930,8 @@
14418
14930
  }
14419
14931
  ],
14420
14932
  "tokensUsed": [
14421
- "input",
14422
- "card",
14933
+ "field-border",
14934
+ "field",
14423
14935
  "foreground",
14424
14936
  "muted-foreground",
14425
14937
  "accent",
@@ -14768,8 +15280,8 @@
14768
15280
  }
14769
15281
  ],
14770
15282
  "tokensUsed": [
14771
- "input",
14772
- "card",
15283
+ "field-border",
15284
+ "field",
14773
15285
  "foreground",
14774
15286
  "muted-foreground",
14775
15287
  "border-strong",
@@ -15911,7 +16423,12 @@
15911
16423
  "popover",
15912
16424
  "popover-foreground",
15913
16425
  "foreground",
15914
- "background"
16426
+ "background",
16427
+ "tone-warning-mark",
16428
+ "tone-info-mark",
16429
+ "tone-1-mark",
16430
+ "tone-2-mark",
16431
+ "tone-3-mark"
15915
16432
  ],
15916
16433
  "motion": [
15917
16434
  {
@@ -16639,7 +17156,12 @@
16639
17156
  "popover",
16640
17157
  "popover-foreground",
16641
17158
  "foreground",
16642
- "background"
17159
+ "background",
17160
+ "tone-warning-mark",
17161
+ "tone-info-mark",
17162
+ "tone-1-mark",
17163
+ "tone-2-mark",
17164
+ "tone-3-mark"
16643
17165
  ],
16644
17166
  "motion": [
16645
17167
  {
@@ -16863,7 +17385,12 @@
16863
17385
  "popover",
16864
17386
  "popover-foreground",
16865
17387
  "foreground",
16866
- "background"
17388
+ "background",
17389
+ "tone-warning-mark",
17390
+ "tone-info-mark",
17391
+ "tone-1-mark",
17392
+ "tone-2-mark",
17393
+ "tone-3-mark"
16867
17394
  ],
16868
17395
  "motion": [
16869
17396
  {
@@ -17235,6 +17762,178 @@
17235
17762
  }
17236
17763
  ]
17237
17764
  },
17765
+ {
17766
+ "name": "ReactionPicker",
17767
+ "status": "stable",
17768
+ "group": "Actions",
17769
+ "register": "either",
17770
+ "path": "components/reaction-picker",
17771
+ "summary": "The quick row of emoji you react to a message with: a toolbar of large emoji toggles that bloom in one after another as it opens and magnify under the pointer, with MenuReactions as the same row inside a right-click menu.",
17772
+ "keywords": [
17773
+ "reaction",
17774
+ "reactions",
17775
+ "emoji",
17776
+ "emoji picker",
17777
+ "react",
17778
+ "tapback",
17779
+ "quick reactions",
17780
+ "message actions",
17781
+ "chat"
17782
+ ],
17783
+ "usage": {
17784
+ "description": "Use ReactionPicker wherever a reader chooses a reaction for something: in a Popover from an add-reaction control (BubbleReactionAdd is that, ready-made), or as a hover bar over a message. Use MenuReactions for the same row as the first group of the menu a message opens on right-click or long-press, above Reply and Copy. Both report which emoji the reader has already used, so choosing one again removes it.",
17785
+ "bestPractices": [
17786
+ {
17787
+ "guidance": "do",
17788
+ "description": "Keep the quick set short — six to eight. It is the reactions people actually use; a full emoji catalogue belongs behind a trailing \"more\" control passed as children."
17789
+ },
17790
+ {
17791
+ "guidance": "do",
17792
+ "description": "Give every reaction a `label`. It is the accessible name and the menu's typeahead, and an emoji's own spoken name differs between platforms."
17793
+ },
17794
+ {
17795
+ "guidance": "do",
17796
+ "description": "Pass `selected` so the reader's own reactions show pressed, and toggle in `onSelect`: choosing a reaction you already gave takes it back."
17797
+ },
17798
+ {
17799
+ "guidance": "do",
17800
+ "description": "Offer the right-click menu and a visible add-reaction control together. A context menu is invisible until you know it is there; the pill is how a first-time reader finds reactions."
17801
+ },
17802
+ {
17803
+ "guidance": "dont",
17804
+ "description": "Do not put a ReactionPicker inside a Menu or ContextMenu. A toolbar is not a menu item, so the menu's arrow keys would skip it; use MenuReactions there.",
17805
+ "instead": [
17806
+ "MenuReactions"
17807
+ ]
17808
+ },
17809
+ {
17810
+ "guidance": "dont",
17811
+ "description": "Do not use it to rate an answer or collect a verdict. Reactions are social and many-valued; a helpful / not-helpful question is Feedback.",
17812
+ "instead": [
17813
+ "Feedback"
17814
+ ]
17815
+ }
17816
+ ],
17817
+ "anatomy": [
17818
+ {
17819
+ "name": "ReactionPicker",
17820
+ "required": true,
17821
+ "description": "The toolbar: one tab stop, arrow keys move between emoji, each a toggle with `aria-pressed`."
17822
+ },
17823
+ {
17824
+ "name": "MenuReactions",
17825
+ "required": false,
17826
+ "description": "The same row as a group of checkbox menu items, for MenuContent and ContextMenuContent. Choosing one closes the menu."
17827
+ }
17828
+ ]
17829
+ },
17830
+ "related": {
17831
+ "components": [
17832
+ "Bubble",
17833
+ "ContextMenu",
17834
+ "Popover",
17835
+ "Toolbar",
17836
+ "Feedback"
17837
+ ]
17838
+ },
17839
+ "props": [
17840
+ {
17841
+ "name": "reactions",
17842
+ "type": "readonly { emoji: string; label: string }[]",
17843
+ "default": "DEFAULT_REACTIONS",
17844
+ "description": "The choices, in order. The default is 👍 ❤️ 😂 🎉 😮 😢 🙏."
17845
+ },
17846
+ {
17847
+ "name": "selected",
17848
+ "type": "readonly string[]",
17849
+ "default": "[]",
17850
+ "description": "The emoji the reader has already reacted with: shown pressed, and choosing one again removes it."
17851
+ },
17852
+ {
17853
+ "name": "onSelect",
17854
+ "type": "(emoji: string) => void",
17855
+ "description": "Called with the emoji the reader chose. Toggle it in or out of `selected`."
17856
+ },
17857
+ {
17858
+ "name": "children",
17859
+ "type": "ReactNode",
17860
+ "description": "ReactionPicker only: trailing controls after the emoji, such as a button that opens a full emoji picker."
17861
+ },
17862
+ {
17863
+ "name": "aria-label",
17864
+ "type": "string",
17865
+ "default": "\"Reactions\"",
17866
+ "description": "The name of the toolbar or menu group."
17867
+ },
17868
+ {
17869
+ "name": "motion",
17870
+ "type": "\"full\" | \"subtle\" | \"off\"",
17871
+ "default": "\"full\"",
17872
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps the press and drops the wave of emoji blooming in and the magnify, `off` stills it. It can only turn motion down."
17873
+ }
17874
+ ],
17875
+ "subcomponents": [
17876
+ {
17877
+ "name": "MenuReactions",
17878
+ "type": "{ reactions?; selected?; onSelect?; motion? } & ComponentProps<typeof Menu.Group>",
17879
+ "description": "The quick row as checkbox menu items, for the first group of a MenuContent or ContextMenuContent. Same props as ReactionPicker, without children."
17880
+ }
17881
+ ],
17882
+ "tokensUsed": [
17883
+ "accent",
17884
+ "primary",
17885
+ "ring"
17886
+ ],
17887
+ "motion": [
17888
+ {
17889
+ "slot": "arrival",
17890
+ "recipe": "motion-bloom",
17891
+ "part": "Glyph",
17892
+ "why": "Opening the picker is the reader's own action, so the emoji answer it: each blooms in 24ms after the one before, a short wave from first to last. The surface it sits in (a Popover or a menu) arrives with its own motion-pop. Expressive only — under `subtle` the emoji are simply there."
17893
+ },
17894
+ {
17895
+ "slot": "response",
17896
+ "recipe": "custom",
17897
+ "part": "Glyph",
17898
+ "why": "The emoji under the pointer or the keyboard highlight magnifies to 1.25 on a fast standard ease, so you can see which one a release will choose; each cell also carries motion-interactive and motion-press. The magnify rides an inner layer so it never fights the bloom over `scale`."
17899
+ },
17900
+ {
17901
+ "slot": "transition",
17902
+ "recipe": "motion-interactive",
17903
+ "part": "Cell",
17904
+ "why": "Pressed and not pressed differ by a tint, eased by motion-interactive. The celebration belongs to the reaction that lands under the message (BubbleReaction), not to the picker that chose it."
17905
+ },
17906
+ {
17907
+ "slot": "activity",
17908
+ "recipe": "none",
17909
+ "part": "Root",
17910
+ "why": "Choosing is instant; there is no in-between state to show."
17911
+ }
17912
+ ],
17913
+ "a11y": [
17914
+ "ReactionPicker is a Base UI toolbar: one tab stop, Left/Right move between emoji, Home/End jump to the ends.",
17915
+ "Each emoji is a button named by its `label` and reporting `aria-pressed`, so a screen-reader user hears whether choosing it adds or removes their reaction. The glyph itself is aria-hidden.",
17916
+ "MenuReactions renders checkbox menu items (`menuitemcheckbox` with `aria-checked`), reached by the menu's arrow keys and typeahead like every other row.",
17917
+ "Under reduced motion the emoji appear at once and do not magnify; pressed state is carried by `aria-pressed` and a tint, never motion."
17918
+ ],
17919
+ "examples": [
17920
+ {
17921
+ "label": "In a popover",
17922
+ "code": "<Popover>\n\t<PopoverTrigger render={<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Add reaction\" />}>\n\t\t<Icon name=\"emoji\" />\n\t</PopoverTrigger>\n\t<PopoverContent side=\"top\" className=\"w-auto rounded-full p-1\">\n\t\t<ReactionPicker selected={mine} onSelect={toggle} />\n\t</PopoverContent>\n</Popover>",
17923
+ "imports": "import { Button, Icon, Popover, PopoverContent, PopoverTrigger, ReactionPicker } from \"@sableui/react\""
17924
+ },
17925
+ {
17926
+ "label": "In a right-click menu",
17927
+ "code": "<ContextMenu>\n\t<ContextMenuTrigger render={<Bubble />}>See you at 6.</ContextMenuTrigger>\n\t<ContextMenuContent>\n\t\t<MenuReactions selected={mine} onSelect={toggle} />\n\t\t<ContextMenuSeparator />\n\t\t<ContextMenuItem>Reply</ContextMenuItem>\n\t\t<ContextMenuItem>Copy text</ContextMenuItem>\n\t</ContextMenuContent>\n</ContextMenu>",
17928
+ "imports": "import { Bubble, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, MenuReactions } from \"@sableui/react\""
17929
+ },
17930
+ {
17931
+ "label": "A custom set",
17932
+ "code": "<ReactionPicker\n\treactions={[\n\t\t{ emoji: \"✅\", label: \"Done\" },\n\t\t{ emoji: \"👀\", label: \"Looking\" },\n\t\t{ emoji: \"🚀\", label: \"Shipped\" },\n\t]}\n\tselected={mine}\n\tonSelect={toggle}\n/>",
17933
+ "imports": "import { ReactionPicker } from \"@sableui/react\""
17934
+ }
17935
+ ]
17936
+ },
17238
17937
  {
17239
17938
  "name": "RelativeTime",
17240
17939
  "status": "stable",
@@ -17719,7 +18418,7 @@
17719
18418
  "group": "Forms",
17720
18419
  "register": "either",
17721
18420
  "path": "components/search-input",
17722
- "summary": "Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), card surface, 2px focus ring; the clear button appears only when there is a value.",
18421
+ "summary": "Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), the theme's field surface, 2px focus ring; the clear button appears only when there is a value.",
17723
18422
  "keywords": [
17724
18423
  "search",
17725
18424
  "input",
@@ -17741,6 +18440,10 @@
17741
18440
  "guidance": "do",
17742
18441
  "description": "When a button stands in for search (a header trigger that opens CommandMenu with ⌘K), use CommandMenuTrigger. It wears this field's shell from the same source, so the two cannot drift; a hand-styled button already did once, into a borderless chip that matched neither the field nor a button."
17743
18442
  },
18443
+ {
18444
+ "guidance": "do",
18445
+ "description": "Light the query in the results it filters with `useMatchHighlight(query)` — a callback ref for the results container, with `selector` to keep it to titles. It paints on the `highlight` pair without wrapping matches in `<mark>`, the same paint Combobox and CommandMenu use for their rows."
18446
+ },
17744
18447
  {
17745
18448
  "guidance": "dont",
17746
18449
  "description": "Do not use SearchInput as a command palette; use CommandMenu for global commands, opened from the header by CommandMenuTrigger. A SearchInput that opens a dialog on focus is a text box you cannot type in."
@@ -17808,8 +18511,8 @@
17808
18511
  }
17809
18512
  ],
17810
18513
  "tokensUsed": [
17811
- "input",
17812
- "card",
18514
+ "field-border",
18515
+ "field",
17813
18516
  "foreground",
17814
18517
  "muted-foreground",
17815
18518
  "ring",
@@ -17864,6 +18567,11 @@
17864
18567
  "code": "<Field label=\"Search posts\"><SearchInput defaultValue=\"roadmap\" className=\"max-w-80\" /></Field>",
17865
18568
  "imports": "import { Field, SearchInput } from \"@sableui/react\""
17866
18569
  },
18570
+ {
18571
+ "label": "Highlighting matches in the results",
18572
+ "code": "const [q, setQ] = useState(\"\")\nconst results = useMatchHighlight<HTMLUListElement>(q, { selector: \"[data-title]\" })\nconst shown = posts.filter((post) => post.title.toLowerCase().includes(q.trim().toLowerCase()))\n\n<SearchInput value={q} onValueChange={setQ} placeholder=\"Search posts\" />\n<ul ref={results}>\n\t{shown.map((post) => (\n\t\t<li key={post.id}><span data-title>{post.title}</span></li>\n\t))}\n</ul>",
18573
+ "imports": "import { SearchInput, useMatchHighlight } from \"@sableui/react\""
18574
+ },
17867
18575
  {
17868
18576
  "label": "Disabled",
17869
18577
  "code": "<SearchInput disabled placeholder=\"Search\" />",
@@ -18173,7 +18881,14 @@
18173
18881
  "muted-foreground",
18174
18882
  "success",
18175
18883
  "tone-warning",
18176
- "destructive"
18884
+ "destructive",
18885
+ "destructive-mark",
18886
+ "success-mark",
18887
+ "tone-warning-mark",
18888
+ "tone-info-mark",
18889
+ "tone-1-mark",
18890
+ "tone-2-mark",
18891
+ "tone-3-mark"
18177
18892
  ],
18178
18893
  "motion": [
18179
18894
  {
@@ -18359,8 +19074,8 @@
18359
19074
  }
18360
19075
  ],
18361
19076
  "tokensUsed": [
18362
- "input",
18363
- "card",
19077
+ "field-border",
19078
+ "field",
18364
19079
  "foreground",
18365
19080
  "ring",
18366
19081
  "border",
@@ -19084,6 +19799,10 @@
19084
19799
  "guidance": "do",
19085
19800
  "description": "Set surface=\"page\" when a full-width top bar sits above the rail. A theme's tinted or dark rail is for a full-height rail that frames the app."
19086
19801
  },
19802
+ {
19803
+ "guidance": "do",
19804
+ "description": "Keep surface=\"rail\" inside AppShell frame=\"inset\": the rail's tint becomes the ground the main panel floats on. Do not add a border-r class back — the gap is the divider."
19805
+ },
19087
19806
  {
19088
19807
  "guidance": "dont",
19089
19808
  "description": "Do not use Sidebar for page-local settings, filters, or temporary action groups."
@@ -19136,7 +19855,7 @@
19136
19855
  "name": "surface",
19137
19856
  "type": "\"rail\" | \"page\"",
19138
19857
  "default": "\"rail\"",
19139
- "description": "Which surface the rail is painted on. \"rail\" is its own --sidebar-* surface with a border — right for a full-height rail that frames the app, which a theme may tint or paint dark. \"page\" uses the page's surface and ink with no border — use it when a full-width header sits above the rail, where a tinted column starting under a page-coloured bar reads as pasted on. Use it instead of overriding bg-/text- classes, which the rail's re-pointed tokens defeat."
19858
+ "description": "Which surface the rail is painted on. \"rail\" is its own --sidebar-* surface with a border — right for a full-height rail that frames the app, which a theme may tint or paint dark. \"page\" uses the page's surface and ink with no border — use it when a full-width header sits above the rail, where a tinted column starting under a page-coloured bar reads as pasted on. Use it instead of overriding bg-/text- classes, which the rail's re-pointed tokens defeat. In AppShell's sidebar slot with frame=\"inset\", the rail's surface is also the shell's ground: from lg a \"rail\" rail drops its border (the gap around the main panel divides them) and either surface stretches to the shell's body row instead of sizing to the viewport. AppShell signals this to its sidebar slot only, so a Sidebar elsewhere in the page is unaffected."
19140
19859
  },
19141
19860
  {
19142
19861
  "name": "motion",
@@ -19174,7 +19893,8 @@
19174
19893
  "sidebar-border",
19175
19894
  "sidebar-accent",
19176
19895
  "sidebar-accent-foreground",
19177
- "sidebar-ring"
19896
+ "sidebar-ring",
19897
+ "shell-ground"
19178
19898
  ],
19179
19899
  "motion": [
19180
19900
  {
@@ -19875,7 +20595,14 @@
19875
20595
  "tone-warning",
19876
20596
  "muted-foreground",
19877
20597
  "success",
19878
- "destructive"
20598
+ "destructive",
20599
+ "destructive-mark",
20600
+ "success-mark",
20601
+ "tone-warning-mark",
20602
+ "tone-info-mark",
20603
+ "tone-1-mark",
20604
+ "tone-2-mark",
20605
+ "tone-3-mark"
19879
20606
  ],
19880
20607
  "motion": [
19881
20608
  {
@@ -20288,7 +21015,14 @@
20288
21015
  "tone-info",
20289
21016
  "tone-1",
20290
21017
  "tone-2",
20291
- "destructive"
21018
+ "destructive",
21019
+ "destructive-mark",
21020
+ "success-mark",
21021
+ "tone-warning-mark",
21022
+ "tone-info-mark",
21023
+ "tone-1-mark",
21024
+ "tone-2-mark",
21025
+ "tone-3-mark"
20292
21026
  ],
20293
21027
  "a11y": [
20294
21028
  "Rendered aria-hidden — purely decorative. Convey status in adjacent text, not color alone.",
@@ -20446,7 +21180,8 @@
20446
21180
  "border-strong",
20447
21181
  "destructive",
20448
21182
  "destructive-foreground",
20449
- "ring"
21183
+ "ring",
21184
+ "destructive-mark"
20450
21185
  ],
20451
21186
  "motion": [
20452
21187
  {
@@ -20928,13 +21663,13 @@
20928
21663
  },
20929
21664
  {
20930
21665
  "name": "TableHead",
20931
- "type": "ComponentPropsWithRef<\"th\">",
20932
- "description": "A column header cell (th) in muted label styling."
21666
+ "type": "ComponentPropsWithRef<\"th\"> & { numeric?: boolean }",
21667
+ "description": "A column header cell (th) in muted label styling. `numeric` marks a number column: end-aligned, tabular figures — set it on the column's cells too."
20933
21668
  },
20934
21669
  {
20935
21670
  "name": "TableCell",
20936
- "type": "ComponentPropsWithRef<\"td\">",
20937
- "description": "A data cell (td)."
21671
+ "type": "ComponentPropsWithRef<\"td\"> & { numeric?: boolean }",
21672
+ "description": "A data cell (td). `numeric` end-aligns it with tabular figures, matching its column's header."
20938
21673
  },
20939
21674
  {
20940
21675
  "name": "TableCaption",
@@ -21186,7 +21921,7 @@
21186
21921
  "group": "Forms",
21187
21922
  "register": "either",
21188
21923
  "path": "components/textarea",
21189
- "summary": "Styled native multi-line text control. Soft-square, card surface, --ring focus, and an aria-invalid destructive error state.",
21924
+ "summary": "Styled native multi-line text control. Soft-square, the theme's field surface, --ring focus, and an aria-invalid destructive error state.",
21190
21925
  "keywords": [
21191
21926
  "textarea",
21192
21927
  "multi line input",
@@ -21259,8 +21994,8 @@
21259
21994
  }
21260
21995
  ],
21261
21996
  "tokensUsed": [
21262
- "input",
21263
- "card",
21997
+ "field-border",
21998
+ "field",
21264
21999
  "foreground",
21265
22000
  "muted-foreground",
21266
22001
  "ring",
@@ -21953,7 +22688,7 @@
21953
22688
  "name": "type (toast option)",
21954
22689
  "type": "\"success\" | \"error\" | \"warning\" | \"info\" | \"loading\"",
21955
22690
  "default": "—",
21956
- "description": "Set by the toast.* helpers. Drives a semantic left accent (success→success, error→destructive, warning→tone-warning, info→tone-info); loading shows a spinner. Both loading and error persist until updated or dismissed unless the call supplies a timeout."
22691
+ "description": "Set by the toast.* helpers. Drives a leading status glyph from the shared tone list, the same one Note and InlineStatus use (success→success, error→destructive, warning→warning, info→info), which blooms in as the toast lands; loading shows a spinner, and the glyph blooms in its place when the toast settles. Both loading and error persist until updated or dismissed unless the call supplies a timeout."
21957
22692
  }
21958
22693
  ],
21959
22694
  "subcomponents": [
@@ -22028,6 +22763,12 @@
22028
22763
  "part": "ToastRoot (stacked)",
22029
22764
  "why": "A stacked toast arrives from the anchored edge in front of the others, which step back and shrink to make room; hovering fans the deck out into the list. Moving every card, rather than snapping them, shows that the new one went on top and the old ones are still there."
22030
22765
  },
22766
+ {
22767
+ "slot": "response",
22768
+ "recipe": "motion-bloom",
22769
+ "part": "Status glyph",
22770
+ "why": "The glyph is what says how it went, so it lands as its own beat: it blooms in 120ms after the card, and blooms again in the spinner's place when a loading toast settles. A 16px glyph needs that amplitude to read as an answer; the card itself only slides."
22771
+ },
22031
22772
  {
22032
22773
  "slot": "activity",
22033
22774
  "recipe": "none",
@@ -24111,6 +24852,31 @@
24111
24852
  "imports": "import { useState } from \"react\"\nimport { Button, Card, CardContent, cn, Entity, Icon, MediaImage, Slider } from \"@sableui/react\"",
24112
24853
  "code": "// `cover` is the artwork URL; left empty, the cover paints art seeded by the title.\nconst track = { title: \"Low tide, high noon\", artist: \"Juno Avery\", cover: \"\", duration: 220, position: 84 }\n\n/** Seconds → m:ss, the way the readout and the screen reader both say it. */\nfunction formatTime(seconds: number) {\n\treturn `${Math.floor(seconds / 60)}:${String(Math.floor(seconds % 60)).padStart(2, \"0\")}`\n}\n\n\nexport interface MediaPlayerBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a Sheet or muted section that already frames it. */\n\tframed?: boolean\n\t/** Called with the new playing state. */\n\tonPlayingChange?: (playing: boolean) => void\n\tonPrevious?: () => void\n\tonNext?: () => void\n}\n\nexport function MediaPlayerBlock({ className, framed = true, onPlayingChange, onPrevious, onNext }: MediaPlayerBlockProps) {\n\tconst [playing, setPlaying] = useState(true)\n\tconst [position, setPosition] = useState(track.position)\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs\", className)}>\n\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t{/* Entity's own row padding is for lists; inside a card's content the\n\t\t\t\t card already pads it. */}\n\t\t\t\t<Entity\n\t\t\t\t\tclassName=\"p-0\"\n\t\t\t\t\tleading={<MediaImage src={track.cover} seed={track.title} className=\"size-14 shrink-0 rounded-md\" />}\n\t\t\t\t\ttitle={track.title}\n\t\t\t\t\tsubtitle={track.artist}\n\t\t\t\t\taction={\n\t\t\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Like\">\n\t\t\t\t\t\t\t<Icon name=\"heart\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t<div className=\"flex flex-col gap-1.5\">\n\t\t\t\t\t<Slider\n\t\t\t\t\t\tvalue={position}\n\t\t\t\t\t\tmax={track.duration}\n\t\t\t\t\t\tonValueChange={setPosition}\n\t\t\t\t\t\taria-label=\"Position\"\n\t\t\t\t\t\t// Spoken as a time, not \"38\": the number alone means nothing.\n\t\t\t\t\t\tgetAriaValueText={(_, value) => `${formatTime(value)} of ${formatTime(track.duration)}`}\n\t\t\t\t\t/>\n\t\t\t\t\t<div className=\"flex justify-between text-mono-caption text-muted-foreground\">\n\t\t\t\t\t\t<span>{formatTime(position)}</span>\n\t\t\t\t\t\t<span>-{formatTime(track.duration - position)}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"flex items-center justify-center gap-3\">\n\t\t\t\t\t<Button variant=\"ghost\" size=\"icon\" aria-label=\"Previous\" onClick={onPrevious}>\n\t\t\t\t\t\t<Icon name=\"previous\" variant=\"solid\" />\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\taria-label={playing ? \"Pause\" : \"Play\"}\n\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\tsetPlaying(!playing)\n\t\t\t\t\t\t\tonPlayingChange?.(!playing)\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Icon name={playing ? \"pause\" : \"play\"} variant=\"solid\" />\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button variant=\"ghost\" size=\"icon\" aria-label=\"Next\" onClick={onNext}>\n\t\t\t\t\t\t<Icon name=\"next\" variant=\"solid\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t</Card>\n\t)\n}"
24113
24854
  },
24855
+ {
24856
+ "name": "MessageReactionsBlock",
24857
+ "path": "blocks/message-reactions-block.tsx",
24858
+ "summary": "A short thread you can react to: right-click or long-press a bubble for a menu with a row of quick reactions above Reply and Copy, or use the add-reaction pill under a message. A new reaction blooms in and sprays copies of its emoji.",
24859
+ "intent": "Reach for this when a conversation excerpt should be reactable — a team thread, a comment card, a support transcript. A read-only excerpt is ChatThreadBlock; a working chat with a composer and history is the ChatPanel page shell.",
24860
+ "family": "Social & messaging",
24861
+ "kind": "card",
24862
+ "width": {
24863
+ "min": "18rem",
24864
+ "max": "24rem"
24865
+ },
24866
+ "notes": [
24867
+ "Two ways in, on purpose: the right-click / long-press menu is fast once you know it is there, and the add-reaction pill is how a first-time reader finds reactions at all.",
24868
+ "The quick row in the menu is MenuReactions (checkbox menu items) and the one in the pill's popover is ReactionPicker (a toolbar), so each is reachable by the keyboard model of the surface it sits in.",
24869
+ "Only the reader's own reaction celebrates. Counts from other people roll quietly, and the reactions a page loads with never play.",
24870
+ "The add-reaction pill appears only on messages that already have reactions; a message with none is reacted to from its menu, which keeps quiet rows quiet."
24871
+ ],
24872
+ "a11y": [
24873
+ "Each bubble is focusable, and its menu opens on right click, long press, Shift+F10 or the Menu key; its reactions are checkbox items reporting whether the reader has used each one.",
24874
+ "Each reaction pill is a toggle named by its meaning and count (\"Party, 3\") with `aria-pressed`; the emoji and the burst are hidden from assistive tech.",
24875
+ "Under reduced motion the reaction appears and the count changes at once; nothing else is lost."
24876
+ ],
24877
+ "imports": "import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions, Card, CardContent, CardFooter, cn, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, DEFAULT_REACTIONS, MenuReactions, Message } from \"@sableui/react\"",
24878
+ "code": "const messages: { id: string; text: string; mine: boolean; reactions: Record<string, number> }[] = [\n\t{ id: \"m1\", text: \"The new onboarding flow is live on staging 🚀\", mine: false, reactions: { \"🎉\": 2, \"👀\": 1 } },\n\t{ id: \"m2\", text: \"Clicked through it twice. The progress bar is so smooth.\", mine: true, reactions: {} },\n\t{ id: \"m3\", text: \"Ship it Friday?\", mine: false, reactions: { \"👍\": 1 } },\n]\nconst hint = \"Right-click or long-press a message to react.\"\n// Every emoji that can appear needs a name: it is the pill's accessible label.\nconst labels: Record<string, string> = {\n\t...Object.fromEntries(DEFAULT_REACTIONS.map((r) => [r.emoji, r.label])),\n\t\"👀\": \"Eyes\",\n}\n\nexport interface MessageReactionsBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border. Keep it framed on a muted page: incoming bubbles are the muted surface too. */\n\tframed?: boolean\n\t/** Called when the reader adds or removes a reaction, with the message, the emoji and whether it is now on. */\n\tonReact?: (messageId: string, emoji: string, on: boolean) => void\n}\n\nexport function MessageReactionsBlock({ className, framed = true, onReact }: MessageReactionsBlockProps) {\n\t// The reader's own reactions, per message. Everyone else's are the counts above.\n\tconst [mine, setMine] = useState<Record<string, string[]>>({ m1: [\"🎉\"] })\n\tconst toggle = (messageId: string, emoji: string) => {\n\t\tconst on = !(mine[messageId] ?? []).includes(emoji)\n\t\t// Decided again from `prev`, so two toggles in one batch cannot add an emoji twice.\n\t\tsetMine((prev) => {\n\t\t\tconst current = prev[messageId] ?? []\n\t\t\tconst next = current.includes(emoji) ? current.filter((e) => e !== emoji) : [...current, emoji]\n\t\t\treturn { ...prev, [messageId]: next }\n\t\t})\n\t\tonReact?.(messageId, emoji, on)\n\t}\n\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-sm\", className)}>\n\t\t\t<CardContent className=\"flex flex-col gap-3\">\n\t\t\t\t{messages.map((m) => {\n\t\t\t\t\tconst align = m.mine ? \"end\" : \"start\"\n\t\t\t\t\tconst chosen = mine[m.id] ?? []\n\t\t\t\t\t// Others' counts, plus the reader's own: a reaction only the reader gave\n\t\t\t\t\t// is a pill of its own, ordered after the ones already there.\n\t\t\t\t\tconst counts: Record<string, number> = { ...m.reactions }\n\t\t\t\t\tfor (const emoji of chosen) counts[emoji] = (counts[emoji] ?? 0)\n\t\t\t\t\tconst pills = Object.entries(counts).map(([emoji, others]) => ({\n\t\t\t\t\t\temoji,\n\t\t\t\t\t\tactive: chosen.includes(emoji),\n\t\t\t\t\t\tcount: others + (chosen.includes(emoji) ? 1 : 0),\n\t\t\t\t\t}))\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<Message key={m.id} messageId={m.id} align={m.mine ? \"end\" : undefined}>\n\t\t\t\t\t\t\t<ContextMenu>\n\t\t\t\t\t\t\t\t<ContextMenuTrigger\n\t\t\t\t\t\t\t\t\t// Focusable, so Shift+F10 or the Menu key opens the same menu.\n\t\t\t\t\t\t\t\t\trender={<Bubble tabIndex={0} variant={m.mine ? \"primary\" : undefined} align={align} />}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{m.text}\n\t\t\t\t\t\t\t\t</ContextMenuTrigger>\n\t\t\t\t\t\t\t\t<ContextMenuContent>\n\t\t\t\t\t\t\t\t\t<MenuReactions selected={chosen} onSelect={(emoji) => toggle(m.id, emoji)} />\n\t\t\t\t\t\t\t\t\t<ContextMenuSeparator />\n\t\t\t\t\t\t\t\t\t<ContextMenuItem>Reply</ContextMenuItem>\n\t\t\t\t\t\t\t\t\t<ContextMenuItem>Copy text</ContextMenuItem>\n\t\t\t\t\t\t\t\t</ContextMenuContent>\n\t\t\t\t\t\t\t</ContextMenu>\n\t\t\t\t\t\t\t{/* Rendered even while empty (it hides itself), so the first reaction still celebrates. */}\n\t\t\t\t\t\t\t<BubbleReactions align={align}>\n\t\t\t\t\t\t\t\t{pills.map((p) => (\n\t\t\t\t\t\t\t\t\t<BubbleReaction\n\t\t\t\t\t\t\t\t\t\tkey={p.emoji}\n\t\t\t\t\t\t\t\t\t\temoji={p.emoji}\n\t\t\t\t\t\t\t\t\t\tlabel={labels[p.emoji]}\n\t\t\t\t\t\t\t\t\t\tcount={p.count}\n\t\t\t\t\t\t\t\t\t\tactive={p.active}\n\t\t\t\t\t\t\t\t\t\tonClick={() => toggle(m.id, p.emoji)}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t{pills.length > 0 ? (\n\t\t\t\t\t\t\t\t\t<BubbleReactionAdd selected={chosen} onSelect={(emoji) => toggle(m.id, emoji)} />\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</BubbleReactions>\n\t\t\t\t\t\t</Message>\n\t\t\t\t\t)\n\t\t\t\t})}\n\t\t\t</CardContent>\n\t\t\t<CardFooter className=\"text-caption text-muted-foreground\">{hint}</CardFooter>\n\t\t</Card>\n\t)\n}"
24879
+ },
24114
24880
  {
24115
24881
  "name": "NewsletterBlock",
24116
24882
  "path": "blocks/newsletter-block.tsx",
@@ -24636,7 +25402,7 @@
24636
25402
  "Amounts keep their sign in text (+49.00 / -850.00), so the direction does not depend on colour."
24637
25403
  ],
24638
25404
  "imports": "import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, Entity, Icon } from \"@sableui/react\"",
24639
- "code": "const balance = \"$2,418.50\"\n\nconst activity = [\n\t{ title: \"Subscription\", detail: \"Team plan · 2h ago\", amount: \"+49.00\", tone: \"1\" },\n\t{ title: \"Refund\", detail: \"Order 1042 · Yesterday\", amount: \"-24.00\", tone: \"2\" },\n\t{ title: \"Payout\", detail: \"To checking · Mon\", amount: \"-850.00\", tone: \"3\" },\n]\n\nexport interface WalletBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a Sheet or muted section that already frames it. */\n\tframed?: boolean\n\tonSend?: () => void\n\tonReceive?: () => void\n}\n\nexport function WalletBlock({ className, framed = true, onSend, onReceive }: WalletBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardDescription>Balance</CardDescription>\n\t\t\t\t<CardTitle className=\"text-mono-xl\">{balance}</CardTitle>\n\t\t\t</CardHeader>\n\t\t\t<CardContent className=\"flex flex-col gap-3\">\n\t\t\t\t<div className=\"grid grid-cols-2 gap-2\">\n\t\t\t\t\t<Button iconStart={<Icon name=\"arrow-up\" />} onClick={onSend}>\n\t\t\t\t\t\tSend\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button variant=\"secondary\" iconStart={<Icon name=\"arrow-down\" />} onClick={onReceive}>\n\t\t\t\t\t\tReceive\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"flex flex-col\">\n\t\t\t\t\t{activity.map((item) => (\n\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\tkey={item.title}\n\t\t\t\t\t\t\tclassName=\"px-0\"\n\t\t\t\t\t\t\tleading={\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\taria-hidden\n\t\t\t\t\t\t\t\t\tclassName=\"block size-8 rounded-full\"\n\t\t\t\t\t\t\t\t\t// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look\n\t\t\t\t\t\t\t\t\tstyle={{ background: `oklch(from var(--tone-${item.tone}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h)` }}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttitle={item.title}\n\t\t\t\t\t\t\tsubtitle={item.detail}\n\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t<span className={cn(\"text-mono-md\", item.amount.startsWith(\"+\") ? \"text-success\" : \"text-muted-foreground\")}>\n\t\t\t\t\t\t\t\t\t{item.amount}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t</Card>\n\t)\n}"
25405
+ "code": "const balance = \"$2,418.50\"\n\nconst activity = [\n\t{ title: \"Subscription\", detail: \"Team plan · 2h ago\", amount: \"+49.00\", tone: \"1\" },\n\t{ title: \"Refund\", detail: \"Order 1042 · Yesterday\", amount: \"-24.00\", tone: \"2\" },\n\t{ title: \"Payout\", detail: \"To checking · Mon\", amount: \"-850.00\", tone: \"3\" },\n] as const\n\n/** Each row's mark: its tone at graphic strength, which every theme tunes. */\nconst TONE_MARK = { \"1\": \"bg-tone-1-mark\", \"2\": \"bg-tone-2-mark\", \"3\": \"bg-tone-3-mark\" } as const\n\nexport interface WalletBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a Sheet or muted section that already frames it. */\n\tframed?: boolean\n\tonSend?: () => void\n\tonReceive?: () => void\n}\n\nexport function WalletBlock({ className, framed = true, onSend, onReceive }: WalletBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardDescription>Balance</CardDescription>\n\t\t\t\t<CardTitle className=\"text-mono-xl\">{balance}</CardTitle>\n\t\t\t</CardHeader>\n\t\t\t<CardContent className=\"flex flex-col gap-3\">\n\t\t\t\t<div className=\"grid grid-cols-2 gap-2\">\n\t\t\t\t\t<Button iconStart={<Icon name=\"arrow-up\" />} onClick={onSend}>\n\t\t\t\t\t\tSend\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button variant=\"secondary\" iconStart={<Icon name=\"arrow-down\" />} onClick={onReceive}>\n\t\t\t\t\t\tReceive\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"flex flex-col\">\n\t\t\t\t\t{activity.map((item) => (\n\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\tkey={item.title}\n\t\t\t\t\t\t\tclassName=\"px-0\"\n\t\t\t\t\t\t\tleading={\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\taria-hidden\n\t\t\t\t\t\t\t\t\tclassName={cn(\"block size-8 rounded-full\", TONE_MARK[item.tone])}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttitle={item.title}\n\t\t\t\t\t\t\tsubtitle={item.detail}\n\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t<span className={cn(\"text-mono-md\", item.amount.startsWith(\"+\") ? \"text-success\" : \"text-muted-foreground\")}>\n\t\t\t\t\t\t\t\t\t{item.amount}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t</Card>\n\t)\n}"
24640
25406
  }
24641
25407
  ]
24642
25408
  },
@@ -25363,6 +26129,16 @@
25363
26129
  "value": "base · emphasized · expressive",
25364
26130
  "use": "An element easing between intrinsic sizes when its label changes: a two-step confirm, Copy → Copied."
25365
26131
  },
26132
+ {
26133
+ "name": "motion-bloom",
26134
+ "value": "slower · 0 → 1.2 → 1 · expressive",
26135
+ "use": "A small glyph landing in its on state: a liked heart, a picked rating, a toast's status icon. Remount it (key) to replay; never on the resting state."
26136
+ },
26137
+ {
26138
+ "name": "<Burst>",
26139
+ "value": "slower ring + thrown dots · expressive",
26140
+ "use": "The follow-through behind a bloom, in the theme's palette families. Only on a choice a person made: a like, a star, a finished task."
26141
+ },
25366
26142
  {
25367
26143
  "name": "motion-draw",
25368
26144
  "value": "slower · decelerate",
@@ -25417,7 +26193,7 @@
25417
26193
  {
25418
26194
  "name": "expressive",
25419
26195
  "value": "full only",
25420
- "use": "Glides, rolling numbers (NumberTicker), drawn ticks, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot."
26196
+ "use": "Glides, rolling numbers (NumberTicker), drawn ticks, blooms and bursts, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot."
25421
26197
  },
25422
26198
  {
25423
26199
  "name": "full",
@@ -25512,6 +26288,18 @@
25512
26288
  "rule": "A state change finishes its story: refused shakes and says why, success confirms in place, armed asks again.",
25513
26289
  "why": "Motion that starts a change and leaves the outcome to a snap reads as unfinished. ConfirmButton, OtpField's invalid shake and the copy-to-check swap are the reference cases."
25514
26290
  },
26291
+ {
26292
+ "rule": "Amplitude scales with smallness. A 16px glyph that turns on grows from nothing past its size; a surface travels from 0.96 and a press settles to 0.97.",
26293
+ "why": "The same 4% that is right for a dialog is invisible on an icon, which is why a state change on a small control used to read as a colour swap with no life in it. A dialog that overshot 20% would lurch. Size, not importance, sets how far a thing moves."
26294
+ },
26295
+ {
26296
+ "rule": "Stage a moment: the main object lands first, the follow-through after it.",
26297
+ "why": "A ring that opens, a glyph that blooms 90ms later, dots thrown 60ms after that read as one event with a cause and an effect. Everything starting on the same frame reads as a flash. LikeButton and the toast's status glyph are the reference cases."
26298
+ },
26299
+ {
26300
+ "rule": "Celebrate only what a person chose, and only at full motion.",
26301
+ "why": "A burst is a reply. On a like it is delight; on a system event, on load, or on every row of a list it is a notification wearing confetti. Under subtle the fill alone answers."
26302
+ },
25515
26303
  {
25516
26304
  "rule": "Component motion props and MotionScope only turn motion down.",
25517
26305
  "why": "`full` on a component means inherit. Nothing below the provider can switch motion back on for a reader who asked for it off."
@@ -25704,6 +26492,11 @@
25704
26492
  "why": "The ratio alone passes labels that read muddy. A deliberate brand pair is allowed with a recorded reason (lint disable or preset exceptions).",
25705
26493
  "enforcedBy": "sable-lint: label-contrast"
25706
26494
  },
26495
+ {
26496
+ "never": "A label fill painted on a mark that carries no text — a meter, a segmented bar, a status dot, a chart series, a soft tint.",
26497
+ "instead": "The colour's `-mark` token (`bg-success-mark`, `bg-tone-warning-mark/15`): the same hue lifted and saturated, held to 2:1 on the page. Text contrast is for text; a graphic owes visibility, and 3:1 (WCAG 1.4.11) only when it is the sole carrier of its meaning.",
26498
+ "why": "A fill is pinned dark enough for a white label. On a bar or dot with nothing to label, that darkness only reads as mud: a brown warning, a forest-green success."
26499
+ },
25707
26500
  {
25708
26501
  "never": "Blocking paste, or type=\"number\" for codes and card numbers.",
25709
26502
  "instead": "type=\"text\" inputMode=\"numeric\" with the right autocomplete.",
@@ -26692,6 +27485,29 @@
26692
27485
  "motion-interactive"
26693
27486
  ]
26694
27487
  },
27488
+ {
27489
+ "name": "motion-bloom",
27490
+ "kind": "utility",
27491
+ "family": "motion",
27492
+ "intent": "A small glyph landing in its \"on\" state grows from nothing past its size (1.2) and settles. The amplitude a 16px glyph needs to read as an event; surfaces never bloom. A keyframe that plays on mount, so remount the glyph (a changing `key`) to replay it. `--motion-bloom-delay` holds it at nothing first (raw ms; the recipe scales it). Expressive.",
27493
+ "whenToUse": "A glyph that a person's choice turns on: a liked heart, a starred item, a status icon arriving in a toast. LikeButton and Toast already use it. Pair with <Burst> for the follow-through on a celebratory toggle.",
27494
+ "insteadOf": "A bigger motion-press or motion-pop on the whole control (a button or card that overshoots 20% lurches), or a hand-written scale keyframe with literal durations. Never on the resting state: a page that loads with a post already liked must not bloom every heart.",
27495
+ "example": "<span key={likes} className={liked ? \"motion-bloom\" : undefined}><Icon name=\"heart\" variant={liked ? \"solid\" : \"regular\"} /></span>",
27496
+ "keywords": [
27497
+ "pop",
27498
+ "overshoot",
27499
+ "like",
27500
+ "heart",
27501
+ "star",
27502
+ "celebrate",
27503
+ "glyph",
27504
+ "icon pop",
27505
+ "delight"
27506
+ ],
27507
+ "pairsWith": [
27508
+ "motion-interactive"
27509
+ ]
27510
+ },
26695
27511
  {
26696
27512
  "name": "motion-size",
26697
27513
  "kind": "utility",
@@ -27074,6 +27890,29 @@
27074
27890
  "transparent pattern",
27075
27891
  "swatch background"
27076
27892
  ]
27893
+ },
27894
+ {
27895
+ "name": "texture-dither",
27896
+ "kind": "utility",
27897
+ "family": "helper",
27898
+ "intent": "A faint pixel / halftone dither field that dissolves: a small pseudo-random tile of squares painted in `--border-strong` and faded out by a gradient mask. Static, pure CSS, follows every theme and mode. Hooks: `--dither-fade` (direction, any linear-gradient side or angle, default `to bottom`), `--dither-size` (tile, default 48px = 2px squares) and `--dither-color` (default `var(--border-strong)`).",
27899
+ "whenToUse": "Decoration at the top of a content area, a panel or a card, or behind an empty-state heading. Put it on its own `aria-hidden` element, `absolute -z-10` as the first child of a `relative isolate` parent, so the content after it paints on top with no z-index of its own. It sets `pointer-events: none` and no position, so it never takes layout or catches clicks.",
27900
+ "insteadOf": "A hand-written `radial-gradient` dot grid (reads as graph paper, not dither), a PNG noise texture, a canvas, or hardcoded greys. Deliberately not named `bg-dither`, so `cn()` never merges it away against a real `bg-*` colour class.",
27901
+ "example": "<div className=\"relative isolate\"><div aria-hidden className=\"texture-dither absolute inset-x-0 top-0 -z-10 h-40\" /></div>",
27902
+ "keywords": [
27903
+ "dither",
27904
+ "halftone",
27905
+ "pixel pattern",
27906
+ "dot pattern",
27907
+ "noise texture",
27908
+ "texture",
27909
+ "fade pattern",
27910
+ "empty state background",
27911
+ "decorative background"
27912
+ ],
27913
+ "pairsWith": [
27914
+ "checkerboard"
27915
+ ]
27077
27916
  }
27078
27917
  ]
27079
27918
  },
@@ -27179,6 +28018,7 @@
27179
28018
  "bottomNav",
27180
28019
  "children",
27181
28020
  "dock",
28021
+ "frame",
27182
28022
  "header",
27183
28023
  "overlays",
27184
28024
  "sidebar"
@@ -27339,11 +28179,29 @@
27339
28179
  "BubbleReaction": [
27340
28180
  "active",
27341
28181
  "count",
27342
- "emoji"
28182
+ "emoji",
28183
+ "label",
28184
+ "motion"
28185
+ ],
28186
+ "BubbleReactionAdd": [
28187
+ "label",
28188
+ "motion",
28189
+ "onSelect",
28190
+ "reactions",
28191
+ "selected"
27343
28192
  ],
27344
28193
  "BubbleReactions": [
27345
28194
  "align"
27346
28195
  ],
28196
+ "Burst": [
28197
+ "className",
28198
+ "count",
28199
+ "motion",
28200
+ "palette",
28201
+ "particle",
28202
+ "playOnMount",
28203
+ "trigger"
28204
+ ],
27347
28205
  "Button": [
27348
28206
  "badge",
27349
28207
  "bleed",
@@ -27639,6 +28497,7 @@
27639
28497
  "form",
27640
28498
  "grid",
27641
28499
  "highlightItemOnHover",
28500
+ "highlightMatches",
27642
28501
  "id",
27643
28502
  "inline",
27644
28503
  "inputRef",
@@ -27765,6 +28624,7 @@
27765
28624
  "emptyMessage",
27766
28625
  "filter",
27767
28626
  "finalFocus",
28627
+ "highlightMatches",
27768
28628
  "inputProps",
27769
28629
  "items",
27770
28630
  "listClassName",
@@ -27794,6 +28654,7 @@
27794
28654
  "children",
27795
28655
  "emptyMessage",
27796
28656
  "filter",
28657
+ "highlightMatches",
27797
28658
  "inputProps",
27798
28659
  "items",
27799
28660
  "listClassName",
@@ -28205,6 +29066,7 @@
28205
29066
  "orientation"
28206
29067
  ],
28207
29068
  "GradientArt": [
29069
+ "intensity",
28208
29070
  "seed",
28209
29071
  "tones",
28210
29072
  "variant"
@@ -28261,6 +29123,21 @@
28261
29123
  "hint",
28262
29124
  "size"
28263
29125
  ],
29126
+ "LikeButton": [
29127
+ "bleed",
29128
+ "count",
29129
+ "defaultPressed",
29130
+ "format",
29131
+ "icon",
29132
+ "label",
29133
+ "motion",
29134
+ "onClick",
29135
+ "onPressedChange",
29136
+ "pressed",
29137
+ "size",
29138
+ "tone",
29139
+ "variant"
29140
+ ],
28264
29141
  "LineChart": [
28265
29142
  "activeIndex",
28266
29143
  "animate",
@@ -28462,6 +29339,15 @@
28462
29339
  "style",
28463
29340
  "value"
28464
29341
  ],
29342
+ "MenuReactions": [
29343
+ "className",
29344
+ "motion",
29345
+ "onSelect",
29346
+ "reactions",
29347
+ "render",
29348
+ "selected",
29349
+ "style"
29350
+ ],
28465
29351
  "MenuSeparator": [
28466
29352
  "className",
28467
29353
  "orientation",
@@ -28540,6 +29426,7 @@
28540
29426
  "max",
28541
29427
  "min",
28542
29428
  "render",
29429
+ "segments",
28543
29430
  "shape",
28544
29431
  "showValue",
28545
29432
  "size",
@@ -29015,6 +29902,19 @@
29015
29902
  "style",
29016
29903
  "value"
29017
29904
  ],
29905
+ "ReactionPicker": [
29906
+ "children",
29907
+ "className",
29908
+ "disabled",
29909
+ "loopFocus",
29910
+ "motion",
29911
+ "onSelect",
29912
+ "orientation",
29913
+ "reactions",
29914
+ "render",
29915
+ "selected",
29916
+ "style"
29917
+ ],
29018
29918
  "Region": [
29019
29919
  "as",
29020
29920
  "bleed",