@lumiose/ui 0.0.0-stage → 0.1.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 (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
@@ -0,0 +1,208 @@
1
+ # Accordion
2
+
3
+ > 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.
4
+
5
+ ```ts
6
+ import { Accordion } from "@lumiose/ui"; // or "@lumiose/ui/accordion"
7
+ import "@lumiose/ui/accordion.css"; // the root is a block; disabled triggers ignore the pointer
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <Accordion.Root>
15
+ <Accordion.Item>
16
+ <Accordion.Header>
17
+ <Accordion.Trigger />
18
+ </Accordion.Header>
19
+ <Accordion.Panel />
20
+ </Accordion.Item>
21
+ </Accordion.Root>
22
+ ```
23
+
24
+ | Part | Renders |
25
+ | --------- | ------------------------------------------------------------------------------------------------------------------ |
26
+ | `Root` | `<lu-accordion>`, around the items. |
27
+ | `Item` | `<details name="…">`, right inside `Root`, `open` while its panel shows; no `name` when several may be open. |
28
+ | `Header` | `<h3>` around the trigger's label: inside the `<summary>`, where HTML allows a heading. |
29
+ | `Trigger` | `<summary aria-controls="…">`, the first child of `Item`: it opens and closes the panel. |
30
+ | `Panel` | `<div role="region" aria-labelledby="…">`, labelled by its trigger; the browser hides it while the item is closed. |
31
+
32
+ HTML puts a heading inside a `<summary>`, not a `<summary>` inside a heading, so `Header` renders
33
+ its `<h3>` inside the trigger's `<summary>`: the trigger names the heading, and the browser
34
+ exposes the `<summary>` as a button with its expanded state. Set `aria-level` on `Header` for
35
+ another heading level.
36
+
37
+ ## API
38
+
39
+ ### Accordion.Root
40
+
41
+ | Prop | Type | Default | |
42
+ | --------------- | ---------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------- |
43
+ | `value` | `string[]` | | The values of the open items: opens and closes them when it changes. Server HTML starts as it says. |
44
+ | `defaultValue` | `string[]` | `[]` | The items open at first, when there is no `value`. |
45
+ | `onValueChange` | `(value: string[]) => void` | | Called after an item opens or closes, with the values of the open items. |
46
+ | `bind:value` | `Signal<string[]>` | | Stands in for `value` and `onValueChange`. |
47
+ | `multiple` | `boolean` | `false` | Several items may be open at once. By default opening one closes the other, and only the first value starts open. |
48
+ | `disabled` | `boolean` | `false` | Every item is disabled. |
49
+ | `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | The direction the arrow keys follow: Up and Down, or Left and Right. |
50
+ | `loopFocus` | `boolean` | `true` | The arrow keys go from the last trigger to the first and back. |
51
+
52
+ Without `value`, `onValueChange` or `bind:value` the items keep their own state.
53
+
54
+ ### Accordion.Item
55
+
56
+ | Prop | Type | Default | |
57
+ | -------------- | ------------------------- | ---------------- | ---------------------------------------------------------------- |
58
+ | `value` | `string` | an id of its own | Identifies the item in the root's `value`. |
59
+ | `disabled` | `boolean` | `false` | Its trigger takes no focus and does not toggle the panel. |
60
+ | `onOpenChange` | `(open: boolean) => void` | | Called after the item opens or closes; not when it renders open. |
61
+
62
+ ### The other parts
63
+
64
+ Each part takes the props of the element it renders (`class`, `style`, `aria-*`, `data-*`, event
65
+ handlers, `ref`) and passes them through. A `Trigger`'s `id` names it, and its `Panel` points at
66
+ it; a `Panel` keeps its own `id`, which its trigger points at (see [Ids](README.md#ids)). `name`,
67
+ `aria-controls` and `aria-labelledby` stay the parts' own, since they connect the parts.
68
+
69
+ - **`Trigger`**: a disabled trigger gets `aria-disabled="true"` and `tabindex="-1"`.
70
+ - **`Panel`**: the client script sets `--lu-accordion-panel-height` and `--lu-accordion-panel-width` on
71
+ it while it opens and closes, and `data-starting-style` and `data-ending-style`
72
+ ([Styling](#styling)).
73
+
74
+ ## Example
75
+
76
+ A FAQ in a server-rendered page, with no island, styled by the default theme:
77
+
78
+ ```tsx check
79
+ import { Accordion } from "@lumiose/ui";
80
+
81
+ export function ShippingFaq() {
82
+ return (
83
+ <Accordion.Root class="lu-accordion" defaultValue={["shipping"]}>
84
+ <Accordion.Item value="shipping">
85
+ <Accordion.Header>
86
+ <Accordion.Trigger>When will my order ship?</Accordion.Trigger>
87
+ </Accordion.Header>
88
+ <Accordion.Panel>Within two working days.</Accordion.Panel>
89
+ </Accordion.Item>
90
+ <Accordion.Item value="returns">
91
+ <Accordion.Header>
92
+ <Accordion.Trigger>Can I return it?</Accordion.Trigger>
93
+ </Accordion.Header>
94
+ <Accordion.Panel>Yes, within 30 days, for free.</Accordion.Panel>
95
+ </Accordion.Item>
96
+ <Accordion.Item value="gifts" disabled>
97
+ <Accordion.Header>
98
+ <Accordion.Trigger>Gift cards</Accordion.Trigger>
99
+ </Accordion.Header>
100
+ <Accordion.Panel>Coming soon.</Accordion.Panel>
101
+ </Accordion.Item>
102
+ </Accordion.Root>
103
+ );
104
+ }
105
+ ```
106
+
107
+ In an island, several items open, bound to a signal: `bind:value={open}` is short for
108
+ `value={open()}` plus `onValueChange={(next) => open.set(next)}`.
109
+
110
+ ```tsx check
111
+ import { Accordion } from "@lumiose/ui";
112
+ import { signal } from "lumiose";
113
+
114
+ export function SettingsSections() {
115
+ const open = signal(["profile"]);
116
+ return (
117
+ <>
118
+ <Accordion.Root class="lu-accordion" multiple bind:value={open}>
119
+ <Accordion.Item value="profile">
120
+ <Accordion.Header>
121
+ <Accordion.Trigger>Profile</Accordion.Trigger>
122
+ </Accordion.Header>
123
+ <Accordion.Panel>Your name and photo.</Accordion.Panel>
124
+ </Accordion.Item>
125
+ <Accordion.Item value="security">
126
+ <Accordion.Header>
127
+ <Accordion.Trigger>Security</Accordion.Trigger>
128
+ </Accordion.Header>
129
+ <Accordion.Panel>Password and recovery keys.</Accordion.Panel>
130
+ </Accordion.Item>
131
+ </Accordion.Root>
132
+ <p>Open: {open().join(", ") || "none"}</p>
133
+ </>
134
+ );
135
+ }
136
+ ```
137
+
138
+ ## Styling
139
+
140
+ **With the default theme**, put `lu-accordion` on `Root`: items between borders, triggers as rows
141
+ with a chevron that turns while the item is open, and panels whose height animates.
142
+
143
+ **With your own CSS**, style the parts through `class`, `[open]` on the items and
144
+ `[aria-disabled]` on the triggers. The client script gives each panel what Base UI's does, for an
145
+ open and close animation: `--lu-accordion-panel-height` (the content's height in pixels while it
146
+ moves, `auto` once open), `data-starting-style` for the frame an opening starts from and
147
+ `data-ending-style` while it closes. The browser hides a panel as soon as its item closes, so keep
148
+ it visible while it closes with a transition on `::details-content`:
149
+
150
+ ```css
151
+ .accordion > details > .panel {
152
+ overflow: hidden;
153
+ block-size: var(--lu-accordion-panel-height, auto);
154
+ transition: block-size 200ms ease-out;
155
+ }
156
+ .accordion > details > .panel:is([data-starting-style], [data-ending-style]) {
157
+ block-size: 0;
158
+ }
159
+ .accordion > details::details-content {
160
+ transition: content-visibility 200ms allow-discrete;
161
+ }
162
+ ```
163
+
164
+ Opening an item closes the open one, and both animate. `@lumiose/ui/accordion.css`, in the
165
+ `lu.core` layer, makes `<lu-accordion>` a block, keeps a trigger's heading on the line of the
166
+ `<summary>`'s marker (`display: inline`), and makes a disabled trigger ignore the pointer.
167
+
168
+ ## Keyboard and focus
169
+
170
+ The [WAI-ARIA accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/), with its
171
+ optional keys, as in Base UI:
172
+
173
+ | Key | Does |
174
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------- |
175
+ | Enter, Space | On a trigger: opens or closes its item. |
176
+ | Tab, Shift+Tab | Move through the triggers and the open panels' content. |
177
+ | Down, Up | Move focus to the next or the previous trigger; Right and Left when `orientation="horizontal"`, swapped in right-to-left. |
178
+ | Home, End | Move focus to the first or the last trigger. |
179
+
180
+ - **Disabled triggers** are out of the tab order, and the arrow keys skip them.
181
+ - **Looping**: from the last trigger, Down goes to the first, unless `loopFocus={false}`.
182
+
183
+ ## Without JavaScript, and the element's script
184
+
185
+ Opening and closing, one open item at a time (the `name` group), find-in-page and links into a
186
+ closed item are HTML features: an accordion in a server-rendered page works before any script
187
+ loads, and with none. Disabled triggers ignore the pointer through CSS. `value`, `onValueChange`
188
+ and `bind:value` need the component to run in an island.
189
+
190
+ A page that renders an `Accordion` loads a 1 kB (brotli) module once, without the Lumiose runtime;
191
+ islands bundle it. It defines `<lu-accordion>`, which adds the arrow keys, Home and End, and sets
192
+ the panels' animation hooks (shared with [Collapsible](collapsible.md)); a disabled trigger does
193
+ not toggle, also from the keyboard.
194
+
195
+ ## Browser support
196
+
197
+ - **`<details name>`** (one open item at a time): Chrome 120, Firefox 130, Safari 17.2. Older
198
+ browsers let several items open.
199
+ - **Find-in-page and links into a closed item** open it: Chrome 97, Firefox 139. Safari has not
200
+ shipped it yet (WebKit bug 304174): there, find-in-page skips a closed item.
201
+ - **`::details-content`**, which the closing transition needs: Chrome 131, Firefox 143, Safari
202
+ 18.4. Elsewhere a panel disappears at once when its item closes.
203
+
204
+ ## Related
205
+
206
+ - [Collapsible](collapsible.md): one panel and its trigger.
207
+ - [Tabs](tabs.md): one panel at a time, from a tab list.
208
+ - [UI components](README.md): the approach, styling and the other components.
@@ -0,0 +1,161 @@
1
+ # Autocomplete
2
+
3
+ > 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](combobox.md), 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.
4
+
5
+ ```ts
6
+ import { Autocomplete } from "@lumiose/ui"; // or "@lumiose/ui/autocomplete"
7
+ import "@lumiose/ui/autocomplete.css"; // Combobox's CSS: places the popup, hides filtered options
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <Autocomplete.Root>
15
+ <Autocomplete.Label />
16
+ <Autocomplete.Input />
17
+ <Autocomplete.Clear />
18
+ <Autocomplete.Trigger />
19
+ <Autocomplete.Popup>
20
+ <Autocomplete.List>
21
+ <Autocomplete.Item />
22
+ <Autocomplete.Group>
23
+ <Autocomplete.GroupLabel />
24
+ <Autocomplete.Item />
25
+ </Autocomplete.Group>
26
+ </Autocomplete.List>
27
+ <Autocomplete.Empty />
28
+ <Autocomplete.Status />
29
+ </Autocomplete.Popup>
30
+ </Autocomplete.Root>
31
+ ```
32
+
33
+ | Part | Renders |
34
+ | --------------------- | ----------------------------------------------------------------------------------- |
35
+ | `Root` | `<lu-combobox free>`: the element, with no box; `inline` with inline completion |
36
+ | `Label` | `<label for>` the input |
37
+ | `Input` | `<input role="combobox" aria-autocomplete name value>`: the form control |
38
+ | `Clear`, `Trigger` | `<button tabindex="-1">`: empty the text, or open and close the suggestions |
39
+ | `Popup`, `List` | `<div popover>` around the `<div role="listbox">`, or the listbox as the popup |
40
+ | `Item` | `<div role="option" data-value>`: a suggestion; its `value` is the text it fills in |
41
+ | `Group`, `GroupLabel` | `<div role="group">` named by its `<div role="presentation">` |
42
+ | `Empty`, `Status` | `<div role="status">`: shown when nothing matches, or a message you update |
43
+
44
+ The parts are [Combobox](combobox.md)'s, under this Root: their props and styling hooks are the
45
+ same.
46
+
47
+ ## API
48
+
49
+ ### Autocomplete.Root
50
+
51
+ | Prop | Type | Default | |
52
+ | ----------------------------------- | ---------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------- |
53
+ | `value` | `string` | | Puts this text in the input when it changes. Pair it with `onValueChange`. |
54
+ | `defaultValue` | `string` | | The text at first; a form reset goes back to it. |
55
+ | `onValueChange` | `(value: string) => void` | | Called as the text changes: typing, choosing a suggestion, clearing. |
56
+ | `bind:value` | `Signal<string>` | | Stands in for `value` and `onValueChange`. |
57
+ | `open`, `onOpenChange`, `bind:open` | `boolean`, `(open: boolean) => void` | | Open or close the suggestions, and hear about it, as in Combobox. |
58
+ | `mode` | `"list" \| "both" \| "inline" \| "none"` | `"list"` | How the suggestions complete the text (below). |
59
+ | `filter` | `"contains" \| "startsWith"` | `"contains"` | How `"list"` and `"both"` filter the suggestions, ignoring case and accents. |
60
+ | `openOnInputClick` | `boolean` | `false` | A click on the input opens the suggestions; otherwise typing and Arrow Down do. |
61
+ | `autoHighlight` | `boolean` | `false` | The first suggestion is highlighted as you type, so Enter fills it in. |
62
+ | `keepHighlight` | `boolean` | `false` | A suggestion the pointer highlighted stays highlighted when it leaves the list. |
63
+ | `submitOnItemClick` | `boolean` | `false` | Choosing a suggestion (a click, or Enter on it) fills it in and submits the form, as a search box does. |
64
+ | `limit` | `number` | no limit | Shows at most this many suggestions; a narrower text shows the others. |
65
+ | `name` | `string` | | The name the text is submitted under (the input's). |
66
+ | `required`, `disabled` | `boolean` | `false` | As on a native input. |
67
+
68
+ The modes follow Base UI's:
69
+
70
+ - **`"list"`**: typing filters the suggestions; the input keeps what is typed while the arrows move
71
+ through them.
72
+ - **`"both"`**: filtered, and the arrows show the highlighted suggestion in the input, its
73
+ completed part selected; Escape takes it back, Tab or a click elsewhere keeps it.
74
+ - **`"inline"`**: the arrows show the highlighted suggestion in the input, unfiltered.
75
+ - **`"none"`**: neither: the page filters the suggestions itself (results from a server).
76
+
77
+ ### The other parts
78
+
79
+ As in [Combobox](combobox.md#the-other-parts). An `Item`'s `value` is the text it fills in (and its
80
+ `label`, when the shown text differs, what typing matches). Inside a [Field](field.md), the input
81
+ takes the field's id, name, description and error, and the field shows its validation.
82
+
83
+ **Differences from Base UI's Autocomplete.** The suggestions are markup, not an `items` array, and
84
+ filtering compares their text (`mode="none"` hands it to you, as Base UI's `filter={null}`).
85
+ `limit` hides the matches past it, as filtering does, so it applies to the server's suggestions
86
+ too. No `Portal`, `Positioner`,
87
+ `Backdrop`, `Arrow`, `Value`, `Icon`, `Row`, `Collection` or `Separator`.
88
+
89
+ ## Example
90
+
91
+ A search with suggestions, in an island:
92
+
93
+ ```tsx check
94
+ import { Autocomplete } from "@lumiose/ui";
95
+ import { signal } from "lumiose";
96
+
97
+ const TOPICS = ["accessibility", "animation", "forms", "islands", "routing", "signals"];
98
+
99
+ export function TopicSearch() {
100
+ const query = signal("");
101
+ return (
102
+ <>
103
+ <Autocomplete.Root name="q" bind:value={query} class="lu-combobox">
104
+ <Autocomplete.Label>Search the docs</Autocomplete.Label>
105
+ <Autocomplete.Input placeholder="Any topic" />
106
+ <Autocomplete.Popup>
107
+ <Autocomplete.List>
108
+ {TOPICS.map((topic) => (
109
+ <Autocomplete.Item value={topic}>{topic}</Autocomplete.Item>
110
+ ))}
111
+ </Autocomplete.List>
112
+ <Autocomplete.Empty>No suggestions: press Enter to search.</Autocomplete.Empty>
113
+ </Autocomplete.Popup>
114
+ </Autocomplete.Root>
115
+ <p>{query() ? `Searching for “${query()}”` : "Type to search."}</p>
116
+ </>
117
+ );
118
+ }
119
+ ```
120
+
121
+ ## Styling
122
+
123
+ **With the default theme**, put `lu-combobox` on `Root`, as for a Combobox: the label, the input
124
+ with its buttons, and the popup with the highlighted suggestion marked.
125
+
126
+ **With your own CSS**, style the parts through `class` and their state: `[aria-expanded]` on the
127
+ input, `:popover-open` on the popup, `[data-highlighted]` and `[aria-disabled]` on the
128
+ suggestions, `[hidden]` on filtered ones.
129
+
130
+ `@lumiose/ui/autocomplete.css` is Combobox's stylesheet: it places the popup next to the input and
131
+ keeps filtered suggestions hidden.
132
+
133
+ ## Keyboard and focus
134
+
135
+ | Key | Does |
136
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
137
+ | Typing | Filters the suggestions (with `"list"` and `"both"`) and opens them; an empty input closes them. |
138
+ | Arrow Down / Arrow Up | Opens the suggestions, then moves the highlight (with `"both"` and `"inline"`, into the input). |
139
+ | Enter | Fills in the highlighted suggestion (the first, with `autoHighlight`); with none, submits. |
140
+ | Escape | Open: closes the suggestions and keeps the typed text (taking back an inline completion). Closed: clears the text, as Base UI's; closed and empty, it goes on to the page. |
141
+ | Tab | Closes the suggestions and moves on, keeping the text. |
142
+
143
+ Focus stays in the input: the highlight moves through `aria-activedescendant`.
144
+
145
+ ## Without JavaScript, and the element's script
146
+
147
+ The input is a native text field with the name: typing, `required`, submission and reset work with
148
+ no script; the suggestions need it. `<lu-combobox>`'s script (5.0 kB brotli, shared with Combobox)
149
+ filters and opens them, moves the highlight, fills in a choice (firing `input` and `change` on the
150
+ input) and runs inline completion.
151
+
152
+ ## Browser support
153
+
154
+ As [Combobox](combobox.md#browser-support): popover, and CSS anchor positioning where available
155
+ (the script places the popup elsewhere).
156
+
157
+ ## Related
158
+
159
+ - [Combobox](combobox.md): a value picked from the options, when the text must match one.
160
+ - [Field](field.md): a label, a description and errors for an autocomplete.
161
+ - [Input](input.md): a text field without suggestions.
package/docs/avatar.md ADDED
@@ -0,0 +1,198 @@
1
+ # Avatar
2
+
3
+ > 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.
4
+
5
+ ```ts
6
+ import { Avatar } from "@lumiose/ui"; // or "@lumiose/ui/avatar"
7
+ import "@lumiose/ui/avatar.css"; // stacks the image over the fallback; hides each in turn
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <Avatar.Root>
15
+ <Avatar.Image />
16
+ <Avatar.Fallback />
17
+ </Avatar.Root>
18
+ ```
19
+
20
+ | Part | Renders |
21
+ | ---------- | ---------------------------------------------------------------------------------------------- |
22
+ | `Root` | `<lu-avatar>`, an inline grid with one cell, `data-loaded` once the image has loaded. |
23
+ | `Image` | `<img data-loading>`: `data-loading` while it loads, `data-error` when it failed, both hidden. |
24
+ | `Fallback` | `<span data-fallback>`, hidden once the image has loaded. |
25
+
26
+ The image is the root's `<img>` child; anything else inside, such as a status badge, is yours to
27
+ place.
28
+
29
+ ## API
30
+
31
+ ### Avatar.Image
32
+
33
+ | Prop | Type | Default | |
34
+ | ----------------------- | ---------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | `onLoadingStatusChange` | `(status: "loading" \| "loaded" \| "error") => void` | | Called when the loading status changes; `"error"` also when there is no `src`. An island hears the status the image had when it hydrated. |
36
+
37
+ Every native `<img>` prop passes through: `src`, `srcset`, `sizes`, `alt`, `width`, `height`,
38
+ `loading`, `decoding`, `fetchpriority`. A new `src` or `srcset` starts over at `"loading"`.
39
+
40
+ ### Avatar.Fallback
41
+
42
+ | Prop | Type | Default | |
43
+ | ------- | -------- | ------- | --------------------------------------------------------------------------------------------------------- |
44
+ | `delay` | `number` | `0` | How long to wait before showing the fallback, in milliseconds: an image that loads sooner never shows it. |
45
+
46
+ ### The other parts
47
+
48
+ `Root` and `Fallback` take the props of the element they render (`class`, `style`, `aria-*`,
49
+ `data-*`, event handlers) and pass them through. The root element has a `status` property
50
+ (`"loading"`, `"loaded"`, `"error"`, or `undefined` without an image), and the image dispatches
51
+ `loadingstatuschange` (`event.detail.status`) for `on:loadingstatuschange`.
52
+
53
+ **Differences from Base UI's Avatar.** The image is always in the page, as with Base UI's
54
+ `keepMounted`: it starts loading with the server HTML, supports `loading="lazy"`, and shows without
55
+ JavaScript. A missing `src` is `"error"`, not `"idle"`. No `render`.
56
+
57
+ ## Example
58
+
59
+ Two people in a server-rendered page, with no island: an image that loads, and one that fails and
60
+ leaves the initials. The default theme draws the circles.
61
+
62
+ ```tsx check
63
+ import { Avatar } from "@lumiose/ui";
64
+
65
+ // A portrait drawn inline, so the example runs anywhere.
66
+ const portrait = `data:image/svg+xml,${encodeURIComponent(
67
+ `<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80">
68
+ <rect width="80" height="80" fill="#c7d2fe"/>
69
+ <circle cx="40" cy="31" r="14" fill="#4338ca"/>
70
+ <path d="M12 80a28 28 0 0 1 56 0z" fill="#4338ca"/>
71
+ </svg>`,
72
+ )}`;
73
+
74
+ export function Reviewers() {
75
+ return (
76
+ <ul aria-label="Reviewers" style={{ display: "flex", gap: "0.5rem", "list-style": "none" }}>
77
+ <li>
78
+ <Avatar.Root class="lu-avatar">
79
+ <Avatar.Image src={portrait} alt="Ada Lovelace" />
80
+ <Avatar.Fallback role="img" aria-label="Ada Lovelace" delay={600}>
81
+ AL
82
+ </Avatar.Fallback>
83
+ </Avatar.Root>
84
+ </li>
85
+ <li>
86
+ <Avatar.Root class="lu-avatar">
87
+ <Avatar.Image src="/avatars/missing.jpg" alt="Grace Hopper" />
88
+ <Avatar.Fallback role="img" aria-label="Grace Hopper">
89
+ GH
90
+ </Avatar.Fallback>
91
+ </Avatar.Root>
92
+ </li>
93
+ </ul>
94
+ );
95
+ }
96
+ ```
97
+
98
+ In an island, the loading status in a signal, as the picture breaks and comes back:
99
+
100
+ ```tsx check
101
+ import { Avatar, type AvatarLoadingStatus } from "@lumiose/ui";
102
+ import { signal } from "lumiose";
103
+
104
+ const portrait = `data:image/svg+xml,${encodeURIComponent(
105
+ `<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80">
106
+ <rect width="80" height="80" fill="#fde68a"/>
107
+ <circle cx="40" cy="31" r="14" fill="#b45309"/>
108
+ <path d="M12 80a28 28 0 0 1 56 0z" fill="#b45309"/>
109
+ </svg>`,
110
+ )}`;
111
+
112
+ export function ProfilePicture() {
113
+ const broken = signal(false);
114
+ const status = signal<AvatarLoadingStatus>("loading");
115
+ return (
116
+ <>
117
+ <Avatar.Root class="lu-avatar">
118
+ <Avatar.Image
119
+ src={broken() ? "/avatars/missing.jpg" : portrait}
120
+ alt=""
121
+ onLoadingStatusChange={(next) => status.set(next)}
122
+ />
123
+ <Avatar.Fallback aria-hidden="true">AL</Avatar.Fallback>
124
+ </Avatar.Root>
125
+ <p>Ada Lovelace (picture: {status()})</p>
126
+ <button type="button" class="lu-button" onClick={() => broken.set(!broken())}>
127
+ {broken() ? "Fix the picture" : "Break the picture"}
128
+ </button>
129
+ </>
130
+ );
131
+ }
132
+ ```
133
+
134
+ ## Styling
135
+
136
+ **With the default theme**, put `lu-avatar` on `Root`: a 40px circle on a muted surface, the image
137
+ covering it, the fallback's initials centred.
138
+
139
+ **With your own CSS**, give the root a size, so the image loading or failing never changes the
140
+ layout, and style the parts through `class`, `[data-loaded]`, `[data-loading]`, `[data-error]` and
141
+ `[data-fallback]`:
142
+
143
+ ```css
144
+ .avatar {
145
+ place-items: center;
146
+ inline-size: 3rem;
147
+ block-size: 3rem;
148
+ overflow: hidden;
149
+ border-radius: 50%;
150
+ background: #f4f4f5;
151
+ }
152
+ .avatar > img {
153
+ inline-size: 100%;
154
+ block-size: 100%;
155
+ object-fit: cover;
156
+ }
157
+ ```
158
+
159
+ `@lumiose/ui/avatar.css`, in the `lu.core` layer, puts the parts in one grid cell, the image above
160
+ the fallback, and hides them in turn with `visibility` (so a `loading="lazy"` image still loads):
161
+ the image while it loads, where scripting is enabled, and when it failed; the fallback once the
162
+ image has loaded. The fallback's `delay` is a zero-length `lu-avatar-delay` animation: an animation
163
+ of your own on the fallback replaces it, so list both (`animation: lu-avatar-delay 0s
164
+ var(--lu-avatar-delay, 0s) backwards, my-fade 200ms`).
165
+
166
+ ## Keyboard and focus
167
+
168
+ An avatar takes no focus and has no keys. Name it once:
169
+
170
+ - **Next to the person's name**, it repeats it: `alt=""` on the image and `aria-hidden="true"` on
171
+ the fallback.
172
+ - **Alone**, name both parts, since only one shows at a time: the image's `alt`, and on the
173
+ fallback `role="img"` with an `aria-label`, so the initials are not read letter by letter.
174
+
175
+ ## Without JavaScript, and the client script
176
+
177
+ The image and the fallback are in the server HTML. Without JavaScript nothing tells a failed image
178
+ from a loaded one, so an avatar with an image shows the image alone (the browser's broken-image box
179
+ when it fails) and hides its fallback, from sight and from assistive technology, which would name
180
+ the avatar twice; an avatar without an image shows its fallback.
181
+ `onLoadingStatusChange` needs the component to run in an island.
182
+
183
+ A page that renders an `Avatar` loads a 0.5 kB (brotli) script, without the Lumiose runtime;
184
+ islands bundle it. `<lu-avatar>` reads the image's state when it connects (a cached image shows
185
+ at once), then follows its `load` and `error` events and changes to its `src` and `srcset`. An
186
+ image that is complete without a size (an SVG without intrinsic dimensions, or a broken image) is
187
+ told apart with `decode()`.
188
+
189
+ ## Browser support
190
+
191
+ - **The `scripting` media query**, which keeps a loading image hidden only where scripts run:
192
+ Chrome and Edge 120, Firefox 113, Safari 17. Elsewhere a loading image shows as it arrives.
193
+ - **CSS grid and `visibility`**: every browser.
194
+
195
+ ## Related
196
+
197
+ - [UI components](README.md): the approach, styling and the other components.
198
+ - [Theming](theming.md): the default theme's classes and custom properties.
package/docs/button.md ADDED
@@ -0,0 +1,98 @@
1
+ # Button
2
+
3
+ > 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.
4
+
5
+ ```ts
6
+ import { Button } from "@lumiose/ui"; // or "@lumiose/ui/button"
7
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
8
+ ```
9
+
10
+ ## Anatomy
11
+
12
+ ```tsx
13
+ <Button>Save</Button>
14
+ ```
15
+
16
+ | Part | Renders |
17
+ | -------- | ----------------------------------------------------------------------------- |
18
+ | `Button` | `<button type="button">`; `aria-disabled="true"` with `focusableWhenDisabled` |
19
+
20
+ ## API
21
+
22
+ ### Button
23
+
24
+ | Prop | Type | Default | |
25
+ | ----------------------- | --------- | ---------- | ------------------------------------------------------------------------------------------------------------------- |
26
+ | `type` | `string` | `"button"` | `"submit"` or `"reset"` to act on its form. |
27
+ | `disabled` | `boolean` | `false` | Natively disabled: out of the tab order, its clicks ignored. |
28
+ | `focusableWhenDisabled` | `boolean` | `false` | With `disabled`: it stays focusable as `aria-disabled="true"`, and its clicks, presses, Enter and Space do nothing. |
29
+
30
+ Every other native prop passes through: `name`, `value`, `form`, `command`, `commandfor`,
31
+ `popovertarget`, `id`, `class`, `aria-*`, `onClick`.
32
+
33
+ **Differences from Base UI's Button.** It is always a `<button>`: no `nativeButton` or `render`. For
34
+ a link that looks like a button, style an `<a>` with the same class.
35
+
36
+ ## Example
37
+
38
+ A save button that keeps focus while it saves, in an island:
39
+
40
+ ```tsx check
41
+ import { Button } from "@lumiose/ui";
42
+ import { signal } from "lumiose";
43
+
44
+ export function SavingButton() {
45
+ const saving = signal(false);
46
+ return (
47
+ <Button
48
+ class="lu-button"
49
+ data-variant="primary"
50
+ disabled={saving()}
51
+ focusableWhenDisabled
52
+ onClick={() => {
53
+ saving.set(true);
54
+ setTimeout(() => saving.set(false), 1500);
55
+ }}
56
+ >
57
+ {saving() ? "Saving…" : "Save"}
58
+ </Button>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## Styling
64
+
65
+ **With the default theme**, put `lu-button` on it (`data-variant="primary"` or
66
+ `data-variant="destructive"` for the others): a 36px tall button with a focus ring, faded when
67
+ `:disabled` or `[aria-disabled="true"]`.
68
+
69
+ **With your own CSS**, style the button and its states: `:hover`, `:focus-visible`, `:disabled`,
70
+ `[aria-disabled="true"]`.
71
+
72
+ Button ships no stylesheet: it needs none to work.
73
+
74
+ ## Keyboard and focus
75
+
76
+ | Key | Does |
77
+ | ------------- | ----------------------------------------------------------------------------- |
78
+ | Enter / Space | Presses it, unless it is disabled. |
79
+ | Tab | Moves on. A disabled button is skipped, unless it is focusable when disabled. |
80
+
81
+ ## Without JavaScript, and the client script
82
+
83
+ A native button: it works, submits and resets with no script. The client script (0.2 kB brotli)
84
+ only cancels the clicks, Enter and Space of an `aria-disabled` button (one without a `role` of its
85
+ own), and keeps its presses (`pointerdown`, `mousedown`) and Enter and Space keys from its
86
+ handlers and its ancestors' bubbling ones, as Base UI does; listeners that capture still see them,
87
+ and the other keys (Tab, a toolbar's arrows) go on. Without it such a button still acts, so a
88
+ server page that disables a button until something happens should use `disabled` alone.
89
+
90
+ ## Browser support
91
+
92
+ Native `<button>`: every browser.
93
+
94
+ ## Related
95
+
96
+ - [Toggle](toggle.md): a button that stays pressed.
97
+ - [Form](form.md): a form whose submit button is a Button.
98
+ - [Dialog](dialog.md): a Button with `command="show-modal"` opens a dialog without JavaScript.