@lumiose/ui 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -2
  3. package/dist/accordion/accordion.css +20 -0
  4. package/dist/accordion/accordion.script.ts +66 -0
  5. package/dist/accordion/accordion.tsx +288 -0
  6. package/dist/animation/animation.ts +34 -0
  7. package/dist/autocomplete/autocomplete.tsx +239 -0
  8. package/dist/avatar/avatar.css +54 -0
  9. package/dist/avatar/avatar.script.ts +96 -0
  10. package/dist/avatar/avatar.tsx +110 -0
  11. package/dist/button/button.script.ts +35 -0
  12. package/dist/button/button.tsx +40 -0
  13. package/dist/checkbox/checkbox.script.ts +225 -0
  14. package/dist/checkbox/checkbox.tsx +156 -0
  15. package/dist/checkbox/context.ts +17 -0
  16. package/dist/checkbox-group/checkbox-group.tsx +111 -0
  17. package/dist/collapsible/collapsible.css +12 -0
  18. package/dist/collapsible/collapsible.script.ts +7 -0
  19. package/dist/collapsible/collapsible.tsx +153 -0
  20. package/dist/collapsible/panel.ts +115 -0
  21. package/dist/combobox/combobox.css +37 -0
  22. package/dist/combobox/combobox.script.ts +974 -0
  23. package/dist/combobox/combobox.tsx +556 -0
  24. package/dist/combobox/context.ts +46 -0
  25. package/dist/context-menu/context-menu.css +16 -0
  26. package/dist/context-menu/context-menu.script.ts +172 -0
  27. package/dist/context-menu/context-menu.tsx +178 -0
  28. package/dist/dialog/dialog.css +35 -0
  29. package/dist/dialog/dialog.script.ts +269 -0
  30. package/dist/dialog/dialog.tsx +214 -0
  31. package/dist/drawer/drawer.css +38 -0
  32. package/dist/drawer/drawer.script.ts +207 -0
  33. package/dist/drawer/drawer.tsx +113 -0
  34. package/dist/field/context.ts +156 -0
  35. package/dist/field/field.css +16 -0
  36. package/dist/field/field.script.ts +422 -0
  37. package/dist/field/field.tsx +465 -0
  38. package/dist/fieldset/fieldset.tsx +41 -0
  39. package/dist/form/form.tsx +50 -0
  40. package/dist/form/native.ts +21 -0
  41. package/dist/form/reset.ts +53 -0
  42. package/dist/index.ts +261 -0
  43. package/dist/input/input.tsx +67 -0
  44. package/dist/layer/layer.ts +191 -0
  45. package/dist/locale/digits.ts +21 -0
  46. package/dist/menu/menu.css +22 -0
  47. package/dist/menu/menu.script.ts +591 -0
  48. package/dist/menu/menu.tsx +520 -0
  49. package/dist/menubar/menubar.css +16 -0
  50. package/dist/menubar/menubar.script.ts +195 -0
  51. package/dist/menubar/menubar.tsx +67 -0
  52. package/dist/meter/meter.tsx +134 -0
  53. package/dist/navigation-menu/navigation-menu.css +14 -0
  54. package/dist/navigation-menu/navigation-menu.script.ts +466 -0
  55. package/dist/navigation-menu/navigation-menu.tsx +266 -0
  56. package/dist/number-field/format.ts +70 -0
  57. package/dist/number-field/number-field.css +38 -0
  58. package/dist/number-field/number-field.script.ts +490 -0
  59. package/dist/number-field/number-field.tsx +403 -0
  60. package/dist/otp-field/otp-field.css +45 -0
  61. package/dist/otp-field/otp-field.script.ts +330 -0
  62. package/dist/otp-field/otp-field.tsx +320 -0
  63. package/dist/placement/place.ts +148 -0
  64. package/dist/placement/placement.css +155 -0
  65. package/dist/placement/placement.ts +60 -0
  66. package/dist/popover/hover.ts +178 -0
  67. package/dist/popover/popover.css +20 -0
  68. package/dist/popover/popover.script.ts +256 -0
  69. package/dist/popover/popover.tsx +317 -0
  70. package/dist/preview-card/preview-card.css +7 -0
  71. package/dist/preview-card/preview-card.script.ts +136 -0
  72. package/dist/preview-card/preview-card.tsx +128 -0
  73. package/dist/progress/progress.tsx +155 -0
  74. package/dist/progress/value.ts +23 -0
  75. package/dist/radio-group/radio-group.tsx +156 -0
  76. package/dist/scroll-area/scroll-area.css +72 -0
  77. package/dist/scroll-area/scroll-area.script.ts +281 -0
  78. package/dist/scroll-area/scroll-area.tsx +118 -0
  79. package/dist/select/select.css +25 -0
  80. package/dist/select/select.tsx +171 -0
  81. package/dist/separator/separator.css +12 -0
  82. package/dist/separator/separator.tsx +38 -0
  83. package/dist/slider/format.ts +33 -0
  84. package/dist/slider/slider.css +93 -0
  85. package/dist/slider/slider.script.ts +355 -0
  86. package/dist/slider/slider.tsx +374 -0
  87. package/dist/styles.css +30 -0
  88. package/dist/switch/switch.tsx +92 -0
  89. package/dist/tabs/tabs.css +54 -0
  90. package/dist/tabs/tabs.script.ts +225 -0
  91. package/dist/tabs/tabs.tsx +272 -0
  92. package/dist/themes/default.css +2111 -0
  93. package/dist/toast/command.ts +3 -0
  94. package/dist/toast/toast.css +68 -0
  95. package/dist/toast/toast.script.ts +516 -0
  96. package/dist/toast/toast.tsx +432 -0
  97. package/dist/toggle/context.ts +16 -0
  98. package/dist/toggle/toggle.tsx +99 -0
  99. package/dist/toggle-group/toggle-group.script.ts +166 -0
  100. package/dist/toggle-group/toggle-group.tsx +117 -0
  101. package/dist/toolbar/context.ts +18 -0
  102. package/dist/toolbar/toolbar.css +12 -0
  103. package/dist/toolbar/toolbar.script.ts +123 -0
  104. package/dist/toolbar/toolbar.tsx +180 -0
  105. package/dist/tooltip/tooltip.css +8 -0
  106. package/dist/tooltip/tooltip.script.ts +222 -0
  107. package/dist/tooltip/tooltip.tsx +149 -0
  108. package/docs/README.md +174 -0
  109. package/docs/accordion.md +208 -0
  110. package/docs/autocomplete.md +161 -0
  111. package/docs/avatar.md +198 -0
  112. package/docs/button.md +98 -0
  113. package/docs/checkbox-group.md +112 -0
  114. package/docs/checkbox.md +133 -0
  115. package/docs/collapsible.md +165 -0
  116. package/docs/combobox.md +369 -0
  117. package/docs/context-menu.md +197 -0
  118. package/docs/dialog.md +256 -0
  119. package/docs/drawer.md +204 -0
  120. package/docs/field.md +295 -0
  121. package/docs/fieldset.md +92 -0
  122. package/docs/form.md +131 -0
  123. package/docs/input.md +87 -0
  124. package/docs/installation.md +65 -0
  125. package/docs/llms-full.txt +6923 -0
  126. package/docs/llms.txt +53 -0
  127. package/docs/menu.md +365 -0
  128. package/docs/menubar.md +186 -0
  129. package/docs/meter.md +103 -0
  130. package/docs/navigation-menu.md +257 -0
  131. package/docs/number-field.md +181 -0
  132. package/docs/otp-field.md +160 -0
  133. package/docs/popover.md +271 -0
  134. package/docs/preview-card.md +130 -0
  135. package/docs/progress.md +115 -0
  136. package/docs/radio-group.md +127 -0
  137. package/docs/scroll-area.md +147 -0
  138. package/docs/select.md +236 -0
  139. package/docs/separator.md +116 -0
  140. package/docs/slider.md +184 -0
  141. package/docs/switch.md +98 -0
  142. package/docs/tabs.md +254 -0
  143. package/docs/theming.md +132 -0
  144. package/docs/toast.md +311 -0
  145. package/docs/toggle-group.md +119 -0
  146. package/docs/toggle.md +94 -0
  147. package/docs/toolbar.md +138 -0
  148. package/docs/tooltip.md +128 -0
  149. package/package.json +102 -3
