@lumiose/ui 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -2
  3. package/dist/accordion/accordion.css +20 -0
  4. package/dist/accordion/accordion.script.ts +66 -0
  5. package/dist/accordion/accordion.tsx +288 -0
  6. package/dist/animation/animation.ts +34 -0
  7. package/dist/autocomplete/autocomplete.tsx +239 -0
  8. package/dist/avatar/avatar.css +54 -0
  9. package/dist/avatar/avatar.script.ts +96 -0
  10. package/dist/avatar/avatar.tsx +110 -0
  11. package/dist/button/button.script.ts +35 -0
  12. package/dist/button/button.tsx +40 -0
  13. package/dist/checkbox/checkbox.script.ts +225 -0
  14. package/dist/checkbox/checkbox.tsx +156 -0
  15. package/dist/checkbox/context.ts +17 -0
  16. package/dist/checkbox-group/checkbox-group.tsx +111 -0
  17. package/dist/collapsible/collapsible.css +12 -0
  18. package/dist/collapsible/collapsible.script.ts +7 -0
  19. package/dist/collapsible/collapsible.tsx +153 -0
  20. package/dist/collapsible/panel.ts +115 -0
  21. package/dist/combobox/combobox.css +37 -0
  22. package/dist/combobox/combobox.script.ts +974 -0
  23. package/dist/combobox/combobox.tsx +556 -0
  24. package/dist/combobox/context.ts +46 -0
  25. package/dist/context-menu/context-menu.css +16 -0
  26. package/dist/context-menu/context-menu.script.ts +172 -0
  27. package/dist/context-menu/context-menu.tsx +178 -0
  28. package/dist/dialog/dialog.css +35 -0
  29. package/dist/dialog/dialog.script.ts +269 -0
  30. package/dist/dialog/dialog.tsx +214 -0
  31. package/dist/drawer/drawer.css +38 -0
  32. package/dist/drawer/drawer.script.ts +207 -0
  33. package/dist/drawer/drawer.tsx +113 -0
  34. package/dist/field/context.ts +156 -0
  35. package/dist/field/field.css +16 -0
  36. package/dist/field/field.script.ts +422 -0
  37. package/dist/field/field.tsx +465 -0
  38. package/dist/fieldset/fieldset.tsx +41 -0
  39. package/dist/form/form.tsx +50 -0
  40. package/dist/form/native.ts +21 -0
  41. package/dist/form/reset.ts +53 -0
  42. package/dist/index.ts +261 -0
  43. package/dist/input/input.tsx +67 -0
  44. package/dist/layer/layer.ts +191 -0
  45. package/dist/locale/digits.ts +21 -0
  46. package/dist/menu/menu.css +22 -0
  47. package/dist/menu/menu.script.ts +591 -0
  48. package/dist/menu/menu.tsx +520 -0
  49. package/dist/menubar/menubar.css +16 -0
  50. package/dist/menubar/menubar.script.ts +195 -0
  51. package/dist/menubar/menubar.tsx +67 -0
  52. package/dist/meter/meter.tsx +134 -0
  53. package/dist/navigation-menu/navigation-menu.css +14 -0
  54. package/dist/navigation-menu/navigation-menu.script.ts +466 -0
  55. package/dist/navigation-menu/navigation-menu.tsx +266 -0
  56. package/dist/number-field/format.ts +70 -0
  57. package/dist/number-field/number-field.css +38 -0
  58. package/dist/number-field/number-field.script.ts +490 -0
  59. package/dist/number-field/number-field.tsx +403 -0
  60. package/dist/otp-field/otp-field.css +45 -0
  61. package/dist/otp-field/otp-field.script.ts +330 -0
  62. package/dist/otp-field/otp-field.tsx +320 -0
  63. package/dist/placement/place.ts +148 -0
  64. package/dist/placement/placement.css +155 -0
  65. package/dist/placement/placement.ts +60 -0
  66. package/dist/popover/hover.ts +178 -0
  67. package/dist/popover/popover.css +20 -0
  68. package/dist/popover/popover.script.ts +256 -0
  69. package/dist/popover/popover.tsx +317 -0
  70. package/dist/preview-card/preview-card.css +7 -0
  71. package/dist/preview-card/preview-card.script.ts +136 -0
  72. package/dist/preview-card/preview-card.tsx +128 -0
  73. package/dist/progress/progress.tsx +155 -0
  74. package/dist/progress/value.ts +23 -0
  75. package/dist/radio-group/radio-group.tsx +156 -0
  76. package/dist/scroll-area/scroll-area.css +72 -0
  77. package/dist/scroll-area/scroll-area.script.ts +281 -0
  78. package/dist/scroll-area/scroll-area.tsx +118 -0
  79. package/dist/select/select.css +25 -0
  80. package/dist/select/select.tsx +171 -0
  81. package/dist/separator/separator.css +12 -0
  82. package/dist/separator/separator.tsx +38 -0
  83. package/dist/slider/format.ts +33 -0
  84. package/dist/slider/slider.css +93 -0
  85. package/dist/slider/slider.script.ts +355 -0
  86. package/dist/slider/slider.tsx +374 -0
  87. package/dist/styles.css +30 -0
  88. package/dist/switch/switch.tsx +92 -0
  89. package/dist/tabs/tabs.css +54 -0
  90. package/dist/tabs/tabs.script.ts +225 -0
  91. package/dist/tabs/tabs.tsx +272 -0
  92. package/dist/themes/default.css +2111 -0
  93. package/dist/toast/command.ts +3 -0
  94. package/dist/toast/toast.css +68 -0
  95. package/dist/toast/toast.script.ts +516 -0
  96. package/dist/toast/toast.tsx +432 -0
  97. package/dist/toggle/context.ts +16 -0
  98. package/dist/toggle/toggle.tsx +99 -0
  99. package/dist/toggle-group/toggle-group.script.ts +166 -0
  100. package/dist/toggle-group/toggle-group.tsx +117 -0
  101. package/dist/toolbar/context.ts +18 -0
  102. package/dist/toolbar/toolbar.css +12 -0
  103. package/dist/toolbar/toolbar.script.ts +123 -0
  104. package/dist/toolbar/toolbar.tsx +180 -0
  105. package/dist/tooltip/tooltip.css +8 -0
  106. package/dist/tooltip/tooltip.script.ts +222 -0
  107. package/dist/tooltip/tooltip.tsx +149 -0
  108. package/docs/README.md +174 -0
  109. package/docs/accordion.md +208 -0
  110. package/docs/autocomplete.md +161 -0
  111. package/docs/avatar.md +198 -0
  112. package/docs/button.md +98 -0
  113. package/docs/checkbox-group.md +112 -0
  114. package/docs/checkbox.md +133 -0
  115. package/docs/collapsible.md +165 -0
  116. package/docs/combobox.md +369 -0
  117. package/docs/context-menu.md +197 -0
  118. package/docs/dialog.md +256 -0
  119. package/docs/drawer.md +204 -0
  120. package/docs/field.md +295 -0
  121. package/docs/fieldset.md +92 -0
  122. package/docs/form.md +131 -0
  123. package/docs/input.md +87 -0
  124. package/docs/installation.md +65 -0
  125. package/docs/llms-full.txt +6923 -0
  126. package/docs/llms.txt +53 -0
  127. package/docs/menu.md +365 -0
  128. package/docs/menubar.md +186 -0
  129. package/docs/meter.md +103 -0
  130. package/docs/navigation-menu.md +257 -0
  131. package/docs/number-field.md +181 -0
  132. package/docs/otp-field.md +160 -0
  133. package/docs/popover.md +271 -0
  134. package/docs/preview-card.md +130 -0
  135. package/docs/progress.md +115 -0
  136. package/docs/radio-group.md +127 -0
  137. package/docs/scroll-area.md +147 -0
  138. package/docs/select.md +236 -0
  139. package/docs/separator.md +116 -0
  140. package/docs/slider.md +184 -0
  141. package/docs/switch.md +98 -0
  142. package/docs/tabs.md +254 -0
  143. package/docs/theming.md +132 -0
  144. package/docs/toast.md +311 -0
  145. package/docs/toggle-group.md +119 -0
  146. package/docs/toggle.md +94 -0
  147. package/docs/toolbar.md +138 -0
  148. package/docs/tooltip.md +128 -0
  149. package/package.json +102 -3
