@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,403 @@
1
+ // Number Field (decision-62, TASK-375): a number input with stepper buttons, after Base UI's
2
+ // NumberField and the APG spinbutton. Root renders `<lu-number-field>`, whose script
3
+ // (number-field.script.ts) adds the keys, press-and-hold buttons, scrubbing, the wheel and
4
+ // commit-time clamping and formatting.
5
+ //
6
+ // The input is native either way. Without `format` it is an `<input type="number">`, which steps,
7
+ // validates and submits without JavaScript. With `format` (Intl.NumberFormat options) it is an
8
+ // `<input type="text" role="spinbutton">` that shows the formatted value, and a hidden input
9
+ // submits the number: a native number input cannot show grouping, a currency or a percentage.
10
+ // The `value` prop drives the element when it changes; the input's `input` and `change` events
11
+ // report back through `onValueChange` and `onValueCommitted`.
12
+ import {
13
+ createContext,
14
+ effect,
15
+ ref,
16
+ signal,
17
+ untrack,
18
+ useContext,
19
+ useId,
20
+ type Signal,
21
+ } from "lumiose";
22
+ import { controlId, describedBy, useField, useFormValues } from "../field/context.ts";
23
+ import { onFormReset } from "../form/reset.ts";
24
+ import { formatNumber, formatter } from "./format.ts";
25
+ import type { NumberFieldElement } from "./number-field.script.ts";
26
+ import numberFieldScript from "./number-field.script.ts?client";
27
+
28
+ declare module "lumiose/jsx-runtime" {
29
+ namespace JSX {
30
+ interface IntrinsicElements {
31
+ /**
32
+ * Number Field's element. Attributes `min`, `max`, `step`, `large-step`, `small-step`,
33
+ * `snap-on-step`, `format` (`Intl.NumberFormat` options as JSON), `locale`,
34
+ * `allow-wheel-scrub`, `allow-out-of-range`; property `value`.
35
+ */
36
+ "lu-number-field": JSX.HTMLProps<NumberFieldElement> & {
37
+ min?: number;
38
+ max?: number;
39
+ step?: number;
40
+ "large-step"?: number;
41
+ "small-step"?: number;
42
+ "snap-on-step"?: boolean;
43
+ format?: string;
44
+ locale?: string;
45
+ "allow-wheel-scrub"?: boolean;
46
+ "allow-out-of-range"?: boolean;
47
+ };
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * The element props Root passes to `<lu-number-field>`, without the brand of intrinsic elements'
54
+ * props, so that `bind:value` applies to Root.
55
+ */
56
+ type ElementProps = {
57
+ [
58
+ K in keyof JSX.HTMLProps<NumberFieldElement> as K extends string ? K : never
59
+ ]: JSX.HTMLProps<NumberFieldElement>[K];
60
+ };
61
+
62
+ /**
63
+ * `NumberField.Root` renders `<lu-number-field>`, a block around the other parts.
64
+ * `bind:value={signal}` stands in for `value` and `onValueChange`.
65
+ */
66
+ export interface NumberFieldRootProps extends Omit<
67
+ ElementProps,
68
+ "children" | "onChange" | "onInput" | "id"
69
+ > {
70
+ /** Shows this number (`null`: empty) when it changes. Pair it with `onValueChange`. */
71
+ value?: number | null;
72
+ /** The number at first, when `value` is left out; a form reset goes back to it. */
73
+ defaultValue?: number | null;
74
+ /** Called as the number changes: typing, a key, a button, the wheel, a drag. */
75
+ onValueChange?: (value: number | null) => void;
76
+ /** Called when a change is done: the input left, a key pressed, a button or a drag released. */
77
+ onValueCommitted?: (value: number | null) => void;
78
+ /** The smallest number. Typed numbers below it become it when committed. */
79
+ min?: number;
80
+ /** The largest number. Typed numbers above it become it when committed. */
81
+ max?: number;
82
+ /** The step of the arrow keys, the buttons, the wheel and a drag. Default `1`. */
83
+ step?: number;
84
+ /** The step of Page Up, Page Down, and Shift with an arrow key or a button. Default `10`. */
85
+ largeStep?: number;
86
+ /** The step of Alt with an arrow key or a button. Default `0.1`. */
87
+ smallStep?: number;
88
+ /**
89
+ * A step lands on the next multiple of its size (from `min`, or 0): 1.3 steps up to 2, not 2.3.
90
+ */
91
+ snapOnStep?: boolean;
92
+ /**
93
+ * How the number is shown: `Intl.NumberFormat` options (a currency, a percentage, grouping).
94
+ * With it the input is a text field and a hidden input submits the number.
95
+ */
96
+ format?: Intl.NumberFormatOptions;
97
+ /** The locale of the formatted number. Default: the browser's (the server's in server HTML). */
98
+ locale?: string;
99
+ /** The wheel steps the number while the input has focus and the pointer is over it. */
100
+ allowWheelScrub?: boolean;
101
+ /** Typed numbers outside `min` and `max` stay as typed, for the browser's validation to report. */
102
+ allowOutOfRange?: boolean;
103
+ /** The name the number is submitted under. */
104
+ name?: string;
105
+ /** The id of a form the field belongs to, when it is outside it. */
106
+ form?: string;
107
+ /** The input's id, for a `<label for>`. Default: generated, or the Field's. */
108
+ id?: string;
109
+ /** The form needs a number. */
110
+ required?: boolean;
111
+ /** The field cannot be used. */
112
+ disabled?: boolean;
113
+ /** The number can be read and submitted, not changed. */
114
+ readOnly?: boolean;
115
+ /** The other parts: a `Group` with the `Input` and the buttons, a `ScrubArea`. */
116
+ children?: JSX.Element;
117
+ }
118
+
119
+ /** A `<div>` around the input and its buttons. */
120
+ export type NumberFieldGroupProps = JSX.IntrinsicElements["div"];
121
+
122
+ /**
123
+ * The input: an `<input type="number">`, or with `format` an `<input type="text"
124
+ * role="spinbutton">` showing the formatted number.
125
+ */
126
+ export type NumberFieldInputProps = Omit<
127
+ JSX.IntrinsicElements["input"],
128
+ "type" | "value" | "min" | "max" | "step" | "name" | "form" | "required" | "readonly" | "id"
129
+ >;
130
+
131
+ /**
132
+ * A `<button>` that steps the number up, out of the tab order (the arrow keys do it): a press
133
+ * steps once, holding it repeats. Named "Increase" unless you give it an `aria-label`.
134
+ */
135
+ export type NumberFieldIncrementProps = JSX.IntrinsicElements["button"];
136
+
137
+ /** A `<button>` that steps the number down, like `Increment`. Named "Decrease" by default. */
138
+ export type NumberFieldDecrementProps = JSX.IntrinsicElements["button"];
139
+
140
+ /** A `<span>` that steps the number as the pointer drags across it, often around the label. */
141
+ export type NumberFieldScrubAreaProps = JSX.IntrinsicElements["span"] & {
142
+ /** The drag that changes the number: right goes up (`"horizontal"`, default), or up. */
143
+ direction?: "horizontal" | "vertical";
144
+ /** How many pixels of drag make one step. Default `2`. */
145
+ pixelSensitivity?: number;
146
+ };
147
+
148
+ interface NumberFieldContextValue {
149
+ inputId: string;
150
+ value: Signal<number | null>;
151
+ min: () => number | undefined;
152
+ max: () => number | undefined;
153
+ step: () => number | undefined;
154
+ format: () => Intl.NumberFormatOptions | undefined;
155
+ locale: () => string | undefined;
156
+ name: () => string | undefined;
157
+ form: () => string | undefined;
158
+ required: () => boolean | undefined;
159
+ disabled: () => boolean;
160
+ readOnly: () => boolean | undefined;
161
+ }
162
+
163
+ const NumberFieldContext = createContext<NumberFieldContextValue>();
164
+
165
+ function useNumberField(part: string): NumberFieldContextValue {
166
+ const context = useContext(NumberFieldContext);
167
+ if (!context) throw new Error(`NumberField.${part} must be inside <NumberField.Root>.`);
168
+ return context;
169
+ }
170
+
171
+ function Root({
172
+ value,
173
+ defaultValue,
174
+ onValueChange,
175
+ onValueCommitted,
176
+ min,
177
+ max,
178
+ step,
179
+ largeStep,
180
+ smallStep,
181
+ snapOnStep,
182
+ format,
183
+ locale,
184
+ allowWheelScrub,
185
+ allowOutOfRange,
186
+ name,
187
+ form,
188
+ id,
189
+ required,
190
+ disabled,
191
+ readOnly,
192
+ children,
193
+ ...rest
194
+ }: NumberFieldRootProps) {
195
+ // The keys, the buttons, scrubbing and commit-time formatting (decision-63).
196
+ numberFieldScript();
197
+ const generated = useId();
198
+ const field = useField();
199
+ const element = ref<NumberFieldElement>();
200
+ // A failed submission's value (a Form's `values`) replaces the default (TASK-430).
201
+ const sent = useFormValues(
202
+ untrack(() => name ?? field?.name()),
203
+ untrack(() => disabled),
204
+ )?.[0];
205
+ const echoed = sent ? Number(sent) : Number.NaN;
206
+ const current = signal<number | null>(
207
+ untrack(() => value ?? (Number.isFinite(echoed) ? echoed : (defaultValue ?? null))),
208
+ );
209
+ // The `value` prop drives the input when it changes.
210
+ effect(() => {
211
+ const next = value;
212
+ if (next === undefined) return;
213
+ current.set(next);
214
+ const host = element();
215
+ // Not the echo of what is being typed: rewriting the text then would format it under the
216
+ // caret ("7." loses its point) and, in Firefox and WebKit, cancel the `change` on blur
217
+ // (TASK-416).
218
+ if (host?.isConnected && host.value !== next) host.value = next;
219
+ });
220
+ const context: NumberFieldContextValue = {
221
+ // Ids do not change.
222
+ inputId: untrack(() => id ?? (field ? controlId(field) : `${generated}-input`)),
223
+ value: current,
224
+ min: () => min,
225
+ max: () => max,
226
+ step: () => step,
227
+ format: () => format,
228
+ locale: () => locale,
229
+ name: () => name ?? field?.name(),
230
+ form: () => form,
231
+ required: () => required,
232
+ disabled: () => disabled === true || field?.disabled() === true,
233
+ readOnly: () => readOnly,
234
+ };
235
+ const report = () => {
236
+ const next = element()?.value ?? null;
237
+ if (next === untrack(current)) return;
238
+ current.set(next);
239
+ onValueChange?.(next);
240
+ };
241
+ // A form reset puts the number back without an `input` event: it is reported (TASK-435).
242
+ onFormReset(
243
+ () => element()?.input ?? undefined,
244
+ () => element()?.value ?? null,
245
+ report,
246
+ );
247
+ return (
248
+ <NumberFieldContext value={context}>
249
+ <lu-number-field
250
+ {...rest}
251
+ ref={element}
252
+ id={generated}
253
+ min={min}
254
+ max={max}
255
+ step={step}
256
+ large-step={largeStep}
257
+ small-step={smallStep}
258
+ snap-on-step={snapOnStep}
259
+ format={format ? JSON.stringify(format) : undefined}
260
+ locale={locale}
261
+ allow-wheel-scrub={allowWheelScrub}
262
+ allow-out-of-range={allowOutOfRange}
263
+ on:input={report}
264
+ on:change={() => {
265
+ report();
266
+ onValueCommitted?.(element()?.value ?? null);
267
+ }}
268
+ >
269
+ <input
270
+ type="hidden"
271
+ form={form}
272
+ value={untrack(() => (format && current() !== null ? String(current()) : undefined))}
273
+ />
274
+ {children}
275
+ </lu-number-field>
276
+ </NumberFieldContext>
277
+ );
278
+ }
279
+
280
+ function Group({ children, ...rest }: NumberFieldGroupProps) {
281
+ useNumberField("Group");
282
+ return <div {...rest}>{children}</div>;
283
+ }
284
+
285
+ function Input(props: NumberFieldInputProps) {
286
+ const field = useNumberField("Input");
287
+ const owner = useField();
288
+ const formatted = untrack(field.format) !== undefined;
289
+ // The server HTML's text; the element keeps it from then on (typing, the keys, the buttons).
290
+ const text = untrack(() => {
291
+ const number = field.value();
292
+ if (number === null) return undefined;
293
+ return formatted
294
+ ? formatNumber(number, formatter(field.format(), field.locale()))
295
+ : String(number);
296
+ });
297
+ const initial = untrack(field.value);
298
+ // The limits follow the field (TASK-477); the value's ARIA is the server HTML's, which the
299
+ // element keeps current from then on, as it does the text.
300
+ const spin = () =>
301
+ formatted
302
+ ? {
303
+ role: "spinbutton",
304
+ "aria-valuenow": initial ?? undefined,
305
+ "aria-valuemin": field.min(),
306
+ "aria-valuemax": field.max(),
307
+ "aria-valuetext": text,
308
+ }
309
+ : { min: field.min(), max: field.max(), step: field.step() };
310
+ return (
311
+ <input
312
+ autocomplete="off"
313
+ spellcheck={false}
314
+ aria-describedby={owner ? describedBy(owner) : undefined}
315
+ aria-invalid={owner?.invalid() ? "true" : undefined}
316
+ {...props}
317
+ {...spin()}
318
+ type={formatted ? "text" : "number"}
319
+ // A minus key where negative numbers are allowed: iOS's decimal keyboard has none (M36).
320
+ inputmode={(field.min() ?? -1) >= 0 ? "decimal" : "text"}
321
+ id={field.inputId}
322
+ name={field.name()}
323
+ form={field.form()}
324
+ value={text}
325
+ required={field.required()}
326
+ readonly={field.readOnly()}
327
+ disabled={props.disabled || field.disabled() || undefined}
328
+ />
329
+ );
330
+ }
331
+
332
+ /** Whether a button stepping toward `limit` is at it. */
333
+ function atLimit(field: NumberFieldContextValue, up: boolean): boolean {
334
+ const value = field.value();
335
+ const limit = up ? field.max() : field.min();
336
+ return value !== null && limit !== undefined && (up ? value >= limit : value <= limit);
337
+ }
338
+
339
+ function Increment({ children, ...rest }: NumberFieldIncrementProps) {
340
+ const field = useNumberField("Increment");
341
+ return (
342
+ <button
343
+ type="button"
344
+ tabindex={-1}
345
+ aria-label="Increase"
346
+ {...rest}
347
+ data-action="increment"
348
+ aria-controls={field.inputId}
349
+ disabled={rest.disabled || field.disabled() || field.readOnly() || atLimit(field, true)}
350
+ >
351
+ {children}
352
+ </button>
353
+ );
354
+ }
355
+
356
+ function Decrement({ children, ...rest }: NumberFieldDecrementProps) {
357
+ const field = useNumberField("Decrement");
358
+ return (
359
+ <button
360
+ type="button"
361
+ tabindex={-1}
362
+ aria-label="Decrease"
363
+ {...rest}
364
+ data-action="decrement"
365
+ aria-controls={field.inputId}
366
+ disabled={rest.disabled || field.disabled() || field.readOnly() || atLimit(field, false)}
367
+ >
368
+ {children}
369
+ </button>
370
+ );
371
+ }
372
+
373
+ function ScrubArea({ children, direction, pixelSensitivity, ...rest }: NumberFieldScrubAreaProps) {
374
+ useNumberField("ScrubArea");
375
+ return (
376
+ <span
377
+ {...rest}
378
+ data-scrub=""
379
+ data-direction={direction ?? "horizontal"}
380
+ data-pixel-sensitivity={pixelSensitivity}
381
+ >
382
+ {children}
383
+ </span>
384
+ );
385
+ }
386
+
387
+ /**
388
+ * A number input with buttons to step it, after Base UI's NumberField: the arrow keys, Page Up and
389
+ * Page Down, Home and End, press-and-hold buttons, a scrub area, the wheel when asked, and
390
+ * `Intl.NumberFormat` formatting with parsing back. Native `<input type="number">` unless it is
391
+ * formatted. Unstyled: style the parts through `class`, `:disabled`, `[data-scrubbing]` and
392
+ * `:focus-visible`.
393
+ *
394
+ * @example
395
+ * <NumberField.Root name="quantity" defaultValue={1} min={1} max={99}>
396
+ * <NumberField.Group>
397
+ * <NumberField.Decrement>−</NumberField.Decrement>
398
+ * <NumberField.Input aria-label="Quantity" />
399
+ * <NumberField.Increment>+</NumberField.Increment>
400
+ * </NumberField.Group>
401
+ * </NumberField.Root>
402
+ */
403
+ export const NumberField = { Root, Group, Input, Increment, Decrement, ScrubArea };
@@ -0,0 +1,45 @@
1
+ /*
2
+ * The CSS OTP Field needs to work, and nothing else (decision-62). Once its script runs
3
+ * (`:defined`), the input lies over the slots, its text, caret and selection transparent, so a
4
+ * press anywhere reaches it and the slots show the code; until then (or without JavaScript) the
5
+ * input is a plain text field and the slots stay hidden. It sits in the `lu.core` layer, so any
6
+ * app CSS wins.
7
+ */
8
+ @layer lu.core, lu.theme;
9
+
10
+ @layer lu.core {
11
+ lu-otp-field {
12
+ display: inline-flex;
13
+ position: relative;
14
+ }
15
+ /* Important: a theme's display for the slots must not show them without the script. */
16
+ lu-otp-field:not(:defined) > [data-slot],
17
+ lu-otp-field:not(:defined) > [data-orientation] {
18
+ display: none !important;
19
+ }
20
+ lu-otp-field:defined > input {
21
+ position: absolute;
22
+ inset: 0;
23
+ inline-size: 100%;
24
+ block-size: 100%;
25
+ margin: 0;
26
+ padding: 0;
27
+ border: 0;
28
+ outline: none;
29
+ background: transparent;
30
+ color: transparent;
31
+ caret-color: transparent;
32
+ /* 16px at least: iOS Safari zooms into a smaller focused field (TASK-440, M35). */
33
+ font-size: 16px;
34
+ letter-spacing: 0;
35
+ -webkit-text-fill-color: transparent;
36
+ appearance: none;
37
+ }
38
+ lu-otp-field:defined > input::selection {
39
+ background: transparent;
40
+ }
41
+ /* Masked: a field without its script shows dots too. */
42
+ lu-otp-field[mask] > input {
43
+ -webkit-text-security: disc;
44
+ }
45
+ }