@sableui/react 0.11.0 → 0.13.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 (90) hide show
  1. package/AGENTS.md +10 -4
  2. package/CHANGELOG.md +199 -0
  3. package/DESIGN.md +3 -3
  4. package/README.md +15 -1
  5. package/bin/sable.mjs +254 -10
  6. package/dist/components/app-header/app-header.meta.js +2 -1
  7. package/dist/components/button/button.js +3 -0
  8. package/dist/components/button/button.meta.js +1 -1
  9. package/dist/components/button/button.variants.js +1 -1
  10. package/dist/components/button-group/button-group.js +17 -0
  11. package/dist/components/button-group/button-group.meta.js +72 -0
  12. package/dist/components/button-group/index.js +7 -0
  13. package/dist/components/caret-menu/caret-menu.js +221 -0
  14. package/dist/components/caret-menu/caret-menu.meta.js +119 -0
  15. package/dist/components/caret-menu/index.js +8 -0
  16. package/dist/components/chat-composer/chat-composer.js +30 -7
  17. package/dist/components/chat-composer/chat-composer.meta.js +27 -8
  18. package/dist/components/combobox/combobox.meta.js +26 -0
  19. package/dist/components/input-group/index.js +7 -0
  20. package/dist/components/input-group/input-group.js +66 -0
  21. package/dist/components/input-group/input-group.meta.js +106 -0
  22. package/dist/components/menu/index.js +2 -0
  23. package/dist/components/menu/menu.js +51 -9
  24. package/dist/components/menu/menu.meta.js +11 -2
  25. package/dist/components/menubar/index.js +8 -0
  26. package/dist/components/menubar/menubar.js +42 -0
  27. package/dist/components/menubar/menubar.meta.js +99 -0
  28. package/dist/components/message-scroller/message-scroller.meta.js +4 -0
  29. package/dist/components/navigation-menu/index.js +19 -0
  30. package/dist/components/navigation-menu/navigation-menu.js +108 -0
  31. package/dist/components/navigation-menu/navigation-menu.meta.js +141 -0
  32. package/dist/components/overflow-menu/overflow-menu.meta.js +13 -1
  33. package/dist/components/pagination/pagination.js +11 -5
  34. package/dist/components/pagination/pagination.meta.js +21 -3
  35. package/dist/components/scroller/scroller.js +8 -4
  36. package/dist/components/scroller/scroller.meta.js +6 -0
  37. package/dist/components/sheet/sheet.js +82 -9
  38. package/dist/components/sheet/sheet.meta.js +41 -4
  39. package/dist/components/sheet/sheet.variants.js +8 -1
  40. package/dist/components/sidebar/sidebar.js +13 -2
  41. package/dist/components/sidebar/sidebar.meta.js +10 -0
  42. package/dist/components/streaming-text/streaming-text.meta.js +5 -1
  43. package/dist/file-hashes.json +49 -33
  44. package/dist/icon-sets/reicon.js +2 -0
  45. package/dist/index.d.ts +660 -164
  46. package/dist/index.js +12 -1
  47. package/dist/lib/palette.js +46 -6
  48. package/dist/lib/taxonomy.js +5 -0
  49. package/dist/lib/use-caret-trigger.js +187 -0
  50. package/dist/lib/use-copy.js +6 -1
  51. package/dist/lib/use-smooth-wheel.js +164 -0
  52. package/dist/palette.css +243 -165
  53. package/dist/patterns/chat-panel.js +1 -1
  54. package/dist/patterns/chat-panel.meta.js +1 -0
  55. package/dist/styles.css +25 -0
  56. package/llms/components/app-header.md +1 -1
  57. package/llms/components/button-group.md +55 -0
  58. package/llms/components/button.md +1 -1
  59. package/llms/components/caret-menu.md +100 -0
  60. package/llms/components/chat-composer.md +24 -7
  61. package/llms/components/combobox.md +12 -0
  62. package/llms/components/input-group.md +78 -0
  63. package/llms/components/menu.md +16 -1
  64. package/llms/components/menubar.md +110 -0
  65. package/llms/components/message-scroller.md +1 -0
  66. package/llms/components/navigation-menu.md +113 -0
  67. package/llms/components/overflow-menu.md +26 -1
  68. package/llms/components/pagination.md +15 -3
  69. package/llms/components/scroller.md +1 -0
  70. package/llms/components/sheet.md +18 -3
  71. package/llms/components/sidebar.md +2 -0
  72. package/llms/components/streaming-text.md +2 -1
  73. package/llms/patterns/chat-panel.md +4 -1
  74. package/llms/reference/token-intent.md +1 -1
  75. package/llms/tasks/building-a-chat.md +158 -0
  76. package/llms/tasks/building-with-sable.md +4 -1
  77. package/llms/tasks/converting-an-app.md +7 -0
  78. package/llms/tasks/extending-a-component.md +170 -0
  79. package/llms/tasks/getting-started.md +22 -4
  80. package/llms/tasks/migrating-from-shadcn.md +351 -0
  81. package/llms-full.txt +1322 -27
  82. package/llms.txt +13 -4
  83. package/package.json +3 -2
  84. package/registry.json +979 -23
  85. package/scripts/lint-rules.mjs +35 -0
  86. package/scripts/wiring.mjs +541 -0
  87. package/skills/entry/SKILL.md +3 -2
  88. package/skills/sable/SKILL.md +26 -5
  89. package/targets/ios.json +45 -0
  90. package/dist/lib/use-wheel-to-horizontal.js +0 -65
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.11.0",
4
+ "version": "0.13.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",
@@ -91,6 +91,16 @@
91
91
  "previewCss": "@sableui/react/ios/frame.css",
92
92
  "guide": "sable docs building-an-ios-prototype",
93
93
  "updates": "Consume the shipped adapter instead of copying demo helpers. Package upgrades update the brand bridge, material, safe areas, navigation, sheets and input bindings."
94
+ },
95
+ "init": {
96
+ "command": "pnpm exec sable init",
97
+ "preview": "pnpm exec sable init --dry-run",
98
+ "after": "Run it once the package is installed; before that `npx sable` resolves an unrelated package.",
99
+ "does": "Installs missing packages with the lockfile's package manager (React, Tailwind and its build plugin), writes the css imports in order into the global stylesheet, adds bundlerDedupe and the Tailwind plugin to a Vite config (a postcss.config.mjs for Next.js), loads the fonts, wraps a Vite starter's <App /> in IconProvider, runs agentSetup, then sable doctor.",
100
+ "frameworks": "Vite and Next.js (App Router) get every step. Other setups get the packages and the stylesheet; the bundler and provider steps are printed.",
101
+ "manual": "Each edit it cannot make safely (an unfamiliar config shape, a Next.js layout, a stylesheet split across files) is printed as the exact snippet and the file it belongs in. Apply them, then run sable doctor.",
102
+ "flags": "--dry-run previews; --no-agent skips the skill; --no-install prints install commands instead of running them.",
103
+ "repair": "In an app that is already wired: pnpm exec sable doctor --fix (or --dry-run) applies the same fixes to whatever doctor flags."
94
104
  }
95
105
  },
96
106
  "themes": {
@@ -1665,6 +1675,10 @@
1665
1675
  {
1666
1676
  "id": "form-required-error",
1667
1677
  "fails-on": "A `required` Field with an `error` prop, inside a `Form` that validates for you. On submit Form checks every Field first, so an empty required one stops the submit with the browser's message (\"Please fill out this field.\") and your onSubmit, which would set `error`, never runs. Either let your code own validation, `<Form noValidate>` (onSubmit always runs and the Field shows your `error`), or let Form validate and word the message on the Field: `messages={{ valueMissing: \"Enter a subject.\" }}`."
1678
+ },
1679
+ {
1680
+ "id": "shadcn-leftover",
1681
+ "fails-on": "Leftover shadcn/ui or Radix code. Sable replaces the stack: `asChild` → `render`, `@radix-ui/*` and `@/components/ui/*` → the Sable component (`sable search \"<name>\"`), `lucide-react` → <Icon name> under IconProvider, `sonner` → ToastProvider + toast, `vaul` → Sheet, `cmdk` → CommandMenu, `input-otp` → OtpField, `next-themes` → SableThemeProvider. `sable docs migrating-from-shadcn` has the full map."
1668
1682
  }
1669
1683
  ],
1670
1684
  "escapeHatch": {
@@ -1699,6 +1713,7 @@
1699
1713
  "chevron-left",
1700
1714
  "chevron-right",
1701
1715
  "arrow-up",
1716
+ "arrow-up-right",
1702
1717
  "expand",
1703
1718
  "eye",
1704
1719
  "eye-off",
@@ -2861,7 +2876,8 @@
2861
2876
  "components": [
2862
2877
  "AppShell",
2863
2878
  "PageHeader",
2864
- "BottomNav"
2879
+ "BottomNav",
2880
+ "NavigationMenu"
2865
2881
  ]
2866
2882
  },
2867
2883
  "props": [
@@ -4928,7 +4944,7 @@
4928
4944
  },
4929
4945
  {
4930
4946
  "guidance": "do",
4931
- "description": "For a copy control, pair an icon Button with `useCopy(text, { timeout? })`. It takes the text when the hook is called and returns `{ copy, copied }`; `copy()` takes no argument, so pass the value the button copies to the hook, not to `copy`."
4947
+ "description": "For a copy control, pair an icon Button with `useCopy(text, { timeout? })`. It takes the text when the hook is called and returns `{ copy, copied }`; `copy()` takes no argument, so pass the value the button copies to the hook, not to `copy`. Text that costs something to produce (a file to fetch) goes in as a function, `useCopy(() => fetch(url).then((r) => r.text()))`, called only on click."
4932
4948
  },
4933
4949
  {
4934
4950
  "guidance": "do",
@@ -5230,6 +5246,90 @@
5230
5246
  }
5231
5247
  ]
5232
5248
  },
5249
+ {
5250
+ "name": "ButtonGroup",
5251
+ "status": "stable",
5252
+ "group": "Actions",
5253
+ "register": "either",
5254
+ "path": "components/button-group",
5255
+ "summary": "Buttons joined into one control: a split button (an action beside the chevron that opens its menu) or a short run of related actions. The children stay ordinary Buttons; the group squares their inner corners, collapses bordered seams into one hairline, and draws a seam between filled ones.",
5256
+ "keywords": [
5257
+ "split button",
5258
+ "button group",
5259
+ "segmented actions",
5260
+ "dropdown button",
5261
+ "joined buttons",
5262
+ "toolbar"
5263
+ ],
5264
+ "usage": {
5265
+ "description": "Use ButtonGroup when one action has close alternatives (Copy page, with Open in… behind a chevron) or a few actions belong together as one object (zoom out, reset, zoom in). Use ToggleGroup for a choice that stays selected, and Tabs to switch views.",
5266
+ "bestPractices": [
5267
+ {
5268
+ "guidance": "do",
5269
+ "description": "Give the children one height: `size=\"sm\"` with `size=\"icon-sm\"`, `md` with `icon`. The group aligns them but cannot reconcile two heights into one edge."
5270
+ },
5271
+ {
5272
+ "guidance": "do",
5273
+ "description": "For a split button, put the main action first and a Menu whose trigger is the chevron Button second, with an aria-label on the trigger (\"More page actions\")."
5274
+ },
5275
+ {
5276
+ "guidance": "do",
5277
+ "description": "Name the group with aria-label when the buttons alone do not say what they act on."
5278
+ },
5279
+ {
5280
+ "guidance": "dont",
5281
+ "description": "Do not round or border the children yourself to make them meet; that is the group's job, and an override breaks under a theme's button shape."
5282
+ },
5283
+ {
5284
+ "guidance": "dont",
5285
+ "description": "Do not use it for a selection that stays on. That is ToggleGroup, which carries the pressed state."
5286
+ }
5287
+ ]
5288
+ },
5289
+ "related": {
5290
+ "components": [
5291
+ "Button",
5292
+ "Menu",
5293
+ "Toggle"
5294
+ ]
5295
+ },
5296
+ "props": [
5297
+ {
5298
+ "name": "role",
5299
+ "type": "string",
5300
+ "default": "\"group\"",
5301
+ "description": "Group semantics, so a screen reader announces the buttons as one set. Pass `toolbar` when it carries arrow-key navigation of your own."
5302
+ },
5303
+ {
5304
+ "name": "className",
5305
+ "type": "string",
5306
+ "description": "Layout classes for the group (margins, alignment). The children's look comes from their own variants."
5307
+ },
5308
+ {
5309
+ "name": "...props",
5310
+ "type": "ComponentPropsWithRef<\"div\">",
5311
+ "description": "All native <div> attributes, including aria-label."
5312
+ }
5313
+ ],
5314
+ "tokensUsed": [],
5315
+ "a11y": [
5316
+ "Renders role=\"group\", so assistive tech announces the buttons as one set; name it with aria-label.",
5317
+ "Each child keeps its own focus stop and label; a focused child is raised above its neighbour so its ring is drawn whole.",
5318
+ "An icon-only chevron that opens a menu needs its own aria-label, such as \"More page actions\"."
5319
+ ],
5320
+ "examples": [
5321
+ {
5322
+ "label": "Split button with a menu",
5323
+ "code": "<ButtonGroup aria-label=\"Page actions\"><Button variant=\"outline\" size=\"sm\" iconStart={<Icon name=\"copy\" />} onClick={copy}>Copy page</Button><Menu><MenuTrigger render={<Button variant=\"outline\" size=\"icon-sm\" aria-label=\"More page actions\" />}><Icon name=\"chevron-down\" /></MenuTrigger><MenuContent align=\"end\"><MenuItem icon={<Icon name=\"file\" />} description=\"View this page as plain text\">View as Markdown</MenuItem></MenuContent></Menu></ButtonGroup>",
5324
+ "imports": "import { Button, ButtonGroup, Icon, Menu, MenuContent, MenuItem, MenuTrigger } from \"@sableui/react\""
5325
+ },
5326
+ {
5327
+ "label": "Related actions",
5328
+ "code": "<ButtonGroup aria-label=\"Zoom\"><Button variant=\"secondary\" size=\"icon-sm\" aria-label=\"Zoom out\"><Icon name=\"minus\" /></Button><Button variant=\"secondary\" size=\"sm\">100%</Button><Button variant=\"secondary\" size=\"icon-sm\" aria-label=\"Zoom in\"><Icon name=\"plus\" /></Button></ButtonGroup>",
5329
+ "imports": "import { Button, ButtonGroup, Icon } from \"@sableui/react\""
5330
+ }
5331
+ ]
5332
+ },
5233
5333
  {
5234
5334
  "name": "Calendar",
5235
5335
  "status": "stable",
@@ -5605,6 +5705,152 @@
5605
5705
  }
5606
5706
  ]
5607
5707
  },
