@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.
- package/README.md +2 -2
- package/dist/accordion/accordion.tsx +3 -2
- package/dist/autocomplete/autocomplete.tsx +6 -5
- package/dist/avatar/avatar.tsx +4 -3
- package/dist/batched.ts +11 -0
- package/dist/checkbox/checkbox.tsx +4 -3
- package/dist/checkbox-group/checkbox-group.tsx +3 -2
- package/dist/collapsible/collapsible.tsx +3 -2
- package/dist/combobox/combobox.tsx +6 -5
- package/dist/context-menu/context-menu.tsx +4 -3
- package/dist/dialog/dialog.tsx +2 -1
- package/dist/field/field.tsx +11 -10
- package/dist/form/reset.ts +4 -2
- package/dist/input/input.tsx +4 -1
- package/dist/menu/menu.tsx +9 -8
- package/dist/navigation-menu/navigation-menu.tsx +4 -3
- package/dist/number-field/number-field.tsx +4 -3
- package/dist/otp-field/otp-field.tsx +9 -8
- package/dist/popover/popover-hover.script.ts +27 -0
- package/dist/popover/popover.script.ts +19 -25
- package/dist/popover/popover.tsx +8 -3
- package/dist/preview-card/preview-card.tsx +2 -1
- package/dist/radio-group/radio-group.tsx +3 -2
- package/dist/select/select.script.ts +26 -0
- package/dist/select/select.tsx +36 -10
- package/dist/slider/slider.tsx +4 -3
- package/dist/switch/switch.tsx +4 -3
- package/dist/tabs/tabs.tsx +3 -2
- package/dist/toast/toast.tsx +3 -2
- package/dist/toggle/toggle.tsx +6 -5
- package/dist/toggle-group/toggle-group.script.ts +12 -4
- package/dist/toggle-group/toggle-group.tsx +16 -4
- package/dist/tooltip/tooltip.tsx +4 -1
- package/docs/README.md +9 -6
- package/docs/accordion.md +3 -2
- package/docs/autocomplete.md +2 -2
- package/docs/avatar.md +2 -2
- package/docs/checkbox-group.md +1 -1
- package/docs/checkbox.md +1 -1
- package/docs/collapsible.md +4 -3
- package/docs/combobox.md +4 -3
- package/docs/context-menu.md +3 -3
- package/docs/dialog.md +1 -1
- package/docs/drawer.md +3 -2
- package/docs/field.md +2 -2
- package/docs/installation.md +3 -3
- package/docs/llms-full.txt +107 -72
- package/docs/llms.txt +14 -14
- package/docs/menu.md +4 -3
- package/docs/menubar.md +3 -3
- package/docs/navigation-menu.md +4 -3
- package/docs/otp-field.md +2 -2
- package/docs/popover.md +7 -4
- package/docs/preview-card.md +4 -3
- package/docs/radio-group.md +1 -1
- package/docs/select.md +25 -10
- package/docs/tabs.md +1 -1
- package/docs/toast.md +2 -2
- package/docs/toggle-group.md +10 -4
- package/docs/toolbar.md +2 -2
- package/docs/tooltip.md +4 -3
- 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);
|
package/dist/select/select.tsx
CHANGED
|
@@ -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
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
|
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
|
);
|
package/dist/slider/slider.tsx
CHANGED
|
@@ -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>
|
package/dist/switch/switch.tsx
CHANGED
|
@@ -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
|
}
|
package/dist/tabs/tabs.tsx
CHANGED
|
@@ -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>
|
package/dist/toast/toast.tsx
CHANGED
|
@@ -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>
|
package/dist/toggle/toggle.tsx
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
/**
|
|
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
|
|
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>
|
package/dist/tooltip/tooltip.tsx
CHANGED
|
@@ -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) =>
|
|
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
|
|
6
|
-
other Lumiose packages share
|
|
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
|
|
191
|
-
|
|
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
|
|
package/docs/autocomplete.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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()`.
|
package/docs/checkbox-group.md
CHANGED
|
@@ -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;
|
|
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
|
package/docs/collapsible.md
CHANGED
|
@@ -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.
|
|
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
|
|
145
|
-
|
|
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.
|
|
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,
|
|
317
|
-
|
|
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,
|
package/docs/context-menu.md
CHANGED
|
@@ -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.
|
|
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.
|
|
179
|
-
4
|
|
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
|
-
|
|
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.
|
|
186
|
-
together
|
|
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
|
-
|
|
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
|
package/docs/installation.md
CHANGED
|
@@ -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:**
|
|
6
|
-
|
|
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.
|
|
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
|