@lumiose/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -2
  3. package/dist/accordion/accordion.css +20 -0
  4. package/dist/accordion/accordion.script.ts +66 -0
  5. package/dist/accordion/accordion.tsx +288 -0
  6. package/dist/animation/animation.ts +34 -0
  7. package/dist/autocomplete/autocomplete.tsx +239 -0
  8. package/dist/avatar/avatar.css +54 -0
  9. package/dist/avatar/avatar.script.ts +96 -0
  10. package/dist/avatar/avatar.tsx +110 -0
  11. package/dist/button/button.script.ts +35 -0
  12. package/dist/button/button.tsx +40 -0
  13. package/dist/checkbox/checkbox.script.ts +225 -0
  14. package/dist/checkbox/checkbox.tsx +156 -0
  15. package/dist/checkbox/context.ts +17 -0
  16. package/dist/checkbox-group/checkbox-group.tsx +111 -0
  17. package/dist/collapsible/collapsible.css +12 -0
  18. package/dist/collapsible/collapsible.script.ts +7 -0
  19. package/dist/collapsible/collapsible.tsx +153 -0
  20. package/dist/collapsible/panel.ts +115 -0
  21. package/dist/combobox/combobox.css +37 -0
  22. package/dist/combobox/combobox.script.ts +974 -0
  23. package/dist/combobox/combobox.tsx +556 -0
  24. package/dist/combobox/context.ts +46 -0
  25. package/dist/context-menu/context-menu.css +16 -0
  26. package/dist/context-menu/context-menu.script.ts +172 -0
  27. package/dist/context-menu/context-menu.tsx +178 -0
  28. package/dist/dialog/dialog.css +35 -0
  29. package/dist/dialog/dialog.script.ts +269 -0
  30. package/dist/dialog/dialog.tsx +214 -0
  31. package/dist/drawer/drawer.css +38 -0
  32. package/dist/drawer/drawer.script.ts +207 -0
  33. package/dist/drawer/drawer.tsx +113 -0
  34. package/dist/field/context.ts +156 -0
  35. package/dist/field/field.css +16 -0
  36. package/dist/field/field.script.ts +422 -0
  37. package/dist/field/field.tsx +465 -0
  38. package/dist/fieldset/fieldset.tsx +41 -0
  39. package/dist/form/form.tsx +50 -0
  40. package/dist/form/native.ts +21 -0
  41. package/dist/form/reset.ts +53 -0
  42. package/dist/index.ts +261 -0
  43. package/dist/input/input.tsx +67 -0
  44. package/dist/layer/layer.ts +191 -0
  45. package/dist/locale/digits.ts +21 -0
  46. package/dist/menu/menu.css +22 -0
  47. package/dist/menu/menu.script.ts +591 -0
  48. package/dist/menu/menu.tsx +520 -0
  49. package/dist/menubar/menubar.css +16 -0
  50. package/dist/menubar/menubar.script.ts +195 -0
  51. package/dist/menubar/menubar.tsx +67 -0
  52. package/dist/meter/meter.tsx +134 -0
  53. package/dist/navigation-menu/navigation-menu.css +14 -0
  54. package/dist/navigation-menu/navigation-menu.script.ts +466 -0
  55. package/dist/navigation-menu/navigation-menu.tsx +266 -0
  56. package/dist/number-field/format.ts +70 -0
  57. package/dist/number-field/number-field.css +38 -0
  58. package/dist/number-field/number-field.script.ts +490 -0
  59. package/dist/number-field/number-field.tsx +403 -0
  60. package/dist/otp-field/otp-field.css +45 -0
  61. package/dist/otp-field/otp-field.script.ts +330 -0
  62. package/dist/otp-field/otp-field.tsx +320 -0
  63. package/dist/placement/place.ts +148 -0
  64. package/dist/placement/placement.css +155 -0
  65. package/dist/placement/placement.ts +60 -0
  66. package/dist/popover/hover.ts +178 -0
  67. package/dist/popover/popover.css +20 -0
  68. package/dist/popover/popover.script.ts +256 -0
  69. package/dist/popover/popover.tsx +317 -0
  70. package/dist/preview-card/preview-card.css +7 -0
  71. package/dist/preview-card/preview-card.script.ts +136 -0
  72. package/dist/preview-card/preview-card.tsx +128 -0
  73. package/dist/progress/progress.tsx +155 -0
  74. package/dist/progress/value.ts +23 -0
  75. package/dist/radio-group/radio-group.tsx +156 -0
  76. package/dist/scroll-area/scroll-area.css +72 -0
  77. package/dist/scroll-area/scroll-area.script.ts +281 -0
  78. package/dist/scroll-area/scroll-area.tsx +118 -0
  79. package/dist/select/select.css +25 -0
  80. package/dist/select/select.tsx +171 -0
  81. package/dist/separator/separator.css +12 -0
  82. package/dist/separator/separator.tsx +38 -0
  83. package/dist/slider/format.ts +33 -0
  84. package/dist/slider/slider.css +93 -0
  85. package/dist/slider/slider.script.ts +355 -0
  86. package/dist/slider/slider.tsx +374 -0
  87. package/dist/styles.css +30 -0
  88. package/dist/switch/switch.tsx +92 -0
  89. package/dist/tabs/tabs.css +54 -0
  90. package/dist/tabs/tabs.script.ts +225 -0
  91. package/dist/tabs/tabs.tsx +272 -0
  92. package/dist/themes/default.css +2111 -0
  93. package/dist/toast/command.ts +3 -0
  94. package/dist/toast/toast.css +68 -0
  95. package/dist/toast/toast.script.ts +516 -0
  96. package/dist/toast/toast.tsx +432 -0
  97. package/dist/toggle/context.ts +16 -0
  98. package/dist/toggle/toggle.tsx +99 -0
  99. package/dist/toggle-group/toggle-group.script.ts +166 -0
  100. package/dist/toggle-group/toggle-group.tsx +117 -0
  101. package/dist/toolbar/context.ts +18 -0
  102. package/dist/toolbar/toolbar.css +12 -0
  103. package/dist/toolbar/toolbar.script.ts +123 -0
  104. package/dist/toolbar/toolbar.tsx +180 -0
  105. package/dist/tooltip/tooltip.css +8 -0
  106. package/dist/tooltip/tooltip.script.ts +222 -0
  107. package/dist/tooltip/tooltip.tsx +149 -0
  108. package/docs/README.md +174 -0
  109. package/docs/accordion.md +208 -0
  110. package/docs/autocomplete.md +161 -0
  111. package/docs/avatar.md +198 -0
  112. package/docs/button.md +98 -0
  113. package/docs/checkbox-group.md +112 -0
  114. package/docs/checkbox.md +133 -0
  115. package/docs/collapsible.md +165 -0
  116. package/docs/combobox.md +369 -0
  117. package/docs/context-menu.md +197 -0
  118. package/docs/dialog.md +256 -0
  119. package/docs/drawer.md +204 -0
  120. package/docs/field.md +295 -0
  121. package/docs/fieldset.md +92 -0
  122. package/docs/form.md +131 -0
  123. package/docs/input.md +87 -0
  124. package/docs/installation.md +65 -0
  125. package/docs/llms-full.txt +6923 -0
  126. package/docs/llms.txt +53 -0
  127. package/docs/menu.md +365 -0
  128. package/docs/menubar.md +186 -0
  129. package/docs/meter.md +103 -0
  130. package/docs/navigation-menu.md +257 -0
  131. package/docs/number-field.md +181 -0
  132. package/docs/otp-field.md +160 -0
  133. package/docs/popover.md +271 -0
  134. package/docs/preview-card.md +130 -0
  135. package/docs/progress.md +115 -0
  136. package/docs/radio-group.md +127 -0
  137. package/docs/scroll-area.md +147 -0
  138. package/docs/select.md +236 -0
  139. package/docs/separator.md +116 -0
  140. package/docs/slider.md +184 -0
  141. package/docs/switch.md +98 -0
  142. package/docs/tabs.md +254 -0
  143. package/docs/theming.md +132 -0
  144. package/docs/toast.md +311 -0
  145. package/docs/toggle-group.md +119 -0
  146. package/docs/toggle.md +94 -0
  147. package/docs/toolbar.md +138 -0
  148. package/docs/tooltip.md +128 -0
  149. package/package.json +102 -3
