@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.
- package/LICENSE +21 -0
- package/README.md +53 -2
- package/dist/accordion/accordion.css +20 -0
- package/dist/accordion/accordion.script.ts +66 -0
- package/dist/accordion/accordion.tsx +288 -0
- package/dist/animation/animation.ts +34 -0
- package/dist/autocomplete/autocomplete.tsx +239 -0
- package/dist/avatar/avatar.css +54 -0
- package/dist/avatar/avatar.script.ts +96 -0
- package/dist/avatar/avatar.tsx +110 -0
- package/dist/button/button.script.ts +35 -0
- package/dist/button/button.tsx +40 -0
- package/dist/checkbox/checkbox.script.ts +225 -0
- package/dist/checkbox/checkbox.tsx +156 -0
- package/dist/checkbox/context.ts +17 -0
- package/dist/checkbox-group/checkbox-group.tsx +111 -0
- package/dist/collapsible/collapsible.css +12 -0
- package/dist/collapsible/collapsible.script.ts +7 -0
- package/dist/collapsible/collapsible.tsx +153 -0
- package/dist/collapsible/panel.ts +115 -0
- package/dist/combobox/combobox.css +37 -0
- package/dist/combobox/combobox.script.ts +974 -0
- package/dist/combobox/combobox.tsx +556 -0
- package/dist/combobox/context.ts +46 -0
- package/dist/context-menu/context-menu.css +16 -0
- package/dist/context-menu/context-menu.script.ts +172 -0
- package/dist/context-menu/context-menu.tsx +178 -0
- package/dist/dialog/dialog.css +35 -0
- package/dist/dialog/dialog.script.ts +269 -0
- package/dist/dialog/dialog.tsx +214 -0
- package/dist/drawer/drawer.css +38 -0
- package/dist/drawer/drawer.script.ts +207 -0
- package/dist/drawer/drawer.tsx +113 -0
- package/dist/field/context.ts +156 -0
- package/dist/field/field.css +16 -0
- package/dist/field/field.script.ts +422 -0
- package/dist/field/field.tsx +465 -0
- package/dist/fieldset/fieldset.tsx +41 -0
- package/dist/form/form.tsx +50 -0
- package/dist/form/native.ts +21 -0
- package/dist/form/reset.ts +53 -0
- package/dist/index.ts +261 -0
- package/dist/input/input.tsx +67 -0
- package/dist/layer/layer.ts +191 -0
- package/dist/locale/digits.ts +21 -0
- package/dist/menu/menu.css +22 -0
- package/dist/menu/menu.script.ts +591 -0
- package/dist/menu/menu.tsx +520 -0
- package/dist/menubar/menubar.css +16 -0
- package/dist/menubar/menubar.script.ts +195 -0
- package/dist/menubar/menubar.tsx +67 -0
- package/dist/meter/meter.tsx +134 -0
- package/dist/navigation-menu/navigation-menu.css +14 -0
- package/dist/navigation-menu/navigation-menu.script.ts +466 -0
- package/dist/navigation-menu/navigation-menu.tsx +266 -0
- package/dist/number-field/format.ts +70 -0
- package/dist/number-field/number-field.css +38 -0
- package/dist/number-field/number-field.script.ts +490 -0
- package/dist/number-field/number-field.tsx +403 -0
- package/dist/otp-field/otp-field.css +45 -0
- package/dist/otp-field/otp-field.script.ts +330 -0
- package/dist/otp-field/otp-field.tsx +320 -0
- package/dist/placement/place.ts +148 -0
- package/dist/placement/placement.css +155 -0
- package/dist/placement/placement.ts +60 -0
- package/dist/popover/hover.ts +178 -0
- package/dist/popover/popover.css +20 -0
- package/dist/popover/popover.script.ts +256 -0
- package/dist/popover/popover.tsx +317 -0
- package/dist/preview-card/preview-card.css +7 -0
- package/dist/preview-card/preview-card.script.ts +136 -0
- package/dist/preview-card/preview-card.tsx +128 -0
- package/dist/progress/progress.tsx +155 -0
- package/dist/progress/value.ts +23 -0
- package/dist/radio-group/radio-group.tsx +156 -0
- package/dist/scroll-area/scroll-area.css +72 -0
- package/dist/scroll-area/scroll-area.script.ts +281 -0
- package/dist/scroll-area/scroll-area.tsx +118 -0
- package/dist/select/select.css +25 -0
- package/dist/select/select.tsx +171 -0
- package/dist/separator/separator.css +12 -0
- package/dist/separator/separator.tsx +38 -0
- package/dist/slider/format.ts +33 -0
- package/dist/slider/slider.css +93 -0
- package/dist/slider/slider.script.ts +355 -0
- package/dist/slider/slider.tsx +374 -0
- package/dist/styles.css +30 -0
- package/dist/switch/switch.tsx +92 -0
- package/dist/tabs/tabs.css +54 -0
- package/dist/tabs/tabs.script.ts +225 -0
- package/dist/tabs/tabs.tsx +272 -0
- package/dist/themes/default.css +2111 -0
- package/dist/toast/command.ts +3 -0
- package/dist/toast/toast.css +68 -0
- package/dist/toast/toast.script.ts +516 -0
- package/dist/toast/toast.tsx +432 -0
- package/dist/toggle/context.ts +16 -0
- package/dist/toggle/toggle.tsx +99 -0
- package/dist/toggle-group/toggle-group.script.ts +166 -0
- package/dist/toggle-group/toggle-group.tsx +117 -0
- package/dist/toolbar/context.ts +18 -0
- package/dist/toolbar/toolbar.css +12 -0
- package/dist/toolbar/toolbar.script.ts +123 -0
- package/dist/toolbar/toolbar.tsx +180 -0
- package/dist/tooltip/tooltip.css +8 -0
- package/dist/tooltip/tooltip.script.ts +222 -0
- package/dist/tooltip/tooltip.tsx +149 -0
- package/docs/README.md +174 -0
- package/docs/accordion.md +208 -0
- package/docs/autocomplete.md +161 -0
- package/docs/avatar.md +198 -0
- package/docs/button.md +98 -0
- package/docs/checkbox-group.md +112 -0
- package/docs/checkbox.md +133 -0
- package/docs/collapsible.md +165 -0
- package/docs/combobox.md +369 -0
- package/docs/context-menu.md +197 -0
- package/docs/dialog.md +256 -0
- package/docs/drawer.md +204 -0
- package/docs/field.md +295 -0
- package/docs/fieldset.md +92 -0
- package/docs/form.md +131 -0
- package/docs/input.md +87 -0
- package/docs/installation.md +65 -0
- package/docs/llms-full.txt +6923 -0
- package/docs/llms.txt +53 -0
- package/docs/menu.md +365 -0
- package/docs/menubar.md +186 -0
- package/docs/meter.md +103 -0
- package/docs/navigation-menu.md +257 -0
- package/docs/number-field.md +181 -0
- package/docs/otp-field.md +160 -0
- package/docs/popover.md +271 -0
- package/docs/preview-card.md +130 -0
- package/docs/progress.md +115 -0
- package/docs/radio-group.md +127 -0
- package/docs/scroll-area.md +147 -0
- package/docs/select.md +236 -0
- package/docs/separator.md +116 -0
- package/docs/slider.md +184 -0
- package/docs/switch.md +98 -0
- package/docs/tabs.md +254 -0
- package/docs/theming.md +132 -0
- package/docs/toast.md +311 -0
- package/docs/toggle-group.md +119 -0
- package/docs/toggle.md +94 -0
- package/docs/toolbar.md +138 -0
- package/docs/tooltip.md +128 -0
- package/package.json +102 -3
package/docs/progress.md
ADDED
|
@@ -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`.
|