@lumiose/ui 0.1.0 → 0.1.2

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 (62) hide show
  1. package/README.md +2 -2
  2. package/dist/accordion/accordion.tsx +3 -2
  3. package/dist/autocomplete/autocomplete.tsx +6 -5
  4. package/dist/avatar/avatar.tsx +4 -3
  5. package/dist/batched.ts +11 -0
  6. package/dist/checkbox/checkbox.tsx +4 -3
  7. package/dist/checkbox-group/checkbox-group.tsx +3 -2
  8. package/dist/collapsible/collapsible.tsx +3 -2
  9. package/dist/combobox/combobox.tsx +6 -5
  10. package/dist/context-menu/context-menu.tsx +4 -3
  11. package/dist/dialog/dialog.tsx +2 -1
  12. package/dist/field/field.tsx +11 -10
  13. package/dist/form/reset.ts +4 -2
  14. package/dist/input/input.tsx +4 -1
  15. package/dist/menu/menu.tsx +9 -8
  16. package/dist/navigation-menu/navigation-menu.tsx +4 -3
  17. package/dist/number-field/number-field.tsx +4 -3
  18. package/dist/otp-field/otp-field.tsx +9 -8
  19. package/dist/popover/popover-hover.script.ts +27 -0
  20. package/dist/popover/popover.script.ts +19 -25
  21. package/dist/popover/popover.tsx +8 -3
  22. package/dist/preview-card/preview-card.tsx +2 -1
  23. package/dist/radio-group/radio-group.tsx +3 -2
  24. package/dist/select/select.script.ts +26 -0
  25. package/dist/select/select.tsx +36 -10
  26. package/dist/slider/slider.tsx +4 -3
  27. package/dist/switch/switch.tsx +4 -3
  28. package/dist/tabs/tabs.tsx +3 -2
  29. package/dist/toast/toast.tsx +3 -2
  30. package/dist/toggle/toggle.tsx +6 -5
  31. package/dist/toggle-group/toggle-group.script.ts +12 -4
  32. package/dist/toggle-group/toggle-group.tsx +16 -4
  33. package/dist/tooltip/tooltip.tsx +4 -1
  34. package/docs/README.md +9 -6
  35. package/docs/accordion.md +3 -2
  36. package/docs/autocomplete.md +2 -2
  37. package/docs/avatar.md +2 -2
  38. package/docs/checkbox-group.md +1 -1
  39. package/docs/checkbox.md +1 -1
  40. package/docs/collapsible.md +4 -3
  41. package/docs/combobox.md +4 -3
  42. package/docs/context-menu.md +3 -3
  43. package/docs/dialog.md +1 -1
  44. package/docs/drawer.md +3 -2
  45. package/docs/field.md +2 -2
  46. package/docs/installation.md +3 -3
  47. package/docs/llms-full.txt +107 -72
  48. package/docs/llms.txt +14 -14
  49. package/docs/menu.md +4 -3
  50. package/docs/menubar.md +3 -3
  51. package/docs/navigation-menu.md +4 -3
  52. package/docs/otp-field.md +2 -2
  53. package/docs/popover.md +7 -4
  54. package/docs/preview-card.md +4 -3
  55. package/docs/radio-group.md +1 -1
  56. package/docs/select.md +25 -10
  57. package/docs/tabs.md +1 -1
  58. package/docs/toast.md +2 -2
  59. package/docs/toggle-group.md +10 -4
  60. package/docs/toolbar.md +2 -2
  61. package/docs/tooltip.md +4 -3
  62. package/package.json +1 -1
@@ -7,6 +7,7 @@
7
7
  import { createContext, effect, ref, useContext, useId } from "lumiose";
8
8
  import { anchorName, placement, withStyle, type PlacementProps } from "../placement/placement.ts";
9
9
  import previewCardScript from "./preview-card.script.ts?client";
10
+ import { batched } from "../batched.ts";
10
11
 