5708
+ {
5709
+ "name": "CaretMenu",
5710
+ "status": "beta",
5711
+ "group": "Overlays",
5712
+ "register": "either",
5713
+ "path": "components/caret-menu",
5714
+ "summary": "Suggestions at the caret of a text field, for @mentions and /slash commands. `useCaretTrigger` watches a Textarea, Input or ChatComposer (through `fieldRef`) for a trigger character and reports the query; CaretMenu draws the list beside the trigger and drives it from the field's keys, so focus never leaves the field. The value stays plain text: a mention is inserted as `@Sam Rivera`, a command runs and clears its `/query`.",
5715
+ "keywords": [
5716
+ "mention",
5717
+ "mentions",
5718
+ "at mention",
5719
+ "slash command",
5720
+ "slash commands",
5721
+ "autocomplete",
5722
+ "typeahead",
5723
+ "suggestions",
5724
+ "emoji picker",
5725
+ "hashtag",
5726
+ "trigger"
5727
+ ],
5728
+ "usage": {
5729
+ "description": "Use CaretMenu with `useCaretTrigger` when typing a character in running text should offer choices: `@` for people, `/` for commands, `#` for channels, `:` for emoji. The app owns the items (filter or fetch them from `trigger.match.query`) and what choosing one does; Sable owns detecting the trigger, placing the list at the caret, the keys and the accessibility. Use Combobox when the whole field is a search for one value, and CommandMenu for a global palette.",
5730
+ "bestPractices": [
5731
+ {
5732
+ "guidance": "do",
5733
+ "description": "Attach the hook's `fieldRef` to the field: `ref` on Textarea or Input, `fieldRef` on ChatComposer. On ChatComposer, Enter picks the highlighted suggestion instead of sending, because CaretMenu takes the Enter first."
5734
+ },
5735
+ {
5736
+ "guidance": "do",
5737
+ "description": "Insert a mention with a trailing space, `trigger.replace(`@${person.name} `)`, so the reader keeps typing; run a slash command and then `trigger.replace(\"\")` to clear `/query` from the field."
5738
+ },
5739
+ {
5740
+ "guidance": "do",
5741
+ "description": "For `@` names with spaces set `allowSpaces: true` on that trigger; for `/` commands set `atStart: true` so `and/or` mid-sentence stays text."
5742
+ },
5743
+ {
5744
+ "guidance": "do",
5745
+ "description": "Fetch remote suggestions from `trigger.match?.query` yourself, debounced, and pass `status` while loading or when nothing matches (\"Searching…\", \"No one matches\"). The status row is also what the list announces."
5746
+ },
5747
+ {
5748
+ "guidance": "dont",
5749
+ "description": "Do not expect chips inside the field. A textarea holds text, so a mention is text. Rendering tokens in the field needs a rich-text editor, which Sable does not own (`sable docs extending-a-component`)."
5750
+ },
5751
+ {
5752
+ "guidance": "dont",
5753
+ "description": "Do not move focus into the list or wrap the field in a Popover. Focus stays in the field; the list is pointed at with aria-activedescendant."
5754
+ }
5755
+ ],
5756
+ "anatomy": [
5757
+ {
5758
+ "name": "useCaretTrigger",
5759
+ "required": true,
5760
+ "description": "The hook on the field: trigger detection, the query, the caret anchor, replace() and dismiss()."
5761
+ },
5762
+ {
5763
+ "name": "CaretMenu",
5764
+ "required": true,
5765
+ "description": "The list at the caret, its keys, and its announcements."
5766
+ },
5767
+ {
5768
+ "name": "CaretMenuItem",
5769
+ "required": true,
5770
+ "description": "One suggestion row, with optional icon and description."
5771
+ }
5772
+ ]
5773
+ },
5774
+ "related": {
5775
+ "components": [
5776
+ "ChatComposer",
5777
+ "Textarea",
5778
+ "Input",
5779
+ "Combobox",
5780
+ "CommandMenu",
5781
+ "Menu"
5782
+ ]
5783
+ },
5784
+ "props": [
5785
+ {
5786
+ "name": "trigger",
5787
+ "type": "UseCaretTriggerResult",
5788
+ "description": "The result of `useCaretTrigger({ triggers })`, whose `fieldRef` is attached to the field. Each trigger is `{ char, atStart?, allowSpaces?, maxQueryLength? }`; the result carries `match` (`{ trigger, query, start, end }` or null), `replace(text)`, `dismiss()` and `getAnchorRect()`."
5789
+ },
5790
+ {
5791
+ "name": "label",
5792
+ "type": "string",
5793
+ "description": "The list's accessible name, e.g. \"People\" or \"Commands\"."
5794
+ },
5795
+ {
5796
+ "name": "status",
5797
+ "type": "ReactNode",
5798
+ "description": "A row under the items for what the list is doing: loading, nothing found, an error. Announced politely in place of the suggestion count. With no items and no status the list stays shut."
5799
+ },
5800
+ {
5801
+ "name": "children",
5802
+ "type": "ReactNode",
5803
+ "description": "CaretMenuItems, already filtered for the query."
5804
+ },
5805
+ {
5806
+ "name": "className",
5807
+ "type": "string",
5808
+ "description": "Layout classes for the popup, such as a width."
5809
+ }
5810
+ ],
5811
+ "subcomponents": [
5812
+ {
5813
+ "name": "CaretMenuItem",
5814
+ "type": "div props + onSelect: () => void + disabled?: boolean + icon?: ReactNode + description?: ReactNode",
5815
+ "description": "One suggestion, styled as a Menu row. `onSelect` runs on Enter, Tab or click; `icon` and `description` work as on MenuItem. A disabled row shows but the arrow keys skip it."
5816
+ }
5817
+ ],
5818
+ "tokensUsed": [
5819
+ "popover",
5820
+ "popover-foreground",
5821
+ "border-floating",
5822
+ "accent",
5823
+ "accent-foreground",
5824
+ "muted-foreground"
5825
+ ],
5826
+ "motion": [
5827
+ {
5828
+ "slot": "arrival",
5829
+ "recipe": "motion-pop",
5830
+ "part": "root",
5831
+ "why": "Scales in from the caret's side, so the list reads as coming from the character just typed. It leaves instantly: it answers a keystroke and should be gone by the next one."
5832
+ }
5833
+ ],
5834
+ "a11y": [
5835
+ "Focus stays in the field. The field carries aria-autocomplete=\"list\", aria-controls (the listbox) and aria-activedescendant (the highlighted option), so a screen reader follows the arrow keys without focus moving.",
5836
+ "An <input> becomes role=\"combobox\" with aria-expanded. A <textarea> keeps its textbox role (combobox would drop multiline), and since a textbox cannot carry aria-expanded, opening is announced through a polite status, mounted from the start: \"3 suggestions\", or the `status` text.",
5837
+ "↑/↓ move the highlight and wrap; Enter or Tab choose it; Escape closes the list without reaching an enclosing Dialog or Sheet. Shift+Enter is left alone, so a line break still breaks the line.",
5838
+ "Rows keep focus in the field on pointer down, so a click does not blur it and close the list first.",
5839
+ "Triggers follow a word boundary (`ana@example.com` and `and/or` stay text), wait for IME composition to end, and stay closed after Escape until the caret leaves that trigger."
5840
+ ],
5841
+ "examples": [
5842
+ {
5843
+ "label": "Mentions and slash commands in a ChatComposer",
5844
+ "code": "const trigger = useCaretTrigger({ triggers: [{ char: \"@\", allowSpaces: true }, { char: \"/\", atStart: true }] })\nconst q = trigger.match?.query.toLowerCase() ?? \"\"\nconst commands = trigger.match?.trigger.char === \"/\"\n\n<ChatComposer fieldRef={trigger.fieldRef} onSend={send} />\n<CaretMenu trigger={trigger} label={commands ? \"Commands\" : \"People\"} status={!commands && people.length === 0 ? \"No one matches\" : undefined}>\n\t{commands\n\t\t? slashCommands.filter((c) => c.name.startsWith(q)).map((c) => (\n\t\t\t<CaretMenuItem key={c.name} description={c.hint} onSelect={() => { c.run(); trigger.replace(\"\") }}>/{c.name}</CaretMenuItem>\n\t\t))\n\t\t: people.filter((p) => p.name.toLowerCase().includes(q)).map((p) => (\n\t\t\t<CaretMenuItem key={p.id} icon={<Avatar size=\"sm\" src={p.avatar} fallback={p.initials} />} onSelect={() => trigger.replace(`@${p.name} `)}>{p.name}</CaretMenuItem>\n\t\t))}\n</CaretMenu>",
5845
+ "imports": "import { Avatar, CaretMenu, CaretMenuItem, ChatComposer, useCaretTrigger } from \"@sableui/react\""
5846
+ },
5847
+ {
5848
+ "label": "On a Textarea, with remote results",
5849
+ "code": "const trigger = useCaretTrigger({ triggers: [{ char: \"#\" }] })\n// Fetch from trigger.match?.query yourself, debounced; pass the state as status.\n<Textarea ref={trigger.fieldRef} aria-label=\"Note\" />\n<CaretMenu trigger={trigger} label=\"Channels\" status={loading ? \"Searching…\" : channels.length === 0 ? \"No channels match\" : undefined}>\n\t{channels.map((c) => <CaretMenuItem key={c.id} onSelect={() => trigger.replace(`#${c.name} `)}>#{c.name}</CaretMenuItem>)}\n</CaretMenu>",
5850
+ "imports": "import { CaretMenu, CaretMenuItem, Textarea, useCaretTrigger } from \"@sableui/react\""
5851
+ }
5852
+ ]
5853
+ },
5608
5854
  {
5609
5855
  "name": "Carousel",
5610
5856
  "status": "stable",
@@ -5774,7 +6020,7 @@
5774
6020
  "group": "Chat & AI",
5775
6021
  "register": "either",
5776
6022
  "path": "components/chat-composer",
5777
- "summary": "The input half of a conversation: one surface holding attachments, an auto-growing field, and its controls. Enter sends, Shift+Enter breaks the line (sendOn=\"mod-enter\" swaps them for long-form replies), and send becomes stop while a reply streams.",
6023
+ "summary": "The input half of a conversation: one surface holding attachments, an auto-growing field, and its controls. Enter sends, Shift+Enter breaks the line (sendOn=\"mod-enter\" swaps them for long-form replies; on touch, Return always breaks the line), and send becomes stop while a reply streams. allowEmpty sends attachments without text.",
5778
6024
  "keywords": [
5779
6025
  "chat input",
5780
6026
  "composer",
@@ -5788,6 +6034,10 @@
5788
6034
  "usage": {
5789
6035
  "description": "Pairs with MessageScroller to make a working chat. A single surface rather than an input with buttons parked beside it: attachments above, text in the middle, affordances along the bottom. Works controlled or uncontrolled — uncontrolled is the default because it is what the common case wants, and what lets a hook-free page shell ship a composer that actually types.",
5790
6036
  "bestPractices": [
6037
+ {
6038
+ "guidance": "do",
6039
+ "description": "Wiring it to a streaming source? `sable docs building-a-chat` maps a turn lifecycle (submitted, streaming, done, stopped, error) onto `busy`, `onStop` and `disabled`, and says where each failure goes."
6040
+ },
5791
6041
  {
5792
6042
  "guidance": "do",
5793
6043
  "description": "Pass `busy` while a reply streams, and `onStop` alongside it. Interrupting a long generation is the most common thing a reader wants during one, and the control belongs where their cursor already is."
@@ -5800,6 +6050,10 @@
5800
6050
  "guidance": "do",
5801
6051
  "description": "Render pending uploads as `<Attachment>` chips in `attachments`, so the file and the message it belongs to stay in one box."
5802
6052
  },
6053
+ {
6054
+ "guidance": "do",
6055
+ "description": "Pass `allowEmpty={files.length > 0}` when a file can go without a caption. Send enables on the attachment alone and `onSend` receives `\"\"`. Tie it to the pending files: a constant `allowEmpty` leaves send enabled with nothing to send."
6056
+ },
5803
6057
  {
5804
6058
  "guidance": "do",
5805
6059
  "description": "Put model pickers and tool toggles in `leading`, beside the attach button — they are composer state, not message content, and belong on the chrome row rather than in the field."
@@ -5810,7 +6064,7 @@
5810
6064
  },
5811
6065
  {
5812
6066
  "guidance": "dont",
5813
- "description": "Do not add your own Enter-to-send handler on top. The built-in one already guards against firing mid-IME-composition, which is the line hand-rolled chat inputs miss."
6067
+ "description": "Do not add your own Enter-to-send handler on top. The built-in one already guards against firing mid-IME-composition, and on a touch device lets Return break the line (a soft keyboard has no Shift+Return), which are the lines hand-rolled chat inputs miss."
5814
6068
  },
5815
6069
  {
5816
6070
  "guidance": "do",
@@ -5884,7 +6138,13 @@
5884
6138
  {
5885
6139
  "name": "onSend",
5886
6140
  "type": "(value: string) => void",
5887
- "description": "Called with the trimmed message. An uncontrolled composer clears itself afterwards; a controlled one is the caller's to clear."
6141
+ "description": "Called with the trimmed message. An uncontrolled composer clears itself afterwards; a controlled one is the caller's to clear. With `allowEmpty` the message can be `\"\"`."
6142
+ },
6143
+ {
6144
+ "name": "allowEmpty",
6145
+ "type": "boolean",
6146
+ "default": "false",
6147
+ "description": "Let the reader send with no text, because the app has something else to send (pending attachments). Pass it only while that is true, `allowEmpty={files.length > 0}`; `onSend` then receives `\"\"`."
5888
6148
  },
5889
6149
  {
5890
6150
  "name": "placeholder",
@@ -5896,7 +6156,7 @@
5896
6156
  "name": "sendOn",
5897
6157
  "type": "\"enter\" | \"mod-enter\"",
5898
6158
  "default": "\"enter\"",
5899
- "description": "Which key sends. enter for chat (Shift+Enter breaks the line). mod-enter for long-form replies such as an email or a support answer: Enter breaks the line and Cmd/Ctrl+Enter sends, so a paragraph break never mails a draft. Say so in footer (\"Cmd+Enter to send\")."
6159
+ "description": "Which key sends. enter for chat (Shift+Enter breaks the line). mod-enter for long-form replies such as an email or a support answer: Enter breaks the line and Cmd/Ctrl+Enter sends, so a paragraph break never mails a draft. Say so in footer (\"Cmd+Enter to send\"). On a touch device (pointer: coarse) enter behaves as mod-enter: a soft keyboard has no Shift+Return, so Return breaks the line and the send button sends."
5900
6160
  },
5901
6161
  {
5902
6162
  "name": "busy",
@@ -5961,13 +6221,13 @@
5961
6221
  },
5962
6222
  {
5963
6223
  "name": "footer",
5964
- "type": "ReactNode",
5965
- "description": "Helper text or counter below the controls."
6224
+ "type": "ReactNode | ((state: { sendOn: \"enter\" | \"mod-enter\" }) => ReactNode)",
6225
+ "description": "Helper text or counter below the controls. For a keyboard hint pass a function: it receives the send key in effect (touch turns `enter` into `mod-enter`), and returning null removes the row, e.g. `footer={({ sendOn }) => sendOn === \"enter\" ? \"Enter to send\" : null}`."
5966
6226
  },
5967
6227
  {
5968
6228
  "name": "fieldRef",
5969
6229
  "type": "Ref<HTMLTextAreaElement>",
5970
- "description": "Ref to the underlying textarea — for focusing after a send, or on mount."
6230
+ "description": "Ref to the underlying textarea — for focusing after a send, or on mount, and the extension point for anything that listens to the field. Pass `useCaretTrigger`'s `fieldRef` here for mentions and slash commands (CaretMenu). An Enter another listener already handled (`preventDefault()`) does not send."
5971
6231
  },
5972
6232
  {
5973
6233
  "name": "...form props",
@@ -5999,7 +6259,8 @@
5999
6259
  "A real `<form>`, so the send button is a submit button and the whole thing behaves as one control to assistive tech and to password managers alike.",
6000
6260
  "Enter never submits while an IME composition is open (`isComposing`), so a CJK reader confirming a candidate word does not mail a fragment of their first sentence.",
6001
6261
  "The focus ring is drawn on the surface via `focus-within`, not on the textarea, so the visible focus indicator matches the box a sighted user perceives as the input.",
6002
- "Send is disabled — not merely styled down — until there is non-whitespace to send, so the affordance and the behaviour agree for keyboard users.",
6262
+ "Send is disabled — not merely styled down — until there is non-whitespace to send (or `allowEmpty` says an attachment is enough), so the affordance and the behaviour agree for keyboard users.",
6263
+ "On a touch device Return breaks the line and the send button sends; `aria-keyshortcuts` reports the shortcut actually in effect (⌘/Ctrl+Enter there, Enter on desktop).",
6003
6264
  "Icon-only attach, send and stop buttons all carry accessible labels, each overridable for localisation."
6004
6265
  ],
6005
6266
  "examples": [
@@ -6018,6 +6279,11 @@
6018
6279
  "code": "<ChatComposer\n\tvalue={draft}\n\tonValueChange={setDraft}\n\tonSend={(text) => { post(text, files); setDraft(\"\") }}\n\tonAttach={pickFile}\n\tattachments={files.map((f) => (<Attachment key={f.id} variant=\"media\" name={f.name} thumbnail={f.url} actions={<Button variant=\"floating\" size=\"icon-xs\" aria-label=\"Remove\" onClick={() => drop(f.id)}><Icon name=\"xmark\" /></Button>} />))}\n\tfooter={`${draft.length} / 4000`}\n\tinvalid={draft.length > 4000}\n/>",
6019
6280
  "imports": "import { Attachment, Button, ChatComposer, Icon } from \"@sableui/react\""
6020
6281
  },
6282
+ {
6283
+ "label": "Attachments sent without text",
6284
+ "code": "<ChatComposer\n\tonSend={(text) => post(text, files)}\n\tallowEmpty={files.length > 0}\n\tonAttach={pickFile}\n\tattachments={files.map((f) => <Attachment key={f.id} name={f.name} meta={f.size} />)}\n/>",
6285
+ "imports": "import { Attachment, ChatComposer } from \"@sableui/react\""
6286
+ },
6021
6287
  {
6022
6288
  "label": "A working chat, end to end",
6023
6289
  "code": "<div className=\"flex h-128 flex-col gap-3\">\n\t<MessageScroller className=\"flex-1 min-h-0\">\n\t\t{messages.map((m) => (<Message key={m.id} messageId={m.id} align={m.mine ? \"end\" : \"start\"}><Bubble variant={m.mine ? \"primary\" : \"muted\"} align={m.mine ? \"end\" : \"start\"}>{m.body}</Bubble></Message>))}\n\t\t{streaming ? <Marker icon=\"spinner\" spin shimmer>Thinking…</Marker> : null}\n\t</MessageScroller>\n\t<ChatComposer onSend={send} busy={streaming} onStop={stop} />\n</div>",
@@ -6802,6 +7068,10 @@
6802
7068
  "guidance": "do",
6803
7069
  "description": "Use ComboboxEmpty with helpful copy for zero matches."
6804
7070
  },
7071
+ {
7072
+ "guidance": "do",
7073
+ "description": "For results from a server, pass `filter={null}` so Base UI shows the items as the server returned them, fetch from `onInputValueChange`, and make ComboboxEmpty say which state it is in (searching, no results, type to search). Object items take `itemToStringLabel`."
7074
+ },
6805
7075
  {
6806
7076
  "guidance": "do",
6807
7077
  "description": "Wrap in Field when the combobox is part of a form."
@@ -6898,6 +7168,24 @@
6898
7168
  "default": "false",
6899
7169
  "description": "Select several items instead of one. `value` / `defaultValue` / `onValueChange` become arrays, choosing an item toggles its membership, and the popup stays open between picks. Pair it with ComboboxChips in place of ComboboxInput; every other part is unchanged."
6900
7170
  },
7171
+ {
7172
+ "name": "filter",
7173
+ "type": "null | ((item, query, itemToString?) => boolean)",
7174
+ "default": "Base UI's locale-aware contains",
7175
+ "description": "How items match the query. `null` turns matching off, for items a server already filtered: pair it with `onInputValueChange` to fetch."
7176
+ },
7177
+ {
7178
+ "name": "onInputValueChange",
7179
+ "type": "(inputValue: string, eventDetails) => void",
7180
+ "default": "—",
7181
+ "description": "Fires as the reader types. The place to fetch results for a server-backed combobox."
7182
+ },
7183
+ {
7184
+ "name": "itemToStringLabel",
7185
+ "type": "(item) => string",
7186
+ "default": "item.label for { value, label } items",
7187
+ "description": "The text an object item shows in the input once chosen, and is matched by. Needed for object items without a `label` field."
7188
+ },
6901
7189
  {
6902
7190
  "name": "virtualized",
6903
7191
  "type": "boolean",
@@ -6994,6 +7282,11 @@
6994
7282
  "code": "<Combobox multiple items={tags} value={value} onValueChange={setValue}><ComboboxChips placeholder=\"Add tags…\" /><ComboboxContent><ComboboxEmpty>No matching tags.</ComboboxEmpty><ComboboxList>{(item) => (<ComboboxItem key={item} value={item}>{item}</ComboboxItem>)}</ComboboxList></ComboboxContent></Combobox>",
6995
7283
  "imports": "import { Combobox, ComboboxChips, ComboboxContent, ComboboxEmpty, ComboboxItem, ComboboxList } from \"@sableui/react\""
6996
7284
  },
7285
+ {
7286
+ "label": "Async search (server results)",
7287
+ "code": "<Combobox items={results} filter={null} itemToStringLabel={(person) => person.name} value={person} onValueChange={setPerson} onInputValueChange={setQuery}><ComboboxInput placeholder=\"Search people…\" /><ComboboxContent><ComboboxEmpty>{searching ? \"Searching…\" : query ? \"No one matches.\" : \"Type a name to search.\"}</ComboboxEmpty><ComboboxList>{(item) => (<ComboboxItem key={item.id} value={item}>{item.name}</ComboboxItem>)}</ComboboxList></ComboboxContent></Combobox>",
7288
+ "imports": "import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from \"@sableui/react\""
7289
+ },
6997
7290
  {
6998
7291
  "label": "Virtualized (long list)",
6999
7292
  "code": "<Combobox virtualized items={districts} value={district} onValueChange={setDistrict}><ComboboxInput placeholder=\"Search districts…\" /><ComboboxContent scroll={false}><ComboboxEmpty>No district matches.</ComboboxEmpty><ComboboxVirtualList itemHeight={32}>{(item) => item.label}</ComboboxVirtualList></ComboboxContent></Combobox>",
@@ -10383,6 +10676,128 @@
10383
10676
  }
10384
10677
  ]
10385
10678
  },
10679
+ {
10680
+ "name": "InputGroup",
10681
+ "status": "stable",
10682
+ "group": "Forms",
10683
+ "register": "either",
10684
+ "path": "components/input-group",
10685
+ "summary": "An Input with addons inside the same field: a prefix or suffix (\"https://\", \".com\", \"$\"), an icon, or a borderless Button such as copy or reveal. One shell, one focus ring, one error state; a Field's label and error wiring reaches the Input inside.",
10686
+ "keywords": [
10687
+ "input group",
10688
+ "input addon",
10689
+ "prefix",
10690
+ "suffix",
10691
+ "adornment",
10692
+ "url input",
10693
+ "currency input",
10694
+ "password reveal",
10695
+ "copy field",
10696
+ "icon in input"
10697
+ ],
10698
+ "usage": {
10699
+ "description": "Use InputGroup when something belongs inside the field with the text: a unit, a fixed prefix, an icon, or a small action on the value itself (copy, reveal, clear). Use Field's `trailing` for an action beside the field (Apply next to a promo code), SearchInput for search, and NumberField for numbers with steppers.",
10700
+ "bestPractices": [
10701
+ {
10702
+ "guidance": "do",
10703
+ "description": "Keep text addons short and fixed (\"https://\", \"kg\", \"USD\"). They are part of the field, not its label."
10704
+ },
10705
+ {
10706
+ "guidance": "do",
10707
+ "description": "For a button addon, use a borderless Button (`variant=\"ghost\"`, `size=\"icon-sm\"`) with an aria-label. The addon tightens around it, so it sits as far from the edge as from the top and bottom."
10708
+ },
10709
+ {
10710
+ "guidance": "do",
10711
+ "description": "Wrap the group in Field for a label and errors: `<Field label=\"Website\"><InputGroup leading=\"https://\"><Input /></InputGroup></Field>`."
10712
+ },
10713
+ {
10714
+ "guidance": "dont",
10715
+ "description": "Do not put the value's meaning only in an addon. A screen reader hears the label and the text, so \"Price (USD)\" belongs in the label too."
10716
+ },
10717
+ {
10718
+ "guidance": "dont",
10719
+ "description": "Do not restyle an Input to fake an addon; the group owns the border, the fill and the ring."
10720
+ }
10721
+ ]
10722
+ },
10723
+ "related": {
10724
+ "components": [
10725
+ "Input",
10726
+ "Field",
10727
+ "SearchInput",
10728
+ "NumberField",
10729
+ "Button"
10730
+ ]
10731
+ },
10732
+ "props": [
10733
+ {
10734
+ "name": "leading",
10735
+ "type": "ReactNode",
10736
+ "default": "—",
10737
+ "description": "Inside the field, before the text: a prefix (\"https://\", \"$\"), an icon, or a borderless Button."
10738
+ },
10739
+ {
10740
+ "name": "trailing",
10741
+ "type": "ReactNode",
10742
+ "default": "—",
10743
+ "description": "Inside the field, after the text: a suffix (\".com\", \"kg\"), an icon, or a borderless Button (copy, reveal, clear)."
10744
+ },
10745
+ {
10746
+ "name": "children",
10747
+ "type": "ReactElement",
10748
+ "default": "—",
10749
+ "description": "The single `<Input />`. It keeps its own props and type; the group takes over its border, fill and focus ring."
10750
+ },
10751
+ {
10752
+ "name": "id / required / disabled / aria-describedby / aria-invalid",
10753
+ "type": "control props",
10754
+ "default": "—",
10755
+ "description": "Forwarded to the Input, not set on the group. This is how a Field's wiring reaches the text box when the group is its child."
10756
+ },
10757
+ {
10758
+ "name": "className",
10759
+ "type": "string",
10760
+ "default": "—",
10761
+ "description": "Layout classes for the group (width, margins)."
10762
+ }
10763
+ ],
10764
+ "tokensUsed": [
10765
+ "input",
10766
+ "card",
10767
+ "ring",
10768
+ "destructive",
10769
+ "muted-foreground",
10770
+ "foreground"
10771
+ ],
10772
+ "a11y": [
10773
+ "The Input stays the focusable control; the group draws the focus ring around the whole field while it is focused.",
10774
+ "Inside a Field, the label, description and error are wired to the Input itself, not the group.",
10775
+ "Text addons are plain text beside the input, read in order; put a unit or currency in the label as well so it is not only visual.",
10776
+ "A button addon is a real Button with its own focus stop and needs an aria-label."
10777
+ ],
10778
+ "examples": [
10779
+ {
10780
+ "label": "Prefix and suffix",
10781
+ "code": "<Field label=\"Website\"><InputGroup leading=\"https://\" trailing=\".com\"><Input placeholder=\"example\" /></InputGroup></Field>",
10782
+ "imports": "import { Field, Input, InputGroup } from \"@sableui/react\""
10783
+ },
10784
+ {
10785
+ "label": "Leading icon",
10786
+ "code": "<Field label=\"Email\"><InputGroup leading={<Icon name=\"mail\" />}><Input type=\"email\" placeholder=\"you@example.com\" /></InputGroup></Field>",
10787
+ "imports": "import { Field, Icon, Input, InputGroup } from \"@sableui/react\""
10788
+ },
10789
+ {
10790
+ "label": "Copy button",
10791
+ "code": "<Field label=\"API key\"><InputGroup trailing={<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Copy key\" onClick={copy}><Icon name={copied ? \"check\" : \"copy\"} /></Button>}><Input readOnly value={key} /></InputGroup></Field>",
10792
+ "imports": "import { Button, Field, Icon, Input, InputGroup } from \"@sableui/react\""
10793
+ },
10794
+ {
10795
+ "label": "Password with reveal",
10796
+ "code": "<Field label=\"Password\"><InputGroup trailing={<Button variant=\"ghost\" size=\"icon-sm\" aria-label={shown ? \"Hide password\" : \"Show password\"} aria-pressed={shown} onClick={() => setShown(!shown)}><Icon name={shown ? \"eye-off\" : \"eye\"} /></Button>}><Input type={shown ? \"text\" : \"password\"} autoComplete=\"current-password\" /></InputGroup></Field>",
10797
+ "imports": "import { Button, Field, Icon, Input, InputGroup } from \"@sableui/react\""
10798
+ }
10799
+ ]
10800
+ },
10386
10801
  {
10387
10802
  "name": "Kbd",
10388
10803
  "status": "stable",
@@ -11618,8 +12033,13 @@
11618
12033
  },
11619
12034
  {
11620
12035
  "name": "MenuItem",
11621
- "type": "Base UI Menu.Item props + className (disabled, closeOnClick, onClick) + variant?: \"default\" | \"destructive\"",
11622
- "description": "An interactive row. Highlights via data-[highlighted]; dims via data-[disabled]. variant=\"destructive\" for a delete or irreversible action: it keeps the destructive ink when highlighted (never className=\"text-destructive\", which the highlight repaints)."
12036
+ "type": "Base UI Menu.Item props + className (disabled, closeOnClick, onClick) + variant?: \"default\" | \"destructive\" + icon?: ReactNode + description?: ReactNode",
12037
+ "description": "An interactive row. Highlights via data-[highlighted]; dims via data-[disabled]. variant=\"destructive\" for a delete or irreversible action: it keeps the destructive ink when highlighted (never className=\"text-destructive\", which the highlight repaints). `icon` leads the row; `description` adds a quieter second line saying what the action does, and the icon then sits in a small tile spanning both lines."
12038
+ },
12039
+ {
12040
+ "name": "MenuLinkItem",
12041
+ "type": "Base UI Menu.LinkItem props (href, target, closeOnClick) + className + icon?: ReactNode + description?: ReactNode",
12042
+ "description": "A row that navigates: a real <a>, so it opens in a new tab, middle-clicks and copies as a link. With target=\"_blank\" it adds rel=\"noopener noreferrer\" and a trailing ↗. Takes icon and description like MenuItem."
11623
12043
  },
11624
12044
  {
11625
12045
  "name": "MenuSeparator",
@@ -11693,6 +12113,11 @@
11693
12113
  "code": "<Menu>\n\t<MenuTrigger render={<Button variant=\"secondary\">View</Button>} />\n\t<MenuContent>\n\t\t<MenuItem>Reload</MenuItem>\n\t\t<MenuSubmenu>\n\t\t\t<MenuSubmenuTrigger value={density}>Density</MenuSubmenuTrigger>\n\t\t\t<MenuContent side=\"inline-end\" align=\"start\" sideOffset={4}>\n\t\t\t\t<MenuRadioGroup value={density} onValueChange={setDensity}>\n\t\t\t\t\t<MenuRadioItem value=\"Comfortable\">Comfortable</MenuRadioItem>\n\t\t\t\t\t<MenuRadioItem value=\"Compact\">Compact</MenuRadioItem>\n\t\t\t\t</MenuRadioGroup>\n\t\t\t</MenuContent>\n\t\t</MenuSubmenu>\n\t</MenuContent>\n</Menu>",
11694
12114
  "imports": "import { Button, Menu, MenuContent, MenuItem, MenuRadioGroup, MenuRadioItem, MenuSubmenu, MenuSubmenuTrigger, MenuTrigger } from \"@sableui/react\""
11695
12115
  },
12116
+ {
12117
+ "label": "Rows with icons and descriptions, and links",
12118
+ "code": "<Menu>\n\t<MenuTrigger render={<Button variant=\"outline\" size=\"sm\">Share</Button>} />\n\t<MenuContent align=\"end\">\n\t\t<MenuItem icon={<Icon name=\"copy\" />} description=\"Copy page as Markdown for LLMs\" onClick={copy}>Copy page</MenuItem>\n\t\t<MenuLinkItem icon={<Icon name=\"file\" />} description=\"View this page as plain text\" href={markdownUrl} target=\"_blank\">View as Markdown</MenuLinkItem>\n\t</MenuContent>\n</Menu>",
12119
+ "imports": "import { Button, Icon, Menu, MenuContent, MenuItem, MenuLinkItem, MenuTrigger } from \"@sableui/react\""
12120
+ },
11696
12121
  {
11697
12122
  "label": "Toggling rows",
11698
12123
  "code": "<Menu>\n\t<MenuTrigger render={<Button variant=\"secondary\">Columns</Button>} />\n\t<MenuContent>\n\t\t<MenuCheckboxItem checked={cols.status} onCheckedChange={(v) => setCol(\"status\", v)}>Status</MenuCheckboxItem>\n\t\t<MenuCheckboxItem checked={cols.updated} onCheckedChange={(v) => setCol(\"updated\", v)}>Updated</MenuCheckboxItem>\n\t</MenuContent>\n</Menu>",
@@ -11700,6 +12125,140 @@
11700
12125
  }
11701
12126
  ]
11702
12127
  },
12128
+ {
12129
+ "name": "Menubar",
12130
+ "status": "stable",
12131
+ "group": "Actions",
12132
+ "register": "either",
12133
+ "path": "components/menubar",
12134
+ "summary": "An application's row of menus — File, Edit, View — over Base UI's Menubar. Each menu is an ordinary Sable Menu with a MenubarTrigger as its title; the bar joins them into one tab stop with ←/→ between titles, and once one menu is open, pointing at a neighbour opens it.",
12135
+ "keywords": [
12136
+ "menubar",
12137
+ "menu bar",
12138
+ "app menu",
12139
+ "file menu",
12140
+ "edit menu",
12141
+ "application menu",
12142
+ "desktop menu"
12143
+ ],
12144
+ "usage": {
12145
+ "description": "Use Menubar for a tool or editor whose commands are too many for a toolbar and belong under named menus (File, Edit, View, Help). Use Toolbar for a row of direct actions, a single Menu or OverflowMenu for one set of options, and NavigationMenu-style links for site navigation.",
12146
+ "bestPractices": [
12147
+ {
12148
+ "guidance": "do",
12149
+ "description": "Compose each menu from the Menu parts: `<Menu><MenubarTrigger>File</MenubarTrigger><MenuContent>…</MenuContent></Menu>`. Rows, groups, submenus, checkbox and radio items are the same parts a lone Menu uses."
12150
+ },
12151
+ {
12152
+ "guidance": "do",
12153
+ "description": "Name the bar with aria-label (\"Editor\") so it is announced as the application's menus, not a list of buttons."
12154
+ },
12155
+ {
12156
+ "guidance": "do",
12157
+ "description": "Keep titles to one short word each. A reader scans a menubar left to right; a phrase in the bar is a toolbar button."
12158
+ },
12159
+ {
12160
+ "guidance": "dont",
12161
+ "description": "Do not use MenubarTrigger outside a Menubar, or a plain MenuTrigger inside one. The bar's arrow keys and hover-to-switch work on the triggers it owns; a Button trigger in the bar looks like a menu title and behaves like a stray button."
12162
+ },
12163
+ {
12164
+ "guidance": "dont",
12165
+ "description": "Do not use a menubar for site navigation. Its items are commands in menus (role=menuitem), not links between pages."
12166
+ }
12167
+ ],
12168
+ "anatomy": [
12169
+ {
12170
+ "name": "Menubar",
12171
+ "required": true,
12172
+ "description": "The bar: role=menubar, one tab stop, arrow keys between titles."
12173
+ },
12174
+ {
12175
+ "name": "Menu",
12176
+ "required": true,
12177
+ "description": "One per title; the ordinary Menu root."
12178
+ },
12179
+ {
12180
+ "name": "MenubarTrigger",
12181
+ "required": true,
12182
+ "description": "The title in the bar that opens its menu."
12183
+ },
12184
+ {
12185
+ "name": "MenuContent",
12186
+ "required": true,
12187
+ "description": "The menu's popup, with MenuItem / MenuSubmenu / MenuCheckboxItem rows inside."
12188
+ }
12189
+ ]
12190
+ },
12191
+ "related": {
12192
+ "components": [
12193
+ "Menu",
12194
+ "Toolbar",
12195
+ "OverflowMenu",
12196
+ "ContextMenu"
12197
+ ]
12198
+ },
12199
+ "props": [
12200
+ {
12201
+ "name": "orientation",
12202
+ "type": "\"horizontal\" | \"vertical\"",
12203
+ "default": "\"horizontal\"",
12204
+ "description": "The bar's axis. Arrow keys follow it: ←/→ along a horizontal bar, ↑/↓ down a vertical one."
12205
+ },
12206
+ {
12207
+ "name": "loopFocus",
12208
+ "type": "boolean",
12209
+ "default": "true",
12210
+ "description": "Wrap from the last title back to the first with the arrow keys."
12211
+ },
12212
+ {
12213
+ "name": "modal",
12214
+ "type": "boolean",
12215
+ "default": "true",
12216
+ "description": "While a menu is open, block pointer interaction outside it and lock page scroll, like a lone Menu."
12217
+ },
12218
+ {
12219
+ "name": "disabled",
12220
+ "type": "boolean",
12221
+ "default": "false",
12222
+ "description": "Disable every menu in the bar."
12223
+ },
12224
+ {
12225
+ "name": "className",
12226
+ "type": "string",
12227
+ "description": "Layout classes for the bar. Titles take their look from MenubarTrigger."
12228
+ }
12229
+ ],
12230
+ "subcomponents": [
12231
+ {
12232
+ "name": "MenubarTrigger",
12233
+ "type": "Base UI Menu.Trigger props + className",
12234
+ "description": "A menu's title in the bar. A small ghost Button in the control type tier, held in the accent wash while its menu is open (`sable-open:`), so the reader sees which title the popup belongs to."
12235
+ }
12236
+ ],
12237
+ "tokensUsed": [
12238
+ "accent",
12239
+ "accent-foreground",
12240
+ "foreground",
12241
+ "ring"
12242
+ ],
12243
+ "a11y": [
12244
+ "Base UI renders role=\"menubar\" with aria-orientation; each title is a menuitem with aria-haspopup and aria-expanded.",
12245
+ "The bar is one tab stop. ←/→ move between titles (↑/↓ when vertical), ↓ or Enter opens a menu and moves into it, and Esc closes it and returns focus to its title.",
12246
+ "With one menu open, pointing at another title opens that menu instead, so a reader can sweep across the bar, as in a desktop application.",
12247
+ "Name the bar with aria-label; titles are their own text."
12248
+ ],
12249
+ "examples": [
12250
+ {
12251
+ "label": "Editor menus",
12252
+ "code": "<Menubar aria-label=\"Editor\">\n\t<Menu>\n\t\t<MenubarTrigger>File</MenubarTrigger>\n\t\t<MenuContent>\n\t\t\t<MenuItem>New</MenuItem>\n\t\t\t<MenuItem>Open…</MenuItem>\n\t\t\t<MenuSeparator />\n\t\t\t<MenuItem>Save</MenuItem>\n\t\t</MenuContent>\n\t</Menu>\n\t<Menu>\n\t\t<MenubarTrigger>Edit</MenubarTrigger>\n\t\t<MenuContent>\n\t\t\t<MenuItem>Undo</MenuItem>\n\t\t\t<MenuItem>Redo</MenuItem>\n\t\t</MenuContent>\n\t</Menu>\n</Menubar>",
12253
+ "imports": "import { Menu, Menubar, MenubarTrigger, MenuContent, MenuItem, MenuSeparator } from \"@sableui/react\""
12254
+ },
12255
+ {
12256
+ "label": "With a submenu and toggling rows",
12257
+ "code": "<Menubar aria-label=\"Editor\">\n\t<Menu>\n\t\t<MenubarTrigger>View</MenubarTrigger>\n\t\t<MenuContent>\n\t\t\t<MenuCheckboxItem checked={ruler} onCheckedChange={setRuler}>Show ruler</MenuCheckboxItem>\n\t\t\t<MenuSubmenu>\n\t\t\t\t<MenuSubmenuTrigger value={zoom}>Zoom</MenuSubmenuTrigger>\n\t\t\t\t<MenuContent side=\"inline-end\" align=\"start\" sideOffset={4}>\n\t\t\t\t\t<MenuRadioGroup value={zoom} onValueChange={setZoom}>\n\t\t\t\t\t\t<MenuRadioItem value=\"100%\">100%</MenuRadioItem>\n\t\t\t\t\t\t<MenuRadioItem value=\"Fit\">Fit</MenuRadioItem>\n\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t</MenuContent>\n\t\t\t</MenuSubmenu>\n\t\t</MenuContent>\n\t</Menu>\n</Menubar>",
12258
+ "imports": "import { Menu, Menubar, MenubarTrigger, MenuCheckboxItem, MenuContent, MenuRadioGroup, MenuRadioItem, MenuSubmenu, MenuSubmenuTrigger } from \"@sableui/react\""
12259
+ }
12260
+ ]
12261
+ },
11703
12262
  {
11704
12263
  "name": "Message",
11705
12264
  "status": "stable",
@@ -11893,6 +12452,10 @@
11893
12452
  "usage": {
11894
12453
  "description": "The scroll region a conversation lives in. It owns the scrolling and nothing else — not your messages, not your AI state, not your transport or persistence — so you bring the content renderer and it handles the behaviors that make streaming chat feel right: pinning to the newest message, releasing when the reader scrolls up, keeping the view still when older history loads above, jumping to a specific message, restoring a saved read position, and reporting what is actually on screen.",
11895
12454
  "bestPractices": [
12455
+ {
12456
+ "guidance": "do",
12457
+ "description": "Building a whole chat? `sable docs building-a-chat` covers what the scroller does during a stream, what the app owns, and the failure states around it."
12458
+ },
11896
12459
  {
11897
12460
  "guidance": "do",
11898
12461
  "description": "Bound it the way you would bound any scroll box — `flex-1 min-h-0` inside a flex column, or `max-h-96`. `className` lands on the element that actually scrolls, so the obvious bound is the correct one (same contract as Scroller)."
@@ -12379,6 +12942,196 @@
12379
12942
  }
12380
12943
  ]
12381
12944
  },
12945
+ {
12946
+ "name": "NavigationMenu",
12947
+ "status": "stable",
12948
+ "group": "Navigation",
12949
+ "register": "page",
12950
+ "path": "components/navigation-menu",
12951
+ "summary": "A site header's primary nav over Base UI NavigationMenu: a bar of links and triggers, where a trigger opens a panel of links on hover, click or Enter. One shared panel glides between triggers and resizes to each one's content. Links take any router's link through `render` and mark the current page with `active`.",
12952
+ "keywords": [
12953
+ "navigation",
12954
+ "nav",
12955
+ "header nav",
12956
+ "site nav",
12957
+ "mega menu",
12958
+ "top nav",
12959
+ "navbar",
12960
+ "dropdown nav"
12961
+ ],
12962
+ "usage": {
12963
+ "description": "Use NavigationMenu for a marketing or docs site's header: a few top-level destinations, some of which open a panel of related pages. Use Menu for actions rather than destinations, Tabs to switch views inside a page, Sidebar or SideNav for an app's persistent navigation, and AppHeader for an app's mobile top bar.",
12964
+ "bestPractices": [
12965
+ {
12966
+ "guidance": "do",
12967
+ "description": "Keep the bar to a handful of entries. A trigger groups related pages; a destination with nothing beneath it is a plain NavigationMenuLink in its own item."
12968
+ },
12969
+ {
12970
+ "guidance": "do",
12971
+ "description": "Give panel links a `description` when titles alone are ambiguous (\"Sync\" → \"Offline first, conflict free\"), and lay the Content out with layout classes (`grid w-[30rem] grid-cols-2`); the shared panel sizes itself to it."
12972
+ },
12973
+ {
12974
+ "guidance": "do",
12975
+ "description": "Pass your router's link through `render` (`<NavigationMenuLink render={<Link to=\"/pricing\" />}>`) and set `active` on the current page's link, so the bar shows where the reader is and announces it as `aria-current=\"page\"`."
12976
+ },
12977
+ {
12978
+ "guidance": "do",
12979
+ "description": "Below `md`, hide the bar and offer the same destinations in a Sheet opened from a menu Button: SheetContent side=\"left\" holding a SideNav, its groups mirroring the panels. A hover panel has nothing to hover on a phone, and a bar of five entries does not fit one."
12980
+ },
12981
+ {
12982
+ "guidance": "dont",
12983
+ "description": "Do not put actions in it (Sign in, New project). Those are Buttons beside the nav; the nav holds places to go."
12984
+ },
12985
+ {
12986
+ "guidance": "dont",
12987
+ "description": "Do not assemble the panel yourself. The root renders the one Portal, Positioner, Popup and Viewport every trigger shares; a second one breaks the glide between triggers."
12988
+ }
12989
+ ],
12990
+ "anatomy": [
12991
+ {
12992
+ "name": "NavigationMenu",
12993
+ "required": true,
12994
+ "description": "The `<nav>` root; also renders the shared floating panel."
12995
+ },
12996
+ {
12997
+ "name": "NavigationMenuList",
12998
+ "required": true,
12999
+ "description": "The bar: a `<ul>` the arrow keys rove across."
13000
+ },
13001
+ {
13002
+ "name": "NavigationMenuItem",
13003
+ "required": true,
13004
+ "description": "One `<li>`: a link, or a trigger with its content."
13005
+ },
13006
+ {
13007
+ "name": "NavigationMenuTrigger",
13008
+ "required": false,
13009
+ "description": "Opens its item's content; carries a chevron."
13010
+ },
13011
+ {
13012
+ "name": "NavigationMenuContent",
13013
+ "required": false,
13014
+ "description": "The links a trigger reveals, rendered into the shared panel."
13015
+ },
13016
+ {
13017
+ "name": "NavigationMenuLink",
13018
+ "required": true,
13019
+ "description": "A destination: a bar entry in an item, a two-line row in content."
13020
+ }
13021
+ ]
13022
+ },
13023
+ "related": {
13024
+ "components": [
13025
+ "Menu",
13026
+ "SideNav",
13027
+ "Sheet",
13028
+ "AppHeader",
13029
+ "Tabs"
13030
+ ]
13031
+ },
13032
+ "props": [
13033
+ {
13034
+ "name": "NavigationMenu (Root)",
13035
+ "type": "Base UI NavigationMenu.Root props (value, defaultValue, onValueChange, delay, closeDelay, orientation) + className",
13036
+ "default": "—",
13037
+ "description": "The `<nav>`. `value` is the open item's value (null when closed); `delay` / `closeDelay` tune hover-to-open."
13038
+ },
13039
+ {
13040
+ "name": "align",
13041
+ "type": "\"start\" | \"center\" | \"end\"",
13042
+ "default": "\"start\"",
13043
+ "description": "Alignment of the shared panel along its trigger."
13044
+ },
13045
+ {
13046
+ "name": "sideOffset",
13047
+ "type": "number",
13048
+ "default": "8",
13049
+ "description": "Gap between the bar and the panel, in px. The pointer crosses it on a bridge, so the panel does not close on the way in."
13050
+ }
13051
+ ],
13052
+ "subcomponents": [
13053
+ {
13054
+ "name": "NavigationMenuList",
13055
+ "type": "Base UI NavigationMenu.List props + className",
13056
+ "description": "The bar of items, a `<ul>`. Arrow keys move between entries."
13057
+ },
13058
+ {
13059
+ "name": "NavigationMenuItem",
13060
+ "type": "Base UI NavigationMenu.Item props + className + value?: string",
13061
+ "description": "One `<li>` holding either a NavigationMenuLink or a NavigationMenuTrigger with its NavigationMenuContent."
13062
+ },
13063
+ {
13064
+ "name": "NavigationMenuTrigger",
13065
+ "type": "Base UI NavigationMenu.Trigger props + className",
13066
+ "description": "Opens its item's content on hover, click or Enter, with a chevron that turns while open. Styled like the bar's links: muted at rest, full ink when hovered or open."
13067
+ },
13068
+ {
13069
+ "name": "NavigationMenuContent",
13070
+ "type": "Base UI NavigationMenu.Content props + className + keepMounted?: boolean",
13071
+ "description": "The item's panel content. Size and lay it out with layout classes; the shared panel follows its size and slides it in from the side it was reached from."
13072
+ },
13073
+ {
13074
+ "name": "NavigationMenuLink",
13075
+ "type": "Base UI NavigationMenu.Link props + className + href?: string + active?: boolean + closeOnClick?: boolean + render?: ReactElement + description?: ReactNode",
13076
+ "description": "A destination. In an item it is a bar entry; in content it is a row with an optional `description` line. `active` sets `aria-current=\"page\"`; `render={<Link to=\"…\" />}` keeps the roving focus on your router's link."
13077
+ }
13078
+ ],
13079
+ "tokensUsed": [
13080
+ "popover",
13081
+ "popover-foreground",
13082
+ "border-floating",
13083
+ "accent",
13084
+ "accent-foreground",
13085
+ "muted-foreground",
13086
+ "foreground",
13087
+ "ring"
13088
+ ],
13089
+ "motion": [
13090
+ {
13091
+ "slot": "arrival",
13092
+ "recipe": "custom",
13093
+ "part": "Panel",
13094
+ "why": "The panel fades and scales in from its trigger like a popover, but it also resizes as the reader moves between triggers, which motion-pop cannot express: Base UI measures each content into --popup-width/--popup-height and the popup transitions to them on the motion tokens."
13095
+ },
13096
+ {
13097
+ "slot": "transition",
13098
+ "recipe": "custom",
13099
+ "part": "Panel",
13100
+ "why": "Moving from one trigger to the next keeps one panel open: it glides along the bar and the new content slides in from the direction of travel, so the reader sees one surface changing rather than two popups swapping. Snaps on first open and on resize (data-instant), where a glide would mean nothing."
13101
+ },
13102
+ {
13103
+ "slot": "response",
13104
+ "recipe": "motion-interactive",
13105
+ "part": "Trigger, Link",
13106
+ "why": "Bar entries and panel rows fill on hover and focus like the other interactive surfaces, so the bar reads as live before anything opens."
13107
+ },
13108
+ {
13109
+ "slot": "activity",
13110
+ "recipe": "none",
13111
+ "part": "root",
13112
+ "why": "Nothing in a nav moves on its own."
13113
+ }
13114
+ ],
13115
+ "a11y": [
13116
+ "The root is a <nav>; give it an aria-label when the page has more than one nav (\"Main\").",
13117
+ "Arrow keys move across the bar; Enter, Space or ArrowDown opens a trigger's content and moves focus into it; Esc closes and returns focus to the trigger.",
13118
+ "A trigger is a button with aria-expanded; its content is reachable by Tab in reading order once open.",
13119
+ "`active` on a link sets aria-current=\"page\" and gives it full ink, so the current page is announced as well as shown.",
13120
+ "Panel motion runs on the motion tokens, so reduced motion (or MotionProvider \"reduced\") collapses the glide and resize to instant."
13121
+ ],
13122
+ "examples": [
13123
+ {
13124
+ "label": "Bar with a panel",
13125
+ "code": "<NavigationMenu aria-label=\"Main\">\n\t<NavigationMenuList>\n\t\t<NavigationMenuItem>\n\t\t\t<NavigationMenuTrigger>Product</NavigationMenuTrigger>\n\t\t\t<NavigationMenuContent className=\"grid w-[30rem] grid-cols-2\">\n\t\t\t\t<NavigationMenuLink href=\"/editor\" description=\"Write and review together.\">Editor</NavigationMenuLink>\n\t\t\t\t<NavigationMenuLink href=\"/sync\" description=\"Offline first, conflict free.\">Sync</NavigationMenuLink>\n\t\t\t</NavigationMenuContent>\n\t\t</NavigationMenuItem>\n\t\t<NavigationMenuItem>\n\t\t\t<NavigationMenuLink href=\"/pricing\">Pricing</NavigationMenuLink>\n\t\t</NavigationMenuItem>\n\t</NavigationMenuList>\n</NavigationMenu>",
13126
+ "imports": "import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger } from \"@sableui/react\""
13127
+ },
13128
+ {
13129
+ "label": "With a router, marking the current page",
13130
+ "code": "<NavigationMenu aria-label=\"Main\">\n\t<NavigationMenuList>\n\t\t<NavigationMenuItem>\n\t\t\t<NavigationMenuLink active={path === \"/docs\"} render={<Link to=\"/docs\" />}>Docs</NavigationMenuLink>\n\t\t</NavigationMenuItem>\n\t\t<NavigationMenuItem>\n\t\t\t<NavigationMenuLink active={path === \"/blog\"} render={<Link to=\"/blog\" />}>Blog</NavigationMenuLink>\n\t\t</NavigationMenuItem>\n\t</NavigationMenuList>\n</NavigationMenu>",
13131
+ "imports": "import { NavigationMenu, NavigationMenuItem, NavigationMenuLink, NavigationMenuList } from \"@sableui/react\"\n// <Link> = your router's link (react-router, next/link, @tanstack/router, …)"
13132
+ }
13133
+ ]
13134
+ },
12382
13135
  {
12383
13136
  "name": "Note",
12384
13137
  "status": "stable",
@@ -12955,7 +13708,11 @@
12955
13708
  "dots menu",
12956
13709
  "more actions",
12957
13710
  "ellipsis",
12958
- "kebab"
13711
+ "kebab",
13712
+ "responsive actions",
13713
+ "responsive action menu",
13714
+ "collapse actions",
13715
+ "actions overflow on mobile"
12959
13716
  ],
12960
13717
  "usage": {
12961
13718
  "description": "Use OverflowMenu for compact overflow actions when the primary action is already visible elsewhere.",
@@ -12968,6 +13725,10 @@
12968
13725
  "guidance": "do",
12969
13726
  "description": "Keep labels explicit so the icon-only trigger does not obscure the command meaning."
12970
13727
  },
13728
+ {
13729
+ "guidance": "do",
13730
+ "description": "For actions that fit inline on a wide screen but not a narrow one, render them twice: inline (a Row inside a wrapper shown from a container width; Row sets its own display, so the wrapper does the hiding), and as MenuItems in an OverflowMenu hidden from that width (`@container` + `@md:`). display:none takes the hidden copy out of the accessibility tree, so only one set is ever announced. Keep the primary action inline at every width."
13731
+ },
12971
13732
  {
12972
13733
  "guidance": "dont",
12973
13734
  "description": "Do not hide the most important or most frequent action inside OverflowMenu."
@@ -13046,6 +13807,11 @@
13046
13807
  "label": "With leading icons",
13047
13808
  "code": "<OverflowMenu><MenuItem><Icon name=\"check\" className=\"text-sm\" />Mark as read</MenuItem><MenuSeparator /><MenuItem variant=\"destructive\"><Icon name=\"xmark\" className=\"text-sm\" />Delete</MenuItem></OverflowMenu>",
13048
13809
  "imports": "import { Icon, MenuItem, MenuSeparator, OverflowMenu } from \"@sableui/react\""
13810
+ },
13811
+ {
13812
+ "label": "Responsive actions: inline when wide, in a menu when narrow",
13813
+ "code": "<div className=\"@container/actions flex w-full justify-end\">\n\t<Row gap={1} align=\"center\">\n\t\t<div className=\"hidden @md/actions:block\">\n\t\t\t<Row gap={1}>\n\t\t\t\t<Button variant=\"ghost\" size=\"sm\" iconStart={<Icon name=\"share\" />}>Share</Button>\n\t\t\t\t<Button variant=\"ghost\" size=\"sm\" iconStart={<Icon name=\"download\" />}>Export</Button>\n\t\t\t</Row>\n\t\t</div>\n\t\t<div className=\"@md/actions:hidden\">\n\t\t\t<OverflowMenu label=\"Report actions\" size=\"icon-sm\">\n\t\t\t\t<MenuItem icon={<Icon name=\"share\" />}>Share</MenuItem>\n\t\t\t\t<MenuItem icon={<Icon name=\"download\" />}>Export</MenuItem>\n\t\t\t</OverflowMenu>\n\t\t</div>\n\t\t<Button size=\"sm\" iconStart={<Icon name=\"edit\" />}>Edit report</Button>\n\t</Row>\n</div>",
13814
+ "imports": "import { Button, Icon, MenuItem, OverflowMenu, Row } from \"@sableui/react\""
13049
13815
  }
13050
13816
  ]
13051
13817
  },
