@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.
- 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/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()`.
|
package/docs/fieldset.md
ADDED
|
@@ -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.
|