@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,116 @@
1
+ # Separator
2
+
3
+ > A native `<hr>` between groups of content, after Base UI's Separator: announced as a separator, horizontal by default, or vertical between the items of a row with `aria-orientation="vertical"`. No script.
4
+
5
+ ```ts
6
+ import { Separator } from "@lumiose/ui"; // or "@lumiose/ui/separator"
7
+ import "@lumiose/ui/separator.css"; // a vertical separator's margins and height in a row
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <Separator />
15
+ ```
16
+
17
+ | Part | Renders |
18
+ | ----------- | ------------------------------------------------------------------------------------ |
19
+ | `Separator` | `<hr data-orientation="…">`, with `aria-orientation="vertical"` when it is vertical. |
20
+
21
+ An `<hr>` is a separator for assistive technology with no role to add. It is flow content: it
22
+ cannot go inside a `<p>`, a `<span>` or a `<button>`, where the browser would move it out of the
23
+ element.
24
+
25
+ ## API
26
+
27
+ ### Separator
28
+
29
+ | Prop | Type | Default | |
30
+ | ------------- | ---------------------------- | -------------- | ------------------------------------------------------------------------- |
31
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Which way it runs: `"vertical"` between the items of a row (a toolbar's). |
32
+
33
+ Every other native prop passes through: `class`, `style`, `id`, `aria-*`, `data-*`.
34
+
35
+ **Differences from Base UI's Separator.** It is always an `<hr>`, not a `<div role="separator">`:
36
+ no `render`. A horizontal one leaves out `aria-orientation="horizontal"`, an `<hr>`'s default.
37
+
38
+ ## Example
39
+
40
+ Links in a row, split into groups, styled by the default theme:
41
+
42
+ ```tsx check
43
+ import { Separator } from "@lumiose/ui";
44
+
45
+ export function FooterLinks() {
46
+ return (
47
+ <nav aria-label="Footer" style={{ display: "flex", gap: "0.75rem", "align-items": "center" }}>
48
+ <a href="#docs">Docs</a>
49
+ <a href="#blog">Blog</a>
50
+ <Separator orientation="vertical" class="lu-separator" />
51
+ <a href="#privacy">Privacy</a>
52
+ <a href="#terms">Terms</a>
53
+ </nav>
54
+ );
55
+ }
56
+ ```
57
+
58
+ Between sections of a page or a card, a horizontal one:
59
+
60
+ ```tsx check
61
+ import { Separator } from "@lumiose/ui";
62
+
63
+ export function AccountCard() {
64
+ return (
65
+ <section aria-label="Account">
66
+ <p>Signed in as ada@example.com</p>
67
+ <Separator class="lu-separator" />
68
+ <p>Plan: Free</p>
69
+ </section>
70
+ );
71
+ }
72
+ ```
73
+
74
+ ## Styling
75
+
76
+ **With the default theme**, put `lu-separator` on it: a 1px line in `--lu-border`, with a margin
77
+ above and below when horizontal, and as tall as its row when vertical.
78
+
79
+ **With your own CSS**, style it through `class` and `[data-orientation]`. Reset the borders an
80
+ `<hr>` has by default, and draw the line with a background:
81
+
82
+ ```css
83
+ .separator {
84
+ border: 0;
85
+ background: #e4e4e7;
86
+ }
87
+ .separator[data-orientation="horizontal"] {
88
+ block-size: 1px;
89
+ margin: 1rem 0;
90
+ }
91
+ .separator[data-orientation="vertical"] {
92
+ inline-size: 1px;
93
+ }
94
+ ```
95
+
96
+ `@lumiose/ui/separator.css`, in the `lu.core` layer, gives a vertical separator no margins (an
97
+ `<hr>`'s automatic ones would push the row's items apart) and the full height of its row, even one
98
+ that centres its items (`align-self: stretch`).
99
+
100
+ ## Keyboard and focus
101
+
102
+ A separator takes no focus and has no keys: it only structures the content around it.
103
+
104
+ ## Without JavaScript, and the client script
105
+
106
+ A native `<hr>` with attributes: it needs no script, on the server or in an island.
107
+
108
+ ## Browser support
109
+
110
+ Native `<hr>` and `aria-orientation`: every browser.
111
+
112
+ ## Related
113
+
114
+ - [Menu](menu.md): `Menu.Separator` between a menu's items.
115
+ - [Select](select.md): separators between a select's options.
116
+ - [UI components](README.md): the approach, styling and the other components.
package/docs/slider.md ADDED
@@ -0,0 +1,184 @@
1
+ # Slider
2
+
3
+ > A value picked within a range by dragging or with the keys, or a range with two thumbs, after Base UI's Slider. Each thumb is a native `<input type="range">`, so it drags, takes the arrow keys, Home and End, follows the text direction and submits with its form, in server HTML and without JavaScript. `<lu-slider>` adds Page Up and Page Down, keeps a range's thumbs in order, moves the nearest thumb on a press on the track and keeps the value text current. `bind:value` keeps a signal in sync.
4
+
5
+ ```ts
6
+ import { Slider } from "@lumiose/ui"; // or "@lumiose/ui/slider"
7
+ import "@lumiose/ui/slider.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
+ <Slider.Root>
15
+ <Slider.Label />
16
+ <Slider.Value />
17
+ <Slider.Control>
18
+ <Slider.Track>
19
+ <Slider.Indicator />
20
+ </Slider.Track>
21
+ <Slider.Thumb />
22
+ </Slider.Control>
23
+ </Slider.Root>
24
+ ```
25
+
26
+ | Part | Renders |
27
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
28
+ | `Root` | `<lu-slider data-orientation data-thumb-alignment>` with `--lu-slider-start` and `--lu-slider-end` |
29
+ | `Label` | `<label for>` the first thumb; thumbs without an `aria-label` are labelled by it |
30
+ | `Value` | `<output for>` the thumbs, with the formatted values (`20 – 80`) |
31
+ | `Control` | `<div>`: the track and the thumbs stacked in one place; presses on it move a thumb. A range's is a `role="group"` the Label names |
32
+ | `Track` | `<div>`: the rail |
33
+ | `Indicator` | `<div>`: the selected part, from `--lu-slider-start` to `--lu-slider-end` |
34
+ | `Thumb` | `<input type="range" min max step value name>`, one per value, in order |
35
+
36
+ ## API
37
+
38
+ ### Slider.Root
39
+
40
+ | Prop | Type | Default | |
41
+ | ------------------------ | -------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
+ | `value` | `number \| number[]` | | Moves the thumbs when it changes (an array: one value per thumb). Pair it with `onValueChange`. |
43
+ | `defaultValue` | `number \| number[]` | `min` | The value at first; a form reset goes back to it. |
44
+ | `onValueChange` | `(value) => void` | | Called with the new value as a thumb moves. |
45
+ | `onValueCommitted` | `(value) => void` | | Called when a move ends: a release, a key press. |
46
+ | `bind:value` | `Signal<number \| number[]>` | | Stands in for `value` and `onValueChange`. |
47
+ | `min` | `number` | `0` | The smallest value. |
48
+ | `max` | `number` | `100` | The largest value. |
49
+ | `step` | `number` | `1` | The step of the arrow keys; values snap to it. |
50
+ | `largeStep` | `number` | `10` | The step of Page Up, Page Down and Shift with an arrow key. |
51
+ | `minStepsBetweenValues` | `number` | `0` | With several thumbs: how many steps apart they stay. |
52
+ | `thumbCollisionBehavior` | `"push"` \| `"swap"` \| `"none"` | `"push"` | With several thumbs: a thumb moved into another pushes it along, swaps places with it (a drag), or stops at it. |
53
+ | `thumbAlignment` | `"edge"` \| `"center"` | `"edge"` | The thumbs stop inside the control at its ends, or with their centre on its ends (overhanging it). |
54
+ | `orientation` | `"horizontal"` \| `"vertical"` | `"horizontal"` | Vertical sliders run bottom to top. |
55
+ | `disabled` | `boolean` | `false` | Disables every thumb. |
56
+ | `name` | `string` | | The thumbs submit their values under it, one entry each. |
57
+ | `form` | `string` | | The id of a form the thumbs belong to, when they are outside it. |
58
+ | `format` | `Intl.NumberFormatOptions` | plain numbers | How the values are written; with it, each thumb's `aria-valuetext` is its formatted value. Options or a `locale` the browser refuses fall back to its defaults. |
59
+ | `locale` | `string` | the browser's | The locale of the formatted values (the server's in server HTML). |
60
+
61
+ The value's type follows `value`, `defaultValue` or `bind:value`: a number for one thumb, a
62
+ `number[]` for a range. Every other prop passes through to `<lu-slider>`: `id`, `class`, `style`.
63
+
64
+ ### The other parts
65
+
66
+ `Label` takes `<label>` props, its `id` among them, which the `Control` and the thumbs point at. `Value` takes `<output>` props, and its `children` may be a function
67
+ `(formattedValues, values) => JSX.Element` (then it has no `for`, and you render its text). `Control`,
68
+ `Track` and `Indicator` take `<div>` props. `Thumb` takes `<input>` props except the range's own
69
+ (`aria-label`, `aria-valuetext`, `disabled`, `class`). A thumb keeps its own `id`, which the
70
+ `Label` and the `Value` before it point at (in a Field, the first thumb's is the field's control;
71
+ see [Ids](README.md#ids)). Give each thumb of a range an
72
+ `aria-label` with the label's text in it ("Minimum price" under "Price"), so speech users can say
73
+ what they see (WCAG 2.5.3); the Label names the range's group (`Control`), so screen readers
74
+ announce "Price, group" before the thumbs. Inside a Field, the first thumb takes the field's id,
75
+ name, description and error.
76
+
77
+ - **`thumbCollisionBehavior="swap"`.** A thumb dragged past another (with the pointer, or a press
78
+ on the track) trades places with it: the values stay in order, the other thumb takes the
79
+ dragged value and focus, and the drag goes on with it. A native drag keeps its own thumb, so
80
+ that thumb stays put and hands its value on. The keys stop at the other thumb, as with
81
+ `"none"`, and `minStepsBetweenValues` applies to keys only.
82
+ - **Blocked moves.** A move that changes no value, such as a thumb pushed against its neighbour
83
+ with `"none"`, fires no `input` or `change`, so `onValueChange` and `onValueCommitted` do not
84
+ run.
85
+ - **`thumbAlignment` and `--lu-slider-thumb-size`.** A native thumb stays inside its input. With
86
+ the thumbs' size in `--lu-slider-thumb-size` (the default theme sets it), slider.css runs the
87
+ track between the thumbs' centres at the ends, so the Indicator ends at a thumb's centre
88
+ whatever the value (`"edge"`); with `"center"` the inputs outgrow the control by half a thumb
89
+ at each end, so the thumbs' centres reach its ends and the track spans it. Without the
90
+ variable, the Indicator can end up to half a thumb from its centre.
91
+
92
+ **Differences from Base UI's Slider.** Thumbs are the range inputs themselves, so there is no hidden
93
+ input, no `inputRef` and no `getAriaLabel` (use `aria-label` on each Thumb). `thumbAlignment`
94
+ defaults to `"edge"` (Base UI's to `"center"`), and has no `"edge-client-only"`: the server HTML
95
+ is already right. `format` is `Intl.NumberFormatOptions` and `locale` a string, both written to
96
+ the element for its script.
97
+
98
+ ## Example
99
+
100
+ A price range, in an island:
101
+
102
+ ```tsx check
103
+ import { Slider } from "@lumiose/ui";
104
+ import { signal } from "lumiose";
105
+
106
+ export function PriceRange() {
107
+ const price = signal([20, 80]);
108
+ return (
109
+ <Slider.Root
110
+ name="price"
111
+ bind:value={price}
112
+ minStepsBetweenValues={5}
113
+ format={{ style: "currency", currency: "EUR", maximumFractionDigits: 0 }}
114
+ class="lu-slider"
115
+ >
116
+ <Slider.Label>Price</Slider.Label>
117
+ <Slider.Value />
118
+ <Slider.Control>
119
+ <Slider.Track>
120
+ <Slider.Indicator />
121
+ </Slider.Track>
122
+ <Slider.Thumb aria-label="Minimum price" />
123
+ <Slider.Thumb aria-label="Maximum price" />
124
+ </Slider.Control>
125
+ </Slider.Root>
126
+ );
127
+ }
128
+ ```
129
+
130
+ ## Styling
131
+
132
+ **With the default theme**, put `lu-slider` on `Root`: the label and the value on a row above a 24px
133
+ tall control, an 8px track edged in `--lu-input` (3:1), the selected part in the primary colour,
134
+ and 24px round thumbs (`--lu-slider-thumb-size`) with a focus ring around the thumb. Vertical
135
+ sliders are 10rem tall. A center-aligned control is inset by half a thumb, so its thumbs overhang
136
+ it inside the slider's box.
137
+
138
+ **With your own CSS**, style the parts through `class`, the thumbs' pseudo-elements
139
+ (`::-webkit-slider-thumb`, `::-moz-range-thumb`), `[data-orientation="vertical"]`, `:disabled`,
140
+ `:focus-visible`, and the Indicator through `--lu-slider-start` and `--lu-slider-end`. Over a Track,
141
+ the thumbs' own tracks are transparent (`appearance: none`). Set `--lu-slider-thumb-size` to your
142
+ thumbs' size on the Root, so the track lines up with their centres.
143
+
144
+ `@lumiose/ui/slider.css` (0.4 kB brotli) stacks the track and thumbs, spans the thumbs over the
145
+ control and the Indicator over the selected part, lines the track up with the thumbs' centres
146
+ (`--lu-slider-thumb-size`, `data-thumb-alignment`), writes vertical thumbs bottom to top and lets
147
+ only the thumbs of a range take the pointer.
148
+
149
+ ## Keyboard and focus
150
+
151
+ | Key | Does |
152
+ | ----------------------- | ---------------------------------------------------------------------------------------------- |
153
+ | Tab | Moves between the thumbs, then on: one tab stop per thumb. |
154
+ | Arrow Right / Arrow Up | One step up (Arrow Left goes up right to left). |
155
+ | Arrow Left / Arrow Down | One step down (Arrow Right goes down right to left). |
156
+ | Page Up / Page Down | `largeStep` up or down. |
157
+ | Shift + an arrow | `largeStep` in that direction. |
158
+ | Home / End | The minimum or the maximum; in a range, at most the neighbouring thumb, which they never push. |
159
+
160
+ Every drag has these keys as its alternative (WCAG 2.5.7). A press on a range's track moves the
161
+ nearest thumb there and focuses it; a press on a single slider's track moves its thumb.
162
+
163
+ ## Without JavaScript, and the element's script
164
+
165
+ The thumbs are native range inputs: dragging, the arrows, Home, End, right to left, vertical,
166
+ submission, `disabled` and reset work with no script, and the server HTML has the Indicator's
167
+ custom properties and the value text. `<lu-slider>`'s script (2.0 kB brotli) adds Page Up and Page
168
+ Down, runs the arrow keys the same in every browser (WebKit's native range does not mirror them
169
+ right to left), stops Home and End at a range's neighbouring thumb, keeps a range's thumbs in order and apart (or swaps them), moves the nearest thumb
170
+ on a press on its track, and keeps the Indicator, `aria-valuetext` and the Value text current (also
171
+ after a form reset).
172
+ Without it, a range's thumbs can cross, and the Value text and the Indicator stay where the server
173
+ put them.
174
+
175
+ ## Browser support
176
+
177
+ Native range inputs: every browser. Vertical sliders use `writing-mode: vertical-lr` on the thumbs
178
+ (Chrome 124, Safari 17.4, Firefox 120 and later).
179
+
180
+ ## Related
181
+
182
+ - [Field](field.md): a label, description and error for a slider.
183
+ - [Meter](meter.md): show a value in a range, without picking it.
184
+ - [Input](input.md): `type="number"` for an exact value.
package/docs/switch.md ADDED
@@ -0,0 +1,98 @@
1
+ # Switch
2
+
3
+ > On or off, for a setting: a native `<input type="checkbox" role="switch">`, after Base UI's Switch. Space and a label click toggle it, the form submits its value when on, `required` and reset work, in server HTML and without JavaScript, and assistive technology announces a switch. `bind:checked` keeps a signal in sync.
4
+
5
+ ```ts
6
+ import { Switch } from "@lumiose/ui"; // or "@lumiose/ui/switch"
7
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
8
+ ```
9
+
10
+ ## Anatomy
11
+
12
+ ```tsx
13
+ <label>
14
+ <Switch.Root /> Label
15
+ </label>
16
+ ```
17
+
18
+ | Part | Renders |
19
+ | ------ | ---------------------------------------------------------------------- |
20
+ | `Root` | `<input type="checkbox" role="switch">`; `aria-readonly` if read-only. |
21
+
22
+ ## API
23
+
24
+ ### Switch.Root
25
+
26
+ | Prop | Type | Default | |
27
+ | ----------------- | ---------------------------- | ------- | ----------------------------------------------------------------------------------- |
28
+ | `checked` | `boolean` | | Turns it on or off when it changes. Pair it with `onCheckedChange`. |
29
+ | `defaultChecked` | `boolean` | `false` | Whether it starts on; a form reset goes back to it. |
30
+ | `onCheckedChange` | `(checked: boolean) => void` | | Called after the user turns it on or off. |
31
+ | `bind:checked` | `Signal<boolean>` | | Stands in for `checked` and `onCheckedChange`. |
32
+ | `value` | `string` | `"on"` | What it submits when on. |
33
+ | `readOnly` | `boolean` | `false` | It cannot be toggled, but stays focusable and is submitted. |
34
+ | `uncheckedValue` | `string` | | What it submits when off (by the script); by default an off switch submits nothing. |
35
+
36
+ Every other native prop passes through: `name`, `required`, `disabled`, `form`, `id`, `class`,
37
+ `aria-*`. Inside a Field it takes the field's wiring.
38
+
39
+ **Differences from Base UI's Switch.** The switch is the native input, so there is no
40
+ `Switch.Thumb`: draw the track and thumb with `appearance: none` and `::before`. No `nativeButton` or
41
+ `inputRef`; `uncheckedValue` needs the client script (without it an off switch submits nothing). `<input switch>` exists only in
42
+ Safari, so `role="switch"` stays on a checkbox everywhere.
43
+
44
+ ## Example
45
+
46
+ In an island, bound to a signal:
47
+
48
+ ```tsx check
49
+ import { Switch } from "@lumiose/ui";
50
+ import { signal } from "lumiose";
51
+
52
+ export function AirplaneMode() {
53
+ const on = signal(false);
54
+ return (
55
+ <p>
56
+ <label>
57
+ <Switch.Root name="airplane" bind:checked={on} class="lu-switch" /> Airplane mode
58
+ </label>{" "}
59
+ {on() ? "Radios off." : ""}
60
+ </p>
61
+ );
62
+ }
63
+ ```
64
+
65
+ ## Styling
66
+
67
+ **With the default theme**, put `lu-switch` on `Root`: a 40 by 24px track (`--lu-input` when off,
68
+ the primary colour when on) and a white thumb on the side that tells on from off, a focus ring.
69
+
70
+ **With your own CSS**, style the input and its state: `:checked`, `:disabled`, `:focus-visible`,
71
+ `[aria-readonly="true"]`, `[aria-invalid="true"]`.
72
+
73
+ Switch ships no stylesheet: it needs none to work.
74
+
75
+ ## Keyboard and focus
76
+
77
+ | Key | Does |
78
+ | ----- | ---------------------------------------- |
79
+ | Space | Turns it on or off (not when read-only). |
80
+ | Tab | Moves on: one tab stop. |
81
+
82
+ Enter does not toggle it. A click on its label does.
83
+
84
+ ## Without JavaScript, and the client script
85
+
86
+ A native checkbox with the switch role: toggling, submission, `required`, reset and `disabled`
87
+ work with no script. The shared checkbox script (1.0 kB brotli,
88
+ [Checkbox](checkbox.md#without-javascript-and-the-client-script)) adds read-only and
89
+ `uncheckedValue`.
90
+
91
+ ## Browser support
92
+
93
+ `role="switch"` on a checkbox: every supported browser and screen reader.
94
+
95
+ ## Related
96
+
97
+ - [Checkbox](checkbox.md): a choice to confirm, submitted with a form.
98
+ - [Field](field.md): a description and errors for a switch.
package/docs/tabs.md ADDED
@@ -0,0 +1,254 @@
1
+ # Tabs
2
+
3
+ > A tab list and the panel of the selected tab, after the WAI-ARIA tabs pattern: the server HTML holds the whole state (roles, `aria-selected`, the roving tab stop, the hidden panels), and `<lu-tabs>`, a 1.2 kB light-DOM element, adds the arrow keys and the selection. Manual or automatic activation, horizontal or vertical, right-to-left aware, with an optional indicator that slides to the selected tab; `bind:value` (or `value` and `onValueChange`) keeps a signal in sync.
4
+
5
+ ```ts
6
+ import { Tabs } from "@lumiose/ui"; // or "@lumiose/ui/tabs"
7
+ import "@lumiose/ui/tabs.css"; // makes <lu-tabs> a block and places the indicator
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <Tabs.Root>
15
+ <Tabs.List>
16
+ <Tabs.Tab />
17
+ <Tabs.Indicator />
18
+ </Tabs.List>
19
+ <Tabs.Panel />
20
+ </Tabs.Root>
21
+ ```
22
+
23
+ | Part | Renders |
24
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------- |
25
+ | `Root` | `<lu-tabs>`, the element that handles the keys and the selection. |
26
+ | `List` | `<div role="tablist">`, with `aria-orientation="vertical"` when vertical. Name it with `aria-label` or `aria-labelledby`. |
27
+ | `Tab` | `<button type="button" role="tab" value="…" aria-selected="…" aria-controls="…" tabindex="…">`. |
28
+ | `Indicator` | `<span aria-hidden="true" data-indicator>`, over the selected tab's box and behind its text. Optional. |
29
+ | `Panel` | `<div role="tabpanel" tabindex="0" aria-labelledby="…">`, `hidden` unless its tab is selected. |
30
+
31
+ A `Tab` and a `Panel` with the same `value` belong together. Every panel stays in the page, hidden
32
+ when its tab is not selected, so the browser can switch them before any script loads. Panels go
33
+ inside their `Tabs.Root`: `<lu-tabs>` shows and hides only its own panels, whatever ids its tabs
34
+ name.
35
+
36
+ ## API
37
+
38
+ ### Tabs.Root
39
+
40
+ | Prop | Type | Default | |
41
+ | --------------- | ---------------------------- | --------------------- | ------------------------------------------------ |
42
+ | `value` | `string` | | Selects the tab with this value when it changes. |
43
+ | `defaultValue` | `string` | the first enabled tab | The tab selected at first, without `value`. |
44
+ | `onValueChange` | `(value: string) => void` | | Called after the user selects another tab. |
45
+ | `bind:value` | `Signal<string>` | | Stands in for `value` and `onValueChange`. |
46
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Which arrow keys move between the tabs. |
47
+
48
+ `Root` also takes the props of an element (`class`, `style`, `data-*`, events) and passes them to
49
+ `<lu-tabs>`.
50
+
51
+ ### Tabs.List
52
+
53
+ | Prop | Type | Default | |
54
+ | ----------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
55
+ | `activateOnFocus` | `boolean` | `false` | Selects a tab as soon as it receives focus (automatic activation), as Zag does by default. |
56
+ | `loopFocus` | `boolean` | `true` | The arrow keys go from the last tab to the first and back. |
57
+
58
+ The element reads them from the list's attributes: `data-activate-on-focus` and
59
+ `data-loop-focus="false"`.
60
+
61
+ ### Tabs.Tab and Tabs.Panel
62
+
63
+ | Prop | On | Type | |
64
+ | ---------- | ----- | --------- | ------------------------------------------------------------- |
65
+ | `value` | both | `string` | Pairs a tab with its panel. Required. |
66
+ | `disabled` | `Tab` | `boolean` | The tab takes focus but cannot be selected (`aria-disabled`). |
67
+
68
+ Each takes the props of the element it renders. A `Tab`'s `id` names it, and its `Panel` points at
69
+ it; a `Panel` keeps its own `id`, which its tab points at (see [Ids](README.md#ids)). `role`,
70
+ `value`, `aria-selected`, `aria-controls`, `aria-labelledby`, `tabindex` on tabs and `hidden` stay
71
+ the parts' own.
72
+
73
+ - **Disabled tabs.** A selected tab that becomes disabled gives its selection to the first enabled
74
+ tab. With every tab disabled, none is selected and the first tab is the tab stop. There is one
75
+ tab stop: in the server HTML, the tab `value` or `defaultValue` names (a disabled one keeps it
76
+ until the script selects another), else the first enabled tab; once the script runs, the
77
+ selected tab.
78
+ - **The element.** `<lu-tabs>`' `value` property reads and sets the selection, and it fires
79
+ `valuechange` (not bubbling) when the user changes it, so `bind:value` works on the element too.
80
+
81
+ ### Tabs.Indicator
82
+
83
+ Takes the props of a `<span>`, without children. Put it inside `Tabs.List`, after the tabs. It is
84
+ `hidden` while no tab can be selected.
85
+
86
+ - **Placed before any script.** CSS anchor positioning puts it over the selected tab from the
87
+ server HTML on (the selected tab carries the anchor `Root` names in `--lu-tabs-anchor`), so it
88
+ is in place at the first paint and nothing shifts when the script loads (doc-9 §8).
89
+ - **Then measured.** Once `<lu-tabs>`' script runs, it writes the selected tab's box, from the
90
+ list's padding edge, on the Root as `--lu-tabs-indicator-left`, `--lu-tabs-indicator-top`,
91
+ `--lu-tabs-indicator-width` and `--lu-tabs-indicator-height`, and keeps them current as the
92
+ selection changes (a click, the keys, an island), as the list or a tab resizes, and as a `dir`
93
+ attribute changes in the page. The indicator reads them over its anchor, so a `transition` can
94
+ slide it from tab to tab, and they place it in browsers without anchor positioning.
95
+ - **Behind the text.** The list isolates its stacking (`isolation: isolate`) and the indicator
96
+ sits at `z-index: -1`, so a highlight never covers a tab's text.
97
+
98
+ **Differences from Base UI's Tabs.Indicator.** The variables are on the Root, with these names,
99
+ instead of Base UI's `--active-tab-*` on the indicator, and there is no `renderBeforeHydration`:
100
+ anchor positioning places it from the server HTML. No `data-activation-direction`.
101
+
102
+ ## Example
103
+
104
+ ```tsx check
105
+ import { Tabs } from "@lumiose/ui";
106
+
107
+ export function AccountSettings() {
108
+ return (
109
+ <Tabs.Root class="lu-tabs" defaultValue="account">
110
+ <Tabs.List aria-label="Account settings">
111
+ <Tabs.Tab value="account">Account</Tabs.Tab>
112
+ <Tabs.Tab value="password">Password</Tabs.Tab>
113
+ <Tabs.Tab value="billing" disabled>
114
+ Billing
115
+ </Tabs.Tab>
116
+ </Tabs.List>
117
+ <Tabs.Panel value="account">Your name and email.</Tabs.Panel>
118
+ <Tabs.Panel value="password">Change your password.</Tabs.Panel>
119
+ <Tabs.Panel value="billing">Invoices and payment methods.</Tabs.Panel>
120
+ </Tabs.Root>
121
+ );
122
+ }
123
+ ```
124
+
125
+ In an island, bound to a signal: `bind:value={tab}` is short for `value={tab()}` plus
126
+ `onValueChange={(next) => tab.set(next)}`.
127
+
128
+ ```tsx check
129
+ import { Tabs } from "@lumiose/ui";
130
+ import { signal } from "lumiose";
131
+
132
+ export function Project() {
133
+ const tab = signal("overview");
134
+ return (
135
+ <>
136
+ <Tabs.Root class="lu-tabs" bind:value={tab}>
137
+ <Tabs.List aria-label="Project" activateOnFocus>
138
+ <Tabs.Tab value="overview">Overview</Tabs.Tab>
139
+ <Tabs.Tab value="settings">Settings</Tabs.Tab>
140
+ </Tabs.List>
141
+ <Tabs.Panel value="overview">The project at a glance.</Tabs.Panel>
142
+ <Tabs.Panel value="settings">Name and members.</Tabs.Panel>
143
+ </Tabs.Root>
144
+ <button type="button" onClick={() => tab.set("settings")}>
145
+ Open the settings
146
+ </button>
147
+ </>
148
+ );
149
+ }
150
+ ```
151
+
152
+ With an indicator that slides to the selected tab:
153
+
154
+ ```tsx check
155
+ import { Tabs } from "@lumiose/ui";
156
+
157
+ export function Release() {
158
+ return (
159
+ <Tabs.Root class="lu-tabs" defaultValue="notes">
160
+ <Tabs.List aria-label="Release">
161
+ <Tabs.Tab value="notes">Notes</Tabs.Tab>
162
+ <Tabs.Tab value="changelog">Full changelog</Tabs.Tab>
163
+ <Tabs.Indicator />
164
+ </Tabs.List>
165
+ <Tabs.Panel value="notes">The highlights.</Tabs.Panel>
166
+ <Tabs.Panel value="changelog">Every change.</Tabs.Panel>
167
+ </Tabs.Root>
168
+ );
169
+ }
170
+ ```
171
+
172
+ ## Styling
173
+
174
+ **With the default theme**, put `lu-tabs` on `Root`: tabs in a row (or a column when vertical)
175
+ over a rule, the selected one underlined (beside it when vertical) in the foreground colour,
176
+ disabled ones faded, and focus rings on tabs and panels. The list wraps on narrow screens. With a
177
+ `Tabs.Indicator`, the indicator draws that bar and slides to the next tab in 0.2s (at once where
178
+ motion is reduced).
179
+
180
+ **With your own CSS**, style the parts through `class` and their state: `[role="tab"]`,
181
+ `[aria-selected="true"]`, `[aria-disabled="true"]`, `[role="tablist"][aria-orientation="vertical"]`
182
+ and `[role="tabpanel"]`. Mark the selected tab with more than a colour (WCAG 1.4.1), such as a bar
183
+ or a weight, at 3:1 against its background (WCAG 1.4.11).
184
+
185
+ ```css
186
+ .tabs [role="tab"] {
187
+ padding: 0.5rem 1rem;
188
+ border: 0;
189
+ background: none;
190
+ color: #52525b;
191
+ }
192
+ .tabs [role="tab"][aria-selected="true"] {
193
+ color: #09090b;
194
+ box-shadow: inset 0 -2px currentColor;
195
+ }
196
+ ```
197
+
198
+ A highlight behind the selected tab, with `Tabs.Indicator`:
199
+
200
+ ```css
201
+ .tabs [data-indicator] {
202
+ border-radius: 6px;
203
+ background: #f4f4f5;
204
+ transition:
205
+ left 0.2s,
206
+ width 0.2s;
207
+ }
208
+ ```
209
+
210
+ `@lumiose/ui/tabs.css` (0.3 kB brotli), in the `lu.core` layer, makes `<lu-tabs>` a block and
211
+ places the indicator: over the selected tab, behind the tabs' text, from `--lu-tabs-anchor` and the
212
+ script's `--lu-tabs-indicator-*` variables.
213
+
214
+ ## Keyboard and focus
215
+
216
+ The [WAI-ARIA tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/):
217
+
218
+ | Key | Does |
219
+ | --------------------- | -------------------------------------------------------------------------------------------------- |
220
+ | Tab | Into the list, onto the selected tab; out of it, onto the panel. |
221
+ | ArrowRight, ArrowLeft | The next or previous tab (horizontal). Swapped in right-to-left. Loops unless `loopFocus={false}`. |
222
+ | ArrowDown, ArrowUp | The next or previous tab (vertical). |
223
+ | Home, End | The first or last tab. |
224
+ | Enter, Space | Selects the focused tab (manual activation). |
225
+
226
+ - **Manual activation** (the default, as in Base UI): the arrows move focus and Enter, Space or a
227
+ click selects. With `activateOnFocus`, moving focus selects too. Either way only the panels
228
+ change: nothing navigates or submits.
229
+ - **Only the main button** selects: a right-click does not. Disabled tabs take focus but are
230
+ never selected.
231
+ - The arrow keys do nothing while an input method is composing, or with Alt, Ctrl or Meta held.
232
+
233
+ ## Without JavaScript, and the element's script
234
+
235
+ The server HTML is complete: the selected tab, its panel and the tab stop are right before any
236
+ script loads, and the panels of the other tabs are `hidden`. Selecting another tab needs
237
+ `<lu-tabs>`' script, a 1.2 kB (brotli) module that a page rendering `Tabs` loads once, without
238
+ the Lumiose runtime; islands bundle it. A `Tabs.Indicator` is placed by CSS alone until then; the
239
+ script makes it follow a new selection. `value`, `onValueChange` and `bind:value` need the
240
+ component to run in an island.
241
+
242
+ ## Browser support
243
+
244
+ - **Custom elements** (`<lu-tabs>`) and everything the element uses: every current browser.
245
+ - The default theme's vertical tabs follow the direction with logical properties
246
+ (`border-inline-end`): every current browser.
247
+ - **CSS anchor positioning** places `Tabs.Indicator` before the script runs: Chrome and Edge 125,
248
+ Firefox 147, Safari 26. Elsewhere it appears once the script has measured the selected tab.
249
+
250
+ ## Related
251
+
252
+ - [UI components](README.md): the approach, styling, the default theme and the other components.
253
+ - [Web components](https://itsjavi.com/lumiose/docs/guides/web-components/): custom elements, their properties and
254
+ `bind:` on them.