@lumiose/ui 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
package/dist/index.ts ADDED
@@ -0,0 +1,261 @@
1
+ // @lumiose/ui: accessible UI primitives (decision-54, decision-62). Each component also has its
2
+ // own entry (`@lumiose/ui/dialog`), and its CSS its own stylesheet (`@lumiose/ui/dialog.css`).
3
+ export { Dialog } from "./dialog/dialog.tsx";
4
+ export type {
5
+ DialogCloseProps,
6
+ DialogDescriptionProps,
7
+ DialogPopupProps,
8
+ DialogRootProps,
9
+ DialogTitleProps,
10
+ DialogTriggerProps,
11
+ } from "./dialog/dialog.tsx";
12
+ export { Popover } from "./popover/popover.tsx";
13
+ export type {
14
+ PopoverCloseProps,
15
+ PopoverDescriptionProps,
16
+ PopoverPopupProps,
17
+ PopoverRootProps,
18
+ PopoverTitleProps,
19
+ PopoverTriggerProps,
20
+ } from "./popover/popover.tsx";
21
+ export { Tooltip } from "./tooltip/tooltip.tsx";
22
+ export type {
23
+ TooltipPopupProps,
24
+ TooltipRootProps,
25
+ TooltipTriggerProps,
26
+ } from "./tooltip/tooltip.tsx";
27
+ export type { Align, PlacementProps, Side } from "./placement/placement.ts";
28
+ export { Tabs } from "./tabs/tabs.tsx";
29
+ export type {
30
+ TabsIndicatorProps,
31
+ TabsListProps,
32
+ TabsPanelProps,
33
+ TabsRootProps,
34
+ TabsTabProps,
35
+ } from "./tabs/tabs.tsx";
36
+ export { Menu } from "./menu/menu.tsx";
37
+ export type {
38
+ MenuCheckboxItemProps,
39
+ MenuGroupLabelProps,
40
+ MenuGroupProps,
41
+ MenuItemProps,
42
+ MenuLinkItemProps,
43
+ MenuPopupProps,
44
+ MenuRadioGroupProps,
45
+ MenuRadioItemProps,
46
+ MenuRootProps,
47
+ MenuSeparatorProps,
48
+ MenuSubmenuRootProps,
49
+ MenuSubmenuTriggerProps,
50
+ MenuTriggerProps,
51
+ } from "./menu/menu.tsx";
52
+ export { Combobox } from "./combobox/combobox.tsx";
53
+ export type {
54
+ ComboboxChipsProps,
55
+ ComboboxClearProps,
56
+ ComboboxEmptyProps,
57
+ ComboboxGroupLabelProps,
58
+ ComboboxGroupProps,
59
+ ComboboxInputProps,
60
+ ComboboxItemProps,
61
+ ComboboxLabelProps,
62
+ ComboboxListProps,
63
+ ComboboxPopupProps,
64
+ ComboboxRootProps,
65
+ ComboboxStatusProps,
66
+ ComboboxTriggerProps,
67
+ ComboboxValue,
68
+ } from "./combobox/combobox.tsx";
69
+ export { Select } from "./select/select.tsx";
70
+ export type {
71
+ SelectGroupProps,
72
+ SelectItemProps,
73
+ SelectRootProps,
74
+ SelectSeparatorProps,
75
+ SelectTriggerProps,
76
+ SelectValueProps,
77
+ } from "./select/select.tsx";
78
+ export { Field } from "./field/field.tsx";
79
+ export type {
80
+ FieldControlProps,
81
+ FieldDescriptionProps,
82
+ FieldErrorMatch,
83
+ FieldErrorProps,
84
+ FieldLabelProps,
85
+ FieldRootProps,
86
+ FieldValidate,
87
+ FieldValidityProps,
88
+ FieldValidityState,
89
+ } from "./field/field.tsx";
90
+ export type { ValidationMode } from "./field/context.ts";
91
+ export { Fieldset } from "./fieldset/fieldset.tsx";
92
+ export type { FieldsetLegendProps, FieldsetRootProps } from "./fieldset/fieldset.tsx";
93
+ export { Form } from "./form/form.tsx";
94
+ export type { FormProps } from "./form/form.tsx";
95
+ export type { FormErrors } from "./field/context.ts";
96
+ export { Input } from "./input/input.tsx";
97
+ export type { InputProps } from "./input/input.tsx";
98
+ export { Checkbox } from "./checkbox/checkbox.tsx";
99
+ export type { CheckboxRootProps } from "./checkbox/checkbox.tsx";
100
+ export { CheckboxGroup } from "./checkbox-group/checkbox-group.tsx";
101
+ export type { CheckboxGroupProps } from "./checkbox-group/checkbox-group.tsx";
102
+ export { Radio, RadioGroup } from "./radio-group/radio-group.tsx";
103
+ export type { RadioGroupProps, RadioRootProps } from "./radio-group/radio-group.tsx";
104
+ export { Switch } from "./switch/switch.tsx";
105
+ export type { SwitchRootProps } from "./switch/switch.tsx";
106
+ export { Collapsible } from "./collapsible/collapsible.tsx";
107
+ export type {
108
+ CollapsiblePanelProps,
109
+ CollapsibleRootProps,
110
+ CollapsibleTriggerProps,
111
+ } from "./collapsible/collapsible.tsx";
112
+ export { Accordion } from "./accordion/accordion.tsx";
113
+ export type {
114
+ AccordionHeaderProps,
115
+ AccordionItemProps,
116
+ AccordionPanelProps,
117
+ AccordionRootProps,
118
+ AccordionTriggerProps,
119
+ } from "./accordion/accordion.tsx";
120
+ export { Toast } from "./toast/toast.tsx";
121
+ export type {
122
+ ToastActionProps,
123
+ ToastCloseProps,
124
+ ToastContentProps,
125
+ ToastDescriptionProps,
126
+ ToastManager,
127
+ ToastObject,
128
+ ToastOptions,
129
+ ToastPromiseOptions,
130
+ ToastProviderProps,
131
+ ToastRootProps,
132
+ ToastTitleProps,
133
+ ToastViewportProps,
134
+ } from "./toast/toast.tsx";
135
+ export { Button } from "./button/button.tsx";
136
+ export type { ButtonProps } from "./button/button.tsx";
137
+ export { Toggle } from "./toggle/toggle.tsx";
138
+ export type { ToggleProps } from "./toggle/toggle.tsx";
139
+ export { ToggleGroup } from "./toggle-group/toggle-group.tsx";
140
+ export type { ToggleGroupProps } from "./toggle-group/toggle-group.tsx";
141
+ export { Progress } from "./progress/progress.tsx";
142
+ export type {
143
+ ProgressLabelProps,
144
+ ProgressRootProps,
145
+ ProgressTrackProps,
146
+ ProgressValueProps,
147
+ } from "./progress/progress.tsx";
148
+ export { Meter } from "./meter/meter.tsx";
149
+ export type {
150
+ MeterLabelProps,
151
+ MeterRootProps,
152
+ MeterTrackProps,
153
+ MeterValueProps,
154
+ } from "./meter/meter.tsx";
155
+ export { Slider } from "./slider/slider.tsx";
156
+ export type {
157
+ SliderControlProps,
158
+ SliderIndicatorProps,
159
+ SliderLabelProps,
160
+ SliderRootProps,
161
+ SliderThumbProps,
162
+ SliderTrackProps,
163
+ SliderValue,
164
+ SliderValueProps,
165
+ } from "./slider/slider.tsx";
166
+ export { ContextMenu } from "./context-menu/context-menu.tsx";
167
+ export type {
168
+ ContextMenuPopupProps,
169
+ ContextMenuRootProps,
170
+ ContextMenuTriggerProps,
171
+ } from "./context-menu/context-menu.tsx";
172
+ export { Drawer } from "./drawer/drawer.tsx";
173
+ export type {
174
+ DrawerCloseProps,
175
+ DrawerContentProps,
176
+ DrawerDescriptionProps,
177
+ DrawerPopupProps,
178
+ DrawerRootProps,
179
+ DrawerSwipeDirection,
180
+ DrawerTitleProps,
181
+ DrawerTriggerProps,
182
+ } from "./drawer/drawer.tsx";
183
+ export { NumberField } from "./number-field/number-field.tsx";
184
+ export type {
185
+ NumberFieldDecrementProps,
186
+ NumberFieldGroupProps,
187
+ NumberFieldIncrementProps,
188
+ NumberFieldInputProps,
189
+ NumberFieldRootProps,
190
+ NumberFieldScrubAreaProps,
191
+ } from "./number-field/number-field.tsx";
192
+ export { Autocomplete } from "./autocomplete/autocomplete.tsx";
193
+ export type {
194
+ AutocompleteClearProps,
195
+ AutocompleteEmptyProps,
196
+ AutocompleteGroupLabelProps,
197
+ AutocompleteGroupProps,
198
+ AutocompleteInputProps,
199
+ AutocompleteItemProps,
200
+ AutocompleteLabelProps,
201
+ AutocompleteListProps,
202
+ AutocompleteMode,
203
+ AutocompletePopupProps,
204
+ AutocompleteRootProps,
205
+ AutocompleteStatusProps,
206
+ AutocompleteTriggerProps,
207
+ } from "./autocomplete/autocomplete.tsx";
208
+ export { Separator } from "./separator/separator.tsx";
209
+ export type { SeparatorOrientation, SeparatorProps } from "./separator/separator.tsx";
210
+ export { Avatar } from "./avatar/avatar.tsx";
211
+ export type {
212
+ AvatarFallbackProps,
213
+ AvatarImageProps,
214
+ AvatarLoadingStatus,
215
+ AvatarRootProps,
216
+ } from "./avatar/avatar.tsx";
217
+ export { OTPField } from "./otp-field/otp-field.tsx";
218
+ export type {
219
+ OTPFieldInputProps,
220
+ OTPFieldRootProps,
221
+ OTPFieldSeparatorProps,
222
+ OTPFieldSlotProps,
223
+ OTPFieldValidationType,
224
+ } from "./otp-field/otp-field.tsx";
225
+ export { PreviewCard } from "./preview-card/preview-card.tsx";
226
+ export type {
227
+ PreviewCardPopupProps,
228
+ PreviewCardRootProps,
229
+ PreviewCardTriggerProps,
230
+ } from "./preview-card/preview-card.tsx";
231
+ export { Toolbar } from "./toolbar/toolbar.tsx";
232
+ export type {
233
+ ToolbarButtonProps,
234
+ ToolbarGroupProps,
235
+ ToolbarInputProps,
236
+ ToolbarLinkProps,
237
+ ToolbarRootProps,
238
+ ToolbarSeparatorProps,
239
+ } from "./toolbar/toolbar.tsx";
240
+ export { ScrollArea } from "./scroll-area/scroll-area.tsx";
241
+ export type {
242
+ ScrollAreaContentProps,
243
+ ScrollAreaCornerProps,
244
+ ScrollAreaRootProps,
245
+ ScrollAreaScrollbarProps,
246
+ ScrollAreaThumbProps,
247
+ ScrollAreaViewportProps,
248
+ } from "./scroll-area/scroll-area.tsx";
249
+ export { Menubar } from "./menubar/menubar.tsx";
250
+ export type { MenubarOrientation, MenubarProps } from "./menubar/menubar.tsx";
251
+ export { NavigationMenu } from "./navigation-menu/navigation-menu.tsx";
252
+ export type {
253
+ NavigationMenuContentProps,
254
+ NavigationMenuIconProps,
255
+ NavigationMenuItemProps,
256
+ NavigationMenuLinkProps,
257
+ NavigationMenuListProps,
258
+ NavigationMenuOrientation,
259
+ NavigationMenuRootProps,
260
+ NavigationMenuTriggerProps,
261
+ } from "./navigation-menu/navigation-menu.tsx";
@@ -0,0 +1,67 @@
1
+ // Input (decision-62, TASK-365): a native `<input>` that takes its id, name, disabled state and ARIA
2
+ // references from the Field around it, after Base UI's Input (Field.Control). Everything else is
3
+ // the platform's: types, constraints, autofill, forms.
4
+ import { ref, untrack } from "lumiose";
5
+ import { fieldProps, useField, useFormValues } from "../field/context.ts";
6
+ import { onFormReset } from "../form/reset.ts";
7
+
8
+ /**
9
+ * An element's props without the brand of intrinsic elements' props, so that the component's
10
+ * `value` and `onValueChange` offer `bind:value`.
11
+ */
12
+ type BindableProps<Tag extends keyof JSX.IntrinsicElements, Omitted extends string = never> = Omit<
13
+ JSX.IntrinsicElements[Tag],
14
+ symbol | Omitted
15
+ >;
16
+
17
+ /**
18
+ * An `<input>`, with every native prop (`type`, `required`, `pattern`, `placeholder`, …). Inside a
19
+ * `Field.Root` it takes the field's id, name and ARIA references. `bind:value={signal}` stands in
20
+ * for `value` and `onValueChange`.
21
+ */
22
+ export type InputProps = BindableProps<"input", "value" | "bind:value"> & {
23
+ /** Sets the value when it changes. Pair it with `onValueChange` to keep a signal in sync. */
24
+ value?: string;
25
+ /** The value at first, when `value` is left out; a form reset goes back to it. */
26
+ defaultValue?: string;
27
+ /** Called with the new value on every edit. */
28
+ onValueChange?: (value: string) => void;
29
+ };
30
+
31
+ /**
32
+ * A native text input, part of a `Field` when it is inside one.
33
+ *
34
+ * @example
35
+ * <Input name="q" type="search" aria-label="Search" bind:value={query} />
36
+ */
37
+ /**
38
+ * Types a Form's `values` never fill: secrets, files, buttons, and toggles, which have `checked`.
39
+ * HTML matches `type` in any case, so `PASSWORD` is a password too (TASK-472).
40
+ */
41
+ const UNFILLED = /^\s*(password|file|hidden|submit|reset|button|image|checkbox|radio)\s*$/i;
42
+
43
+ export function Input({ value, defaultValue, onValueChange, ...rest }: InputProps) {
44
+ const field = useField();
45
+ // The value a failed submission sent back (a Form's `values`) replaces the default (TASK-430).
46
+ const sent = UNFILLED.test(String(untrack(() => rest.type) ?? ""))
47
+ ? undefined
48
+ : useFormValues(
49
+ untrack(() => rest.name ?? field?.name()),
50
+ untrack(() => rest.disabled),
51
+ );
52
+ // A form reset fires no `input`: the reset value goes to `onValueChange` (TASK-435).
53
+ const input = ref<HTMLInputElement>();
54
+ if (onValueChange) onFormReset(input, () => input()!.value, onValueChange);
55
+ return (
56
+ <input
57
+ {...rest}
58
+ {...fieldProps(field, rest)}
59
+ ref={(element: HTMLInputElement) => {
60
+ input(element);
61
+ rest.ref?.(element);
62
+ }}
63
+ value={value !== undefined ? value : untrack(() => sent?.[0] ?? defaultValue)}
64
+ on:input={(event: Event) => onValueChange?.((event.currentTarget as HTMLInputElement).value)}
65
+ />
66
+ );
67
+ }
@@ -0,0 +1,191 @@
1
+ // The dismissal model of the anchored layers that @lumiose/ui's client scripts open: Menu, Popover
2
+ // and Combobox (doc-9 section 1, TASK-396). A layer is a manual popover (`popover="manual"`) that
3
+ // its script shows and that this module closes, in one way for all of them:
4
+ //
5
+ // - On an outside press: a press of the main button by the primary pointer whose press and release
6
+ // both land outside the layer, as in Base UI and Zag. A press on a scrollbar, on a node the press
7
+ // removed, inside the popup's box (a password manager's overlay) or in a toast is not outside.
8
+ // Native light dismiss would also close on a right-click or a second finger, and cannot be
9
+ // cancelled: that is why the scripts make their popups manual instead of leaving dismissal to
10
+ // `popover="auto"`.
11
+ // - On a close request: an Escape that no handler took (`preventDefault`) and that no input method
12
+ // composition owns (Safari's, with `keyCode` 229, TASK-438 M21), or the Android back gesture. One request closes the topmost layer only. Escape is handled on `keydown` in every
13
+ // browser (and cancelled, so no native close request follows): a `CloseWatcher` created without
14
+ // user activation joins the previous one's group, and one close request would close the whole
15
+ // group, a hover-opened popover and the dialog it is in, say (TASK-427). A layer opened with user
16
+ // activation also gets a `CloseWatcher`, for the Android back gesture.
17
+ // - When another layer opens outside it, as the auto popover stack does: opening a layer closes the
18
+ // open layers it is not inside of. Layers nest through the DOM (a submenu inside its menu's
19
+ // popup, a combobox inside a popover).
20
+ // - With it: a layer that closes, whatever closed it (a bound `open` set to false), closes the
21
+ // popovers and dialogs open inside it, innermost first (TASK-436 M1).
22
+ //
23
+ // Server HTML keeps `popover="auto"` where a popup should open without JavaScript (Menu, Popover):
24
+ // native light dismiss and Escape work there until a script takes the popup over.
25
+ // It must not import `lumiose`: server-rendered pages load it without the runtime.
26
+
27
+ export interface Layer {
28
+ /** The popup, open while the layer is. */
29
+ popup: HTMLElement;
30
+ /** Whether `node` is part of the layer: its popup, and what opens it (its trigger, its input). */
31
+ contains(node: Node): boolean;
32
+ /** Closes the layer, after an outside press (`press` is where it landed) or a close request. */
33
+ dismiss(press?: Element): void;
34
+ }
35
+
36
+ /** Chrome 126 and Firefox 149; not in TypeScript's DOM types yet. */
37
+ declare const CloseWatcher: (new () => EventTarget & { destroy(): void }) | undefined;
38
+
39
+ interface Entry {
40
+ layer: Layer;
41
+ watcher?: { destroy(): void };
42
+ }
43
+
44
+ /** The open layers, outermost first. */
45
+ const stack: Entry[] = [];
46
+ /** The last press while a layer was open, and whether it counts for an outside press. */
47
+ let press: { target: Element; main: boolean; x: number; y: number } | undefined;
48
+
49
+ /** Whether a press at `x`, `y` on `node` is outside `layer`. */
50
+ function outside({ layer }: Entry, node: Element, x: number, y: number): boolean {
51
+ if (!node.isConnected || layer.contains(node)) return false;
52
+ // Toasts persist: a press in one leaves the open layers alone (TASK-371).
53
+ if (node.closest("lu-toast-viewport")) return false;
54
+ const box = layer.popup.getBoundingClientRect();
55
+ const empty = box.width === 0 && box.height === 0;
56
+ return empty || !(x >= box.left && x <= box.right && y >= box.top && y <= box.bottom);
57
+ }
58
+
59
+ /** Whether a press is on a scrollbar of `target`: past its client box. */
60
+ function onScrollbar(target: Element, event: PointerEvent): boolean {
61
+ const { clientWidth: width, clientHeight: height } = target;
62
+ return width > 0 && height > 0 && (event.offsetX > width || event.offsetY > height);
63
+ }
64
+
65
+ /**
66
+ * Forgets the layers whose popup left the page while open: removing a popover hides it without
67
+ * events, so its script never released it.
68
+ */
69
+ function prune() {
70
+ for (const entry of stack.toReversed()) {
71
+ if (!entry.layer.popup.isConnected) {
72
+ stack.splice(stack.indexOf(entry), 1);
73
+ entry.watcher?.destroy();
74
+ }
75
+ }
76
+ }
77
+
78
+ function onPointerDown(event: PointerEvent) {
79
+ prune();
80
+ if (stack.length === 0) return;
81
+ const target = event.target as Element;
82
+ press = {
83
+ target,
84
+ main: event.button === 0 && event.isPrimary && !onScrollbar(target, event),
85
+ x: event.clientX,
86
+ y: event.clientY,
87
+ };
88
+ }
89
+
90
+ function onPointerUp(event: PointerEvent) {
91
+ const pressed = press;
92
+ press = undefined;
93
+ if (!pressed?.main) return;
94
+ const target = event.target as Element;
95
+ // Innermost first: closing a layer may close the layers inside it.
96
+ for (const entry of stack.toReversed()) {
97
+ if (
98
+ stack.includes(entry) &&
99
+ outside(entry, pressed.target, pressed.x, pressed.y) &&
100
+ outside(entry, target, event.clientX, event.clientY)
101
+ ) {
102
+ entry.layer.dismiss(target);
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Whether a key belongs to an input method's composition: Safari sends the key that commits or
109
+ * cancels one (after `compositionend`) with `keyCode` 229 and `isComposing` false (TASK-438 M21).
110
+ */
111
+ export const composing = (event: KeyboardEvent) => event.isComposing || event.keyCode === 229;
112
+
113
+ function onKeyDown(event: KeyboardEvent) {
114
+ if (event.key !== "Escape" || event.defaultPrevented || composing(event)) return;
115
+ const top = stack.at(-1);
116
+ if (!top) return;
117
+ // Handled: a parent dialog keeps its own close request for the next Escape.
118
+ event.preventDefault();
119
+ top.layer.dismiss();
120
+ }
121
+
122
+ if (typeof document !== "undefined") {
123
+ document.addEventListener("pointerdown", onPointerDown, true);
124
+ document.addEventListener("pointerup", onPointerUp, true);
125
+ // Before the close request an Escape makes, so a removed layer's watcher does not take it.
126
+ document.addEventListener("keydown", prune, true);
127
+ // In the bubble phase, after a layer's own handlers (Menu's, Tooltip's), which cancel the key.
128
+ document.addEventListener("keydown", onKeyDown);
129
+ }
130
+
131
+ /** Whether the page has transient user activation (a click, a key) to give a CloseWatcher. */
132
+ const activated = () =>
133
+ (navigator as Navigator & { userActivation?: { isActive: boolean } }).userActivation?.isActive ===
134
+ true;
135
+
136
+ /**
137
+ * Starts dismissing `layer`, whose popup opens, and closes the open layers it is not inside of.
138
+ * Call the returned function when it closes, whatever closed it.
139
+ */
140
+ export function openLayer(layer: Layer): () => void {
141
+ prune();
142
+ for (const entry of stack.toReversed()) {
143
+ if (stack.includes(entry) && !entry.layer.contains(layer.popup)) entry.layer.dismiss();
144
+ }
145
+ const entry: Entry = { layer };
146
+ // Without activation, a watcher would join the group of the one before (see above).
147
+ if (typeof CloseWatcher === "function" && activated()) {
148
+ const watcher = new CloseWatcher();
149
+ watcher.addEventListener("close", () => layer.dismiss());
150
+ entry.watcher = watcher;
151
+ }
152
+ stack.push(entry);
153
+ return () => {
154
+ const index = stack.indexOf(entry);
155
+ if (index === -1) return;
156
+ stack.splice(index, 1);
157
+ // Now, not a task later: the next close request belongs to the layer below.
158
+ entry.watcher?.destroy();
159
+ closeInside(layer.popup);
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Closes the popovers and dialogs open inside `popup`, innermost first: they must not outlive it
165
+ * (a modal dialog left open in a hidden popover has no box, and takes every click). Toasts, whose
166
+ * region may sit in a dialog, stay.
167
+ */
168
+ export function closeInside(popup: HTMLElement): void {
169
+ const open = [...popup.querySelectorAll<HTMLElement>(":popover-open, dialog[open]")];
170
+ for (const each of open.toReversed()) {
171
+ if (each.localName === "lu-toast-viewport" || each.closest("lu-toast-viewport")) continue;
172
+ if (each instanceof HTMLDialogElement) each.close();
173
+ else if (each.matches(":popover-open")) each.hidePopover();
174
+ }
175
+ }
176
+
177
+ // Pointer moves that are not movements (doc-9 section 1, TASK-361): when the keyboard scrolls a
178
+ // layer's list under a resting pointer, WebKit fires moves at the same position (with zero deltas),
179
+ // and the list must not highlight what passes under it. The position tells, not the deltas: some
180
+ // engines send none (Playwright's WebKit reports `movementX` and `movementY` as 0 for every move).
181
+
182
+ /** The last position seen per element. */
183
+ const last = new WeakMap<object, { x: number; y: number }>();
184
+
185
+ /** Whether `event` moved the pointer since the last move `owner` saw (touch never counts). */
186
+ export function moved(owner: object, event: PointerEvent): boolean {
187
+ if (event.pointerType === "touch") return false;
188
+ const before = last.get(owner);
189
+ last.set(owner, { x: event.clientX, y: event.clientY });
190
+ return before === undefined || before.x !== event.clientX || before.y !== event.clientY;
191
+ }
@@ -0,0 +1,21 @@
1
+ // Decimal digits of any script as ASCII (TASK-440, M34): full-width `1`, Arabic-Indic `٤`,
2
+ // Devanagari `४`… A phone's keyboard or an IME may type them into a number. Number Field's
3
+ // `parseNumber` uses it; OTP Field's script keeps a copy (a shared module would be a chunk of its
4
+ // own).
5
+
6
+ const DIGIT = /\p{Nd}/u;
7
+
8
+ /**
9
+ * The value of a decimal digit (`\p{Nd}`), else `undefined`. Unicode encodes each script's digits
10
+ * as runs of ten, 0 to 9, in order: the count of digits before it in its run, modulo ten.
11
+ */
12
+ export function digitValue(char: string): number | undefined {
13
+ if (!DIGIT.test(char)) return undefined;
14
+ let code = char.codePointAt(0)!;
15
+ let index = 0;
16
+ while (DIGIT.test(String.fromCodePoint(code - 1))) {
17
+ code--;
18
+ index++;
19
+ }
20
+ return index % 10;
21
+ }
@@ -0,0 +1,22 @@
1
+ /*
2
+ * The CSS Menu needs to work, and nothing else (decision-62): `lu-menu` adds no box and the items
3
+ * stack. The popups are placed next to their triggers by the shared placement rules (their
4
+ * `data-side` and `data-align`, `--lu-side-offset` and `--lu-align-offset`, with flips), which
5
+ * Popover and Tooltip use too; where CSS anchor positioning is missing, the client script places
6
+ * them. It sits in the `lu.core` layer, so any app CSS wins.
7
+ */
8
+ @layer lu.core, lu.theme;
9
+ @import "../placement/placement.css";
10
+
11
+ @layer lu.core {
12
+ lu-menu {
13
+ display: contents;
14
+ }
15
+ lu-menu :is([role="menu"]:popover-open, [role="menu"] [role="group"]) {
16
+ display: grid;
17
+ }
18
+ lu-menu [role^="menuitem"] {
19
+ display: flex;
20
+ align-items: center;
21
+ }
22
+ }