@@ -13246,7 +14012,7 @@
13246
14012
  "group": "Navigation",
13247
14013
  "register": "either",
13248
14014
  "path": "components/pagination",
13249
- "summary": "Page navigation built from our own Button + Icon (no Base UI). A <nav aria-label='Pagination'> with prev/next icon buttons that disable at the bounds and numbered page buttons; the current page is a secondary button marked aria-current. Collapses to first/last/current plus a sibling window, bridging skipped runs with an ellipsis.",
14015
+ "summary": "Page navigation built from our own Button + Icon (no Base UI). A <nav aria-label='Pagination'> with prev/next icon buttons that disable at the bounds and numbered page buttons; the current page is a secondary button marked aria-current. With pageLink every control is a real link (crawlable, opens in a new tab, any router through render). Collapses to first/last/current plus a sibling window, bridging skipped runs with an ellipsis.",
13250
14016
  "keywords": [
13251
14017
  "pagination",
13252
14018
  "pager",
@@ -13255,7 +14021,10 @@
13255
14021
  "results",
13256
14022
  "navigation",
13257
14023
  "next",
13258
- "previous"
14024
+ "previous",
14025
+ "page links",
14026
+ "seo",
14027
+ "url"
13259
14028
  ],
13260
14029
  "usage": {
13261
14030
  "description": "Use Pagination when a result set is split into discrete pages and the user needs predictable movement through them. Pair it with DataTable, list results, or server-paged views; use a load-more Button or infinite scroll when page numbers are not meaningful.",
@@ -13268,6 +14037,10 @@
13268
14037
  "guidance": "do",
13269
14038
  "description": "Use a specific label when multiple pagers appear on a page, such as \"Customer pages\"."
13270
14039
  },