@@ -0,0 +1,195 @@
1
+ // Menubar's client script (decision-62, decision-63). `<lu-menubar role="menubar">` holds Menus,
2
+ // whose `lu-menu` elements (../menu/menu.script.ts) run each menu; it adds the WAI-ARIA menubar
3
+ // pattern's bar behaviour, after Base UI's Menubar:
4
+ //
5
+ // - One tab stop: the item focused last (the first at first) keeps `tabindex="0"`, the others get
6
+ // `-1`. Without this script every trigger stays a tab stop. When the tab stop is removed,
7
+ // disabled or hidden, the first item takes it (TASK-437 M11).
8
+ // - On an item: Arrow Right and Left (Down and Up when vertical; swapped right to left) move
9
+ // between the items, looping unless `data-loop-focus="false"`; Home and End; a letter moves to the
10
+ // next item starting with it. A menu that is open follows: moving opens the next item's menu.
11
+ // - In an open menu: Arrow Right and Left on an item that opens no submenu (Menu's script handles
12
+ // the ones that do) close it and open the next or previous item's menu on its first item; in a
13
+ // vertical bar, the arrow pointing back closes the menu and returns to its item.
14
+ // - Pointer: once a menu is open, hovering another item opens its menu instead.
15
+ // - `data-has-submenu-open` on the bar while one of its menus is open.
16
+ // It must not import `lumiose`: server-rendered pages load it without the runtime.
17
+
18
+ import type { MenuElement } from "../menu/menu.script.ts";
19
+
20
+ const enabled = (item: Element) =>
21
+ item.getAttribute("aria-disabled") !== "true" && !item.matches(":disabled");
22
+
23
+ export class MenubarElement extends HTMLElement {
24
+ /** The tab stop, kept while it stays an item. */
25
+ #stop: HTMLElement | undefined;
26
+ /** Moves the tab stop on when items come, go, or are disabled or hidden. */
27
+ #observer = new MutationObserver(() => this.#rove(undefined));
28
+
29
+ connectedCallback(): void {
30
+ this.addEventListener("keydown", this);
31
+ this.addEventListener("pointerover", this);
32
+ this.addEventListener("focusin", this);
33
+ // `openchange` does not bubble: the capture phase sees each menu's.
34
+ this.addEventListener("openchange", this, true);
35
+ this.#observer.observe(this, {
36
+ subtree: true,
37
+ childList: true,
38
+ attributeFilter: ["disabled", "hidden"],
39
+ });
40
+ // The element may connect while the page is still parsing, before its items: it makes the bar
41
+ // one tab stop once the document has loaded (or after the current task, in an island).
42
+ if (document.readyState === "loading") {
43
+ document.addEventListener("DOMContentLoaded", () => this.#start(), { once: true });
44
+ } else {
45
+ queueMicrotask(() => this.#start());
46
+ }
47
+ }
48
+
49
+ #start(): void {
50
+ if (!this.isConnected) return;
51
+ const items = this.#items();
52
+ this.#rove(items.find((item) => item.tabIndex === 0) ?? items[0]);
53
+ }
54
+
55
+ disconnectedCallback(): void {
56
+ this.removeEventListener("keydown", this);
57
+ this.removeEventListener("pointerover", this);
58
+ this.removeEventListener("focusin", this);
59
+ this.removeEventListener("openchange", this, true);
60
+ this.#observer.disconnect();
61
+ }
62
+
63
+ handleEvent(event: Event): void {
64
+ switch (event.type) {
65
+ case "keydown":
66
+ return this.#onKeyDown(event as KeyboardEvent);
67
+ case "pointerover":
68
+ return this.#onPointerOver(event as PointerEvent);
69
+ case "focusin": {
70
+ const item = event.target as HTMLElement;
71
+ if (this.#items().includes(item)) this.#rove(item);
72
+ return;
73
+ }
74
+ default:
75
+ this.toggleAttribute("data-has-submenu-open", this.#openMenu() !== undefined);
76
+ }
77
+ }
78
+
79
+ /** The bar's items: its menus' triggers, in order, without natively disabled or hidden ones. */
80
+ #items(): HTMLElement[] {
81
+ return [...this.querySelectorAll<HTMLElement>("[role=menuitem]")].filter(
82
+ (item) =>
83
+ item.closest("lu-menubar") === this &&
84
+ item.closest("[role=menu]") === null &&
85
+ !item.matches(":disabled") &&
86
+ (item.checkVisibility?.() ?? true),
87
+ );
88
+ }
89
+
90
+ #menuOf(item: HTMLElement): MenuElement | null {
91
+ return item.closest<MenuElement>("lu-menu");
92
+ }
93
+
94
+ /** The bar's open menu, if any. */
95
+ #openMenu(): MenuElement | undefined {
96
+ for (const item of this.#items()) {
97
+ const menu = this.#menuOf(item);
98
+ if (menu?.open) return menu;
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ /** Makes `item` the bar's one tab stop; without one, keeps the current one, else the first. */
104
+ #rove(item: HTMLElement | undefined): void {
105
+ const items = this.#items();
106
+ const kept = this.#stop && items.includes(this.#stop) ? this.#stop : undefined;
107
+ const stop = item ?? kept ?? items[0];
108
+ if (!stop) return;
109
+ this.#stop = stop;
110
+ for (const other of items) other.tabIndex = other === stop ? 0 : -1;
111
+ }
112
+
113
+ /** The item `step` places from `from` (wrapping unless the bar does not loop). */
114
+ #step(items: HTMLElement[], from: number, step: number): HTMLElement | undefined {
115
+ const next = from + step;
116
+ if (this.dataset.loopFocus === "false") return items[next];
117
+ return items[(next + items.length) % items.length];
118
+ }
119
+
120
+ /** Moves to `item`; with `open`, opens its menu on its first item (closing the open one). */
121
+ #go(item: HTMLElement | undefined, open: boolean): void {
122
+ if (!item) return;
123
+ const current = this.#openMenu();
124
+ if (current && current !== this.#menuOf(item)) current.open = false;
125
+ this.#rove(item);
126
+ item.focus();
127
+ const menu = this.#menuOf(item);
128
+ if (open && menu && enabled(item) && !menu.open) menu.show(true);
129
+ }
130
+
131
+ #onKeyDown(event: KeyboardEvent): void {
132
+ if (event.isComposing || event.defaultPrevented || event.ctrlKey || event.metaKey) return;
133
+ const target = event.target as HTMLElement;
134
+ const items = this.#items();
135
+ const vertical = this.getAttribute("aria-orientation") === "vertical";
136
+ const rtl = this.matches(":dir(rtl)");
137
+ const [next, previous] = vertical
138
+ ? ["ArrowDown", "ArrowUp"]
139
+ : rtl
140
+ ? ["ArrowLeft", "ArrowRight"]
141
+ : ["ArrowRight", "ArrowLeft"];
142
+ const index = items.indexOf(target);
143
+ if (index >= 0) {
144
+ // On an item of the bar.
145
+ const open = this.#openMenu() !== undefined;
146
+ let to: HTMLElement | undefined;
147
+ if (event.key === next) to = this.#step(items, index, 1);
148
+ else if (event.key === previous) to = this.#step(items, index, -1);
149
+ else if (event.key === "Home") to = items[0];
150
+ else if (event.key === "End") to = items.at(-1);
151
+ else if (event.key.length === 1 && event.key !== " " && !event.altKey) {
152
+ const key = event.key.toLowerCase();
153
+ const ordered = [...items.slice(index + 1), ...items.slice(0, index + 1)];
154
+ to = ordered.find((item) => item.textContent?.trim().toLowerCase().startsWith(key));
155
+ } else return;
156
+ event.preventDefault();
157
+ this.#go(to, open);
158
+ return;
159
+ }
160
+ // In one of the bar's open menus, on an item that opens no submenu.
161
+ const menu = this.#openMenu();
162
+ const trigger = menu ? items.find((item) => this.#menuOf(item) === menu) : undefined;
163
+ if (!menu || !trigger || !target.closest("[role=menu]")) return;
164
+ const back = rtl ? "ArrowRight" : "ArrowLeft";
165
+ if (vertical) {
166
+ if (event.key !== back) return;
167
+ event.preventDefault();
168
+ menu.open = false;
169
+ trigger.focus();
170
+ return;
171
+ }
172
+ if (event.key !== next && event.key !== previous) return;
173
+ const to = this.#step(items, items.indexOf(trigger), event.key === next ? 1 : -1);
174
+ if (!to) return;
175
+ event.preventDefault();
176
+ this.#go(to, true);
177
+ }
178
+
179
+ /** Once a menu is open, hovering another item of the bar opens its menu instead. */
180
+ #onPointerOver(event: PointerEvent): void {
181
+ if (event.pointerType === "touch") return;
182
+ const item = (event.target as Element).closest?.<HTMLElement>("[role=menuitem]");
183
+ if (!item || !this.#items().includes(item) || !enabled(item)) return;
184
+ const current = this.#openMenu();
185
+ const menu = this.#menuOf(item);
186
+ if (!current || !menu || current === menu) return;
187
+ current.open = false;
188
+ this.#rove(item);
189
+ menu.show(false);
190
+ }
191
+ }
192
+
193
+ if (!customElements.get("lu-menubar")) {
194
+ customElements.define("lu-menubar", MenubarElement);
195
+ }
@@ -0,0 +1,67 @@
1
+ // Menubar (decision-62, TASK-379): a row of Menus, after Base UI's Menubar and the WAI-ARIA
2
+ // menubar pattern. It renders `<lu-menubar role="menubar">` (./menubar.script.ts) around
3
+ // `Menu.Root`s: inside it, each top-level `<lu-menu>` is `role="none"` and its `Menu.Trigger` a
4
+ // `role="menuitem"`, so the bar's items are the triggers, as in the APG example. Menu's script runs
5
+ // each menu (keys, items, submenus, dismissal); the menubar's adds one tab stop for the bar, the
6
+ // arrow keys between its items and between open menus, and hover switching once a menu is open.
7
+ // Without JavaScript every trigger is a tab stop that opens its popover.
8
+ import { MenubarContext } from "../menu/menu.tsx";
9
+ import menubarScript from "./menubar.script.ts?client";
10
+ import type { MenubarElement } from "./menubar.script.ts";
11
+
12
+ declare module "lumiose/jsx-runtime" {
13
+ namespace JSX {
14
+ interface IntrinsicElements {
15
+ /** Menubar's element: a `role="menubar"` that moves focus between its menus' triggers. */
16
+ "lu-menubar": JSX.HTMLProps<MenubarElement>;
17
+ }
18
+ }
19
+ }
20
+
21
+ /** Which way a menubar's items run. */
22
+ export type MenubarOrientation = "horizontal" | "vertical";
23
+
24
+ /** `Menubar` renders `<lu-menubar role="menubar">` around `Menu.Root`s. */
25
+ export type MenubarProps = JSX.IntrinsicElements["lu-menubar"] & {
26
+ /**
27
+ * Which way the items run: `"vertical"` stacks them, moves between them with Arrow Up and Down,
28
+ * opens a menu with the arrow that points at its popup, and places popups at the inline end.
29
+ * Default `"horizontal"`.
30
+ */
31
+ orientation?: MenubarOrientation;
32
+ /** Whether the arrow keys wrap from the last item to the first and back. Default `true`. */
33
+ loopFocus?: boolean;
34
+ };
35
+
36
+ /**
37
+ * A bar of menus, such as an app's File, Edit and View: put `Menu.Root`s inside, each with its
38
+ * `Menu.Trigger` and `Menu.Popup`. Give it an `aria-label`.
39
+ *
40
+ * @example
41
+ * <Menubar aria-label="Editor">
42
+ * <Menu.Root>
43
+ * <Menu.Trigger>File</Menu.Trigger>
44
+ * <Menu.Popup>
45
+ * <Menu.Item>New</Menu.Item>
46
+ * </Menu.Popup>
47
+ * </Menu.Root>
48
+ * </Menubar>
49
+ */
50
+ export function Menubar({ orientation, loopFocus, children, ...rest }: MenubarProps) {
51
+ // The bar's keys and hover switching; the page loads it once (decision-63).
52
+ menubarScript();
53
+ const vertical = () => orientation === "vertical";
54
+ return (
55
+ <MenubarContext value={{ orientation: vertical() ? "vertical" : "horizontal" }}>
56
+ <lu-menubar
57
+ {...rest}
58
+ role="menubar"
59
+ aria-orientation={vertical() ? "vertical" : "horizontal"}
60
+ data-orientation={vertical() ? "vertical" : "horizontal"}
61
+ data-loop-focus={loopFocus === false ? "false" : undefined}
62
+ >
63
+ {children}
64
+ </lu-menubar>
65
+ </MenubarContext>
66
+ );
67
+ }
@@ -0,0 +1,134 @@
1
+ // Meter (decision-62, TASK-373): a native `<meter>`, after Base UI's Meter: a measurement within a
2
+ // known range (disk usage, a password's strength), not a task's progress. The element carries the
3
+ // role, the range and `aria-valuetext` with the formatted value; its native `low`, `high` and
4
+ // `optimum` pass through. The Root around it holds the label and the value text, and sets
5
+ // `--lu-value`. No script.
6
+ import { createContext, useContext, useId } from "lumiose";
7
+ import { withStyle } from "../placement/placement.ts";
8
+ import { formatValue, percent } from "../progress/value.ts";
9
+
10
+ interface MeterContextValue {
11
+ id: string;
12
+ value: () => number;
13
+ min: () => number;
14
+ max: () => number;
15
+ text: () => string;
16
+ valueText: () => string;
17
+ }
18
+
19
+ const MeterContext = createContext<MeterContextValue>();
20
+
21
+ /** `Meter.Root` renders a `<div>` around the parts, with `--lu-value` (0% to 100%). */
22
+ export type MeterRootProps = JSX.HTMLProps<HTMLElement> & {
23
+ /** The measured value, from `min` to `max`. */
24
+ value: number;
25
+ /** The bottom of the range. Default `0`. */
26
+ min?: number;
27
+ /** The top of the range. Default `100`. */
28
+ max?: number;
29
+ /** How the value is written: `Intl.NumberFormat` options. Default: a percentage of the range. */
30
+ format?: Intl.NumberFormatOptions;
31
+ /** The locale of the formatted value. Default: the browser's (the server's in server HTML). */
32
+ locale?: Intl.LocalesArgument;
33
+ /** The text assistive technology reads for the value. Default: the formatted value. */
34
+ getAriaValueText?: (formattedValue: string, value: number) => string;
35
+ };
36
+
37
+ /** The `<label>` of the meter. */
38
+ export type MeterLabelProps = JSX.IntrinsicElements["label"];
39
+
40
+ /**
41
+ * The meter: a native `<meter>` with the Root's value and range, plus the native `low`, `high`
42
+ * and `optimum`. Styled with `appearance: none` and its `::-webkit-meter-*` and `::-moz-meter-bar`
43
+ * pseudo-elements, or `--lu-value`.
44
+ */
45
+ export type MeterTrackProps = Omit<JSX.IntrinsicElements["meter"], "value" | "min" | "max">;
46
+
47
+ /** A `<span>` with the formatted value. */
48
+ export type MeterValueProps = Omit<JSX.IntrinsicElements["span"], "children"> & {
49
+ /** Renders the value your way, from the formatted value and the number. */
50
+ children?: (formattedValue: string, value: number) => JSX.Element;
51
+ };
52
+
53
+ function useMeter(part: string): MeterContextValue {
54
+ const context = useContext(MeterContext);
55
+ if (!context) throw new Error(`Meter.${part} must be inside <Meter.Root>.`);
56
+ return context;
57
+ }
58
+
59
+ function Root({
60
+ value,
61
+ min,
62
+ max,
63
+ format,
64
+ locale,
65
+ getAriaValueText,
66
+ children,
67
+ style,
68
+ ...rest
69
+ }: MeterRootProps) {
70
+ const id = useId();
71
+ const low = () => min ?? 0;
72
+ const high = () => max ?? 100;
73
+ const text = () => formatValue(value, low(), high(), format, locale);
74
+ const context: MeterContextValue = {
75
+ id,
76
+ value: () => value,
77
+ min: low,
78
+ max: high,
79
+ text,
80
+ valueText: () => (getAriaValueText ? getAriaValueText(text(), value) : text()),
81
+ };
82
+ return (
83
+ <MeterContext value={context}>
84
+ <div
85
+ {...rest}
86
+ style={withStyle(style, { "--lu-value": `${percent(value, low(), high())}%` })}
87
+ >
88
+ {children}
89
+ </div>
90
+ </MeterContext>
91
+ );
92
+ }
93
+
94
+ function Label({ children, ...rest }: MeterLabelProps) {
95
+ const meter = useMeter("Label");
96
+ return (
97
+ // Nothing points at the label: a caller's `id` names it (TASK-441 M39).
98
+ <label {...rest} id={rest.id ?? `${meter.id}-label`} for={`${meter.id}-track`}>
99
+ {children}
100
+ </label>
101
+ );
102
+ }
103
+
104
+ function Track(props: MeterTrackProps) {
105
+ const meter = useMeter("Track");
106
+ return (
107
+ <meter
108
+ {...props}
109
+ id={`${meter.id}-track`}
110
+ value={meter.value()}
111
+ min={meter.min()}
112
+ max={meter.max()}
113
+ aria-valuetext={meter.valueText()}
114
+ />
115
+ );
116
+ }
117
+
118
+ function Value({ children, ...rest }: MeterValueProps) {
119
+ const meter = useMeter("Value");
120
+ return <span {...rest}>{children ? children(meter.text(), meter.value()) : meter.text()}</span>;
121
+ }
122
+
123
+ /**
124
+ * A measurement within a range, as a native `<meter>`, with a label and the value as text.
125
+ * Unstyled: style it through `class`, the meter's pseudo-elements and `--lu-value`.
126
+ *
127
+ * @example
128
+ * <Meter.Root value={72}>
129
+ * <Meter.Label>Storage used</Meter.Label>
130
+ * <Meter.Value />
131
+ * <Meter.Track />
132
+ * </Meter.Root>
133
+ */
134
+ export const Meter = { Root, Label, Track, Value };
@@ -0,0 +1,14 @@
1
+ /*
2
+ * The CSS Navigation Menu needs to work, and nothing else (decision-62): `<lu-navigation-menu>`
3
+ * adds no box around its `<nav>`, and each content is placed next to its trigger by the shared
4
+ * placement rules (`data-side`, `data-align`, offsets, flips), which Popover and Menu use too. It
5
+ * sits in the `lu.core` layer, so any app CSS wins.
6
+ */
7
+ @layer lu.core, lu.theme;
8
+ @import "../placement/placement.css";
9
+
10
+ @layer lu.core {
11
+ lu-navigation-menu {
12
+ display: contents;
13
+ }
14
+ }