package/docs/field.md ADDED
@@ -0,0 +1,295 @@
1
+ # Field
2
+
3
+ > A form field: a label, a native control, a description and its errors, wired with `for`, `aria-describedby` and `aria-invalid`, after Base UI's Field. Validation is the browser's constraint validation (`required`, `type`, `pattern`, `minlength`…); a 1.7 kB script shows it in the field's Error parts at the right time, instead of the browser's bubble, and focuses the first invalid field on submit. A Form's server errors render in the HTML, so they work without JavaScript. In an island, `validate` adds checks of your own, async ones included, and `Field.Validity` renders from the field's validity.
4
+
5
+ ```ts
6
+ import { Field } from "@lumiose/ui"; // or "@lumiose/ui/field"
7
+ import "@lumiose/ui/field.css"; // makes <lu-field> a block, keeps hidden errors hidden
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <Field.Root>
15
+ <Field.Label />
16
+ <Field.Control />
17
+ <Field.Description />
18
+ <Field.Error />
19
+ <Field.Validity />
20
+ </Field.Root>
21
+ ```
22
+
23
+ | Part | Renders |
24
+ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `Root` | `<lu-field>`, a block around the other parts, with `validation-mode`. |
26
+ | `Label` | `<label for="…">`, pointing at the control. |
27
+ | `Control` | `<input>`: [Input](input.md), with the field's `id`, `name`, `aria-describedby` and `aria-invalid`. |
28
+ | `Description` | `<p>` that describes the control; several get ids of their own, and the control is described by each. |
29
+ | `Error` | `<output hidden>`: without children, the field's message (`for` the control); with children, your message. Shown while the field is invalid. |
30
+ | `Validity` | No element: what its function child returns for the field's validity, in an island. |
31
+
32
+ Instead of `Field.Control`, a field can hold a [Checkbox](checkbox.md), a [Switch](switch.md), a
33
+ [Select](select.md), a [Radio Group](radio-group.md), a [Checkbox Group](checkbox-group.md), a
34
+ [Combobox](combobox.md), an [Autocomplete](autocomplete.md), a [Number Field](number-field.md) or a
35
+ [Slider](slider.md): they take the field's wiring the same way (a Combobox and an Autocomplete
36
+ through their input). A Radio Group or Checkbox Group takes it as a group: `Field.Label` names the
37
+ group (`aria-labelledby`, as there is no single radio or checkbox for its `for`), and the group,
38
+ not each control, gets `aria-describedby` and `aria-invalid`.
39
+
40
+ ## API
41
+
42
+ ### Field.Root
43
+
44
+ | Prop | Type | Default | |
45
+ | ------------------------ | ---------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | `name` | `string` | | The control's `name` (unless it has its own), and the key of the field's errors in the Form's `errors`. |
47
+ | `disabled` | `boolean` | `false` | Disables the control. |
48
+ | `invalid` | `boolean` | `false` | Marks the field invalid, as a server error does: its Error shows and the control gets `aria-invalid`, until the value changes. |
49
+ | `validationMode` | `"onSubmit" \| "onBlur" \| "onChange"` | `"onSubmit"` | When errors show: after a submit attempt, also when the control loses focus, or as soon as it is edited. Once shown, they follow the value. |
50
+ | `validate` | `(value: string \| boolean) => string \| null` | | Your own check, run on every edit in an island: return a message when the value is invalid. It becomes the control's custom validity. It may return a promise. |
51
+ | `validationDebounceTime` | `number` | `0` | How long typing must pause before `validate` runs, in ms: fewer server checks while the user types. |
52
+
53
+ `Root` passes `class`, `style` and other element props to `<lu-field>`. A Form's `validationMode`
54
+ applies to the fields that set none.
55
+
56
+ ### Field.Error
57
+
58
+ | Prop | Type | Default | |
59
+ | ---------- | ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------ |
60
+ | `match` | `keyof ValidityState \| true` | | Shows only for that problem (`"valueMissing"`, `"typeMismatch"`, `"tooShort"`, …), or always (`true`). |
61
+ | `children` | `JSX.Element` | | Your message. Without children, the Error shows the server's message or the browser's. |
62
+
63
+ ### Async validate
64
+
65
+ `validate` may return a promise of the message: a check on the server, such as whether a username
66
+ is taken.
67
+
68
+ - **The latest edit wins.** Each edit starts a new check; an older check that settles later is
69
+ dropped, so a slow answer never overwrites a newer one.
70
+ - **While it runs**, the control has no custom error (the last verdict was about another value),
71
+ `<lu-field>` has `data-validating`, and `Field.Validity` reports `validating: true`. When it
72
+ settles, its message becomes the control's custom validity, and the field shows it if it shows
73
+ its errors by then (with `validationMode="onChange"`, at once).
74
+ - **A submit that comes first is not blocked**, as in Base UI: validate on the server too (a
75
+ Form action's `fail()`). A check that rejects (the network) counts as no error.
76
+ - **One check per value.** The `change` a blur fires after an edit does not check the same value
77
+ again; `validationDebounceTime` waits for a pause in typing before it checks. A submit attempt
78
+ inside that pause runs the waiting check first: a synchronous `validate` that fails stops the
79
+ submission and shows its message (an async one, as above, does not block it).
80
+ - The check runs once, quietly, when the island starts and after a form reset (`Field.Validity`
81
+ stays unchecked).
82
+
83
+ ### Field.Validity
84
+
85
+ | Prop | Type | |
86
+ | ---------- | -------------------------------------------- | -------------------------------------------------- |
87
+ | `children` | `(state: FieldValidityState) => JSX.Element` | Renders from the state, again whenever it changes. |
88
+
89
+ `FieldValidityState` holds `validity` (the control's `ValidityState` keys, with `valid` `null`
90
+ until the first check, or `false` from a server error), `error` (the first problem's message, or
91
+ `""`), `errors` (every message: the server's, or `error`), `value` (the control's value, or
92
+ `checked`) and `validating` (an async `validate` is running). It updates on every edit, blur and
93
+ submit attempt, whatever the `validationMode`, and when a `validate` result or a server error
94
+ arrives; a form reset makes it unchecked again. On the server, and without JavaScript, it renders
95
+ the unchecked state (or the server's error) once.
96
+
97
+ ### Field.Control
98
+
99
+ [Input](input.md)'s props: every native `<input>` prop, plus `value`, `defaultValue`,
100
+ `onValueChange` and `bind:value`.
101
+
102
+ ### The other parts
103
+
104
+ `Label` and `Description` take the props of the element they render (`class`, `style`, `aria-*`)
105
+ and pass them through; their `id` and `for` stay the field's.
106
+
107
+ **Differences from Base UI's Field.** The control is native and so is validation: `validate`
108
+ returns one message (not a list) and gets no other form values, and there is no `dirty`, `touched`,
109
+ `actionsRef` or `Field.Item`. State is exposed the platform's way, `aria-invalid` on the control
110
+ and `:user-invalid`, `:invalid`, `:focus-within` in CSS, rather than Base UI's `data-dirty`,
111
+ `data-touched` and friends. `Field.Error` renders an `<output>`, not a `<div>`.
112
+
113
+ ## Example
114
+
115
+ An email field in a server-rendered page, with no island:
116
+
117
+ ```tsx check
118
+ import { Field } from "@lumiose/ui";
119
+
120
+ export function EmailField() {
121
+ return (
122
+ <form method="post">
123
+ <Field.Root name="email" validationMode="onBlur" class="lu-field">
124
+ <Field.Label>Email</Field.Label>
125
+ <Field.Control type="email" required autocomplete="email" class="lu-input" />
126
+ <Field.Description>We never share it.</Field.Description>
127
+ <Field.Error match="valueMissing">Enter your email.</Field.Error>
128
+ <Field.Error match="typeMismatch">Enter an email like ada@example.com.</Field.Error>
129
+ </Field.Root>
130
+ <button class="lu-button">Subscribe</button>
131
+ </form>
132
+ );
133
+ }
134
+ ```
135
+
136
+ With a form action, a [Form](form.md)'s `errors` (the data the action passes to `fail()`) show in
137
+ the field of the same `name`, in the page the server renders back:
138
+
139
+ ```tsx check
140
+ import { Field, Form } from "@lumiose/ui";
141
+
142
+ export function UsernameField(props: { errors?: { username?: string } }) {
143
+ return (
144
+ <Form method="post" errors={props.errors}>
145
+ <Field.Root name="username" class="lu-field">
146
+ <Field.Label>Username</Field.Label>
147
+ <Field.Control required minlength={3} class="lu-input" />
148
+ <Field.Error />
149
+ </Field.Root>
150
+ <button class="lu-button">Sign up</button>
151
+ </Form>
152
+ );
153
+ }
154
+ ```
155
+
156
+ In an island, an async `validate` asks the server while the user types, and `Field.Validity`
157
+ shows its state:
158
+
159
+ ```tsx check
160
+ import { Field } from "@lumiose/ui";
161
+
162
+ const TAKEN = ["admin", "lumiose"];
163
+
164
+ /** A server check, here a delay and a list: a `fetch` in a real form. */
165
+ async function available(value: string | boolean) {
166
+ await new Promise((resolve) => setTimeout(resolve, 300));
167
+ return TAKEN.includes(String(value)) ? "That handle is taken." : null;
168
+ }
169
+
170
+ export function HandleField() {
171
+ return (
172
+ <Field.Root
173
+ name="handle"
174
+ validationMode="onChange"
175
+ validationDebounceTime={300}
176
+ validate={available}
177
+ class="lu-field"
178
+ >
179
+ <Field.Label>Handle</Field.Label>
180
+ <Field.Control required class="lu-input" />
181
+ <Field.Error />
182
+ <Field.Validity>
183
+ {(state) => (
184
+ <p>
185
+ {state.validating ? "Checking…" : state.validity.valid ? `${state.value} is free.` : ""}
186
+ </p>
187
+ )}
188
+ </Field.Validity>
189
+ </Field.Root>
190
+ );
191
+ }
192
+ ```
193
+
194
+ ## Styling
195
+
196
+ **With the default theme**, put `lu-field` on `Root` and `lu-input` on the control: the parts
197
+ stack with a small gap, the label is medium weight, the description muted, the errors red (a
198
+ lighter red in dark, to keep 4.5:1), and an invalid control gets a red border.
199
+
200
+ **With your own CSS**, style the parts through `class` and state:
201
+
202
+ - the control: `[aria-invalid="true"]` while an error shows, `:user-invalid` (invalid after the
203
+ user interacted), `:invalid`, `:focus-visible`, `:disabled`;
204
+ - the field: `lu-field:has([aria-invalid="true"])`, `lu-field:focus-within`, and
205
+ `lu-field[data-validating]` while an async `validate` runs;
206
+ - the errors: `output` in the field (`[hidden]` while they do not apply).
207
+
208
+ ```css
209
+ .field {
210
+ display: grid;
211
+ gap: 0.25rem;
212
+ }
213
+ .field [aria-invalid="true"] {
214
+ border-color: #dc2626;
215
+ }
216
+ .field output {
217
+ color: #b91c1c;
218
+ font-size: 0.875rem;
219
+ }
220
+ ```
221
+
222
+ `@lumiose/ui/field.css` holds what Field needs to work, in the `lu.core` layer: `lu-field` is a
223
+ block, and the errors the script hides stay hidden whatever `display` your CSS gives them.
224
+
225
+ ## Keyboard and focus
226
+
227
+ The control's own keyboard. Field adds focus on a submit attempt: the first invalid control of the
228
+ form, in DOM order, takes focus, and its error is part of its accessible description, so a screen
229
+ reader reads it there.
230
+
231
+ - **On a submit attempt**, every invalid field shows its errors; the browser's bubble does not
232
+ appear for a field with an Error part that shows the problem. A problem none of its Errors
233
+ match (a missing value where the only Error has `match="tooShort"`) still gets the bubble.
234
+ - **`validationMode="onBlur"`** shows a field's error when focus leaves it, except a missing value
235
+ in a field the user has not edited yet (Base UI's rule). **`"onChange"`** shows it as the user
236
+ types.
237
+ - **Once shown**, an error follows the value: it updates as the problem changes and goes away when
238
+ the value is valid. A server error goes away as soon as the value changes, and shows again
239
+ each time an action fails with it, the same message included.
240
+ - **A form reset** hides every error.
241
+
242
+ ## Without JavaScript, and the field's script
243
+
244
+ The control, its `for` label and its constraints are native: the browser validates on submit and
245
+ reports the first problem in its own bubble, and the form submits when everything is valid. Server
246
+ errors from a Form render in the HTML, visible and linked with `aria-describedby` and
247
+ `aria-invalid`.
248
+
249
+ Two limits of the server HTML, which the script corrects once it runs (decision-62): the server
250
+ writes the control's attributes before it renders the parts after it, so a Description below the
251
+ control is linked only by the script, and an invalid field's control points `aria-describedby` at
252
+ the message Error's id (`Field.Error` without children or `match`) even when the field only has
253
+ Errors with children or a `match`. Give a field that must read well without JavaScript a message
254
+ Error, and its Descriptions before the control.
255
+
256
+ A page that renders a `Field` loads a 1.7 kB (brotli) script once, without the Lumiose runtime;
257
+ islands bundle it. On a submit attempt (a submit button, Enter in a field, `requestSubmit()` or
258
+ the form's `reportValidity()`; a silent `checkValidity()` changes nothing) it cancels the browser's
259
+ `invalid` bubble for fields that have an Error part for the problem and shows it there instead (the
260
+ browser's message, or one your `validate` set), with the timing above, and focuses the first
261
+ invalid control, with no bubble on a later control outside a field. A form action's errors move
262
+ focus to the first field that shows one, when the page renders back (unless an `autofocus` element
263
+ has it) and when an island's errors arrive. It also keeps `aria-invalid` and
264
+ `aria-describedby` current (a description rendered after the control is linked once the script
265
+ runs), and hides server errors once the value changes. When an async `validate` settles, Field.Root
266
+ fires `validitychange` (not bubbling) on the control, and the script shows the new verdict. When a
267
+ form action's errors arrive in an island, Field.Root fires it on `<lu-field>` with the messages
268
+ (`detail.errors`), and the script shows them as the server's, even when they repeat.
269
+
270
+ **`requestSubmit()` and `reportValidity()`.** The browser fires the same `invalid` events for a
271
+ silent `checkValidity()` as for a submit attempt, and only a submit attempt should show errors and
272
+ move focus. A submit button's click and Enter are seen as they happen; the two methods are not, so
273
+ the script wraps `HTMLFormElement.prototype.requestSubmit` and `reportValidity` once, when it
274
+ loads. The wrappers only mark the form as attempting a submit for the duration of the call, and
275
+ then call the browser's own method with the same `this`, arguments and return value, throwing what
276
+ it throws (decision-62).
277
+
278
+ ## Browser support
279
+
280
+ - **Constraint validation**, `:user-invalid` (Chrome 119, Firefox 88, Safari 16.5) and the
281
+ `invalid` event: in every supported browser.
282
+ - **`<output for>`**: in every browser; Chromium and screen readers include an output's text in
283
+ the control's description. Playwright's own `toHaveAccessibleDescription` leaves it out; the
284
+ @lumiose/ui tests read Chromium's computation instead.
285
+
286
+ ## Related
287
+
288
+ - [Form](form.md): server errors for the fields, by name.
289
+ - [Input](input.md): `Field.Control`.
290
+ - [Fieldset](fieldset.md): a group of fields with a legend.
291
+ - [Checkbox](checkbox.md), [Switch](switch.md), [Radio Group](radio-group.md),
292
+ [Select](select.md), [Combobox](combobox.md), [Autocomplete](autocomplete.md),
293
+ [Number Field](number-field.md), [Slider](slider.md): controls that take a field's wiring.
294
+ - [Data and actions](https://itsjavi.com/lumiose/docs/concepts/data-and-actions/): form actions,
295
+ `fail()` and `actionData()`.
@@ -0,0 +1,92 @@
1
+ # Fieldset
2
+
3
+ > A group of related fields with a caption: a native `<fieldset>` and `<legend>`, after Base UI's Fieldset. The browser names the group by its legend and disables every control inside a disabled fieldset, with no JavaScript and no CSS of its own. The legend also names a Checkbox Group or a Radio Group.
4
+
5
+ ```ts
6
+ import { Fieldset } from "@lumiose/ui"; // or "@lumiose/ui/fieldset"
7
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
8
+ ```
9
+
10
+ ## Anatomy
11
+
12
+ ```tsx
13
+ <Fieldset.Root>
14
+ <Fieldset.Legend />
15
+ </Fieldset.Root>
16
+ ```
17
+
18
+ | Part | Renders |
19
+ | -------- | --------------------------------------------------- |
20
+ | `Root` | `<fieldset>`, a group (`role="group"`). |
21
+ | `Legend` | `<legend>`, which names the group; the first child. |
22
+
23
+ ## API
24
+
25
+ ### Fieldset.Root
26
+
27
+ | Prop | Type | Default | |
28
+ | ---------- | --------- | ------- | ----------------------------------------------------------------------------- |
29
+ | `disabled` | `boolean` | `false` | Disables every control inside, except those in its legend; none is submitted. |
30
+
31
+ Every other `<fieldset>` prop (`form`, `name`, `class`, `aria-*`) passes through.
32
+
33
+ ### Fieldset.Legend
34
+
35
+ The `<legend>`'s props pass through. Put it first in the fieldset, or first in a
36
+ [CheckboxGroup](checkbox-group.md) or [RadioGroup](radio-group.md), which render a fieldset too.
37
+
38
+ **Differences from Base UI's Fieldset.** `Legend` is a native `<legend>` (Base UI renders a
39
+ `<div>` labelling the fieldset), so the group is named without ARIA.
40
+
41
+ ## Example
42
+
43
+ ```tsx check
44
+ import { Field, Fieldset } from "@lumiose/ui";
45
+
46
+ export function AddressFields() {
47
+ return (
48
+ <Fieldset.Root class="lu-fieldset">
49
+ <Fieldset.Legend>Shipping address</Fieldset.Legend>
50
+ <Field.Root name="street" class="lu-field">
51
+ <Field.Label>Street</Field.Label>
52
+ <Field.Control autocomplete="street-address" class="lu-input" />
53
+ </Field.Root>
54
+ <Field.Root name="city" class="lu-field">
55
+ <Field.Label>City</Field.Label>
56
+ <Field.Control autocomplete="address-level2" class="lu-input" />
57
+ </Field.Root>
58
+ </Fieldset.Root>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## Styling
64
+
65
+ **With the default theme**, put `lu-fieldset` on `Root`: no border or padding, the legend
66
+ semi-bold, the fields in a column.
67
+
68
+ **With your own CSS**, style the `fieldset` and `legend`, and `:disabled` (`fieldset:disabled`
69
+ matches a disabled fieldset; its controls match `:disabled`). A fieldset's minimum width defaults
70
+ to its content's: set `min-inline-size: 0` to let it shrink.
71
+
72
+ Fieldset ships no stylesheet: it needs none to work.
73
+
74
+ ## Keyboard and focus
75
+
76
+ The controls' own. A fieldset is not focusable; a disabled fieldset takes its controls out of the
77
+ tab order. Screen readers announce the legend when focus enters the group.
78
+
79
+ ## Without JavaScript
80
+
81
+ Everything is native: the group, its name, and `disabled`, in server HTML. No script.
82
+
83
+ ## Browser support
84
+
85
+ `<fieldset>`, `<legend>` and `fieldset[disabled]`: every browser.
86
+
87
+ ## Related
88
+
89
+ - [Field](field.md): the fields inside.
90
+ - [Checkbox Group](checkbox-group.md) and [Radio Group](radio-group.md): groups of choices, named
91
+ by a `Fieldset.Legend`.
92
+ - [Form](form.md).
package/docs/form.md ADDED
@@ -0,0 +1,131 @@
1
+ # Form
2
+
3
+ > A native `<form>` that hands its fields the server's errors, after Base UI's Form. Pass the data a form action gives `fail()` as `errors`, and each [Field](field.md) shows the message for its `name`, in the HTML the server renders back (no JavaScript needed) or, with `submit()`, in an island. Submission, validation and the first invalid field's focus are the platform's and Field's.
4
+
5
+ ```ts
6
+ import { Form } from "@lumiose/ui"; // or "@lumiose/ui/form"
7
+ ```
8
+
9
+ ## Anatomy
10
+
11
+ ```tsx
12
+ <Form>
13
+ <Field.Root />
14
+ </Form>
15
+ ```
16
+
17
+ | Part | Renders |
18
+ | ------ | ------------------------------------- |
19
+ | `Form` | `<form>`, with the props it is given. |
20
+
21
+ ## API
22
+
23
+ ### Form
24
+
25
+ | Prop | Type | Default | |
26
+ | ---------------- | ----------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
27
+ | `errors` | `object` | | The server's errors by field name: a message or several for each `Field.Root` `name`, such as `actionData()` after a `fail()`. |
28
+ | `validationMode` | `"onSubmit" \| "onBlur" \| "onChange"` | `"onSubmit"` | When the fields show their errors, unless a field sets its own. |
29
+ | `values` | `FormData`, `URLSearchParams` or `{ [name]: string \| string[] }` | | The values to fill the controls with after a failed submission, by name, such as the record of fields an action passes back with `fail()`. |
30
+
31
+ Every other `<form>` prop passes through: `method`, `action`, `onSubmit` (Lumiose's `submit`),
32
+ `novalidate`, `class`.
33
+
34
+ **`values`** replace the controls' defaults, by `name` (the control's, or its Field's): the text of
35
+ an Input (Field.Control), an Autocomplete, a Select, a Number Field or a Slider, the checked state of
36
+ a Checkbox or Switch (a name the submission left out means unchecked), and a Checkbox Group's,
37
+ Radio Group's or Combobox's selection. Controls with a `value` (or `bind:`) keep theirs, and
38
+ passwords, files, hidden inputs (whatever the case of their `type`), OTP Fields and disabled
39
+ controls (also in a disabled `Fieldset.Root`) are never filled. So after a failed action the page
40
+ renders the form as the user left it, with JavaScript or without (WCAG 3.3.7, redundant entry).
41
+ Values match by name only: give a password and a text field different names, or the text field gets
42
+ the password. A native `<fieldset disabled>` is not seen while rendering, so its controls are
43
+ filled; use `Fieldset.Root disabled` instead.
44
+
45
+ **Differences from Base UI's Form.** No `onFormSubmit` or `actionsRef`: the form is native and
46
+ Lumiose's form actions post it (`action="?/name"`, `onSubmit={submit}`). Base UI sets
47
+ `noValidate`; here the browser's validation stays on, so it still works without JavaScript, and
48
+ Field's script shows the problems in the fields instead of the browser's bubble.
49
+
50
+ ## Example
51
+
52
+ A sign-up page whose action checks the username on the server:
53
+
54
+ ```tsx check
55
+ import { Field, Form } from "@lumiose/ui";
56
+ import { actionData, fail, type ActionContext } from "lumiose";
57
+
58
+ export const actions = {
59
+ async signup(ctx: ActionContext) {
60
+ const form = await ctx.formData();
61
+ const username = String(form.get("username") ?? "");
62
+ if (username === "admin") {
63
+ // The errors, and the fields to fill in again (never a password): the form renders back
64
+ // filled in.
65
+ return fail(400, { errors: { username: "That username is taken." }, values: { username } });
66
+ }
67
+ return { welcome: username };
68
+ },
69
+ };
70
+
71
+ export function SignupForm() {
72
+ const result = actionData<typeof actions>();
73
+ const failed = result && "errors" in result ? result : undefined;
74
+ return (
75
+ <Form method="post" action="?/signup" errors={failed?.errors} values={failed?.values}>
76
+ <Field.Root name="username" class="lu-field">
77
+ <Field.Label>Username</Field.Label>
78
+ <Field.Control required minlength={3} autocomplete="username" class="lu-input" />
79
+ <Field.Error />
80
+ </Field.Root>
81
+ <button class="lu-button" data-variant="primary">
82
+ Sign up
83
+ </button>
84
+ </Form>
85
+ );
86
+ }
87
+ ```
88
+
89
+ Only the keys of `errors` that name a field and hold a string or an array of strings become
90
+ errors, so `errors={result}` also works for an action that fails with the errors alone. The
91
+ `values` reach the browser with the page, so send back the fields to fill in by name, as above. The
92
+ submitted `FormData` works too (its text entries; files are left out), but it carries every field,
93
+ a password included: Form never fills a password field, yet the password would sit in the page's
94
+ HTML. In development, Lumiose warns when a `FormData` it serializes carries a name that looks
95
+ secret. In an island, add `onSubmit={submit}` and the errors arrive without a page load; the
96
+ controls keep what the user typed anyway.
97
+
98
+ ## Styling
99
+
100
+ Form adds nothing to style: style the `form` and its state, `form:invalid` (a field is invalid)
101
+ and `form:has(:user-invalid)` (one the user touched is), and its fields
102
+ ([Field](field.md#styling)). Form ships no stylesheet.
103
+
104
+ **A form reset** (a `type="reset"` button, `form.reset()`) puts every control back to its default
105
+ and reports it: `onValueChange` / `onCheckedChange` get the reset value a moment later, when it
106
+ differs, so `bind:` signals in an island follow the form (the platform fires no `input` or
107
+ `change` for a reset).
108
+
109
+ ## Keyboard and focus
110
+
111
+ The platform's: Enter in a text field submits the form. On a submit attempt with invalid fields,
112
+ nothing is sent, the fields show their errors, and the first invalid control takes focus (Field's
113
+ script; without it, the browser focuses it and shows its bubble).
114
+
115
+ ## Without JavaScript
116
+
117
+ A native form posts to its action and the page renders back with `actionData()`, so the server's
118
+ errors show in their fields with no script, and with `values` the controls show what was
119
+ submitted. The browser validates the fields before it posts.
120
+
121
+ ## Browser support
122
+
123
+ Every browser. Field's script needs the `invalid` event and constraint validation, which every
124
+ supported browser has.
125
+
126
+ ## Related
127
+
128
+ - [Field](field.md): the fields and their errors.
129
+ - [Fieldset](fieldset.md): groups of fields.
130
+ - [Data and actions](https://itsjavi.com/lumiose/docs/concepts/data-and-actions/): form actions,
131
+ `fail()`, `actionData()` and `submit()`.
package/docs/input.md ADDED
@@ -0,0 +1,87 @@
1
+ # Input
2
+
3
+ > A native `<input>`, after Base UI's Input: every type, constraint and autofill the browser has, plus `onValueChange` and `bind:value`. Inside a [Field](field.md) (as `Field.Control`) it takes the field's id, name and ARIA references. No script, no CSS of its own.
4
+
5
+ ```ts
6
+ import { Input } from "@lumiose/ui"; // or "@lumiose/ui/input"
7
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
8
+ ```
9
+
10
+ ## Anatomy
11
+
12
+ ```tsx
13
+ <Input />
14
+ ```
15
+
16
+ | Part | Renders |
17
+ | ------- | -------------------------------------- |
18
+ | `Input` | `<input>`, with the props it is given. |
19
+
20
+ ## API
21
+
22
+ ### Input
23
+
24
+ | Prop | Type | Default | |
25
+ | --------------- | ------------------------- | ------- | --------------------------------------------------------------------------- |
26
+ | `value` | `string` | | Sets the value when it changes. Pair it with `onValueChange`. |
27
+ | `defaultValue` | `string` | | The value at first, when `value` is left out; a form reset goes back to it. |
28
+ | `onValueChange` | `(value: string) => void` | | Called with the new value on every edit. |
29
+ | `bind:value` | `Signal<string>` | | Stands in for `value` and `onValueChange`. |
30
+
31
+ Every native `<input>` prop passes through: `type`, `name`, `required`, `pattern`, `minlength`,
32
+ `placeholder`, `autocomplete`, `readonly`, `disabled`, `class`, `aria-*`, events. Inside a Field,
33
+ its own `id`, `name`, `aria-describedby` and `aria-invalid` win over the field's.
34
+
35
+ **Differences from Base UI's Input.** The same element and props; `bind:value` stands in for the
36
+ pair.
37
+
38
+ ## Example
39
+
40
+ A search box in an island, bound to a signal:
41
+
42
+ ```tsx check
43
+ import { Input } from "@lumiose/ui";
44
+ import { signal } from "lumiose";
45
+
46
+ const BOOKS = ["Dune", "Emma", "Ulysses", "Middlemarch"];
47
+
48
+ export function BookSearch() {
49
+ const query = signal("");
50
+ const found = () => BOOKS.filter((book) => book.toLowerCase().includes(query().toLowerCase()));
51
+ return (
52
+ <search>
53
+ <Input type="search" aria-label="Search books" bind:value={query} class="lu-input" />
54
+ <p>{found().join(", ") || "No books."}</p>
55
+ </search>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ## Styling
61
+
62
+ **With the default theme**, put `lu-input` on it: a bordered field, a muted placeholder, a focus
63
+ ring, a red border with `aria-invalid="true"`.
64
+
65
+ **With your own CSS**, style the `input` and its state: `:focus-visible`, `:disabled`,
66
+ `:read-only`, `:placeholder-shown`, `:user-invalid`, `[aria-invalid="true"]`.
67
+
68
+ Input ships no stylesheet: it needs none to work.
69
+
70
+ ## Keyboard and focus
71
+
72
+ The browser's text-field keyboard. One tab stop.
73
+
74
+ ## Without JavaScript
75
+
76
+ A native input: typing, autofill, validation and submission work with no script. `value`,
77
+ `onValueChange` and `bind:value` need the component to run in an island.
78
+
79
+ ## Browser support
80
+
81
+ Every browser; input types and constraints as the browser supports them.
82
+
83
+ ## Related
84
+
85
+ - [Field](field.md): label, description and errors for an Input (`Field.Control`).
86
+ - [Combobox](combobox.md): an input that filters options.
87
+ - [JSX](https://itsjavi.com/lumiose/docs/concepts/jsx/): `bind:value`.
@@ -0,0 +1,65 @@
1
+ # Installation
2
+
3
+ > Add `@lumiose/ui` to a Lumiose app and import its stylesheets, and the default theme if you want its look. The components then render in server HTML, without an island, and a page loads only the small scripts of the components it renders.
4
+
5
+ **Status:** `@lumiose/ui` is not on npm yet. In this repository's workspace, an app adds it as
6
+ `"@lumiose/ui": "workspace:*"` in its `dependencies`.
7
+
8
+ ## Add the package
9
+
10
+ `@lumiose/ui` needs a Lumiose app: it ships its components as TSX, which the app's build compiles
11
+ with the app's own code, and `lumiose` is its peer dependency. Once it is on npm:
12
+
13
+ ```sh
14
+ pnpm add @lumiose/ui
15
+ ```
16
+
17
+ A new app can start with it: `create-lumiose --ui` adds the dependency, imports the stylesheets in
18
+ the root layout and adds the UI agent files (the `lumiose-ui` skill and a UI part in `AGENTS.md`).
19
+ In an app that already depends on it, `lumiose agents init` adds those agent files.
20
+
21
+ ## Import the stylesheets
22
+
23
+ Import them once, in the root layout (`src/routes/layout.stsx`), so every page has them:
24
+
25
+ ```ts
26
+ import "@lumiose/ui/styles.css"; // what every component needs (or "@lumiose/ui/dialog.css", …)
27
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
28
+ ```
29
+
30
+ They sit in cascade layers, so your own CSS overrides them without specificity games
31
+ ([Theming](theming.md#cascade-layers)).
32
+
33
+ ## Use a component
34
+
35
+ Import a component from `@lumiose/ui` (or its own entry, `@lumiose/ui/popover`) and compose its
36
+ parts. In a page or a server component it renders as server HTML that works before any script
37
+ loads; with the default theme, the `lu-` classes give it its look:
38
+
39
+ ```tsx check
40
+ import { Popover } from "@lumiose/ui";
41
+
42
+ export function Help() {
43
+ return (
44
+ <Popover.Root>
45
+ <Popover.Trigger class="lu-button">Help</Popover.Trigger>
46
+ <Popover.Popup class="lu-popover">
47
+ <Popover.Title>Help</Popover.Title>
48
+ <Popover.Description>
49
+ Server HTML: this popover opens without JavaScript.
50
+ </Popover.Description>
51
+ </Popover.Popup>
52
+ </Popover.Root>
53
+ );
54
+ }
55
+ ```
56
+
57
+ To keep a component's state in a signal (`bind:open`, `bind:value`), render it inside an
58
+ [island](https://itsjavi.com/lumiose/docs/concepts/islands/). Each component's page lists its parts,
59
+ props, keyboard contract and what works without JavaScript.
60
+
61
+ ## Related
62
+
63
+ - [UI components](README.md): the approach and the components.
64
+ - [Theming](theming.md): the default theme's classes and `--lu-` properties, and your own CSS.
65
+ - [Getting started](https://itsjavi.com/lumiose/docs/getting-started/): a new Lumiose app.