package/docs/llms.txt ADDED
@@ -0,0 +1,53 @@
1
+ # Lumiose UI
2
+
3
+ > `@lumiose/ui`: accessible, unstyled UI primitives built on native elements (`<dialog>`, popover, invoker commands, CSS anchor positioning), server-rendered without hydration. Style them with your own CSS or the optional default theme.
4
+
5
+ These docs ship in `@lumiose/ui` and match the installed version: `node_modules/@lumiose/ui/docs`.
6
+ In a project that depends on it, `lumiose docs ui` prints the index and `lumiose docs dialog` a
7
+ component's page; the Lumiose MCP server's `ui_components` tool lists every component's parts,
8
+ props, events and styling hooks. The Lumiose docs: `node_modules/lumiose/docs/llms.txt`.
9
+
10
+ ## Overview
11
+
12
+ - [UI components](README.md): `@lumiose/ui`: accessible, unstyled UI primitives built on native elements (`<dialog>`, popover, invoker commands, CSS anchor positioning), server-rendered without hydration. Style them with your own CSS or the optional default theme.
13
+ - [Installation](installation.md): Add `@lumiose/ui` to a Lumiose app and import its stylesheets, and the default theme if you want its look. The components then render in server HTML, without an island, and a page loads only the small scripts of the components it renders.
14
+ - [Theming](theming.md): `@lumiose/ui` is unstyled: its stylesheets hold only what the components need to work. The optional default theme gives them a look, light and dark, through `--lu-` custom properties you can change; or style the parts with your own CSS.
15
+
16
+ ## Components
17
+
18
+ - [Accordion](accordion.md): A stack of items whose triggers show and hide their panels, after the WAI-ARIA accordion pattern: each item is a native `<details>`, one open at a time through the browser's exclusive group (`name`) or several, so it works in server HTML with no JavaScript and find-in-page opens the item it lands in. `<lu-accordion>`, a 1 kB light-DOM element, adds the arrow keys and Base UI's animation hooks. `bind:value` (or `value` and `onValueChange`) keeps a signal in sync.
19
+ - [Autocomplete](autocomplete.md): A text input with suggestions, for text that may be anything (a search, a tag, an address): the WAI-ARIA combobox pattern with list autocomplete, after Base UI's Autocomplete. Unlike a Combobox, the text is the value: the input has the name and submits what is typed, and choosing a suggestion only fills it in. It runs on the Combobox's element (`<lu-combobox free>`, 5.0 kB) and parts, on server-rendered pages and in islands; `bind:value` keeps a signal in sync. Filtering, inline completion, groups, an empty state and a status.
20
+ - [Avatar](avatar.md): A person's picture with a fallback, such as their initials, after Base UI's Avatar: the image loads in the server HTML and shows once it has loaded, the fallback shows until then, after an optional delay, and when the image fails. Both sit in one cell, so neither moves the other. A 0.5 kB script follows the image's loading status; without JavaScript an avatar with an image shows the image alone.
21
+ - [Button](button.md): A native `<button>`, after Base UI's Button. It is `type="button"` unless you say otherwise, so it submits a form only when asked to. Disabled it leaves the tab order; with `focusableWhenDisabled` it stays focusable as `aria-disabled="true"` (a loading state keeps focus where it was) and a tiny script cancels its clicks.
22
+ - [Checkbox](checkbox.md): A native `<input type="checkbox">`, after Base UI's Checkbox: Space and a label click toggle it, the form submits its value when checked, reset and `required` work, in server HTML and without JavaScript. `indeterminate`, `readOnly`, the parent checkbox of a Checkbox Group and `bind:checked` come on top, with a 1.0 kB script shared with Radio Group and Switch.
23
+ - [Checkbox Group](checkbox-group.md): A group of checkboxes with one value, the list of the checked ones, after Base UI's CheckboxGroup: a native `<fieldset>` whose checkboxes share a name, so the form submits each checked value and a disabled group disables them all, without JavaScript. A parent checkbox checks them all and shows checked, mixed or unchecked as they are; `bind:value` keeps a signal in sync.
24
+ - [Collapsible](collapsible.md): A panel that its trigger shows and hides: a native `<details>` whose `<summary>` is the trigger, so it works in server HTML with no JavaScript, keeps the browser's keyboard handling, and opens when find-in-page or a link lands inside it. A 0.7 kB script adds Base UI's animation hooks. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands.
25
+ - [Combobox](combobox.md): A text input that filters a list of options, for picking one value or several from many: the WAI-ARIA combobox pattern with list autocomplete, after Base UI's Combobox. The input shows the option's label and the form gets its value. `<lu-combobox>`, a 5.4 kB form-associated light-DOM element, runs it on server-rendered pages and in islands; `bind:value` and `bind:open` keep signals in sync. Groups, an empty state, a status, a clear button, multiple values with chips, validation and async options.
26
+ - [Context Menu](context-menu.md): A menu opened at the pointer by a right-click in its trigger area, a long press on a touch screen, or Shift+F10 and the ContextMenu key: Menu's popup and items, with Menu's keys, typeahead, submenus and dismissal. A 0.6 kB script on top of Menu's (4.6 kB together) opens it; without JavaScript the browser shows its own menu. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands.
27
+ - [Dialog](dialog.md): A modal dialog, or an alert dialog that needs an answer: a native `<dialog>` opened and closed by invoker commands, so it works in server HTML with no JavaScript, and a 1.5 kB script closes the native element's gaps where JavaScript runs. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands.
28
+ - [Drawer](drawer.md): A panel that slides in from an edge of the window and that a swipe toward that edge closes: a Dialog (a native modal `<dialog>` opened by an invoker command, so it works in server HTML with no JavaScript) against the edge its `swipeDirection` names. A 1 kB script adds the swipe, with the close buttons, Escape and a click on the backdrop as its alternatives. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands.
29
+ - [Field](field.md): A form field: a label, a native control, a description and its errors, wired with `for`, `aria-describedby` and `aria-invalid`, after Base UI's Field. Validation is the browser's constraint validation (`required`, `type`, `pattern`, `minlength`…); a 1.7 kB script shows it in the field's Error parts at the right time, instead of the browser's bubble, and focuses the first invalid field on submit. A Form's server errors render in the HTML, so they work without JavaScript. In an island, `validate` adds checks of your own, async ones included, and `Field.Validity` renders from the field's validity.
30
+ - [Fieldset](fieldset.md): A group of related fields with a caption: a native `<fieldset>` and `<legend>`, after Base UI's Fieldset. The browser names the group by its legend and disables every control inside a disabled fieldset, with no JavaScript and no CSS of its own. The legend also names a Checkbox Group or a Radio Group.
31
+ - [Form](form.md): A native `<form>` that hands its fields the server's errors, after Base UI's Form. Pass the data a form action gives `fail()` as `errors`, and each Field shows the message for its `name`, in the HTML the server renders back (no JavaScript needed) or, with `submit()`, in an island. Submission, validation and the first invalid field's focus are the platform's and Field's.
32
+ - [Input](input.md): A native `<input>`, after Base UI's Input: every type, constraint and autofill the browser has, plus `onValueChange` and `bind:value`. Inside a Field (as `Field.Control`) it takes the field's id, name and ARIA references. No script, no CSS of its own.
33
+ - [Menu](menu.md): A menu of commands, links, checkbox and radio items and submenus, opened by a button: the trigger's `popovertarget` opens it and CSS anchor positioning places it, so it works in server HTML with no JavaScript, and a 4.0 kB script adds the APG Menu Button keyboard contract where JavaScript runs. `bind:open`, `bind:checked` and `bind:value` keep signals in sync in islands.
34
+ - [Menubar](menubar.md): A bar of menus, such as an app's File, Edit and View, after Base UI's Menubar and the WAI-ARIA menubar pattern: `Menu.Root`s inside `<lu-menubar role="menubar">`, whose triggers are the bar's items. Menu's popups, items, submenus and dismissal; the bar is one tab stop, the arrow keys move between its items and between open menus, and once a menu is open, hovering another item opens its menu instead. A 0.7 kB script on top of Menu's (4.7 kB together); without JavaScript each trigger opens its menu.
35
+ - [Meter](meter.md): A measurement within a known range (storage used, a password's strength, a rating), after Base UI's Meter: a native `<meter>` (with its value, range and `aria-valuetext`), labelled, with the value as text (a percentage of the range by default, or your `Intl.NumberFormat` options). No script.
36
+ - [Navigation Menu](navigation-menu.md): A site's navigation whose items open panels of links, after Base UI's NavigationMenu and the WAI-ARIA disclosure navigation pattern: a `<nav>` landmark around a list, each trigger opening its content through `popovertarget` (a native popover placed under it by CSS anchor positioning), so the links work and the panels open on a click, one at a time, in server HTML with no JavaScript. A 3.5 kB script adds hover intent, the arrow keys, closing when focus leaves and the direction a panel comes in from. `bind:value` (or `value` and `onValueChange`) keeps the open item in a signal.
37
+ - [Number Field](number-field.md): A number input with buttons to step it, after Base UI's NumberField and the APG spinbutton. Without `format` it is a native `<input type="number">`, which steps, validates and submits without JavaScript; with `format` (`Intl.NumberFormat` options: a currency, a percentage, grouping) it shows the formatted number, reads what is typed back in the same locale, and a hidden input submits the number. `<lu-number-field>`'s 2.6 kB script adds Page Up and Page Down, Home and End, press-and-hold buttons, a scrub area, the wheel when asked, and commit-time clamping. `bind:value` keeps a signal in sync.
38
+ - [OTP Field](otp-field.md): A one-time code input, after Base UI's OTPField: one native `<input>` shown as one slot per character. SMS autofill (`autocomplete="one-time-code"`), paste, Backspace, the arrow keys, `maxlength`, `pattern`, `required` and the form's value are the platform's, without JavaScript too, and assistive technology meets one labelled text field. `<lu-otp-field>`'s 1.5 kB script lays the input over the slots, keeps the code to the allowed characters, and reports completion. `bind:value` keeps a signal in sync.
39
+ - [Popover](popover.md): A popup anchored to its trigger: a native `popover` opened and closed by the trigger's `popovertarget`, placed with CSS anchor positioning, so it works in server HTML with no JavaScript. A 3.3 kB script moves focus into it and back, dismisses it as Base UI does (not on a right-click), keeps `aria-expanded` current and places it in browsers without anchor positioning. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands. A trigger can open it on hover (`openOnHover`), and a `modal` popover is a modal `<dialog>` that makes the page behind it inert.
40
+ - [Preview Card](preview-card.md): A card that previews a link's destination while the pointer or keyboard focus rests on the link, after Base UI's PreviewCard. The trigger is the link itself, which works as a link without JavaScript; the card is a manual popover placed with Popover's placement and opened with its hover timing by a 2.8 kB script. It is hoverable, persistent and dismissible (WCAG 1.4.13). `bind:open` keeps a signal in sync.
41
+ - [Progress](progress.md): A task's progress, after Base UI's Progress: a native `<progress>` (the progress bar for assistive technology, with its value and `aria-valuetext`), labelled, with the value as text (a percentage by default, or your `Intl.NumberFormat` options). With no value it is indeterminate. No script.
42
+ - [Radio](radio-group.md): A group of radios with one value, after Base UI's RadioGroup and Radio: native radios in a `<fieldset role="radiogroup">`. The browser gives the group one tab stop, arrow keys that move and select (skipping disabled radios), `required`, submission and reset, in server HTML and without JavaScript; a legend names the group. `bind:value` keeps a signal in sync.
43
+ - [Scroll Area](scroll-area.md): A scrolling region with custom scrollbars laid over native scrolling, after Base UI's ScrollArea. The viewport scrolls natively (the wheel, touch, the keyboard, find-in-page), is focusable while it overflows, and traps nothing; without JavaScript its native scrollbars show. `<lu-scroll-area>`'s 1.6 kB script hides them and draws its own, which show on hover, while scrolling and while focused, take a thumb drag and a track press, and expose the overflow as data attributes and CSS variables.
44
+ - [Select](select.md): A native `<select>` for picking one value from a list: it works with no JavaScript, submits with its form and keeps the platform's keyboard, typeahead and mobile pickers. Where the browser has customizable select (`appearance: base-select`, Chrome 135, Safari 26), `Select.Trigger` and `Select.Value` shape the button, the picker is styleable and options may hold more than text; elsewhere it is the classic select. No script; `bind:value` keeps a signal in sync in islands.
45
+ - [Separator](separator.md): A native `<hr>` between groups of content, after Base UI's Separator: announced as a separator, horizontal by default, or vertical between the items of a row with `aria-orientation="vertical"`. No script.
46
+ - [Slider](slider.md): A value picked within a range by dragging or with the keys, or a range with two thumbs, after Base UI's Slider. Each thumb is a native `<input type="range">`, so it drags, takes the arrow keys, Home and End, follows the text direction and submits with its form, in server HTML and without JavaScript. `<lu-slider>` adds Page Up and Page Down, keeps a range's thumbs in order, moves the nearest thumb on a press on the track and keeps the value text current. `bind:value` keeps a signal in sync.
47
+ - [Switch](switch.md): On or off, for a setting: a native `<input type="checkbox" role="switch">`, after Base UI's Switch. Space and a label click toggle it, the form submits its value when on, `required` and reset work, in server HTML and without JavaScript, and assistive technology announces a switch. `bind:checked` keeps a signal in sync.
48
+ - [Tabs](tabs.md): A tab list and the panel of the selected tab, after the WAI-ARIA tabs pattern: the server HTML holds the whole state (roles, `aria-selected`, the roving tab stop, the hidden panels), and `<lu-tabs>`, a 1.2 kB light-DOM element, adds the arrow keys and the selection. Manual or automatic activation, horizontal or vertical, right-to-left aware, with an optional indicator that slides to the selected tab; `bind:value` (or `value` and `onValueChange`) keeps a signal in sync.
49
+ - [Toast](toast.md): Short notices in a region of their own, after Base UI's toast: server HTML renders them (a form action's result), and islands add, update and close them through a toast manager. `<lu-toast-viewport>`, a 2.6 kB light-DOM element, puts them in the top layer, times them (pausing on hover and focus), limits how many show, closes them on a swipe, Escape or their close button, focuses the region on F6, and makes screen readers announce them.
50
+ - [Toggle](toggle.md): A two-state button (bold, mute, pin), after Base UI's Toggle: a native `<button type="button" aria-pressed>`. Its label stays the same; `aria-pressed` tells pressed from not. On its own the component presses it, in an island, and `bind:pressed` keeps a signal in sync; inside a Toggle Group the group presses it, on server pages too.
51
+ - [Toggle Group](toggle-group.md): A set of Toggles with one value, after Base UI's ToggleGroup: text alignment (one pressed at a time) or text style (several). `<lu-toggle-group role="group">` presses its toggles and gives the group one tab stop that the arrow keys, Home and End move, following the orientation and the direction, on server pages and in islands. `bind:value` keeps a signal in sync.
52
+ - [Toolbar](toolbar.md): A set of controls with one tab stop, after Base UI's Toolbar and the APG toolbar pattern: `<lu-toolbar role="toolbar">` moves focus between its items with the arrow keys, Home and End, following the orientation and the text direction. Its items are native: buttons that stay focusable when disabled, links, inputs, and the toggles of a Toggle Group inside it. In the server HTML every item is a tab stop, so each is reachable without JavaScript; a 1.1 kB script makes them one tab stop and moves it.
53
+ - [Tooltip](tooltip.md): A short tip next to its trigger, on hover or keyboard focus: a native `popover="hint"` with `role="tooltip"`, placed with CSS anchor positioning. The trigger is described by the tip in server HTML, so screen readers announce it without hovering; a 2.0 kB script opens and closes it as Base UI's tooltip does. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands.
package/docs/menu.md ADDED
@@ -0,0 +1,365 @@
1
+ # Menu
2
+
3
+ > A menu of commands, links, checkbox and radio items and submenus, opened by a button: the trigger's `popovertarget` opens it and CSS anchor positioning places it, so it works in server HTML with no JavaScript, and a 4.0 kB script adds the APG Menu Button keyboard contract where JavaScript runs. `bind:open`, `bind:checked` and `bind:value` keep signals in sync in islands.
4
+
5
+ ```ts
6
+ import { Menu } from "@lumiose/ui"; // or "@lumiose/ui/menu"
7
+ import "@lumiose/ui/menu.css"; // places the popup next to its trigger and stacks the items
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <Menu.Root>
15
+ <Menu.Trigger />
16
+ <Menu.Popup>
17
+ <Menu.Item />
18
+ <Menu.LinkItem />
19
+ <Menu.CheckboxItem />
20
+ <Menu.Separator />
21
+ <Menu.Group>
22
+ <Menu.GroupLabel />
23
+ <Menu.Item />
24
+ </Menu.Group>
25
+ <Menu.RadioGroup>
26
+ <Menu.GroupLabel />
27
+ <Menu.RadioItem />
28
+ </Menu.RadioGroup>
29
+ <Menu.SubmenuRoot>
30
+ <Menu.SubmenuTrigger />
31
+ <Menu.Popup>…</Menu.Popup>
32
+ </Menu.SubmenuRoot>
33
+ </Menu.Popup>
34
+ </Menu.Root>
35
+ ```
36
+
37
+ | Part | Renders |
38
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
39
+ | `Root` | `<lu-menu>`, an element with no box (`display: contents`) that holds the menu's state and adds the keyboard contract. |
40
+ | `Trigger` | `<button type="button" popovertarget="…" aria-haspopup="menu">`; `aria-expanded` follows the menu once the script runs. |
41
+ | `Popup` | `<div role="menu" popover="auto" tabindex="-1" data-side="…" data-align="…">`, labelled by its trigger; `popover="manual"` once the script opens it. |
42
+ | `Item` | `<button type="button" role="menuitem">`: a command. |
43
+ | `LinkItem` | `<a role="menuitem">`: a link. |
44
+ | `CheckboxItem` | `<button type="button" role="menuitemcheckbox" aria-checked="…">` |
45
+ | `Separator` | `<div role="separator">` |
46
+ | `Group` | `<div role="group">`, labelled by its `GroupLabel`. |
47
+ | `GroupLabel` | `<div role="presentation">`, which names its `Group` or `RadioGroup`. |
48
+ | `RadioGroup` | `<div role="group">` of radio items, one of them checked. |
49
+ | `RadioItem` | `<button type="button" role="menuitemradio" value="…" aria-checked="…">` |
50
+ | `SubmenuRoot` | `<lu-menu>` for a submenu, inside a `Popup`. |
51
+ | `SubmenuTrigger` | `<button type="button" role="menuitem" popovertarget="…" aria-haspopup="menu">`: an item of the parent menu. |
52
+
53
+ The trigger and the popup sit inside `Root`; a submenu's trigger and popup sit inside its
54
+ `SubmenuRoot`, which sits in the parent's `Popup`, as deep as you like. Items, groups and separators
55
+ go inside a `Popup`, radio items inside a `RadioGroup`.
56
+
57
+ ## API
58
+
59
+ ### Menu.Root
60
+
61
+ | Prop | Type | Default | |
62
+ | -------------- | ------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
63
+ | `open` | `boolean` | | Opens or closes the menu when it changes, in the browser. Server HTML always starts closed: a popover opens only from its trigger or script. |
64
+ | `onOpenChange` | `(open: boolean) => void` | | Called after the menu opens or closes, whatever did it: the trigger, a key, a click outside, an item that ran, `open`. |
65
+ | `bind:open` | `Signal<boolean>` | | Stands in for `open` and `onOpenChange`: the signal opens and closes the menu, and follows it. |
66
+
67
+ Without `open`, `onOpenChange` or `bind:open` the menu keeps its own state. A menu opened from the
68
+ server HTML before its island hydrated stays open, and `onOpenChange(true)` tells the signal.
69
+ `Menu.SubmenuRoot` takes the same props, for its submenu.
70
+
71
+ ### Menu.Popup
72
+
73
+ | Prop | Type | Default | |
74
+ | ------------- | -------------------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------- |
75
+ | `side` | `"top" \| "bottom" \| "left" \| "right" \| "inline-start" \| "inline-end"` | `"bottom"`, `"inline-end"` in a submenu | The trigger's side the popup goes on. It flips when it does not fit. |
76
+ | `align` | `"start" \| "center" \| "end"` | `"start"` | How the popup aligns with the trigger along that side. |
77
+ | `sideOffset` | `number` | | The gap between the trigger and the popup, in CSS pixels. |
78
+ | `alignOffset` | `number` | | A shift along the side, away from the aligned edge, in CSS pixels. |
79
+
80
+ ### Menu.Item
81
+
82
+ | Prop | Type | Default | |
83
+ | -------------- | ----------------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
84
+ | `onClick` | `(event: MouseEvent) => void` | | Runs the command, on a click, Enter or Space. The menu has closed and focus is back on the trigger by then. |
85
+ | `disabled` | `boolean` | `false` | Unavailable (`aria-disabled`): the arrows and typeahead still reach it, but it does not run. |
86
+ | `label` | `string` | | The text typeahead matches, when the item's text content is not it. |
87
+ | `closeOnClick` | `boolean` | `true` | Whether running the item closes the menu. |
88
+
89
+ ### Menu.LinkItem
90
+
91
+ | Prop | Type | Default | |
92
+ | -------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | `href` | `string` | | Where the link goes. A Ctrl, Cmd or Shift click, or a link with `download`, leaves the menu open. |
94
+ | `label` | `string` | | The text typeahead matches. |
95
+ | `closeOnClick` | `boolean` | `true` | Whether following the link closes the menu. Base UI keeps it open by default; here it closes, like the other items, since a layout's menu outlives a client-side navigation. |
96
+
97
+ ### Menu.CheckboxItem
98
+
99
+ | Prop | Type | Default | |
100
+ | ----------------- | ---------------------------- | ------- | ---------------------------------------------------------------------------- |
101
+ | `checked` | `boolean` | | Checks or unchecks the item when it changes. Pair it with `onCheckedChange`. |
102
+ | `defaultChecked` | `boolean` | `false` | Whether it starts checked when `checked` is left out. |
103
+ | `onCheckedChange` | `(checked: boolean) => void` | | Called after a click, Enter or Space checks or unchecks it. |
104
+ | `bind:checked` | `Signal<boolean>` | | Stands in for `checked` and `onCheckedChange`. |
105
+ | `disabled` | `boolean` | `false` | Unavailable: it can be highlighted, but it does not toggle. |
106
+ | `label` | `string` | | The text typeahead matches. |
107
+ | `closeOnClick` | `boolean` | `false` | Whether toggling it closes the menu. |
108
+
109
+ ### Menu.RadioGroup
110
+
111
+ | Prop | Type | Default | |
112
+ | --------------- | ------------------------- | ------- | -------------------------------------------------------------------------------------- |
113
+ | `value` | `string` | | Checks the radio item with this `value` when it changes. Pair it with `onValueChange`. |
114
+ | `defaultValue` | `string` | | The radio item checked at first when `value` is left out. |
115
+ | `onValueChange` | `(value: string) => void` | | Called with the `value` of the radio item a click, Enter or Space checks. |
116
+ | `bind:value` | `Signal<string>` | | Stands in for `value` and `onValueChange`. |
117
+
118
+ ### Menu.RadioItem
119
+
120
+ | Prop | Type | Default | |
121
+ | -------------- | --------- | ------- | ---------------------------------------------------------- |
122
+ | `value` | `string` | | Required: the value its group takes when it is checked. |
123
+ | `disabled` | `boolean` | `false` | Unavailable: it can be highlighted, but it is not checked. |
124
+ | `label` | `string` | | The text typeahead matches. |
125
+ | `closeOnClick` | `boolean` | `false` | Whether checking it closes the menu. |
126
+
127
+ ### Menu.SubmenuTrigger
128
+
129
+ | Prop | Type | Default | |
130
+ | ---------- | --------- | ------- | ------------------------------------------------------------------ |
131
+ | `disabled` | `boolean` | `false` | Unavailable: it can be highlighted, but its submenu does not open. |
132
+ | `label` | `string` | | The text typeahead matches in the parent menu. |
133
+
134
+ ### The other parts
135
+
136
+ Each part takes the props of the element it renders (`class`, `style`, `aria-*`, `data-*`, event
137
+ handlers, `ref`) and passes them through. Your props override the defaults (`type`, `tabindex`,
138
+ `aria-labelledby`, `data-side`, `data-align`). A `Trigger`'s (or `SubmenuTrigger`'s) `id` names it,
139
+ and the popup points at it; the `Popup` keeps its own `id`, which the trigger points at (see
140
+ [Ids](README.md#ids)); the roles, `popovertarget` and the anchor stay the parts' own. Your `style` is kept, after the anchor's.
141
+
142
+ - **`Trigger`**: `aria-expanded` is left to the browser in server HTML, where `popovertarget`
143
+ implies it, and set by the script once it runs. Disable it with the native `disabled`.
144
+ - **`Group`** and **`RadioGroup`**: `aria-labelledby` points at their `GroupLabel` while one
145
+ renders; without one, give the group an `aria-label`.
146
+ - **`Separator`**: a horizontal rule between items; `aria-orientation` passes through.
147
+
148
+ **Differences from Base UI's Menu.** No `Portal`, `Positioner`, `Backdrop` or `Arrow`: the top
149
+ layer and anchor positioning replace them, and the placement props go on `Popup`. The menu is not
150
+ modal: the page keeps scrolling (the popup follows its trigger), and a press outside closes the
151
+ menu and reaches what it lands on. Items render buttons, so they run with Enter and Space without
152
+ the script. There are no indicator parts: style `[aria-checked="true"]`. Submenus open on hover
153
+ after 100 ms, which no prop changes, and the root trigger does not open on hover.
154
+
155
+ ## Example
156
+
157
+ A menu in a server-rendered page, with no island, styled by the default theme:
158
+
159
+ ```tsx check
160
+ import { Menu } from "@lumiose/ui";
161
+
162
+ export function ProjectMenu() {
163
+ return (
164
+ <Menu.Root>
165
+ <Menu.Trigger class="lu-button">Project</Menu.Trigger>
166
+ <Menu.Popup class="lu-menu">
167
+ <Menu.LinkItem href="/settings">Settings</Menu.LinkItem>
168
+ <Menu.LinkItem href="/members">Members</Menu.LinkItem>
169
+ <Menu.Separator />
170
+ <Menu.SubmenuRoot>
171
+ <Menu.SubmenuTrigger>Export</Menu.SubmenuTrigger>
172
+ <Menu.Popup class="lu-menu">
173
+ <Menu.LinkItem href="/export.csv" download>
174
+ CSV
175
+ </Menu.LinkItem>
176
+ <Menu.LinkItem href="/export.json" download>
177
+ JSON
178
+ </Menu.LinkItem>
179
+ </Menu.Popup>
180
+ </Menu.SubmenuRoot>
181
+ </Menu.Popup>
182
+ </Menu.Root>
183
+ );
184
+ }
185
+ ```
186
+
187
+ In an island, with a command, a checkbox item and a radio group bound to signals, and a status
188
+ line that follows them. `bind:checked={wrap}` is short for `checked={wrap()}` plus
189
+ `onCheckedChange={(next) => wrap.set(next)}`, and `bind:value` likewise.
190
+
191
+ ```tsx check
192
+ import { Menu } from "@lumiose/ui";
193
+ import { signal } from "lumiose";
194
+
195
+ export function EditorMenu() {
196
+ const formatted = signal(0);
197
+ const wrap = signal(true);
198
+ const indent = signal("spaces");
199
+ return (
200
+ <>
201
+ <Menu.Root>
202
+ <Menu.Trigger class="lu-button">View</Menu.Trigger>
203
+ <Menu.Popup class="lu-menu" align="end">
204
+ <Menu.Item onClick={() => formatted.set(formatted() + 1)}>Format document</Menu.Item>
205
+ <Menu.Item disabled>Fold all</Menu.Item>
206
+ <Menu.Separator />
207
+ <Menu.CheckboxItem bind:checked={wrap}>Wrap lines</Menu.CheckboxItem>
208
+ <Menu.RadioGroup bind:value={indent}>
209
+ <Menu.GroupLabel>Indent with</Menu.GroupLabel>
210
+ <Menu.RadioItem value="spaces">Spaces</Menu.RadioItem>
211
+ <Menu.RadioItem value="tabs">Tabs</Menu.RadioItem>
212
+ </Menu.RadioGroup>
213
+ </Menu.Popup>
214
+ </Menu.Root>
215
+ <p>
216
+ Formatted {formatted()} times, lines {wrap() ? "wrap" : "do not wrap"}, indent with{" "}
217
+ {indent()}.
218
+ </p>
219
+ </>
220
+ );
221
+ }
222
+ ```
223
+
224
+ ## Styling
225
+
226
+ **With the default theme**, put `lu-menu` on every `Popup`, submenus included, and `lu-button` on
227
+ the trigger. `lu-menu` styles the surface, the items (highlighted, disabled, focus ring), the
228
+ checkbox and radio marks, the arrow of a submenu trigger, the separators and the group labels, and
229
+ fades the popup in and out. Its colours come from the theme's `--lu-` properties
230
+ ([UI components](README.md#the-default-theme)), and it sets a 4px gap with `--lu-side-offset`.
231
+
232
+ **With your own CSS**, style the parts through `class` and their state:
233
+
234
+ - the popup: `:popover-open`, `data-side` and `data-align` (where it was asked to go);
235
+ - the trigger and a submenu trigger: `[aria-expanded="true"]` while their menu is open;
236
+ - items: `[data-highlighted]` on the item that has focus (by keyboard or pointer),
237
+ `:focus-visible`, `[aria-disabled="true"]`, and `[aria-checked="true"]` on checkbox and radio
238
+ items;
239
+ - `[role="separator"]`, `[role="group"]` and a group label's `[role="presentation"]`.
240
+
241
+ A starting point:
242
+
243
+ ```css
244
+ .menu {
245
+ min-inline-size: 10rem;
246
+ padding: 0.25rem;
247
+ border: 1px solid #d4d4d8;
248
+ border-radius: 0.5rem;
249
+ box-shadow: 0 10px 30px -10px rgb(0 0 0 / 0.35);
250
+ --lu-side-offset: 4px;
251
+ }
252
+ .menu [role^="menuitem"] {
253
+ padding: 0.375rem 0.5rem;
254
+ border: 0;
255
+ border-radius: 0.25rem;
256
+ background: none;
257
+ font: inherit;
258
+ }
259
+ .menu [role^="menuitem"][data-highlighted] {
260
+ background: #f4f4f5;
261
+ }
262
+ .menu [role^="menuitem"][aria-disabled="true"] {
263
+ opacity: 0.5;
264
+ }
265
+ .menu [aria-checked="true"]::before {
266
+ content: "✓" / "";
267
+ }
268
+ ```
269
+
270
+ `@lumiose/ui/menu.css` holds what the menu needs to work, in the `lu.core` layer: `lu-menu` has
271
+ no box, and the popup and its groups lay the items out in a column (`display: grid`, items
272
+ `display: flex`). It imports the placement rules Menu shares with Popover and Tooltip: with CSS
273
+ anchor positioning, a popup sits on its `data-side` and `data-align`, keeps the gaps of
274
+ `--lu-side-offset` and `--lu-align-offset`, flips when it does not fit (a submenu tries the other
275
+ side, then the other alignment, then below and above its trigger), shrinks to the room beside its
276
+ trigger and scrolls when it fits nowhere (the focused item stays in view), and hides while its
277
+ trigger is scrolled out of view (`data-anchor-hidden`, where the script runs). The `sideOffset` and `alignOffset` props set those two properties on the
278
+ popup.
279
+
280
+ ## Keyboard and focus
281
+
282
+ The [WAI-ARIA Menu Button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/), with
283
+ Base UI's behaviour where the pattern leaves a choice:
284
+
285
+ | Key | Does |
286
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
287
+ | Enter, Space | On the trigger: opens the menu on its first item. On an item: runs it (a link item too), or checks it. On a submenu trigger: opens it. |
288
+ | Arrow Down, Arrow Up | On the trigger: opens the menu on its first item, or on its last. In the menu: the next or previous item, looping. |
289
+ | Home, End | The first or last item. |
290
+ | Arrow Right (Left in RTL) | On a submenu trigger, or the trigger of a menu in a vertical Toolbar or Menubar: opens it on its first item. |
291
+ | Arrow Left (Right in RTL) | In a submenu: closes it, back on its trigger. |
292
+ | A character | The next item whose text starts with it; characters typed within 750 ms add up, and Space among them is text. |
293
+ | Escape | Closes one level: the submenu on top, or the menu. Focus goes back to its trigger. |
294
+ | Tab, Shift+Tab | Close the whole menu. Tab moves on from the trigger; Shift+Tab stays on it. |
295
+
296
+ - **On open**, focus moves to the first item that can run (Arrow Up: the last one). Opened by a
297
+ pointer, the popup itself takes focus and no item is highlighted until a key or the pointer
298
+ picks one.
299
+ - **On close**, focus returns to the trigger without scrolling the page, after Escape, Tab, an
300
+ item that ran or a click outside on nothing focusable; a click on a button or a field outside
301
+ leaves focus there.
302
+ - **Running an item** closes the whole menu and returns focus before the item's `onClick` runs,
303
+ so a handler that opens a dialog gets focus back on the trigger when it closes. Checkbox and
304
+ radio items stay open.
305
+ - **Disabled items** (`aria-disabled`) stay in the arrow order and typeahead, as the pattern
306
+ asks, but do not run.
307
+ - **The pointer** highlights the item under it, and leaving the items clears the highlight. A
308
+ mouse press on the trigger opens the menu at once: releasing it on an item runs the item, and
309
+ releasing it outside closes the menu. A submenu opens after the pointer rests 100 ms on its
310
+ trigger, and stays open while the pointer heads for it across other items.
311
+ - **A click outside** closes the whole menu, submenus included, and a click in a menu closes the
312
+ submenus open from it. A right-click, a middle click or a second finger outside leaves the menu
313
+ open, as in Base UI.
314
+
315
+ ## Without JavaScript, and the client script
316
+
317
+ The trigger opens the popup with `popovertarget`, and Escape or a click outside closes it: a menu
318
+ in a server-rendered page opens before any script loads, and with none. Tab then moves through the
319
+ items in order, and links work; commands, which run JavaScript, need it anyway. Escape in a
320
+ submenu closes it, but the browser leaves focus on the page rather than on the submenu's trigger:
321
+ Tab starts over from the top (the script returns focus to the trigger). `open`, `onOpenChange` and
322
+ the `bind:` props need the component to run in the browser, in an island.
323
+
324
+ The keyboard contract comes from a small script: a page that renders a `Menu` loads it once, as a
325
+ 4.0 kB (brotli) module without the Lumiose runtime, and islands bundle it. It defines `lu-menu`,
326
+ which upgrades every menu on the page in place, and adds what the native popover lacks: focus on
327
+ open and its return on close, the arrows, Home, End and typeahead, Escape one level at a time, Tab,
328
+ submenus, checkbox and radio items, disabled items, `data-highlighted`, and the pointer behaviour
329
+ above. It also takes over dismissal, with the model Menu shares with [Popover](popover.md) and
330
+ [Combobox](combobox.md): the popup becomes a manual popover when the script opens it, and the
331
+ script closes it on a click outside (main button only), on Escape, on a close request such as the
332
+ Android back gesture, and when another popup opens outside it. Until it loads (or without JavaScript) the menu behaves as a
333
+ native `popover="auto"` of buttons, which any click outside closes.
334
+
335
+ The element is also an API of its own, for scripts and other frameworks:
336
+
337
+ - `open`: a property; setting it to `true` or `false` opens or closes the menu.
338
+ - `openchange`: an event (not bubbling, like `toggle`) with `detail.open`, after the menu opens or
339
+ closes.
340
+ - `checkedchange`: a bubbling event that checkbox and radio items fire with `detail.checked` when
341
+ a click, Enter or Space checks or unchecks them.
342
+
343
+ ## Browser support
344
+
345
+ - **Popovers and `popovertarget`**: Chrome and Edge 114, Firefox 125, Safari 17.
346
+ - **CSS anchor positioning** places the popup: Chrome and Edge 125, Firefox 147, Safari 26
347
+ (partly; 27 fully). Elsewhere the client script places the popup with the same sides,
348
+ alignment, offsets and flips; without JavaScript it shows where the browser shows any popover,
349
+ in the middle of the viewport, and still works.
350
+ - **`showPopover({ source })`**, which ties a popup the script opens to its trigger: Chrome 137,
351
+ Firefox 144, Safari 26. Older browsers open it the same way.
352
+ - **`CloseWatcher`**, for the Android back gesture on a menu opened by a click, a tap or a key:
353
+ Chrome 126, Firefox 149. Escape is handled on its own in every browser, one layer at a time.
354
+ - **Exit animations** that keep the popup in the top layer while it fades (`overlay`) are
355
+ Chromium-only.
356
+
357
+ ## Related
358
+
359
+ - [UI components](README.md): the approach, styling, the default theme and the other components.
360
+ - [Dialog](dialog.md): a menu item can open one; focus returns to the menu's trigger when it
361
+ closes.
362
+ - [Islands](https://itsjavi.com/lumiose/docs/concepts/islands/): running a component in the
363
+ browser, for `open`, `onOpenChange` and the `bind:` props.
364
+ - [JSX](https://itsjavi.com/lumiose/docs/concepts/jsx/): `bind:`, native events with `on:` and
365
+ `ref`.
@@ -0,0 +1,186 @@
1
+ # Menubar
2
+
3
+ > A bar of menus, such as an app's File, Edit and View, after Base UI's Menubar and the WAI-ARIA menubar pattern: `Menu.Root`s inside `<lu-menubar role="menubar">`, whose triggers are the bar's items. Menu's popups, items, submenus and dismissal; the bar is one tab stop, the arrow keys move between its items and between open menus, and once a menu is open, hovering another item opens its menu instead. A 0.7 kB script on top of Menu's (4.7 kB together); without JavaScript each trigger opens its menu.
4
+
5
+ ```ts
6
+ import { Menu, Menubar } from "@lumiose/ui"; // or "@lumiose/ui/menu" and "@lumiose/ui/menubar"
7
+ import "@lumiose/ui/menubar.css"; // Menu's layout, and the bar's row (or column)
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <Menubar>
15
+ <Menu.Root>
16
+ <Menu.Trigger />
17
+ <Menu.Popup>
18
+ <Menu.Item />
19
+ <Menu.SubmenuRoot>
20
+ <Menu.SubmenuTrigger />
21
+ <Menu.Popup>…</Menu.Popup>
22
+ </Menu.SubmenuRoot>
23
+ </Menu.Popup>
24
+ </Menu.Root>
25
+ <Menu.Root>…</Menu.Root>
26
+ </Menubar>
27
+ ```
28
+
29
+ | Part | Renders |
30
+ | -------------------------- | --------------------------------------------------------------------------------------------------------------------- |
31
+ | `Menubar` | `<lu-menubar role="menubar" aria-orientation="…" data-orientation="…">`: a row of the menus (a column when vertical). |
32
+ | `Menu.Root` inside it | Menu's `<lu-menu>`, with `role="none"`. |
33
+ | `Menu.Trigger` inside it | Menu's `<button popovertarget="…">`, with `role="menuitem"`: an item of the bar. |
34
+ | `Menu.Popup` and the items | As in [Menu](menu.md#anatomy). |
35
+
36
+ A menubar's items are the triggers of its menus, each inside its `<lu-menu role="none">`, as in
37
+ the APG example. Give the bar an `aria-label`.
38
+
39
+ ## API
40
+
41
+ ### Menubar
42
+
43
+ | Prop | Type | Default | |
44
+ | ------------- | ---------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Which way the items run. Vertical: Arrow Up and Down move between them, the arrow that points at a popup opens it, and popups go at the inline end. |
46
+ | `loopFocus` | `boolean` | `true` | Whether the arrows wrap from the last item to the first and back. |
47
+
48
+ Every other prop passes through to `<lu-menubar>`: `class`, `style`, `aria-label`, `data-*`.
49
+
50
+ ### The menus
51
+
52
+ Each `Menu.Root` keeps [Menu's props](menu.md#api): `open`, `onOpenChange` and `bind:open` per
53
+ menu, and its items' `bind:checked` and `bind:value`. `disabled` on a `Menu.Trigger` takes the menu
54
+ out of the bar: no focus, no opening.
55
+
56
+ **Differences from Base UI's Menubar.** No `modal` (the page stays scrollable and usable while a
57
+ menu is open) and no `disabled` on the bar: disable each trigger instead. No `render`.
58
+
59
+ ## Example
60
+
61
+ An editor's menus in a server-rendered page, with no island, styled by the default theme:
62
+
63
+ ```tsx check
64
+ import { Menu, Menubar } from "@lumiose/ui";
65
+
66
+ export function EditorMenus() {
67
+ return (
68
+ <Menubar aria-label="Editor" class="lu-menubar">
69
+ <Menu.Root>
70
+ <Menu.Trigger>File</Menu.Trigger>
71
+ <Menu.Popup class="lu-menu">
72
+ <Menu.Item>New file</Menu.Item>
73
+ <Menu.Item>Open…</Menu.Item>
74
+ <Menu.SubmenuRoot>
75
+ <Menu.SubmenuTrigger>Share</Menu.SubmenuTrigger>
76
+ <Menu.Popup class="lu-menu">
77
+ <Menu.Item>Email</Menu.Item>
78
+ <Menu.Item>Copy link</Menu.Item>
79
+ </Menu.Popup>
80
+ </Menu.SubmenuRoot>
81
+ </Menu.Popup>
82
+ </Menu.Root>
83
+ <Menu.Root>
84
+ <Menu.Trigger>Edit</Menu.Trigger>
85
+ <Menu.Popup class="lu-menu">
86
+ <Menu.Item>Undo</Menu.Item>
87
+ <Menu.Item>Redo</Menu.Item>
88
+ </Menu.Popup>
89
+ </Menu.Root>
90
+ <Menu.Root>
91
+ <Menu.Trigger>View</Menu.Trigger>
92
+ <Menu.Popup class="lu-menu">
93
+ <Menu.RadioGroup defaultValue="100">
94
+ <Menu.GroupLabel>Zoom</Menu.GroupLabel>
95
+ <Menu.RadioItem value="75">75%</Menu.RadioItem>
96
+ <Menu.RadioItem value="100">100%</Menu.RadioItem>
97
+ </Menu.RadioGroup>
98
+ </Menu.Popup>
99
+ </Menu.Root>
100
+ </Menubar>
101
+ );
102
+ }
103
+ ```
104
+
105
+ In an island, a checkbox item bound to a signal and an item that writes a status line:
106
+
107
+ ```tsx check
108
+ import { Menu, Menubar } from "@lumiose/ui";
109
+ import { signal } from "lumiose";
110
+
111
+ export function DocumentMenus() {
112
+ const wrap = signal(false);
113
+ const status = signal("Not saved yet");
114
+ return (
115
+ <>
116
+ <Menubar aria-label="Document" class="lu-menubar">
117
+ <Menu.Root>
118
+ <Menu.Trigger>File</Menu.Trigger>
119
+ <Menu.Popup class="lu-menu">
120
+ <Menu.Item onClick={() => status.set("Saved")}>Save</Menu.Item>
121
+ </Menu.Popup>
122
+ </Menu.Root>
123
+ <Menu.Root>
124
+ <Menu.Trigger>Edit</Menu.Trigger>
125
+ <Menu.Popup class="lu-menu">
126
+ <Menu.CheckboxItem bind:checked={wrap}>Word wrap</Menu.CheckboxItem>
127
+ </Menu.Popup>
128
+ </Menu.Root>
129
+ </Menubar>
130
+ <p>
131
+ {status()}; word wrap is {wrap() ? "on" : "off"}
132
+ </p>
133
+ </>
134
+ );
135
+ }
136
+ ```
137
+
138
+ ## Styling
139
+
140
+ **With the default theme**, put `lu-menubar` on `Menubar` and `lu-menu` on each `Menu.Popup`: a
141
+ bordered bar of the triggers, the one whose menu is open filled, with a focus ring.
142
+
143
+ **With your own CSS**, style the bar through `class` and `[data-orientation]`, the items as
144
+ `lu-menubar > lu-menu > [role="menuitem"]` with `[aria-expanded="true"]` for the open one, and
145
+ `[data-has-submenu-open]` on the bar while a menu is open. The popups and items are
146
+ [Menu's](menu.md#styling).
147
+
148
+ `@lumiose/ui/menubar.css` holds Menu's rules and, in the `lu.core` layer, makes `<lu-menubar>` a
149
+ flex row (a column when vertical); each `<lu-menu>` adds no box.
150
+
151
+ ## Keyboard and focus
152
+
153
+ | Key | Does |
154
+ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
155
+ | Tab, Shift+Tab | Enter and leave the bar, which is one tab stop: the item focused last (the first, if that one goes, is disabled or hidden). |
156
+ | Arrow Right, Arrow Left | On an item: the next or previous item (Down and Up when vertical; swapped right to left). An open menu follows to the next item. |
157
+ | Home, End | The first or last item. |
158
+ | A letter | The next item whose label starts with it. |
159
+ | Arrow Down, Enter, Space | Open the item's menu on its first item (Arrow Up: the last one). Vertical: the arrow that points at the popup opens it. |
160
+ | Arrow Right, Arrow Left (menu) | On an item that opens no submenu: close the menu and open the next or previous one. Vertical: the arrow pointing back closes it. |
161
+ | Escape | Closes the menu (a submenu first) and returns to its item. |
162
+
163
+ - **The pointer**: a click opens or closes a menu; once one is open, hovering another item opens
164
+ its menu instead. Inside a menu, Menu's rules apply: hover highlights, submenus open on hover.
165
+ - **Focus** returns to the item when its menu closes, as in Menu. Disabled triggers are skipped.
166
+
167
+ ## Without JavaScript, and the client script
168
+
169
+ Each trigger opens its menu through `popovertarget`, and the browser light-dismisses it: without
170
+ JavaScript every trigger is a tab stop, and the menus open with Enter, Space or a click.
171
+
172
+ A page that renders a `Menubar` loads Menu's script and a 0.7 kB (brotli) one of its own, 4.7 kB
173
+ together, without the Lumiose runtime; islands bundle them. The menubar's script makes the bar one
174
+ tab stop once the page has loaded, and adds the arrow keys between items and menus and the hover
175
+ switching. Each menu stays Menu's: its keys, items, submenus and dismissal.
176
+
177
+ ## Browser support
178
+
179
+ As for [Menu](menu.md#browser-support): popovers everywhere current, CSS anchor positioning in
180
+ Chrome, Edge, Firefox 147 and Safari 26 (elsewhere the script places the popups).
181
+
182
+ ## Related
183
+
184
+ - [Menu](menu.md): one menu, opened by a button.
185
+ - [Navigation Menu](navigation-menu.md): site navigation with panels of links.
186
+ - [UI components](README.md): the approach, styling and the other components.