14040
+ {
14041
+ "guidance": "do",
14042
+ "description": "Pass pageLink when the page lives in the URL (search results, a blog, a catalogue): each page becomes a link a crawler can follow and a reader can open in a new tab. Use a router's Link there so navigation stays in the app."
14043
+ },
13271
14044
  {
13272
14045
  "guidance": "do",
13273
14046
  "description": "Reset to page 1 when filters or search terms materially change the result set."
@@ -13332,7 +14105,13 @@
13332
14105
  "name": "onPageChange",
13333
14106
  "type": "(page: number) => void",
13334
14107
  "default": "—",
13335
- "description": "Called with the next page when a control is activated. No-op for out-of-range or unchanged values."
14108
+ "description": "Called with the next page when a control is activated. No-op for out-of-range or unchanged values. Optional with pageLink, where the URL is the page state."
14109
+ },
14110
+ {
14111
+ "name": "pageLink",
14112
+ "type": "(page: number) => ReactElement",
14113
+ "default": "—",
14114
+ "description": "Render each page control as a link: return the element for a page, (page) => <a href={`?page=${page}`} /> or a router's <Link to={`?page=${page}`} />. The current page keeps aria-current; previous/next at the bounds stay disabled buttons. onPageChange becomes optional and, if set, still fires on click."
13336
14115
  },