11
12
  /**
12
13
  * `PreviewCard.Root` renders no element: it holds the ids its parts share. `bind:open={signal}`
@@ -106,7 +107,7 @@ function Popup({
106
107
  id={card.popupId}
107
108
  popover="manual"
108
109
  data-preview-card-popup=""
109
- on:toggle={(event: ToggleEvent) => card.onToggle(event.newState === "open")}
110
+ on:toggle={batched((event: ToggleEvent) => card.onToggle(event.newState === "open"))}
110
111
  >
111
112
  {children}
112
113
  </div>
@@ -7,6 +7,7 @@ import { createContext, effect, ref, signal, untrack, useContext, useId } from "
7
7
  import checkboxScript from "../checkbox/checkbox.script.ts?client";
8
8
  import { describedBy, labelledBy, useField, useFormValues } from "../field/context.ts";
9
9
  import { onFormReset } from "../form/reset.ts";
10
+ import { batched } from "../batched.ts";
10
11
 
11
12
  interface RadioGroupContextValue {
12
13
  name: () => string;
@@ -121,12 +122,12 @@ export function RadioGroup({
121
122
  role="radiogroup"
122
123
  aria-required={required ? "true" : undefined}
123
124
  aria-readonly={readOnly ? "true" : undefined}
124
- on:change={(event: Event) => {
125
+ on:change={batched((event: Event) => {
125
126
  const radio = event.target as HTMLInputElement;
126
127
  if (radio.type !== "radio" || !radio.checked) return;
127
128
  selected.set(radio.value);
128
129
  onValueChange?.(radio.value);
129
- }}
130
+ })}
130
131
  >
131
132
  {children}
132
133
  </fieldset>
@@ -0,0 +1,26 @@
1
+ // Select's client script (decision-63, TASK-537), loaded by a Select whose items have a `label`.
2
+ // A customizable select's picker (`appearance: base-select`) shows an option's `label` attribute
3
+ // instead of its content, through the browser's own style, which no CSS overrides. When such a
4
+ // select is pressed or takes focus, before its picker opens, this script removes the attribute from
5
+ // the options that Select.Item rendered with a label: the picker then shows their content,
6
+ // typeahead matches the label's hidden copy first in it, and their `aria-label` keeps their name.
7
+ // The classic select keeps the attribute, which it shows and typeahead matches; so does server HTML
8
+ // before the script runs, where the customizable picker shows the label.
9
+ // It must not import `lumiose`: server-rendered pages load it without the runtime.
10
+
11
+ /** The hidden copy of a label that Select.Item puts first in its option. */
12
+ const LABELLED = "option[label] > [data-label]:first-child";
13
+
14
+ /** Shows the content of the labelled items of the customizable select at or around `target`. */
15
+ export function showContent(target: EventTarget | null): void {
16
+ const select = (target as Element | null)?.closest?.("select");
17
+ if (!select || getComputedStyle(select).appearance !== "base-select") return;
18
+ for (const copy of select.querySelectorAll(LABELLED))
19
+ copy.parentElement!.removeAttribute("label");
20
+ }
21
+
22
+ for (const type of ["pointerdown", "focusin"]) {
23
+ document.addEventListener(type, (event) => showContent(event.target), true);
24
+ }
25
+ // A select focused before the script ran (`autofocus`).
26
+ showContent(document.activeElement);
@@ -3,14 +3,22 @@
3
3
  // already does everything Base UI's Select does around the value: the combobox role, the keyboard
4
4
  // and typeahead (on the closed control too), the top-layer picker anchored to the control, forms
5
5
  // (`name`, `required`, autofill, reset, a disabled fieldset) and the mobile pickers. So Select
6
- // ships no script: it works in server HTML, with no JavaScript, and in islands, where `value` drives
7
- // the control when it changes and its `change` event reports back through `onValueChange`. Where
8
- // customizable select is supported, `Select.Trigger` and `Select.Value` style the button and show
9
- // the selected option's content, and options may hold more than text. TASK-358 records why it is
10
- // native rather than the listbox pattern.
6
+ // works in server HTML, with no JavaScript, and in islands, where `value` drives the control when
7
+ // it changes and its `change` event reports back through `onValueChange`. Where customizable select
8
+ // is supported, `Select.Trigger` and `Select.Value` style the button and show the selected option's
9
+ // content, and options may hold more than text. TASK-358 records why it is native rather than the
10
+ // listbox pattern.
11
+ //
12
+ // An item's `label` (TASK-537) names it: the option's `label` attribute, which the classic select
13
+ // shows and typeahead matches, a hidden copy first in its content, which typeahead matches once
14
+ // the attribute is gone, and its `aria-label`. A customizable select's picker shows an option's
15
+ // `label` attribute instead of its content (the browser's style, which no CSS overrides), so
16
+ // select.script.ts removes the attribute from a customizable select's items as it takes focus.
11
17
  import { createContext, effect, ref, untrack, useContext } from "lumiose";
18
+ import selectScript from "./select.script.ts?client";
12
19
  import { fieldProps, useField, useFormValues } from "../field/context.ts";
13
20
  import { onFormReset } from "../form/reset.ts";
21
+ import { batched } from "../batched.ts";
14
22
 
