@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,115 @@
1
+ # Progress
2
+
3
+ > 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.
4
+
5
+ ```ts
6
+ import { Progress } from "@lumiose/ui"; // or "@lumiose/ui/progress"
7
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
8
+ ```
9
+
10
+ ## Anatomy
11
+
12
+ ```tsx
13
+ <Progress.Root value={30}>
14
+ <Progress.Label />
15
+ <Progress.Value />
16
+ <Progress.Track />
17
+ </Progress.Root>
18
+ ```
19
+
20
+ | Part | Renders |
21
+ | ------- | ------------------------------------------------------------------------------ |
22
+ | `Root` | `<div>` with `--lu-value` (`0%` to `100%`; unset while indeterminate) |
23
+ | `Label` | `<label for>` the track |
24
+ | `Value` | `<span>` with the formatted value (empty while indeterminate) |
25
+ | `Track` | `<progress value max aria-valuetext>`: the bar; no `value` while indeterminate |
26
+
27
+ ## API
28
+
29
+ ### Progress.Root
30
+
31
+ | Prop | Type | Default | |
32
+ | ------------------ | --------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------- |
33
+ | `value` | `number \| null` | | The progress, from `min` to `max`; `null` while unknown (indeterminate). Required. |
34
+ | `min` | `number` | `0` | The start. |
35
+ | `max` | `number` | `100` | The end: done. |
36
+ | `format` | `Intl.NumberFormatOptions` | a percentage | How the value is written. With it, the value itself is formatted, not a percentage. |
37
+ | `locale` | `Intl.LocalesArgument` | the browser's | The locale of the formatted value (the server's in server HTML). |
38
+ | `getAriaValueText` | `(formattedValue: string, value: number) => string` | the formatted value | The text assistive technology reads. |
39
+
40
+ Every other prop passes through to the `<div>`: `id`, `class`, `style`.
41
+
42
+ ### Progress.Label, Progress.Track and Progress.Value
43
+
44
+ `Label` takes `<label>` props, its `id` among them; the `Track` keeps its own `id`, which the
45
+ `Label` before it points at (see [Ids](README.md#ids)). `Track` takes `<progress>` props (`aria-label` when there is no
46
+ `Label`). `Value` takes `<span>` props, and its `children` may be a function
47
+ `(formattedValue, value) => JSX.Element` that writes the value your way.
48
+
49
+ **Differences from Base UI's Progress.** The `<progress>` element is the track and the indicator:
50
+ there is no `Progress.Indicator` (style the element's value pseudo-elements, or use `--lu-value`).
51
+ Root has no `role`; the `<progress>` has it.
52
+
53
+ ## Example
54
+
55
+ An upload, in an island:
56
+
57
+ ```tsx check
58
+ import { Progress } from "@lumiose/ui";
59
+ import { signal } from "lumiose";
60
+
61
+ export function UploadProgress() {
62
+ const done = signal<number | null>(null);
63
+ return (
64
+ <p>
65
+ <button
66
+ type="button"
67
+ class="lu-button"
68
+ onClick={() => done.set(Math.min(100, (done() ?? 0) + 20))}
69
+ >
70
+ Upload a part
71
+ </button>
72
+ <Progress.Root value={done()} class="lu-progress">
73
+ <Progress.Label>Uploading</Progress.Label>
74
+ <Progress.Value />
75
+ <Progress.Track />
76
+ </Progress.Root>
77
+ </p>
78
+ );
79
+ }
80
+ ```
81
+
82
+ ## Styling
83
+
84
+ **With the default theme**, put `lu-progress` on `Root`: the label and the value on a row above a
85
+ 10px bar, its edge in `--lu-input` (3:1 against the page, so the whole range shows) and its value in
86
+ the primary colour (a lighter blue in dark mode). Indeterminate, a segment slides along it; it
87
+ stands still where motion is reduced.
88
+
89
+ **With your own CSS**, style the bar with `appearance: none` and its pseudo-elements:
90
+ `::-webkit-progress-bar`, `::-webkit-progress-value`, `::-moz-progress-bar`, and
91
+ `progress:indeterminate`. Or draw your own bar from `--lu-value` on `Root`.
92
+
93
+ Progress ships no stylesheet: it needs none to work.
94
+
95
+ ## Keyboard and focus
96
+
97
+ A progress bar is not interactive: it takes no focus and no keys.
98
+
99
+ ## Without JavaScript
100
+
101
+ Fully: the server renders the `<progress>` with its value, the label and the text. It changes when
102
+ the component renders again (in an island, from a signal).
103
+
104
+ Right to left, the native bar fills from the right, and the default theme's indeterminate
105
+ segment slides leftward.
106
+
107
+ ## Browser support
108
+
109
+ Native `<progress>`: every browser. Its pseudo-elements differ between engines, hence the two sets
110
+ above.
111
+
112
+ ## Related
113
+
114
+ - [Meter](meter.md): a measurement within a range, not a task's progress.
115
+ - [Toast](toast.md): tell the user when the task is done.
@@ -0,0 +1,127 @@
1
+ # Radio
2
+
3
+ > 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.
4
+
5
+ ```ts
6
+ import { Radio, RadioGroup, Fieldset } from "@lumiose/ui"; // or "@lumiose/ui/radio-group"
7
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
8
+ ```
9
+
10
+ ## Anatomy
11
+
12
+ ```tsx
13
+ <RadioGroup>
14
+ <Fieldset.Legend />
15
+ <Radio.Root />
16
+ </RadioGroup>
17
+ ```
18
+
19
+ | Part | Renders |
20
+ | ------------ | ---------------------------------------------------------------------------------------- |
21
+ | `RadioGroup` | `<fieldset role="radiogroup">`; a `Fieldset.Legend` first inside names it. |
22
+ | `Radio.Root` | `<input type="radio">` with the group's `name`, checked when its `value` is the group's. |
23
+
24
+ Put each radio in a `<label>` (or name it with `aria-label`).
25
+
26
+ ## API
27
+
28
+ ### RadioGroup
29
+
30
+ | Prop | Type | Default | |
31
+ | --------------- | ------------------------- | --------------- | ------------------------------------------------------------------------------- |
32
+ | `value` | `string` | | Checks the radio with this value when it changes. Pair it with `onValueChange`. |
33
+ | `defaultValue` | `string` | | The radio checked at first; none when left out. |
34
+ | `onValueChange` | `(value: string) => void` | | Called with the value of the radio the user checks. |
35
+ | `bind:value` | `Signal<string>` | | Stands in for `value` and `onValueChange`. |
36
+ | `name` | `string` | a generated one | The name the radios share and submit under (a Field's `name` inside a Field). |
37
+ | `required` | `boolean` | `false` | A radio must be checked to submit the form (`aria-required` on the group). |
38
+ | `readOnly` | `boolean` | `false` | The radios cannot be checked, but stay focusable and are submitted. |
39
+ | `disabled` | `boolean` | `false` | Disables every radio (a disabled fieldset). |
40
+
41
+ Every other `<fieldset>` prop passes through. Inside a Field, the group takes the field's
42
+ `aria-describedby` and `aria-invalid`, and its radios the field's name.
43
+
44
+ ### Radio.Root
45
+
46
+ | Prop | Type | Default | |
47
+ | ---------- | --------- | ------- | ------------------------------------------------------- |
48
+ | `value` | `string` | | Required: the value the group takes when it is checked. |
49
+ | `disabled` | `boolean` | `false` | It cannot be checked; the arrow keys skip it. |
50
+
51
+ Every other native prop passes through (`id`, `class`, `aria-*`).
52
+
53
+ **Differences from Base UI's RadioGroup and Radio.** The radios are native inputs in a fieldset, so
54
+ there is no `Radio.Indicator` (style `:checked` with `appearance: none`) and no `nativeButton` or
55
+ `inputRef`.
56
+
57
+ ## Example
58
+
59
+ In an island, bound to a signal:
60
+
61
+ ```tsx check
62
+ import { Fieldset, Radio, RadioGroup } from "@lumiose/ui";
63
+ import { signal } from "lumiose";
64
+
65
+ export function ShippingChoice() {
66
+ const speed = signal("standard");
67
+ return (
68
+ <div>
69
+ <RadioGroup name="shipping" bind:value={speed} class="lu-fieldset">
70
+ <Fieldset.Legend>Shipping</Fieldset.Legend>
71
+ <label>
72
+ <Radio.Root value="standard" class="lu-radio" /> Standard
73
+ </label>
74
+ <label>
75
+ <Radio.Root value="express" class="lu-radio" /> Express
76
+ </label>
77
+ <label>
78
+ <Radio.Root value="drone" disabled class="lu-radio" /> Drone (soon)
79
+ </label>
80
+ </RadioGroup>
81
+ <p>Arrives {speed() === "express" ? "tomorrow" : "in three days"}.</p>
82
+ </div>
83
+ );
84
+ }
85
+ ```
86
+
87
+ ## Styling
88
+
89
+ **With the default theme**, put `lu-fieldset` on the group and `lu-radio` on each radio: an 18px
90
+ circle in a 24px target, a thick primary ring when checked (a shape, not only a colour), a focus
91
+ ring.
92
+
93
+ **With your own CSS**, style the inputs and their state: `:checked`, `:disabled`,
94
+ `:focus-visible`; the group: `[role="radiogroup"]`, `[aria-invalid="true"]`.
95
+
96
+ Radio Group ships no stylesheet: it needs none to work.
97
+
98
+ ## Keyboard and focus
99
+
100
+ | Key | Does |
101
+ | ----------------------- | --------------------------------------------------------------------------------- |
102
+ | Tab | Into the group, on the checked radio (or the first, from either side); out of it. |
103
+ | Arrow Down, Arrow Right | Checks the next enabled radio, around at the end. |
104
+ | Arrow Up, Arrow Left | Checks the previous enabled radio, around at the start. |
105
+ | Space | Checks the focused radio. |
106
+
107
+ Enter does not check a radio. Read-only radios move focus but do not change. Right to left, Arrow
108
+ Left checks the next radio and Arrow Right the previous one.
109
+
110
+ ## Without JavaScript, and the client script
111
+
112
+ Native radios: the roving tab stop, the arrow keys, `required`, submission and reset work with no
113
+ script. The shared checkbox script (1.0 kB brotli,
114
+ [Checkbox](checkbox.md#without-javascript-and-the-client-script)) adds read-only, and runs the
115
+ arrow keys itself so that every browser follows the table above: without it, WebKit's native
116
+ radios stop at the ends and keep Left and Right as they are right to left.
117
+
118
+ ## Browser support
119
+
120
+ Native radios and `role="radiogroup"` on a fieldset: every supported browser.
121
+
122
+ ## Related
123
+
124
+ - [Checkbox Group](checkbox-group.md): several values.
125
+ - [Select](select.md): one value from a longer list.
126
+ - [Field](field.md): a description and errors for the group.
127
+ - [Fieldset](fieldset.md): the legend.
@@ -0,0 +1,147 @@
1
+ # Scroll Area
2
+
3
+ > 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.
4
+
5
+ ```ts
6
+ import { ScrollArea } from "@lumiose/ui"; // or "@lumiose/ui/scroll-area"
7
+ import "@lumiose/ui/scroll-area.css"; // or "@lumiose/ui/styles.css" for every component
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <ScrollArea.Root>
15
+ <ScrollArea.Viewport>
16
+ <ScrollArea.Content />
17
+ </ScrollArea.Viewport>
18
+ <ScrollArea.Scrollbar>
19
+ <ScrollArea.Thumb />
20
+ </ScrollArea.Scrollbar>
21
+ <ScrollArea.Scrollbar orientation="horizontal">
22
+ <ScrollArea.Thumb />
23
+ </ScrollArea.Scrollbar>
24
+ <ScrollArea.Corner />
25
+ </ScrollArea.Root>
26
+ ```
27
+
28
+ | Part | Renders |
29
+ | ----------- | --------------------------------------------------------------------------------------------------- |
30
+ | `Root` | `<lu-scroll-area>`: give it a size (a height or a max height); the viewport fills it |
31
+ | `Viewport` | `<div tabindex="0" data-viewport>`: scrolls natively; `tabindex="-1"` while nothing overflows |
32
+ | `Content` | `<div>`, at least as wide as what it holds |
33
+ | `Scrollbar` | `<div aria-hidden="true" data-scrollbar data-orientation>` over an edge; hidden while its axis fits |
34
+ | `Thumb` | `<div data-thumb>`, sized and moved by the script |
35
+ | `Corner` | `<div data-corner hidden>`, where the scrollbars meet, shown while both axes overflow |
36
+
37
+ ## API
38
+
39
+ ### ScrollArea.Root
40
+
41
+ | Prop | Type | Default | |
42
+ | ----------------------- | -------- | ------- | ------------------------------------------------------------------------------------- |
43
+ | `overflowEdgeThreshold` | `number` | `0` | How many px of content past an edge before `data-overflow-*-start` or `-end` applies. |
44
+
45
+ ### ScrollArea.Scrollbar
46
+
47
+ | Prop | Type | Default | |
48
+ | ------------- | ---------------------------- | ------------ | ----------------------------------------------------------- |
49
+ | `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | The edge and axis: the inline end, or the bottom. |
50
+ | `keepMounted` | `boolean` | `false` | Keeps the scrollbar shown while its axis does not overflow. |
51
+
52
+ ### The other parts
53
+
54
+ Each part passes the props of its element through. Give the `Viewport` an `aria-label`, and
55
+ `role="region"` when it holds a section people should find: it is a tab stop while it overflows
56
+ (and in server HTML before the script, which cannot know whether it will). The scrollbars are
57
+ `aria-hidden` pointer conveniences, since the viewport itself scrolls from the keyboard.
58
+
59
+ **Data attributes and CSS variables.** On Root and Viewport: `data-has-overflow-x`,
60
+ `data-has-overflow-y`, `data-overflow-x-start`, `-x-end`, `-y-start`, `-y-end`, `data-scrolling`
61
+ (Root). On the Viewport: `--lu-scroll-area-overflow-x-start` (and `-x-end`, `-y-start`, `-y-end`),
62
+ the px of content past each edge, for fades. On the Scrollbars: `data-orientation`,
63
+ `data-hovering`, `data-scrolling`, and `--lu-scroll-area-thumb-height` (or `-width`). On Root:
64
+ `--lu-scroll-area-corner-width` and `-height`.
65
+
66
+ **What CSS can do alone.** `scrollbar-color`, `scrollbar-width` and `scrollbar-gutter` recolour
67
+ and thin the native scrollbar in every current browser, with no script; they cannot shape it, lay
68
+ it over the content, or show it only on hover. Style the native scrollbar that way for pages
69
+ without JavaScript; the custom scrollbars take over once the script runs.
70
+
71
+ **Differences from Base UI's ScrollArea.** The custom properties are `--lu-scroll-area-*` (Base
72
+ UI's `--scroll-area-*`); the viewport is focusable while it overflows; no `render`.
73
+
74
+ ## Example
75
+
76
+ ```tsx check
77
+ import { ScrollArea } from "@lumiose/ui";
78
+
79
+ const RELEASES = ["1.4.0", "1.3.2", "1.3.1", "1.3.0", "1.2.0", "1.1.0", "1.0.0"];
80
+
81
+ export function ReleaseList() {
82
+ return (
83
+ <ScrollArea.Root class="lu-scroll-area" style={{ "block-size": "8rem" }}>
84
+ <ScrollArea.Viewport role="region" aria-label="Releases">
85
+ <ScrollArea.Content>
86
+ <ul>
87
+ {RELEASES.map((release) => (
88
+ <li>{release}</li>
89
+ ))}
90
+ </ul>
91
+ </ScrollArea.Content>
92
+ </ScrollArea.Viewport>
93
+ <ScrollArea.Scrollbar>
94
+ <ScrollArea.Thumb />
95
+ </ScrollArea.Scrollbar>
96
+ </ScrollArea.Root>
97
+ );
98
+ }
99
+ ```
100
+
101
+ ## Styling
102
+
103
+ **With the default theme**, put `lu-scroll-area` on `Root`: a bordered box, padded content, 10px
104
+ scrollbars whose rounded thumbs (`--lu-muted-foreground`, 3:1) show while the pointer is over the
105
+ area, while it scrolls and while it has focus, and an inward focus ring on the viewport. Each
106
+ thumb takes presses over 24 by 24 px at least (WCAG 2.5.8), reaching 16px into the content; the
107
+ track stays 10px, which the wheel, the keyboard and the thumb stand in for.
108
+
109
+ **With your own CSS**, style the parts through `class`, `[data-scrollbar]`, `[data-thumb]`,
110
+ `[data-orientation]`, `[data-hovering]`, `[data-scrolling]` and the overflow attributes and
111
+ custom properties above; a fade at an edge reads `--lu-scroll-area-overflow-y-end`.
112
+
113
+ `@lumiose/ui/scroll-area.css` (0.3 kB brotli) fills the root with the viewport, hides the native
114
+ scrollbar once the script runs (`:defined`) and the custom ones until then, places the scrollbars
115
+ over the edges leaving room for the corner, and sizes the thumbs from the custom properties.
116
+
117
+ ## Keyboard and focus
118
+
119
+ | Key | Does |
120
+ | --------------------------- | ------------------------------------------------------- |
121
+ | Tab | Onto the viewport while it overflows, then on: no trap. |
122
+ | Arrow keys | Scroll a line. |
123
+ | Page Up / Page Down / Space | Scroll a page. |
124
+ | Home / End | The top or the bottom. |
125
+
126
+ The keys are the browser's own scrolling of the focused viewport. A focusable child (a link) takes
127
+ focus first, and the viewport scrolls it into view.
128
+
129
+ ## Without JavaScript, and the element's script
130
+
131
+ The viewport scrolls with its native scrollbars, from the keyboard too. It keeps its
132
+ `tabindex="0"` then, so an area that does not overflow is a tab stop with nothing to scroll: one
133
+ reason to name it. `<lu-scroll-area>`'s script
134
+ (1.6 kB brotli) hides them and runs the custom ones: thumbs sized and moved on every scroll and
135
+ resize (a ResizeObserver follows content that grows), dragging and track presses, the wheel over
136
+ a scrollbar (which scrolls the viewport, and the page once the viewport is at its end), hover and
137
+ scrolling states, the overflow attributes and variables, the corner, right to left.
138
+
139
+ ## Browser support
140
+
141
+ `scrollbar-width: none` and `::-webkit-scrollbar`, ResizeObserver and pointer capture: every
142
+ supported browser.
143
+
144
+ ## Related
145
+
146
+ - [Dialog](dialog.md): a modal whose long content scrolls in a Scroll Area.
147
+ - [Combobox](combobox.md) and [Select](select.md): lists that scroll on their own.
package/docs/select.md ADDED
@@ -0,0 +1,236 @@
1
+ # Select
2
+
3
+ > 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.
4
+
5
+ ```ts
6
+ import { Select } from "@lumiose/ui"; // or "@lumiose/ui/select"
7
+ import "@lumiose/ui/select.css"; // makes a select with a Trigger a customizable select
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <Select.Root>
15
+ <Select.Trigger>
16
+ <Select.Value />
17
+ </Select.Trigger>
18
+ <Select.Item />
19
+ <Select.Separator />
20
+ <Select.Group>
21
+ <Select.Item />
22
+ </Select.Group>
23
+ </Select.Root>
24
+ ```
25
+
26
+ | Part | Renders |
27
+ | ----------- | ------------------------------------------------------------------------------------------------------- |
28
+ | `Root` | `<select>`, the control (`role="combobox"`), with the props it is given. |
29
+ | `Trigger` | `<button type="button">`, customizable select's button. Optional; it must be the first child of `Root`. |
30
+ | `Value` | `<selectedcontent>`, where the browser copies the selected option's content. Inside the `Trigger`. |
31
+ | `Item` | `<option value="…">`. |
32
+ | `Group` | `<optgroup label="…">` of options. |
33
+ | `Separator` | `<hr>` between options or groups. |
34
+
35
+ The picker, the popup of options, is the select's own `::picker(select)`: there is no part for it.
36
+ Browsers without customizable select drop the `Trigger` and show their own control and picker, with
37
+ the options' text.
38
+
39
+ ## API
40
+
41
+ ### Select.Root
42
+
43
+ | Prop | Type | Default | |
44
+ | --------------- | ------------------------- | ---------------- | --------------------------------------------------------------------------------- |
45
+ | `value` | `string` | | Selects the option with this value when it changes. Pair it with `onValueChange`. |
46
+ | `defaultValue` | `string` | the first option | The option selected at first, when `value` is left out. |
47
+ | `onValueChange` | `(value: string) => void` | | Called after the user selects another option, with its value. |
48
+ | `bind:value` | `Signal<string>` | | Stands in for `value` and `onValueChange`. |
49
+ | `name` | `string` | | The name the value is submitted under. |
50
+ | `required` | `boolean` | `false` | The form needs a value: an option whose value is `""` counts as none. |
51
+ | `disabled` | `boolean` | `false` | The select cannot be used. |
52
+
53
+ Every other `<select>` prop passes through: `id`, `autocomplete`, `form`, `class`, `aria-*`, event
54
+ handlers. Name it with a `<label for>` pointing at its `id`, or `aria-label`, or put it in a
55
+ [Field](field.md) with a `Field.Label`: it takes the field's id, name and ARIA references.
56
+
57
+ ### Select.Item
58
+
59
+ | Prop | Type | Default | |
60
+ | ---------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
61
+ | `value` | `string` | | Required: the value the select takes, and submits, when selected. |
62
+ | `disabled` | `boolean` | `false` | The option cannot be selected; the arrow keys skip it. |
63
+ | `label` | `string` | | The option's text for typeahead and the classic select, when its content starts with an icon or holds more than text. |
64
+
65
+ ### Select.Group
66
+
67
+ | Prop | Type | Default | |
68
+ | ------- | -------- | ------- | -------------------------------------------------- |
69
+ | `label` | `string` | | Required: the group's name, shown above its items. |
70
+
71
+ ### The other parts
72
+
73
+ `Trigger`, `Value` and `Separator` take the props of the element they render (`class`, `style`,
74
+ `aria-*`, `data-*`) and pass them through.
75
+
76
+ **Differences from Base UI's Select.** Select is the native control, chosen over the listbox
77
+ pattern because it works without JavaScript, carries forms, autofill, validation and the mobile
78
+ pickers, and ships no script. So it has no `open`, `onOpenChange` or `modal` (the platform fires no
79
+ event when the picker opens; style `:open` instead), no `Label` (use `<label for>`), no `multiple`
80
+ (use [Combobox](combobox.md) with `multiple`), no `alignItemWithTrigger` (the picker opens below the
81
+ control and flips), no `Portal`, `Positioner`, `Popup`, `List`, `Icon`, `ItemText`,
82
+ `ItemIndicator`, `GroupLabel`, `Arrow` or scroll arrows (the picker, `::picker-icon` and
83
+ `option::checkmark` are the platform's), and Enter does not open it (Space and the arrows do, as on
84
+ any select).
85
+
86
+ ## Example
87
+
88
+ A select in a server-rendered page, with no island, styled by the default theme:
89
+
90
+ ```tsx check
91
+ import { Select } from "@lumiose/ui";
92
+
93
+ export function PlanField() {
94
+ return (
95
+ <form method="post">
96
+ <label for="plan">Plan</label>
97
+ <Select.Root id="plan" name="plan" defaultValue="pro" class="lu-select">
98
+ <Select.Trigger>
99
+ <Select.Value />
100
+ </Select.Trigger>
101
+ <Select.Item value="free">Free</Select.Item>
102
+ <Select.Item value="pro">Pro</Select.Item>
103
+ <Select.Separator />
104
+ <Select.Group label="Teams">
105
+ <Select.Item value="team">Team</Select.Item>
106
+ <Select.Item value="enterprise" disabled>
107
+ Enterprise
108
+ </Select.Item>
109
+ </Select.Group>
110
+ </Select.Root>
111
+ <button class="lu-button">Save</button>
112
+ </form>
113
+ );
114
+ }
115
+ ```
116
+
117
+ In an island, bound to a signal. `bind:value={theme}` is short for `value={theme()}` plus
118
+ `onValueChange={(next) => theme.set(next)}`.
119
+
120
+ ```tsx check
121
+ import { Select } from "@lumiose/ui";
122
+ import { signal } from "lumiose";
123
+
124
+ export function ThemePicker() {
125
+ const theme = signal("system");
126
+ return (
127
+ <Select.Root aria-label="Theme" bind:value={theme} class="lu-select">
128
+ <Select.Trigger>
129
+ <Select.Value />
130
+ </Select.Trigger>
131
+ <Select.Item value="light">Light</Select.Item>
132
+ <Select.Item value="dark">Dark</Select.Item>
133
+ <Select.Item value="system">System</Select.Item>
134
+ </Select.Root>
135
+ );
136
+ }
137
+ ```
138
+
139
+ ## Styling
140
+
141
+ **With the default theme**, put `lu-select` on `Root`. It makes the select a customizable select
142
+ and styles the control, the picker, the options (hover and keyboard focus, disabled), the groups and
143
+ the separators; in browsers without customizable select it styles the closed control. Its colours
144
+ come from the theme's `--lu-` properties ([UI components](README.md#the-default-theme)).
145
+
146
+ **With your own CSS**, opt in to customizable select and style the select's parts and states:
147
+
148
+ - the control: the `select` itself, `:open` while its picker is open, `:focus-visible`,
149
+ `:disabled`, `:invalid` and `:user-invalid`, and `::picker-icon`;
150
+ - the picker: `::picker(select)`, placed with anchor positioning (`position-area`,
151
+ `position-try-fallbacks` work on it);
152
+ - options: `option`, `option:checked`, `option:hover`, `option:focus-visible`, `option:disabled`,
153
+ and `option::checkmark`;
154
+ - `optgroup`, `hr`, and inside the `Trigger`, `selectedcontent`.
155
+
156
+ A starting point:
157
+
158
+ ```css
159
+ .select,
160
+ .select::picker(select) {
161
+ appearance: base-select;
162
+ }
163
+ .select {
164
+ padding: 0.5rem 0.75rem;
165
+ border: 1px solid #85858e;
166
+ border-radius: 0.5rem;
167
+ }
168
+ .select::picker(select) {
169
+ padding: 0.25rem;
170
+ border: 1px solid #e4e4e7;
171
+ border-radius: 0.5rem;
172
+ }
173
+ .select option:focus-visible {
174
+ background: #f4f4f5;
175
+ outline: 2px solid #2563eb;
176
+ outline-offset: -2px;
177
+ }
178
+ ```
179
+
180
+ `@lumiose/ui/select.css` holds what Select needs to work, in the `lu.core` layer: a select whose
181
+ first child is a `Trigger` gets `appearance: base-select` (and so does its picker), since only a
182
+ customizable select renders the trigger, `Value` and options with more than text. Where
183
+ customizable select is missing, it hides the `Trigger` (Firefox keeps it in the select, a nameless
184
+ button for assistive technology otherwise). A select with no `Trigger` keeps the browser's look
185
+ until your CSS or the theme opts in.
186
+
187
+ ## Keyboard and focus
188
+
189
+ The platform's: the select is one tab stop with `role="combobox"`, and the browser runs the
190
+ keyboard. In Chromium's customizable select:
191
+
192
+ | Key | Does |
193
+ | --------------------------- | ------------------------------------------------------------------------------------------------------- |
194
+ | Space, Arrow Down, Arrow Up | On the control: opens the picker, with focus on the selected option. |
195
+ | Alt+Arrow Down, F4 | On the control: opens the picker. |
196
+ | Arrow Down, Arrow Up | In the picker: the next or previous option; disabled options are skipped. |
197
+ | Home, End | In the picker: the first or last option. |
198
+ | A character | In the picker: typeahead, the next option whose text starts with it. |
199
+ | Enter, Space | In the picker: selects the focused option and closes the picker. |
200
+ | Escape | Closes the picker without selecting, focus back on the control; closed, it goes on, so a dialog closes. |
201
+ | Tab | In the picker: closes it, focus back on the control; from there Tab moves on. |
202
+
203
+ A click on a `<label for>` focuses the select without opening it. A click on the control opens the
204
+ picker; a click on an option selects it; a click outside closes the picker. Other browsers keep
205
+ their own select keyboard and pickers, which differ by platform.
206
+
207
+ ## Without JavaScript
208
+
209
+ Select needs none: it is a native control, in server HTML and with JavaScript off, on every
210
+ browser. It submits under its `name`, is validated (`required`), resets with its form, takes the
211
+ browser's autofill and is disabled by a disabled `<fieldset>`. `value`, `onValueChange` and
212
+ `bind:value` need the component to run in the browser, in an island; there `value` sets the
213
+ control's value when it changes, and its `change` event calls `onValueChange`.
214
+
215
+ ## Browser support
216
+
217
+ - **Customizable select** (`appearance: base-select`, `::picker(select)`, `<selectedcontent>`, a
218
+ `<button>` and rich content inside `<select>`): Chrome and Edge 135, Safari 26 (the end-to-end
219
+ tests run it in Chromium and WebKit 26.6). Firefox has none yet: it shows its classic select,
220
+ styled by the theme when closed, with the options' text (or their `label`).
221
+ - Firefox 155 keeps the `Trigger` and the tags inside options in the DOM (`select.css` hides the
222
+ `Trigger`). Older browsers without customizable select drop them from server HTML, so an
223
+ island's select falls back to rendering on the client there
224
+ ([LUM4006](https://itsjavi.com/lumiose/docs/errors/LUM4006/)).
225
+ - **Typeahead** matches an option's text, or its `label`: give options that start with an icon a
226
+ `label`.
227
+ - **`:open`** comes with customizable select.
228
+
229
+ ## Related
230
+
231
+ - [Combobox](combobox.md): when the list is long enough to filter, for several values, or for
232
+ async options.
233
+ - [Menu](menu.md): commands rather than a value.
234
+ - [UI components](README.md): the approach, styling, the default theme and the other components.
235
+ - [Islands](https://itsjavi.com/lumiose/docs/concepts/islands/): running a component in the
236
+ browser, for `bind:value`.