13337
14116
  {
13338
14117
  "name": "siblingCount",
@@ -13366,6 +14145,7 @@
13366
14145
  "Renders a landmark <nav> with aria-label so assistive tech can jump to the pager.",
13367
14146
  "The current page button carries aria-current='page'.",
13368
14147
  "Prev/next buttons are real <button>s with aria-label and become disabled at the bounds.",
14148
+ "With pageLink the controls are real <a> links (or the router's), so they are crawlable and open in a new tab; a bound arrow stays a disabled button rather than a link to nowhere.",
13369
14149
  "Ellipsis slots are aria-hidden — they convey no navigable target."
13370
14150
  ],
13371
14151
  "examples": [
@@ -13384,6 +14164,11 @@
13384
14164
  "code": "<Stack gap={2}><DataTable data={rows} columns={columns} rowKey={(row) => row.id} /><Pagination label=\"Customer pages\" page={page} total={12} onPageChange={setPage} /></Stack>",
13385
14165
  "imports": "import { DataTable, Pagination, Stack } from \"@sableui/react\""
13386
14166
  },
14167
+ {
14168
+ "label": "Page links (URL is the state)",
14169
+ "code": "<Pagination page={page} total={pageCount} pageLink={(page) => <Link to={`/search?q=${query}&page=${page}`} />} />",
14170
+ "imports": "import { Pagination } from \"@sableui/react\"\n// <Link> = your router's link (react-router, next/link, @tanstack/router, …)"
14171
+ },
13387
14172
  {
13388
14173
  "label": "Server query state",
13389
14174
  "code": "const [page, setPage] = useState(searchParams.page ?? 1)\n<Pagination page={page} total={pageCount} onPageChange={(next) => { setPage(next); loadPage(next) }} />",
@@ -15247,6 +16032,12 @@
15247
16032
  "default": "true",
15248
16033
  "description": "On a horizontal Scroller, let a vertical mouse wheel scroll it sideways. Ignored when vertical. Turn it off only if the region is inside something that owns the wheel itself (a map, a canvas)."
15249
16034
  },