15
23
  declare module "lumiose/jsx-runtime" {
16
24
  namespace JSX {
@@ -67,6 +75,11 @@ export type SelectValueProps = Omit<JSX.IntrinsicElements["selectedcontent"], "c
67
75
  export type SelectItemProps = Omit<JSX.IntrinsicElements["option"], "value" | "selected"> & {
68
76
  /** The value the select takes, and submits, when the option is selected. */
69
77
  value: string;
78
+ /**
79
+ * The option's name, for typeahead, assistive technology and the classic select, when its
80
+ * content starts with an icon or holds more than text. The picker shows the content.
81
+ */
82
+ label?: string;
70
83
  /** The option cannot be selected. */
71
84
  disabled?: boolean;
72
85
  };
@@ -112,9 +125,9 @@ function Root({ value, defaultValue, onValueChange, children, ...rest }: SelectR
112
125
  select(element);
113
126
  rest.ref?.(element);
114
127
  }}
115
- on:change={(event: Event) =>
116
- onValueChange?.((event.currentTarget as HTMLSelectElement).value)
117
- }
128
+ on:change={batched((event: Event) =>
129
+ onValueChange?.((event.currentTarget as HTMLSelectElement).value),
130
+ )}
118
131
  >
119
132
  {children}
120
133
  </select>
@@ -134,11 +147,24 @@ function Value(props: SelectValueProps) {
134
147
  return <selectedcontent {...props} />;
135
148
  }
136
149
 
137
- function Item({ children, value, ...rest }: SelectItemProps) {
150
+ function Item({ children, value, label, ...rest }: SelectItemProps) {
138
151
  const select = useContext(SelectContext);
139
152
  if (!select) throw new Error("Select parts must be inside <Select.Root>.");
153
+ // Only items with a label need the script.
154
+ if (untrack(() => label) !== undefined) selectScript();
140
155
  return (
141
- <option {...rest} value={value} selected={select.initial === value || undefined}>
156
+ <option
157
+ aria-label={label}
158
+ {...rest}
159
+ label={label}
160
+ value={value}
161
+ selected={select.initial === value || undefined}
162
+ >
163
+ {label === undefined ? null : (
164
+ <span hidden data-label="">
165
+ {label}
166
+ </span>
167
+ )}
142
168
  {children}
143
169
  </option>
144
170
  );
@@ -13,6 +13,7 @@ import { percent } from "../progress/value.ts";
13
13
  import { formatValues } from "./format.ts";
14
14
  import type { SliderElement } from "./slider.script.ts";
15
15
  import sliderScript from "./slider.script.ts?client";
16
+ import { batched } from "../batched.ts";
16
17
 
17
18
  declare module "lumiose/jsx-runtime" {
18
19
  namespace JSX {
@@ -259,12 +260,12 @@ function Root<V extends SliderValue = number>({
259
260
  format={format ? JSON.stringify(format) : undefined}
260
261
  locale={locale}
261
262
  style={withStyle(style, untrack(range))}
262
- on:input={() => {
263
+ on:input={batched(() => {
263
264
  const next = current();
264
265
  values.set(asList(next, low()));
265
266
  onValueChange?.(next);
266
- }}
267
- on:change={() => onValueCommitted?.(current())}
267
+ })}
268
+ on:change={batched(() => onValueCommitted?.(current()))}
268
269
  >
269
270
  {children}
270
271
  </lu-slider>
@@ -7,6 +7,7 @@ import { ref, untrack } from "lumiose";
7
7
  import checkboxScript from "../checkbox/checkbox.script.ts?client";
8
8
  import { fieldProps, useField, useFormValues } from "../field/context.ts";
9
9
  import { onFormReset } from "../form/reset.ts";
10
+ import { batched } from "../batched.ts";
10
11
 
11
12
  /**
12
13
  * The native checkbox props Root passes through, without the brand of intrinsic elements' props,
@@ -73,9 +74,9 @@ function Root({
73
74
  checked={checked ?? initial}
74
75
  aria-readonly={readOnly ? "true" : undefined}
75
76
  data-unchecked-value={uncheckedValue}
76
- on:change={(event: Event) =>
77
- onCheckedChange?.((event.currentTarget as HTMLInputElement).checked)
78
- }
77
+ on:change={batched((event: Event) =>
78
+ onCheckedChange?.((event.currentTarget as HTMLInputElement).checked),
79
+ )}
79
80
  />
80
81
  );
81
82
  }
@@ -19,6 +19,7 @@ import {
19
19
  import { withStyle } from "../placement/placement.ts";
20
20
  import defineTabs from "./tabs.script.ts?client";
21
21
  import type { LuTabs } from "./tabs.script.ts";
22
+ import { batched } from "../batched.ts";
22
23
 
23
24
  declare module "lumiose/jsx-runtime" {
24
25
  namespace JSX {
@@ -166,12 +167,12 @@ function Root({
166
167
  ref={element}
167
168
  // The anchor the selected tab carries for a Tabs.Indicator (tabs.css).
168
169
  style={withStyle(rest.style, { "--lu-tabs-anchor": `--lu-tabs-${id}` })}
169
- on:valuechange={() => {
170
+ on:valuechange={batched(() => {
170
171
  const next = element()?.value;
171
172
  if (next == null) return;
172
173
  selected.set(next);
173
174
  onValueChange?.(next);
174
- }}
175
+ })}
175
176
  >
176
177
  {children}
177
178
  </lu-tabs>
@@ -16,6 +16,7 @@ import { afterAnimations } from "../animation/animation.ts";
16
16
  import toastScript from "./toast.script.ts?client";
17
17
  import { CLOSE_COMMAND } from "./command.ts";
18
18
  import type { LuToastViewport } from "./toast.script.ts";
19
+ import { batched } from "../batched.ts";
19
20
 
20
21
  declare module "lumiose/jsx-runtime" {
21
22
  namespace JSX {
@@ -326,12 +327,12 @@ function Root({ toast, swipeDirection, children, ref: forwarded, ...rest }: Toas
326
327
  data-timeout={String(toast.timeout ?? provider?.timeout() ?? DEFAULT_TIMEOUT)}
327
328
  data-swipe-directions={(swipeDirection ?? ["down", "right"]).join(" ") || undefined}
328
329
  data-ending-style={ending() ? "" : undefined}
329
- on:close={(event: Event) => {
330
+ on:close={batched((event: Event) => {
330
331
  // The script closes it: a managed toast leaves through its manager.
331
332
  if (!owner) return;
332
333
  event.preventDefault();
333
334
  owner.close(toast.id!);
334
- }}
335
+ })}
335
336
  >
336
337
  {children}
337
338
  </div>
@@ -6,6 +6,7 @@ import { effect, onCleanup, signal, untrack, useContext } from "lumiose";
6
6
  import type { ToggleChangeDetail } from "../toggle-group/toggle-group.script.ts";
7
7
  import { ToolbarContext } from "../toolbar/context.ts";
8
8
  import { ToggleGroupContext, type ToggleRecord } from "./context.ts";
9
+ import { batched } from "../batched.ts";
9
10
 
10
11
  /**
11
12
  * The button props Toggle passes through, without the brand of intrinsic elements' props, so that
@@ -82,16 +83,16 @@ export function Toggle({
82
83
  aria-disabled={toolbar?.disabled() ? "true" : rest["aria-disabled"]}
83
84
  value={value}
84
85
  aria-pressed={on() ? "true" : "false"}
85
- on:click={(event: MouseEvent) => {
86
+ on:click={batched((event: MouseEvent) => {
86
87
  // In a group, the group's element presses it and reports `pressedchange`.
87
88
  if (group || event.defaultPrevented) return;
88
89
  const next = !state();
89
90
  state.set(next);
90
91
  onPressedChange?.(next);
91
- }}
92
- on:pressedchange={(event: CustomEvent<ToggleChangeDetail>) =>
93
- onPressedChange?.(event.detail.pressed)
94
- }
92
+ })}
93
+ on:pressedchange={batched((event: CustomEvent<ToggleChangeDetail>) =>
94
+ onPressedChange?.(event.detail.pressed),
95
+ )}
95
96
  >
96
97
  {children}
97
98
  </button>
@@ -51,16 +51,24 @@ export class ToggleGroupElement extends HTMLElement {
51
51
  );
52
52
  }
53
53
 
54
- /** The pressed toggles' values. Setting it presses those toggles, without events. */
54
+ /**
55
+ * The pressed toggles' values. Setting it presses those toggles, without events; a value equal
56
+ * to the current one changes nothing and measures nothing (TASK-539).
57
+ */
55
58
  get value(): string[] {
56
59
  return this.items.filter(pressed).map((item) => item.value);
57
60
  }
58
61
 
59
62
  set value(next: readonly string[] | null | undefined) {
60
63
  const values = next ?? [];
61
- for (const item of this.items)
62
- item.setAttribute("aria-pressed", String(values.includes(item.value)));
63
- this.#rove();
64
+ let changed = false;
65
+ for (const item of this.items) {
66
+ const on = String(values.includes(item.value));
67
+ if (item.getAttribute("aria-pressed") === on) continue;
68
+ item.setAttribute("aria-pressed", on);
69
+ changed = true;
70
+ }
71
+ if (changed) this.#rove();
64
72
  }
65
73
 
66
74
  /** Whether several toggles can be pressed; reflects the `multiple` attribute. */
@@ -11,6 +11,7 @@ import {
11
11
  } from "../toggle/context.ts";
12
12
  import type { ToggleGroupElement } from "./toggle-group.script.ts";
13
13
  import toggleGroupScript from "./toggle-group.script.ts?client";
14
+ import { batched } from "../batched.ts";
14
15
 
15
16
  declare module "lumiose/jsx-runtime" {
16
17
  namespace JSX {
@@ -39,7 +40,10 @@ type ElementProps = {
39
40
  * `aria-labelledby`. `bind:value={signal}` stands in for `value` and `onValueChange`.
40
41
  */
41
42
  export interface ToggleGroupProps extends Omit<ElementProps, "children"> {
42
- /** Presses the toggles with these values when it changes. Pair it with `onValueChange`. */
43
+ /**
44
+ * The pressed toggles. Pair it with `onValueChange`, which updates it: the toggles show `value`
45
+ * again after each press, so one that `onValueChange` does not take is undone.
46
+ */
43
47
  value?: string[];
44
48
  /** The toggles pressed at first, when `value` is left out. */
45
49
  defaultValue?: string[];
@@ -104,11 +108,19 @@ export function ToggleGroup({
104
108
  multiple={multiple}
105
109
  data-orientation={orientation ?? "horizontal"}
106
110
  data-loop-focus={loopFocus === false ? "false" : undefined}
107
- on:valuechange={() => {
108
- const next = element()!.value;
111
+ on:valuechange={batched(() => {
112
+ const host = element()!;
113
+ const next = host.value;
109
114
  values.set(next);
110
115
  onValueChange?.(next);
111
- }}
116
+ // Controlled: when the callback leaves `value` as it was, the toggles show it again, as
117
+ // in Base UI (TASK-538). A changed `value` equals the element's, which then does nothing.
118
+ const held = value;
119
+ if (held !== undefined) {
120
+ values.set(held);
121
+ host.value = held;
122
+ }
123
+ })}
112
124
  >
113
125
  {children}
114
126
  </lu-toggle-group>
@@ -8,6 +8,7 @@
8
8
  import { createContext, effect, ref, useContext, useId } from "lumiose";
9
9
  import { anchorName, placement, withStyle, type PlacementProps } from "../placement/placement.ts";
10
10
  import tooltipScript from "./tooltip.script.ts?client";
11
+ import { batched } from "../batched.ts";
11
12
 
12
13
  /**
13
14
  * `Tooltip.Root` renders no element: it holds the ids its parts share. `bind:open={signal}`
@@ -128,7 +129,9 @@ function Popup({
128
129
  }}
129
130
  id={tooltip.popupId}
130
131
  popover="hint"
131
- on:toggle={(event: ToggleEvent) => tooltip.props.onOpenChange?.(event.newState === "open")}
132
+ on:toggle={batched((event: ToggleEvent) =>
133
+ tooltip.props.onOpenChange?.(event.newState === "open"),
134
+ )}
132
135
  >
133
136
  {children}
134
137
  </span>
package/docs/README.md CHANGED
@@ -2,9 +2,8 @@
2
2
 
3
3
  > `@lumiose/ui`: accessible, unstyled UI primitives built on native elements (`<dialog>`, popover, invoker commands, CSS anchor positioning), server-rendered without hydration. Style them with your own CSS or the optional default theme.
4
4
 
5
- **Status:** in development. `@lumiose/ui` keeps its own 0.x versions, apart from the version all
6
- other Lumiose packages share, and is not on npm yet: today it is used from this repository's
7
- workspace. The goal for its 1.0 is parity with [Base UI](https://base-ui.com)'s components; the
5
+ **Status:** in development. `@lumiose/ui` is on npm and keeps its own 0.x versions, apart from
6
+ the version all other Lumiose packages share. The goal for its 1.0 is parity with [Base UI](https://base-ui.com)'s components; the
8
7
  table below lists them as they land.
9
8
 
10
9
  These pages ship in the package, in `node_modules/@lumiose/ui/docs`, so they match the installed
@@ -37,7 +36,8 @@ Anatomy, API, Example, Styling, Keyboard and focus, Without JavaScript, Browser
37
36
  with `onCheckedChange`, `value` with `onValueChange`. Pass a signal's value and write it back in
38
37
  the callback to keep both in sync, or leave both out and the element keeps its own state. Custom
39
38
  elements report changes with lowercase DOM events, like native ones: `openchange`,
40
- `checkedchange` and `change`.
39
+ `checkedchange` and `change`. The callbacks run inside `batch()`, as event handlers do: what
40
+ they write renders together, and the DOM is current as soon as the event returns.
41
41
  - **Keyboard contracts.** Every component follows its WAI-ARIA Authoring Practices pattern, and
42
42
  its end-to-end tests check the keyboard paths.
43
43
  - **WCAG 2.2 AA.** Every component meets it: the components own semantics, keyboard and
@@ -70,7 +70,7 @@ custom properties, named as in shadcn/ui, are listed in [Theming](theming.md#the
70
70
  | [Tooltip](tooltip.md) | Available: native hint popover, opened by its script |
71
71
  | [Menu](menu.md) | Available: popover menu, opens without JavaScript |
72
72
  | [Combobox](combobox.md) | Available: `<lu-combobox>`, a form control that filters its options |
73
- | [Select](select.md) | Available: native `<select>`, customizable where supported, no script |
73
+ | [Select](select.md) | Available: native `<select>`, customizable where supported, no script but for `label`s |
74
74
  | [Tabs](tabs.md) | Available: server HTML with the full state, `<lu-tabs>` adds the keys |
75
75
  | [Field](field.md) | Available: label, description, errors; native validation in the field |
76
76
  | [Fieldset](fieldset.md) | Available: native `<fieldset>` and `<legend>`, no script |
@@ -165,7 +165,10 @@ errors are yours.
165
165
  `@lumiose/ui` is opt-in, so its size is reported apart from the runtime's caps:
166
166
  `pnpm --filter @lumiose/ui size` prints, per component, the JavaScript a server-rendered page loads
167
167
  for its custom elements, the JavaScript an island that uses it loads, and its CSS (minified, gzip
168
- and brotli).
168
+ and brotli). The scripts load as separate modules, in islands too: each component's script, and
169
+ the modules several scripts share (the dismissal model, the placement fallback, the hover timing),
170
+ each a file of its own, as an app's build splits them. The sizes on the component pages are those
171
+ files' brotli sizes added up.
169
172
 
170
173
  ## Related
171
174
 
package/docs/accordion.md CHANGED
@@ -187,8 +187,9 @@ closed item are HTML features: an accordion in a server-rendered page works befo
187
187
  loads, and with none. Disabled triggers ignore the pointer through CSS. `value`, `onValueChange`
188
188
  and `bind:value` need the component to run in an island.
189
189
 
190
- A page that renders an `Accordion` loads a 1 kB (brotli) module once, without the Lumiose runtime;
191
- islands bundle it. It defines `<lu-accordion>`, which adds the arrow keys, Home and End, and sets
190
+ A page that renders an `Accordion` loads its script once, with the panel and animation modules it
191
+ shares with Collapsible: 1.3 kB (brotli) without the Lumiose runtime; an island loads the same
192
+ modules. It defines `<lu-accordion>`, which adds the arrow keys, Home and End, and sets
192
193
  the panels' animation hooks (shared with [Collapsible](collapsible.md)); a disabled trigger does
193
194
  not toggle, also from the keyboard.
194
195
 
@@ -1,6 +1,6 @@
1
1
  # Autocomplete
2
2
 
3
- > A text input with suggestions, for text that may be anything (a search, a tag, an address): the WAI-ARIA combobox pattern with list autocomplete, after Base UI's Autocomplete. Unlike a [Combobox](combobox.md), the text is the value: the input has the name and submits what is typed, and choosing a suggestion only fills it in. It runs on the Combobox's element (`<lu-combobox free>`, 5.0 kB) and parts, on server-rendered pages and in islands; `bind:value` keeps a signal in sync. Filtering, inline completion, groups, an empty state and a status.
3
+ > A text input with suggestions, for text that may be anything (a search, a tag, an address): the WAI-ARIA combobox pattern with list autocomplete, after Base UI's Autocomplete. Unlike a [Combobox](combobox.md), the text is the value: the input has the name and submits what is typed, and choosing a suggestion only fills it in. It runs on the Combobox's element (`<lu-combobox free>`, 5.8 kB) and parts, on server-rendered pages and in islands; `bind:value` keeps a signal in sync. Filtering, inline completion, groups, an empty state and a status.
4
4
 
5
5
  ```ts
6
6
  import { Autocomplete } from "@lumiose/ui"; // or "@lumiose/ui/autocomplete"
@@ -145,7 +145,7 @@ Focus stays in the input: the highlight moves through `aria-activedescendant`.
145
145
  ## Without JavaScript, and the element's script
146
146
 
147
147
  The input is a native text field with the name: typing, `required`, submission and reset work with
148
- no script; the suggestions need it. `<lu-combobox>`'s script (5.0 kB brotli, shared with Combobox)
148
+ no script; the suggestions need it. `<lu-combobox>`'s script (5.8 kB brotli, shared with Combobox)
149
149
  filters and opens them, moves the highlight, fills in a choice (firing `input` and `change` on the
150
150
  input) and runs inline completion.
151
151
 
package/docs/avatar.md CHANGED
@@ -180,8 +180,8 @@ when it fails) and hides its fallback, from sight and from assistive technology,
180
180
  the avatar twice; an avatar without an image shows its fallback.
181
181
  `onLoadingStatusChange` needs the component to run in an island.
182
182
 
183
- A page that renders an `Avatar` loads a 0.5 kB (brotli) script, without the Lumiose runtime;
184
- islands bundle it. `<lu-avatar>` reads the image's state when it connects (a cached image shows
183
+ A page that renders an `Avatar` loads a 0.5 kB (brotli) script, without the Lumiose runtime; an
184
+ island loads the same module. `<lu-avatar>` reads the image's state when it connects (a cached image shows
185
185
  at once), then follows its `load` and `error` events and changes to its `src` and `srcset`. An
186
186
  image that is complete without a size (an SVG without intrinsic dimensions, or a broken image) is
187
187
  told apart with `decode()`.
@@ -3,7 +3,7 @@
3
3
  > A group of checkboxes with one value, the list of the checked ones, after Base UI's CheckboxGroup: a native `<fieldset>` whose checkboxes share a name, so the form submits each checked value and a disabled group disables them all, without JavaScript. A parent checkbox checks them all and shows checked, mixed or unchecked as they are; `bind:value` keeps a signal in sync.
4
4
 
5
5
  ```ts
6
- import { CheckboxGroup, Checkbox, Fieldset } from "@lumiose/ui"; // or "@lumiose/ui/checkbox-group"
6
+ import { CheckboxGroup, Checkbox, Fieldset } from "@lumiose/ui"; // or "@lumiose/ui/checkbox-group", "@lumiose/ui/checkbox" and "@lumiose/ui/fieldset"
7
7
  import "@lumiose/ui/themes/default.css"; // optional: the default theme
8
8
  ```
9
9
 
package/docs/checkbox.md CHANGED
@@ -112,7 +112,7 @@ A native checkbox: toggling, submission, `required`, reset and `disabled` (its o
112
112
  work with no script.
113
113
 
114
114
  A page that renders a Checkbox, Checkbox Group, Radio Group or Switch loads one 1.0 kB (brotli)
115
- script once, without the Lumiose runtime; islands bundle it. It turns server HTML's
115
+ script once, without the Lumiose runtime; an island loads the same module. It turns server HTML's
116
116
  `data-indeterminate` into the `indeterminate` property (which has no attribute), makes read-only
117
117
  inputs ignore clicks, Space and the label, runs the parent checkbox of a group and a [Radio
118
118
  Group](radio-group.md)'s arrow keys, and adds an
@@ -1,6 +1,6 @@
1
1
  # Collapsible
2
2
 
3
- > A panel that its trigger shows and hides: a native `<details>` whose `<summary>` is the trigger, so it works in server HTML with no JavaScript, keeps the browser's keyboard handling, and opens when find-in-page or a link lands inside it. A 0.7 kB script adds Base UI's animation hooks. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands.
3
+ > A panel that its trigger shows and hides: a native `<details>` whose `<summary>` is the trigger, so it works in server HTML with no JavaScript, keeps the browser's keyboard handling, and opens when find-in-page or a link lands inside it. A 0.9 kB script adds Base UI's animation hooks. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands.
4
4
 
5
5
  ```ts
6
6
  import { Collapsible } from "@lumiose/ui"; // or "@lumiose/ui/collapsible"
@@ -141,8 +141,9 @@ Collapsible in a server-rendered page works before any script loads, and with no
141
141
  trigger ignores the pointer through CSS. `open`, `onOpenChange` and `bind:open` need the component
142
142
  to run in an island.
143
143
 
144
- A page that renders a `Collapsible` loads a 0.7 kB (brotli) module once, without the Lumiose
145
- runtime; islands bundle it. It covers every Collapsible and [Accordion](accordion.md) panel on the
144
+ A page that renders a `Collapsible` loads 0.9 kB (brotli) of modules once (its script, and the
145
+ panel and animation modules it shares with Accordion), without the Lumiose runtime; an island
146
+ loads the same modules. It covers every Collapsible and [Accordion](accordion.md) panel on the
146
147
  page:
147
148
 
148
149
  - the animation hooks above, set on every opening and closing, whatever caused it (the trigger,
package/docs/combobox.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Combobox
2
2
 
3
- > A text input that filters a list of options, for picking one value or several from many: the WAI-ARIA combobox pattern with list autocomplete, after Base UI's Combobox. The input shows the option's label and the form gets its value. `<lu-combobox>`, a 5.4 kB form-associated light-DOM element, runs it on server-rendered pages and in islands; `bind:value` and `bind:open` keep signals in sync. Groups, an empty state, a status, a clear button, multiple values with chips, validation and async options.
3
+ > A text input that filters a list of options, for picking one value or several from many: the WAI-ARIA combobox pattern with list autocomplete, after Base UI's Combobox. The input shows the option's label and the form gets its value. `<lu-combobox>`, a 5.8 kB form-associated light-DOM element, runs it on server-rendered pages and in islands; `bind:value` and `bind:open` keep signals in sync. Groups, an empty state, a status, a clear button, multiple values with chips, validation and async options.
4
4
 
5
5
  ```ts
6
6
  import { Combobox } from "@lumiose/ui"; // or "@lumiose/ui/combobox"
@@ -313,8 +313,9 @@ In the server HTML the input is empty even with a selected value: the input rend
313
313
  options, so the server does not know the selected option's label when it writes the input. The
314
314
  script puts the label in as it upgrades the element (decision-62).
315
315
 
316
- A page that renders a `Combobox` loads the script once, as a 5.4 kB (brotli) module without the
317
- Lumiose runtime, and islands bundle it. It defines `lu-combobox`, which upgrades every combobox on
316
+ A page that renders a `Combobox` loads the script once, with the dismissal and placement modules
317
+ it shares with Menu and Popover: 5.8 kB (brotli) without the Lumiose runtime, and an island loads
318
+ the same modules. It defines `lu-combobox`, which upgrades every combobox on
318
319
  the page in place: it reads the selection the server rendered (`aria-selected="true"`), puts its
319
320
  label in the input (text typed before it ran is kept, as a search), renders the chips, and from
320
321
  then on owns the value, the filtering, the popup,
@@ -1,6 +1,6 @@
1
1
  # Context Menu
2
2
 
3
- > A menu opened at the pointer by a right-click in its trigger area, a long press on a touch screen, or Shift+F10 and the ContextMenu key: Menu's popup and items, with Menu's keys, typeahead, submenus and dismissal. A 0.6 kB script on top of Menu's (4.6 kB together) opens it; without JavaScript the browser shows its own menu. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands.
3
+ > A menu opened at the pointer by a right-click in its trigger area, a long press on a touch screen, or Shift+F10 and the ContextMenu key: Menu's popup and items, with Menu's keys, typeahead, submenus and dismissal. A 0.9 kB script on top of Menu's (5.4 kB together) opens it; without JavaScript the browser shows its own menu. `bind:open` (or `open` and `onOpenChange`) keeps a signal in sync in islands.
4
4
 
5
5
  ```ts
6
6
  import { ContextMenu } from "@lumiose/ui"; // or "@lumiose/ui/context-menu"
@@ -175,8 +175,8 @@ Opening a menu at the pointer needs the script: without JavaScript a right-click
175
175
  browser's own menu, and the popup stays hidden. `open`, `onOpenChange` and `bind:open` need the
176
176
  component to run in an island.
177
177
 
178
- A page that renders a `ContextMenu` loads Menu's script and a 0.6 kB (brotli) one of its own,
179
- 4.6 kB together, without the Lumiose runtime; islands bundle them. The element opens the menu on a
178
+ A page that renders a `ContextMenu` loads Menu's script and a 0.9 kB (brotli) one of its own,
179
+ 5.4 kB together, without the Lumiose runtime; an island loads the same modules. The element opens the menu on a
180
180
  right-click, a long press or the keys, at the pointer, through a zero-size anchor that it moves
181
181
  there (CSS anchor positioning places the popup, with Menu's flips; without it, the shared
182
182
  placement script does). Menu's script does the rest: keys, items, submenus, and dismissal shared
package/docs/dialog.md CHANGED
@@ -221,7 +221,7 @@ server-rendered page works before any script loads, and with none. `open`, `onOp
221
221
 
222
222
  Where the native element falls short of Base UI's dialog, a small script closes the gap: a page
223
223
  that renders a `Dialog` loads it once, as a 1.5 kB (brotli) module without the Lumiose runtime, and
224
- islands bundle it. It covers every `<dialog>` on the page:
224
+ an island loads the same module. It covers every `<dialog>` on the page:
225
225
 
226
226
  - a right-click or a second finger outside does not close the dialog;
227
227
  - `data-nested-dialog-open` and `--lu-nested-dialogs` on a dialog with open dialogs inside;
package/docs/drawer.md CHANGED
@@ -182,8 +182,9 @@ The trigger, the close buttons, Escape and the click outside are HTML features,
182
182
  drawer in a server-rendered page works before any script loads, and with none, against its edge.
183
183
  `open`, `onOpenChange` and `bind:open` need the component to run in an island.
184
184
 
185
- A page that renders a `Drawer` loads Dialog's script and a 1 kB (brotli) one of its own (2.5 kB
186
- together), without the Lumiose runtime; islands bundle them. The drawer's script handles the
185
+ A page that renders a `Drawer` loads Dialog's script and a 1.1 kB (brotli) one of its own (2.8 kB
186
+ together, with the animation module it shares with Toast), without the Lumiose runtime; an island
187
+ loads the same modules. The drawer's script handles the
187
188
  swipe, with the mouse and pen through pointer events and on touch screens through touch events,
188
189
  taking the gesture from scrolling only once it is a swipe.
189
190
 
package/docs/field.md CHANGED
@@ -253,8 +253,8 @@ the message Error's id (`Field.Error` without children or `match`) even when the
253
253
  Errors with children or a `match`. Give a field that must read well without JavaScript a message
254
254
  Error, and its Descriptions before the control.
255
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
256
+ A page that renders a `Field` loads a 1.7 kB (brotli) script once, without the Lumiose runtime; an
257
+ island loads the same module. On a submit attempt (a submit button, Enter in a field, `requestSubmit()` or
258
258
  the form's `reportValidity()`; a silent `checkValidity()` changes nothing) it cancels the browser's
259
259
  `invalid` bubble for fields that have an Error part for the problem and shows it there instead (the
260
260
  browser's message, or one your `validate` set), with the timing above, and focuses the first
@@ -2,13 +2,13 @@
2
2
 
3
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
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`.
5
+ **Status:** in development, on npm with its own 0.x versions, apart from the version the other
6
+ Lumiose packages share.
7
7
 
8
8
  ## Add the package
9
9
 
10
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:
11
+ with the app's own code, and `lumiose` is its peer dependency. Install it from npm:
12
12
 
13
13
  ```sh
14
14
  pnpm add @lumiose/ui