16035
+ {
16036
+ "name": "smoothWheel",
16037
+ "type": "boolean",
16038
+ "default": "true",
16039
+ "description": "Ease mouse-wheel steps toward their target, on either axis: the inertia a smooth-scroll library such as Lenis adds, scoped to this region. Only a mouse wheel is eased; trackpads and touch keep their own momentum, keys and scrollbar drags stay native, and reduced motion (the OS preference or MotionProvider) turns it off. Anything else that moves the region (scrollIntoView, a stick-to-bottom chat) stops the easing at once. For a scroll container of your own, `useSmoothWheel()` returns the same behaviour as a ref callback."
16040
+ },
15250
16041
  {
15251
16042
  "name": "label",
15252
16043
  "type": "string",
@@ -16444,7 +17235,10 @@
16444
17235
  "drawer",
16445
17236
  "side panel",
16446
17237
  "overlay",
16447
- "swipe to dismiss"
17238
+ "swipe to dismiss",
17239
+ "bottom sheet",
17240
+ "snap points",
17241
+ "detents"
16448
17242
  ],
16449
17243
  "usage": {
16450
17244
  "description": "Use Sheet for secondary workflows that slide in while preserving context from the underlying page.",
@@ -16457,6 +17251,10 @@
16457
17251
  "guidance": "do",
16458
17252
  "description": "Include clear title, close path, and footer actions for task-oriented sheets."
16459
17253
  },
17254
+ {
17255
+ "guidance": "do",
17256
+ "description": "Give a bottom sheet snap points when it shows a summary first and detail on demand (a map result, a now-playing panel): `snapPoints={[0.4, 1]}`. The first stop is where it opens; put what matters at that height, because the rest is a drag away."
17257
+ },
16460
17258
  {
16461
17259
  "guidance": "dont",
16462
17260
  "description": "Do not use Sheet for simple confirmations or deeply nested workflows."
@@ -16519,15 +17317,39 @@
16519
17317
  "props": [
16520
17318
  {
16521
17319
  "name": "Sheet (Root)",
16522
- "type": "Base UI Drawer.Root props (open, defaultOpen, onOpenChange, onOpenChangeComplete, modal, disablePointerDismissal, actionsRef, handle) — minus swipeDirection and snap points",
17320
+ "type": "Base UI Drawer.Root props (open, defaultOpen, onOpenChange, onOpenChangeComplete, modal, disablePointerDismissal, actionsRef, handle, snapPoints, snapPoint, defaultSnapPoint, onSnapPointChange, snapToSequentialPoints) — minus swipeDirection",
16523
17321
  "description": "Controls open state. Uncontrolled by default. The dismiss swipe direction is not a prop: it follows SheetContent's side. onOpenChange reports reason \"swipe\" when a swipe closed it."
17322
+ },
17323
+ {
17324
+ "name": "snapPoints",
17325
+ "type": "(number | string)[]",
17326
+ "default": "—",
17327
+ "description": "Stops a bottom sheet rests at, smallest first: a fraction of the viewport (0.4), pixels (320), or a px/rem string (\"20rem\"). The panel is the full stop's height and opens at the first stop; a swipe moves between stops and a swipe below the first closes it. side=\"bottom\" only: other sides ignore them, with a development warning."
17328
+ },
17329
+ {
17330
+ "name": "snapPoint",
17331
+ "type": "number | string | null",
17332
+ "default": "—",
17333
+ "description": "The active stop, controlled. Pair with onSnapPointChange; use defaultSnapPoint to open at a different stop uncontrolled."
17334
+ },
17335
+ {
17336
+ "name": "onSnapPointChange",
17337
+ "type": "(snapPoint: number | string | null, eventDetails) => void",
17338
+ "default": "—",
17339
+ "description": "Fires when a swipe (or your code) moves the sheet to another stop."
17340
+ },
17341
+ {
17342
+ "name": "snapToSequentialPoints",
17343
+ "type": "boolean",
17344
+ "default": "false",
17345
+ "description": "Move one stop per swipe, by distance. Off, a fast flick can skip past a stop."
16524
17346
  }
16525
17347
  ],
16526
17348
  "subcomponents": [
16527
17349
  {
16528
17350
  "name": "SheetContent",
16529
17351
  "type": "Base UI Drawer.Popup props (initialFocus, finalFocus, …) + side: \"right\" | \"left\" | \"top\" | \"bottom\" + className",
16530
- "description": "Backdrop + edge-anchored popup; portals to <body>. side picks which viewport edge the sheet anchors to and slides in from (defaults to \"right\"). Left/right are full-height side panels; top/bottom span the width up to 80vh. On touch it swipes closed toward that same edge (right swipes right, left swipes left, top swipes up, bottom swipes down)."
17352
+ "description": "Backdrop + edge-anchored popup; portals to <body>. side picks which viewport edge the sheet anchors to and slides in from (defaults to \"right\"). Left/right are full-height side panels; top/bottom span the width up to 80vh. A bottom sheet with snapPoints is as tall as its highest stop allows (the viewport less a top gap), shows a drag handle, and translates down to the active stop; the part below the screen becomes bottom padding, so its scroll area and SheetFooter end at the visible edge. A bottom sheet rides above the on-screen keyboard (measured from visualViewport). On touch it swipes closed toward that same edge (right swipes right, left swipes left, top swipes up, bottom swipes down)."
16531
17353
  },
16532
17354
  {
16533
17355
  "name": "SheetTrigger",
@@ -16587,7 +17409,9 @@
16587
17409
  "Focus matches Dialog: the first tabbable on mouse/keyboard open, the panel itself on touch (so the virtual keyboard stays down); focus returns to the trigger on close.",
16588
17410
  "Render a SheetClose inside SheetContent so screen-reader users can escape the popup.",
16589
17411
  "Slide animation runs via data-[starting-style]/data-[ending-style] attributes on the motion-duration token; while swiping the panel tracks the finger directly, and a released swipe finishes on the same token (scaled by flick speed), so reduced motion makes it instant.",
16590
- "A mouse drag that starts on sheet content selects text rather than dragging the panel; mouse dragging only starts from the padding edge."
17412
+ "A mouse drag that starts on sheet content selects text rather than dragging the panel; mouse dragging only starts from the padding edge.",
17413
+ "Snap points add stops, not a gate: at any stop the content scrolls within the visible part and every control stays reachable by keyboard, so nothing needs the drag. The handle is decorative (aria-hidden).",
17414
+ "A bottom sheet stays above the on-screen keyboard: its bottom edge is lifted by the height the keyboard takes from the visual viewport, so a focused field is not covered. An app that sets interactive-widget=resizes-content in its viewport meta gets the same result from the browser, and the inset reads 0."
16591
17415
  ],
16592
17416
  "examples": [
16593
17417
  {
@@ -16605,6 +17429,11 @@
16605
17429
  "code": "<Sheet><SheetTrigger render={<Button variant=\"secondary\">Menu</Button>} /><SheetContent side=\"left\"><SheetHeader><SheetTitle>Navigation</SheetTitle></SheetHeader><SheetFooter><SheetClose render={<Button variant=\"secondary\">Close</Button>} /></SheetFooter></SheetContent></Sheet>",
16606
17430
  "imports": "import { Button, Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from \"@sableui/react\""
16607
17431
  },
17432
+ {
17433
+ "label": "Bottom sheet with snap points",
17434
+ "code": "<Sheet snapPoints={[0.4, 1]}><SheetTrigger render={<Button variant=\"secondary\">Nearby</Button>} /><SheetContent side=\"bottom\"><SheetHeader><SheetTitle>12 places nearby</SheetTitle><SheetDescription>Drag up for the full list.</SheetDescription></SheetHeader><SheetList>{places.map((place) => <Entity key={place.id} title={place.name} subtitle={place.distance} />)}</SheetList></SheetContent></Sheet>",
17435
+ "imports": "import { Button, Entity, Sheet, SheetContent, SheetDescription, SheetHeader, SheetList, SheetTitle, SheetTrigger } from \"@sableui/react\""
17436
+ },
16608
17437
  {
16609
17438
  "label": "Bottom side",
16610
17439
  "code": "<Sheet><SheetTrigger render={<Button variant=\"secondary\">Details</Button>} /><SheetContent side=\"bottom\"><SheetHeader><SheetTitle>Order details</SheetTitle></SheetHeader><SheetFooter><SheetClose render={<Button variant=\"secondary\">Close</Button>} /></SheetFooter></SheetContent></Sheet>",
@@ -16720,6 +17549,10 @@
16720
17549
  "guidance": "do",
16721
17550
  "description": "Keep item labels predictable and active state obvious."
16722
17551
  },
17552
+ {
17553
+ "guidance": "do",
17554
+ "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."
17555
+ },
16723
17556
  {
16724
17557
  "guidance": "dont",
16725
17558
  "description": "Do not use Sidebar for page-local settings, filters, or temporary action groups."
@@ -16767,6 +17600,12 @@
16767
17600
  "type": "\"app\" | \"inline\"",
16768
17601
  "default": "\"app\"",
16769
17602
  "description": "Where the rail lives. \"app\" is the page's chrome column — sticky, full viewport height, hidden below lg where AppHeader + BottomNav take over. \"inline\" is always visible and fills its parent's height, for a rail inside a bounded frame (a nested shell, an embedded preview). Use it instead of overriding the layout with !important classes."
17603
+ },
17604
+ {
17605
+ "name": "surface",
17606
+ "type": "\"rail\" | \"page\"",
17607
+ "default": "\"rail\"",
17608
+ "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."
16770
17609
  }
16771
17610
  ],
16772
17611
  "subcomponents": [
@@ -18035,6 +18874,10 @@
18035
18874
  "usage": {
18036
18875
  "description": "Use StreamingText for text whose final length is unknown while it renders — a model's answer, a live transcript, a log line being written. It is the counterpart to ThinkingOrb and Spinner: those cover the wait BEFORE the first token, this covers the arrival of the tokens themselves.",
18037
18876
  "bestPractices": [
18877
+ {
18878
+ "guidance": "do",
18879
+ "description": "In a chat, read `sable docs building-a-chat`: one status drives `streaming` and the composer, a regenerated answer takes a new key (the text is append-only), and the finished turn is announced once."
18880
+ },
18038
18881
  {
18039
18882
  "guidance": "do",
18040
18883
  "description": "Grow `text` and let the component do the rest. It assumes an append-only string and keys units by index, so React mounts only the new spans — re-rendering a longer string animates what was added and leaves what the reader has already read alone."
@@ -18159,7 +19002,7 @@
18159
19002
  },
18160
19003
  {
18161
19004
  "label": "Inside a message, with the wait reported elsewhere",
18162
- "code": "<Message role=\"assistant\"><StreamingText text={answer} streaming={pending} caret={false} /></Message>",
19005
+ "code": "<Message align=\"start\"><StreamingText text={answer} streaming={pending} caret={false} /></Message>",
18163
19006
  "imports": "import { Message, StreamingText } from \"@sableui/react\""
18164
19007
  },
18165
19008
  {
@@ -20553,6 +21396,7 @@
20553
21396
  "summary": "A full-height conversation surface: titled header, scrolling transcript, and a composer pinned to the bottom — the assembled shape of an AI assistant, a support thread, or a DM.",
20554
21397
  "intent": "Reach for this for any conversation that owns its screen — an assistant, a support inbox thread, a DM. Not for a chat docked beside other work (compose the same three parts inside a Sheet or a PanelGroup instead), and not for a comment thread under an article, which is a FeedPage concern.",
20555
21398
  "notes": [
21399
+ "This is the assembled screen with sample data. To wire it to a real streaming source (a hook such as useChat, or your own transport), read `sable docs building-a-chat`: who owns what, the turn lifecycle mapped to props, and every failure state.",
20556
21400
  "The panel is a fixed-height column, not a growing page. `h-[calc(100dvh_-_--spacing(12))]` on the Card (the viewport minus the Page's py-6, in spacing units so density moves both together — a fixed `3rem` drifts the moment density moves the padding) and `flex-1 min-h-0` on the MessageScroller are what make the transcript scroll instead of the document — the single most common thing to get wrong here, and it fails by pushing the composer off the bottom of the screen.",
20557
21401
  "The panel is a Card, not a hand-built bordered section: CardHeader (title, description, CardAction), the MessageScroller and a CardFooter all inset by the card's own `--card-p`, so the three regions share one edge at any card density.",
20558
21402
  "`dvh` rather than `vh`: on mobile the URL bar collapses, and a `vh`-sized panel puts the composer under it exactly when someone is trying to type.",
@@ -20575,7 +21419,7 @@
20575
21419
  "The composer is a real <form> whose Enter handling skips IME composition, and every icon-only control in the panel carries an accessible label — the answer actions also carry a tooltip, and the stop control is named for what it stops (`stopLabel=\"Stop answering\"`)."
20576
21420
  ],
20577
21421
  "imports": "import { Attachment, Avatar, Bubble, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, ChatComposer, CodeBlock, Icon, Marker, MenuRadioGroup, MenuRadioItem, Message, MessageScroller, Page, SettingsMenu, SettingsRow, SimpleTooltip, StreamingText, Toolbar, ToolbarButton } from \"@sableui/react\"",
20578
- "code": "/**\n * The request is in flight. In an app this is your send state — from `useChat`,\n * a fetch, whatever owns the turn — and it is deliberately ONE flag driving two\n * places: the composer's `busy` and the answer's `streaming`. They are the same\n * wait reported at both ends of the panel, and letting them drift is how you get\n * a composer that still says \"sending\" under an answer that already finished.\n */\nconst pending = true\n\n/** The answer so far. Append tokens to this; `StreamingText` assumes it only grows. */\nconst streamed =\n\t\"Yes, appends are already safe: your layout effect measures before paint. The jump is on the prepend path, which reads scrollHeight after the new rows\"\n\n/**\n * The opening exchange. `step` is what the assistant did before answering — a\n * doc it read, a tool it ran — and it rides in that message's header, so it\n * reads as part of the turn rather than as an event between turns.\n */\nconst turns: { id: string; mine: boolean; step?: string; body: string }[] = [\n\t{\n\t\tid: \"t_01\",\n\t\tmine: true,\n\t\tbody: \"Our chat scroll jumps every time older messages load. What are we doing wrong?\",\n\t},\n\t{\n\t\tid: \"t_02\",\n\t\tmine: false,\n\t\tstep: \"Read message-scroller.md\",\n\t\tbody: \"Almost always the anchor. If you preserve the scroll position by measuring the container, prepending history moves everything under the reader.\",\n\t},\n\t{\n\t\tid: \"t_03\",\n\t\tmine: false,\n\t\tbody: \"Anchor on the oldest rendered message instead and add back however far it moved. Appends leave it alone, so the same rule covers both.\",\n\t},\n]\n\nexport function ChatPanel() {\n\t// One identity for every assistant turn. A glyph rather than initials: \"AI\"\n\t// in a circle reads as a person called A. I. The glyph is sized to the\n\t// circle (the fallback's own size is tuned for two letters, and a lone icon\n\t// at that size reads as a speck); `alt` names it for a screen reader, which\n\t// otherwise gets nothing from a fallback-only avatar.\n\tconst assistant = (\n\t\t<Avatar variant=\"art\"\n\t\t\tsize=\"sm\"\n\t\t\talt=\"Assistant\"\n\t\t\tfallback={<Icon name=\"sparkles\" className=\"text-base\" />}\n\t\t/>\n\t)\n\n\treturn (\n\t\t<Page as=\"main\" max=\"feed\" gap={0} className=\"py-6\">\n\t\t\t{/* h-dvh minus the page's own padding (py-6 = 12 spacing units, in\n\t\t\t spacing units so density moves both together): the panel owns the\n\t\t\t viewport, and the transcript — not the page — is the thing that\n\t\t\t scrolls. The panel is a Card; its parts inset by the card's own\n\t\t\t --card-p, so header, transcript and composer share one edge. */}\n\t\t\t<Card\n\t\t\t\trender={<section aria-labelledby=\"chat-title\" />}\n\t\t\t\tclassName=\"flex h-[calc(100dvh_-_--spacing(12))] flex-col overflow-hidden\"\n\t\t\t>\n\t\t\t\t<CardHeader className=\"pb-(--card-p) shadow-[inset_0_-1px_0_var(--border)]\">\n\t\t\t\t\t{/* CardTitle carries the type; the h1 inside it is the page heading. */}\n\t\t\t\t\t<CardTitle>\n\t\t\t\t\t\t<h1 id=\"chat-title\">Scroll behaviour</h1>\n\t\t\t\t\t</CardTitle>\n\t\t\t\t\t<CardDescription>Assistant · answers from your team's docs</CardDescription>\n\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t{/* Labelled, not a bare plus: the composer already has a plus\n\t\t\t\t\t\t (attach), and two identical glyphs a panel apart that do\n\t\t\t\t\t\t different things is a guessing game. */}\n\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\">\n\t\t\t\t\t\t\t<Icon name=\"edit\" />\n\t\t\t\t\t\t\tNew chat\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</CardAction>\n\t\t\t\t</CardHeader>\n\n\t\t\t\t{/* flex-1 min-h-0 is the whole layout: it bounds the scroll region\n\t\t\t\t against the panel instead of letting the transcript grow the page. */}\n\t\t\t\t<MessageScroller className=\"min-h-0 flex-1 p-(--card-p)\">\n\t\t\t\t\t<Marker variant=\"separator\">Today</Marker>\n\n\t\t\t\t\t{turns.map((turn, i) => {\n\t\t\t\t\t\t// Derived, not hand-set: a turn continues a run when the one\n\t\t\t\t\t\t// before it has the same author. A continuation drops its\n\t\t\t\t\t\t// avatar (Message keeps the column) AND its tail (Bubble keeps\n\t\t\t\t\t\t// the ear's space) — together, that makes the run one block.\n\t\t\t\t\t\tconst continues = i > 0 && turns[i - 1].mine === turn.mine\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<Message\n\t\t\t\t\t\t\t\tkey={turn.id}\n\t\t\t\t\t\t\t\tmessageId={turn.id}\n\t\t\t\t\t\t\t\talign={turn.mine ? \"end\" : \"start\"}\n\t\t\t\t\t\t\t\tgrouped={continues}\n\t\t\t\t\t\t\t\tavatar={turn.mine ? undefined : assistant}\n\t\t\t\t\t\t\t\theader={\n\t\t\t\t\t\t\t\t\tturn.step ? (\n\t\t\t\t\t\t\t\t\t\t<Marker variant=\"note\" icon=\"book\" className=\"justify-start\">\n\t\t\t\t\t\t\t\t\t\t\t{turn.step}\n\t\t\t\t\t\t\t\t\t\t</Marker>\n\t\t\t\t\t\t\t\t\t) : undefined\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Bubble\n\t\t\t\t\t\t\t\t\tvariant={turn.mine ? \"primary\" : \"muted\"}\n\t\t\t\t\t\t\t\t\talign={turn.mine ? \"end\" : \"start\"}\n\t\t\t\t\t\t\t\t\ttail={!continues}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{turn.body}\n\t\t\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t\t</Message>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\n\t\t\t\t\t{/* The same run, continued: long-form output with no bubble,\n\t\t\t\t\t because a tinted box around a code block reads as a quote\n\t\t\t\t\t rather than as a turn. Its footer carries what the reader does\n\t\t\t\t\t WITH a settled answer, as a Toolbar: one tab stop, arrow keys\n\t\t\t\t\t between the actions. */}\n\t\t\t\t\t<Message\n\t\t\t\t\t\tmessageId=\"t_04\"\n\t\t\t\t\t\tgrouped\n\t\t\t\t\t\tavatar={assistant}\n\t\t\t\t\t\tfooter={\n\t\t\t\t\t\t\t<Toolbar aria-label=\"Answer actions\">\n\t\t\t\t\t\t\t\t<SimpleTooltip content=\"Copy answer\">\n\t\t\t\t\t\t\t\t\t<ToolbarButton size=\"icon-sm\" aria-label=\"Copy answer\">\n\t\t\t\t\t\t\t\t\t\t<Icon name=\"copy\" />\n\t\t\t\t\t\t\t\t\t</ToolbarButton>\n\t\t\t\t\t\t\t\t</SimpleTooltip>\n\t\t\t\t\t\t\t\t<SimpleTooltip content=\"Regenerate\">\n\t\t\t\t\t\t\t\t\t<ToolbarButton size=\"icon-sm\" aria-label=\"Regenerate answer\">\n\t\t\t\t\t\t\t\t\t\t<Icon name=\"refresh\" />\n\t\t\t\t\t\t\t\t\t</ToolbarButton>\n\t\t\t\t\t\t\t\t</SimpleTooltip>\n\t\t\t\t\t\t\t</Toolbar>\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t{/* max-w-full: a plain Bubble sizes to its content with no\n\t\t\t\t\t\t measure cap, and a code line is wider than a phone. Bounded\n\t\t\t\t\t\t by the column, the CodeBlock scrolls inside itself instead of\n\t\t\t\t\t\t pushing the whole transcript sideways. */}\n\t\t\t\t\t\t<Bubble variant=\"plain\" className=\"max-w-full\">\n\t\t\t\t\t\t\t<p className=\"text-body-md text-foreground\">\n\t\t\t\t\t\t\t\tHere is the shape of it:\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<CodeBlock\n\t\t\t\t\t\t\t\tcode={\n\t\t\t\t\t\t\t\t\t\"const shift = anchor.offsetTop - lastTop\\nif (shift !== 0) viewport.scrollTop += shift\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfilename=\"preserve.ts\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t</Message>\n\n\t\t\t\t\t{/* The reader's follow-up is one turn: the question and the file\n\t\t\t\t\t it refers to stack in a single Message, so they share a side, an\n\t\t\t\t\t edge and a timestamp rather than reading as two sends. */}\n\t\t\t\t\t<Message messageId=\"t_05\" align=\"end\">\n\t\t\t\t\t\t<Bubble variant=\"primary\" align=\"end\">\n\t\t\t\t\t\t\tHere's ours. Does the same fix cover new messages arriving?\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t<Attachment\n\t\t\t\t\t\t\tname=\"scroller.tsx\"\n\t\t\t\t\t\t\tmeta=\"4.2 KB · TSX\"\n\t\t\t\t\t\t\tonOpen={() => undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</Message>\n\n\t\t\t\t\t{/* The answer arriving. A `Marker` with a spinner — or ThinkingOrb —\n\t\t\t\t\t is what sits here BEFORE the first token; once tokens land it is\n\t\t\t\t\t replaced rather than kept, because two indicators for one wait\n\t\t\t\t\t read as two things happening. What stays is the step it took,\n\t\t\t\t\t as a finished note in the header.\n\n\t\t\t\t\t No bubble, same as the long-form answer above: a tinted box\n\t\t\t\t\t around a growing paragraph makes the reflow look like the box\n\t\t\t\t\t breathing. And no answer actions yet — there is nothing to copy\n\t\t\t\t\t or regenerate until it settles; Stop lives in the composer.\n\n\t\t\t\t\t MessageScroller needs no help with this — it already re-follows on\n\t\t\t\t\t \"a message grew\" (ResizeObserver), not just on \"a message arrived\",\n\t\t\t\t\t so the newest token stays pinned while the reader can still scroll\n\t\t\t\t\t up without being yanked back. */}\n\t\t\t\t\t<Message\n\t\t\t\t\t\tmessageId=\"t_06\"\n\t\t\t\t\t\tavatar={assistant}\n\t\t\t\t\t\theader={\n\t\t\t\t\t\t\t<Marker variant=\"note\" icon=\"file\" className=\"justify-start\">\n\t\t\t\t\t\t\t\tRead scroller.tsx\n\t\t\t\t\t\t\t</Marker>\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Bubble variant=\"plain\">\n\t\t\t\t\t\t\t<StreamingText text={streamed} streaming={pending} />\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t</Message>\n\t\t\t\t</MessageScroller>\n\n\t\t\t\t<CardFooter className=\"pt-(--card-p) shadow-[inset_0_1px_0_var(--border)]\">\n\t\t\t\t\t<ChatComposer\n\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\tplaceholder=\"Ask a follow-up…\"\n\t\t\t\t\t\t// Same flag as the answer's `streaming`, which is what turns Send\n\t\t\t\t\t\t// into Stop. A composer that stays sendable mid-answer invites a\n\t\t\t\t\t\t// second request on top of the one still running.\n\t\t\t\t\t\tbusy={pending}\n\t\t\t\t\t\tonStop={() => undefined}\n\t\t\t\t\t\tstopLabel=\"Stop answering\"\n\t\t\t\t\t\tonAttach={() => undefined}\n\t\t\t\t\t\tfooter=\"Enter to send · Shift+Enter for a new line\"\n\t\t\t\t\t\t// Model and effort sit in `actions`, at the send end of the row:\n\t\t\t\t\t\t// they qualify the message about to be sent, so they belong beside\n\t\t\t\t\t\t// the button that sends it rather than next to the attach control.\n\t\t\t\t\t\tactions={\n\t\t\t\t\t\t\t<SettingsMenu\n\t\t\t\t\t\t\t\tvariant=\"subtle\"\n\t\t\t\t\t\t\t\tsize=\"xs\"\n\t\t\t\t\t\t\t\tlabel=\"Change model and effort\"\n\t\t\t\t\t\t\t\tsummary={\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-foreground\">5.6 Sol</span>{\" \"}\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-muted-foreground\">High</span>\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>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Model\" value=\"5.6 Sol\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"sol\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"sol\">5.6 Sol</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"lite\">5.6 Lite</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Effort\" value=\"High\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"high\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"low\">Low</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"standard\">Standard</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"high\">High</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Speed\" value=\"Standard\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"standard\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"standard\">Standard</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"priority\">Priority</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t</SettingsMenu>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t</CardFooter>\n\t\t\t</Card>\n\t\t</Page>\n\t)\n}"
21422
+ "code": "/**\n * The request is in flight. In an app this is your send state — from `useChat`,\n * a fetch, whatever owns the turn — and it is deliberately ONE flag driving two\n * places: the composer's `busy` and the answer's `streaming`. They are the same\n * wait reported at both ends of the panel, and letting them drift is how you get\n * a composer that still says \"sending\" under an answer that already finished.\n */\nconst pending = true\n\n/** The answer so far. Append tokens to this; `StreamingText` assumes it only grows. */\nconst streamed =\n\t\"Yes, appends are already safe: your layout effect measures before paint. The jump is on the prepend path, which reads scrollHeight after the new rows\"\n\n/**\n * The opening exchange. `step` is what the assistant did before answering — a\n * doc it read, a tool it ran — and it rides in that message's header, so it\n * reads as part of the turn rather than as an event between turns.\n */\nconst turns: { id: string; mine: boolean; step?: string; body: string }[] = [\n\t{\n\t\tid: \"t_01\",\n\t\tmine: true,\n\t\tbody: \"Our chat scroll jumps every time older messages load. What are we doing wrong?\",\n\t},\n\t{\n\t\tid: \"t_02\",\n\t\tmine: false,\n\t\tstep: \"Read message-scroller.md\",\n\t\tbody: \"Almost always the anchor. If you preserve the scroll position by measuring the container, prepending history moves everything under the reader.\",\n\t},\n\t{\n\t\tid: \"t_03\",\n\t\tmine: false,\n\t\tbody: \"Anchor on the oldest rendered message instead and add back however far it moved. Appends leave it alone, so the same rule covers both.\",\n\t},\n]\n\nexport function ChatPanel() {\n\t// One identity for every assistant turn. A glyph rather than initials: \"AI\"\n\t// in a circle reads as a person called A. I. The glyph is sized to the\n\t// circle (the fallback's own size is tuned for two letters, and a lone icon\n\t// at that size reads as a speck); `alt` names it for a screen reader, which\n\t// otherwise gets nothing from a fallback-only avatar.\n\tconst assistant = (\n\t\t<Avatar variant=\"art\"\n\t\t\tsize=\"sm\"\n\t\t\talt=\"Assistant\"\n\t\t\tfallback={<Icon name=\"sparkles\" className=\"text-base\" />}\n\t\t/>\n\t)\n\n\treturn (\n\t\t<Page as=\"main\" max=\"feed\" gap={0} className=\"py-6\">\n\t\t\t{/* h-dvh minus the page's own padding (py-6 = 12 spacing units, in\n\t\t\t spacing units so density moves both together): the panel owns the\n\t\t\t viewport, and the transcript — not the page — is the thing that\n\t\t\t scrolls. The panel is a Card; its parts inset by the card's own\n\t\t\t --card-p, so header, transcript and composer share one edge. */}\n\t\t\t<Card\n\t\t\t\trender={<section aria-labelledby=\"chat-title\" />}\n\t\t\t\tclassName=\"flex h-[calc(100dvh_-_--spacing(12))] flex-col overflow-hidden\"\n\t\t\t>\n\t\t\t\t<CardHeader className=\"pb-(--card-p) shadow-[inset_0_-1px_0_var(--border)]\">\n\t\t\t\t\t{/* CardTitle carries the type; the h1 inside it is the page heading. */}\n\t\t\t\t\t<CardTitle>\n\t\t\t\t\t\t<h1 id=\"chat-title\">Scroll behaviour</h1>\n\t\t\t\t\t</CardTitle>\n\t\t\t\t\t<CardDescription>Assistant · answers from your team's docs</CardDescription>\n\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t{/* Labelled, not a bare plus: the composer already has a plus\n\t\t\t\t\t\t (attach), and two identical glyphs a panel apart that do\n\t\t\t\t\t\t different things is a guessing game. */}\n\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\">\n\t\t\t\t\t\t\t<Icon name=\"edit\" />\n\t\t\t\t\t\t\tNew chat\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</CardAction>\n\t\t\t\t</CardHeader>\n\n\t\t\t\t{/* flex-1 min-h-0 is the whole layout: it bounds the scroll region\n\t\t\t\t against the panel instead of letting the transcript grow the page. */}\n\t\t\t\t<MessageScroller className=\"min-h-0 flex-1 p-(--card-p)\">\n\t\t\t\t\t<Marker variant=\"separator\">Today</Marker>\n\n\t\t\t\t\t{turns.map((turn, i) => {\n\t\t\t\t\t\t// Derived, not hand-set: a turn continues a run when the one\n\t\t\t\t\t\t// before it has the same author. A continuation drops its\n\t\t\t\t\t\t// avatar (Message keeps the column) AND its tail (Bubble keeps\n\t\t\t\t\t\t// the ear's space) — together, that makes the run one block.\n\t\t\t\t\t\tconst continues = i > 0 && turns[i - 1].mine === turn.mine\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<Message\n\t\t\t\t\t\t\t\tkey={turn.id}\n\t\t\t\t\t\t\t\tmessageId={turn.id}\n\t\t\t\t\t\t\t\talign={turn.mine ? \"end\" : \"start\"}\n\t\t\t\t\t\t\t\tgrouped={continues}\n\t\t\t\t\t\t\t\tavatar={turn.mine ? undefined : assistant}\n\t\t\t\t\t\t\t\theader={\n\t\t\t\t\t\t\t\t\tturn.step ? (\n\t\t\t\t\t\t\t\t\t\t<Marker variant=\"note\" icon=\"book\" className=\"justify-start\">\n\t\t\t\t\t\t\t\t\t\t\t{turn.step}\n\t\t\t\t\t\t\t\t\t\t</Marker>\n\t\t\t\t\t\t\t\t\t) : undefined\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Bubble\n\t\t\t\t\t\t\t\t\tvariant={turn.mine ? \"primary\" : \"muted\"}\n\t\t\t\t\t\t\t\t\talign={turn.mine ? \"end\" : \"start\"}\n\t\t\t\t\t\t\t\t\ttail={!continues}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{turn.body}\n\t\t\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t\t</Message>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\n\t\t\t\t\t{/* The same run, continued: long-form output with no bubble,\n\t\t\t\t\t because a tinted box around a code block reads as a quote\n\t\t\t\t\t rather than as a turn. Its footer carries what the reader does\n\t\t\t\t\t WITH a settled answer, as a Toolbar: one tab stop, arrow keys\n\t\t\t\t\t between the actions. */}\n\t\t\t\t\t<Message\n\t\t\t\t\t\tmessageId=\"t_04\"\n\t\t\t\t\t\tgrouped\n\t\t\t\t\t\tavatar={assistant}\n\t\t\t\t\t\tfooter={\n\t\t\t\t\t\t\t<Toolbar aria-label=\"Answer actions\">\n\t\t\t\t\t\t\t\t<SimpleTooltip content=\"Copy answer\">\n\t\t\t\t\t\t\t\t\t<ToolbarButton size=\"icon-sm\" aria-label=\"Copy answer\">\n\t\t\t\t\t\t\t\t\t\t<Icon name=\"copy\" />\n\t\t\t\t\t\t\t\t\t</ToolbarButton>\n\t\t\t\t\t\t\t\t</SimpleTooltip>\n\t\t\t\t\t\t\t\t<SimpleTooltip content=\"Regenerate\">\n\t\t\t\t\t\t\t\t\t<ToolbarButton size=\"icon-sm\" aria-label=\"Regenerate answer\">\n\t\t\t\t\t\t\t\t\t\t<Icon name=\"refresh\" />\n\t\t\t\t\t\t\t\t\t</ToolbarButton>\n\t\t\t\t\t\t\t\t</SimpleTooltip>\n\t\t\t\t\t\t\t</Toolbar>\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t{/* max-w-full: a plain Bubble sizes to its content with no\n\t\t\t\t\t\t measure cap, and a code line is wider than a phone. Bounded\n\t\t\t\t\t\t by the column, the CodeBlock scrolls inside itself instead of\n\t\t\t\t\t\t pushing the whole transcript sideways. */}\n\t\t\t\t\t\t<Bubble variant=\"plain\" className=\"max-w-full\">\n\t\t\t\t\t\t\t<p className=\"text-body-md text-foreground\">\n\t\t\t\t\t\t\t\tHere is the shape of it:\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<CodeBlock\n\t\t\t\t\t\t\t\tcode={\n\t\t\t\t\t\t\t\t\t\"const shift = anchor.offsetTop - lastTop\\nif (shift !== 0) viewport.scrollTop += shift\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfilename=\"preserve.ts\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t</Message>\n\n\t\t\t\t\t{/* The reader's follow-up is one turn: the question and the file\n\t\t\t\t\t it refers to stack in a single Message, so they share a side, an\n\t\t\t\t\t edge and a timestamp rather than reading as two sends. */}\n\t\t\t\t\t<Message messageId=\"t_05\" align=\"end\">\n\t\t\t\t\t\t<Bubble variant=\"primary\" align=\"end\">\n\t\t\t\t\t\t\tHere's ours. Does the same fix cover new messages arriving?\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t<Attachment\n\t\t\t\t\t\t\tname=\"scroller.tsx\"\n\t\t\t\t\t\t\tmeta=\"4.2 KB · TSX\"\n\t\t\t\t\t\t\tonOpen={() => undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</Message>\n\n\t\t\t\t\t{/* The answer arriving. A `Marker` with a spinner — or ThinkingOrb —\n\t\t\t\t\t is what sits here BEFORE the first token; once tokens land it is\n\t\t\t\t\t replaced rather than kept, because two indicators for one wait\n\t\t\t\t\t read as two things happening. What stays is the step it took,\n\t\t\t\t\t as a finished note in the header.\n\n\t\t\t\t\t No bubble, same as the long-form answer above: a tinted box\n\t\t\t\t\t around a growing paragraph makes the reflow look like the box\n\t\t\t\t\t breathing. And no answer actions yet — there is nothing to copy\n\t\t\t\t\t or regenerate until it settles; Stop lives in the composer.\n\n\t\t\t\t\t MessageScroller needs no help with this — it already re-follows on\n\t\t\t\t\t \"a message grew\" (ResizeObserver), not just on \"a message arrived\",\n\t\t\t\t\t so the newest token stays pinned while the reader can still scroll\n\t\t\t\t\t up without being yanked back. */}\n\t\t\t\t\t<Message\n\t\t\t\t\t\tmessageId=\"t_06\"\n\t\t\t\t\t\tavatar={assistant}\n\t\t\t\t\t\theader={\n\t\t\t\t\t\t\t<Marker variant=\"note\" icon=\"file\" className=\"justify-start\">\n\t\t\t\t\t\t\t\tRead scroller.tsx\n\t\t\t\t\t\t\t</Marker>\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Bubble variant=\"plain\">\n\t\t\t\t\t\t\t<StreamingText text={streamed} streaming={pending} />\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t</Message>\n\t\t\t\t</MessageScroller>\n\n\t\t\t\t<CardFooter className=\"pt-(--card-p) shadow-[inset_0_1px_0_var(--border)]\">\n\t\t\t\t\t<ChatComposer\n\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\tplaceholder=\"Ask a follow-up…\"\n\t\t\t\t\t\t// Same flag as the answer's `streaming`, which is what turns Send\n\t\t\t\t\t\t// into Stop. A composer that stays sendable mid-answer invites a\n\t\t\t\t\t\t// second request on top of the one still running.\n\t\t\t\t\t\tbusy={pending}\n\t\t\t\t\t\tonStop={() => undefined}\n\t\t\t\t\t\tstopLabel=\"Stop answering\"\n\t\t\t\t\t\tonAttach={() => undefined}\n\t\t\t\t\t\t// A keyboard hint, so only where Enter sends: on touch Return\n\t\t\t\t\t\t// breaks the line and the button sends, and the row goes away.\n\t\t\t\t\t\tfooter={({ sendOn }) => (sendOn === \"enter\" ? \"Enter to send · Shift+Enter for a new line\" : null)}\n\t\t\t\t\t\t// Model and effort sit in `actions`, at the send end of the row:\n\t\t\t\t\t\t// they qualify the message about to be sent, so they belong beside\n\t\t\t\t\t\t// the button that sends it rather than next to the attach control.\n\t\t\t\t\t\tactions={\n\t\t\t\t\t\t\t<SettingsMenu\n\t\t\t\t\t\t\t\tvariant=\"subtle\"\n\t\t\t\t\t\t\t\tsize=\"xs\"\n\t\t\t\t\t\t\t\tlabel=\"Change model and effort\"\n\t\t\t\t\t\t\t\tsummary={\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-foreground\">5.6 Sol</span>{\" \"}\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-muted-foreground\">High</span>\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>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Model\" value=\"5.6 Sol\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"sol\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"sol\">5.6 Sol</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"lite\">5.6 Lite</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Effort\" value=\"High\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"high\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"low\">Low</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"standard\">Standard</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"high\">High</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Speed\" value=\"Standard\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"standard\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"standard\">Standard</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"priority\">Priority</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t</SettingsMenu>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t</CardFooter>\n\t\t\t</Card>\n\t\t</Page>\n\t)\n}"
20579
21423
  },
20580
21424
  {
20581
21425
  "name": "DashboardPage",
@@ -24199,6 +25043,19 @@
24199
25043
  "CardTitle": [
24200
25044
  "render"
24201
25045
  ],
25046
+ "CaretMenu": [
25047
+ "children",
25048
+ "className",
25049
+ "label",
25050
+ "status",
25051
+ "trigger"
25052
+ ],
25053
+ "CaretMenuItem": [
25054
+ "description",
25055
+ "disabled",
25056
+ "icon",
25057
+ "onSelect"
25058
+ ],
24202
25059
  "Carousel": [
24203
25060
  "arrowClassName",
24204
25061
  "arrows",
@@ -24222,6 +25079,7 @@
24222
25079
  ],
24223
25080
  "ChatComposer": [
24224
25081
  "actions",
25082
+ "allowEmpty",
24225
25083
  "attachments",
24226
25084
  "busy",
24227
25085
  "defaultValue",
@@ -24948,6 +25806,16 @@
24948
25806
  "Input": [
24949
25807
  "size"
24950
25808
  ],
25809
+ "InputGroup": [
25810
+ "aria-describedby",
25811
+ "aria-invalid",
25812
+ "children",
25813
+ "disabled",
25814
+ "id",
25815
+ "leading",
25816
+ "required",
25817
+ "trailing"
25818
+ ],
24951
25819
  "Kbd": [
24952
25820
  "mod",
24953
25821
  "variant"
@@ -25046,6 +25914,28 @@
25046
25914
  "orientation",
25047
25915
  "triggerId"
25048
25916
  ],
25917
+ "Menubar": [
25918
+ "className",
25919
+ "disabled",
25920
+ "loopFocus",
25921
+ "modal",
25922
+ "orientation",
25923
+ "render",
25924
+ "style"
25925
+ ],
25926
+ "MenubarTrigger": [
25927
+ "children",
25928
+ "className",
25929
+ "closeDelay",
25930
+ "delay",
25931
+ "disabled",
25932
+ "handle",
25933
+ "nativeButton",
25934
+ "openOnHover",
25935
+ "payload",
25936
+ "render",
25937
+ "style"
25938
+ ],
25049
25939
  "MenuCheckboxItem": [
25050
25940
  "checked",
25051
25941
  "className",
@@ -25085,7 +25975,9 @@
25085
25975
  "MenuItem": [
25086
25976
  "className",
25087
25977
  "closeOnClick",
25978
+ "description",
25088
25979
  "disabled",
25980
+ "icon",
25089
25981
  "id",
25090
25982
  "label",
25091
25983
  "nativeButton",
@@ -25094,6 +25986,16 @@
25094
25986
  "style",
25095
25987
  "variant"
25096
25988
  ],
25989
+ "MenuLinkItem": [
25990
+ "className",
25991
+ "closeOnClick",
25992
+ "description",
25993
+ "icon",
25994
+ "id",
25995
+ "label",
25996
+ "render",
25997
+ "style"
25998
+ ],
25097
25999
  "MenuPortal": [
25098
26000
  "className",
25099
26001
  "container",
@@ -25244,6 +26146,52 @@
25244
26146
  "policy",
25245
26147
  "target"
25246
26148
  ],
26149
+ "NavigationMenu": [
26150
+ "actionsRef",
26151
+ "align",
26152
+ "className",
26153
+ "closeDelay",
26154
+ "defaultValue",
26155
+ "delay",
26156
+ "onOpenChangeComplete",
26157
+ "onValueChange",
26158
+ "orientation",
26159
+ "render",
26160
+ "sideOffset",
26161
+ "style",
26162
+ "value"
26163
+ ],
26164
+ "NavigationMenuContent": [
26165
+ "className",
26166
+ "keepMounted",
26167
+ "render",
26168
+ "style"
26169
+ ],
26170
+ "NavigationMenuItem": [
26171
+ "className",
26172
+ "render",
26173
+ "style",
26174
+ "value"
26175
+ ],
26176
+ "NavigationMenuLink": [
26177
+ "active",
26178
+ "className",
26179
+ "closeOnClick",
26180
+ "description",
26181
+ "render",
26182
+ "style"
26183
+ ],
26184
+ "NavigationMenuList": [
26185
+ "className",
26186
+ "render",
26187
+ "style"
26188
+ ],
26189
+ "NavigationMenuTrigger": [
26190
+ "className",
26191
+ "nativeButton",
26192
+ "render",
26193
+ "style"
26194
+ ],
25247
26195
  "Note": [
25248
26196
  "action",
25249
26197
  "icon",
@@ -25342,6 +26290,7 @@
25342
26290
  "label",
25343
26291
  "onPageChange",
25344
26292
  "page",
26293
+ "pageLink",
25345
26294
  "siblingCount",
25346
26295
  "total"
25347
26296
  ],
@@ -25654,6 +26603,7 @@
25654
26603
  "Scroller": [
25655
26604
  "label",
25656
26605
  "orientation",
26606
+ "smoothWheel",
25657
26607
  "viewportClassName",
25658
26608
  "wheelScroll"
25659
26609
  ],
@@ -25822,13 +26772,18 @@
25822
26772
  "actionsRef",
25823
26773
  "children",
25824
26774
  "defaultOpen",
26775
+ "defaultSnapPoint",
25825
26776
  "defaultTriggerId",
25826
26777
  "disablePointerDismissal",
25827
26778
  "handle",
25828
26779
  "modal",
25829
26780
  "onOpenChange",
25830
26781
  "onOpenChangeComplete",
26782
+ "onSnapPointChange",
25831
26783
  "open",
26784
+ "snapPoint",
26785
+ "snapPoints",
26786
+ "snapToSequentialPoints",
25832
26787
  "triggerId"
25833
26788
  ],
25834
26789
  "SheetClose": [
@@ -25878,6 +26833,7 @@
25878
26833
  "Sidebar": [
25879
26834
  "collapsed",
25880
26835
  "icons",
26836
+ "surface",
25881
26837
  "variant"
25882
26838
  ],
25883
26839
  "SidebarBrand": [