@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/docs/meter.md ADDED
@@ -0,0 +1,103 @@
1
+ # Meter
2
+
3
+ > A measurement within a known range (storage used, a password's strength, a rating), after Base UI's Meter: a native `<meter>` (with its value, range and `aria-valuetext`), labelled, with the value as text (a percentage of the range by default, or your `Intl.NumberFormat` options). No script.
4
+
5
+ ```ts
6
+ import { Meter } from "@lumiose/ui"; // or "@lumiose/ui/meter"
7
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
8
+ ```
9
+
10
+ ## Anatomy
11
+
12
+ ```tsx
13
+ <Meter.Root value={72}>
14
+ <Meter.Label />
15
+ <Meter.Value />
16
+ <Meter.Track />
17
+ </Meter.Root>
18
+ ```
19
+
20
+ | Part | Renders |
21
+ | ------- | ----------------------------------------------- |
22
+ | `Root` | `<div>` with `--lu-value` (`0%` to `100%`) |
23
+ | `Label` | `<label for>` the track |
24
+ | `Value` | `<span>` with the formatted value |
25
+ | `Track` | `<meter value min max aria-valuetext>`: the bar |
26
+
27
+ ## API
28
+
29
+ ### Meter.Root
30
+
31
+ | Prop | Type | Default | |
32
+ | ------------------ | --------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------- |
33
+ | `value` | `number` | | The measurement, from `min` to `max`. Required. |
34
+ | `min` | `number` | `0` | The bottom of the range. |
35
+ | `max` | `number` | `100` | The top of the range. |
36
+ | `format` | `Intl.NumberFormatOptions` | a percentage | How the value is written. With it, the value itself is formatted, not a percentage. |
37
+ | `locale` | `Intl.LocalesArgument` | the browser's | The locale of the formatted value (the server's in server HTML). |
38
+ | `getAriaValueText` | `(formattedValue: string, value: number) => string` | the formatted value | The text assistive technology reads. |
39
+
40
+ Every other prop passes through to the `<div>`: `id`, `class`, `style`.
41
+
42
+ ### Meter.Label, Meter.Track and Meter.Value
43
+
44
+ `Label` takes `<label>` props, its `id` among them; the `Track` keeps its own `id`, which the
45
+ `Label` before it points at (see [Ids](README.md#ids)). `Track` takes `<meter>` props, among them the native `low`, `high`
46
+ and `optimum`, which tell browsers which band a value is in (`aria-label` when there is no
47
+ `Label`). `Value` takes `<span>` props, and its `children` may be a function
48
+ `(formattedValue, value) => JSX.Element`.
49
+
50
+ **Differences from Base UI's Meter.** The `<meter>` element is the track and the indicator: there is
51
+ no `Meter.Indicator`. Root has no `role`; the `<meter>` has it (`meter`).
52
+
53
+ ## Example
54
+
55
+ ```tsx check
56
+ import { Meter } from "@lumiose/ui";
57
+
58
+ export function StorageMeter() {
59
+ return (
60
+ <Meter.Root
61
+ value={7.2}
62
+ max={10}
63
+ getAriaValueText={(_, gb) => `${gb} of 10 GB`}
64
+ class="lu-meter"
65
+ >
66
+ <Meter.Label>Storage used</Meter.Label>
67
+ <Meter.Value>{(_, gb) => `${gb} GB of 10`}</Meter.Value>
68
+ <Meter.Track low={6} high={9} optimum={0} />
69
+ </Meter.Root>
70
+ );
71
+ }
72
+ ```
73
+
74
+ ## Styling
75
+
76
+ **With the default theme**, put `lu-meter` on `Root`: the label and the value on a row above a 10px
77
+ bar, its edge in `--lu-input` (3:1, so the whole range shows) and its value in the primary colour
78
+ (a lighter blue in dark mode), whatever band it is in.
79
+
80
+ **With your own CSS**, style the bar with `appearance: none` and its pseudo-elements:
81
+ `::-webkit-meter-bar`, `::-webkit-meter-optimum-value`, `::-webkit-meter-suboptimum-value`,
82
+ `::-webkit-meter-even-less-good-value`, `::-moz-meter-bar` (and `:-moz-meter-optimum` and the
83
+ others). Or draw your own bar from `--lu-value` on `Root`.
84
+
85
+ Meter ships no stylesheet: it needs none to work.
86
+
87
+ ## Keyboard and focus
88
+
89
+ A meter is not interactive: it takes no focus and no keys.
90
+
91
+ ## Without JavaScript
92
+
93
+ Fully: the server renders the `<meter>` with its value, range, label and text.
94
+
95
+ ## Browser support
96
+
97
+ Native `<meter>`: every browser. Its pseudo-elements differ between engines, hence the two sets
98
+ above. `role="meter"` reaches screen readers in recent versions; older ones read a progress bar.
99
+
100
+ ## Related
101
+
102
+ - [Progress](progress.md): a task's progress, which can be indeterminate.
103
+ - [Slider](slider.md): a value the user picks within a range.
@@ -0,0 +1,257 @@
1
+ # Navigation Menu
2
+
3
+ > A site's navigation whose items open panels of links, after Base UI's NavigationMenu and the WAI-ARIA disclosure navigation pattern: a `<nav>` landmark around a list, each trigger opening its content through `popovertarget` (a native popover placed under it by CSS anchor positioning), so the links work and the panels open on a click, one at a time, in server HTML with no JavaScript. A 3.5 kB script adds hover intent, the arrow keys, closing when focus leaves and the direction a panel comes in from. `bind:value` (or `value` and `onValueChange`) keeps the open item in a signal.
4
+
5
+ ```ts
6
+ import { NavigationMenu } from "@lumiose/ui"; // or "@lumiose/ui/navigation-menu"
7
+ import "@lumiose/ui/navigation-menu.css"; // places each content next to its trigger
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <NavigationMenu.Root>
15
+ <NavigationMenu.List>
16
+ <NavigationMenu.Item>
17
+ <NavigationMenu.Trigger>
18
+ <NavigationMenu.Icon />
19
+ </NavigationMenu.Trigger>
20
+ <NavigationMenu.Content>
21
+ <NavigationMenu.Link />
22
+ </NavigationMenu.Content>
23
+ </NavigationMenu.Item>
24
+ <NavigationMenu.Item>
25
+ <NavigationMenu.Link />
26
+ </NavigationMenu.Item>
27
+ </NavigationMenu.List>
28
+ </NavigationMenu.Root>
29
+ ```
30
+
31
+ | Part | Renders |
32
+ | --------- | ---------------------------------------------------------------------------------------------------------- |
33
+ | `Root` | `<nav data-orientation="…">` inside `<lu-navigation-menu>`, an element with no box; a `<div>` when nested. |
34
+ | `List` | `<ul>` of the items. |
35
+ | `Item` | `<li data-value="…">`: a trigger with its content, or a link. |
36
+ | `Trigger` | `<button type="button" popovertarget="…">`; `aria-expanded` follows its content where the script runs. |
37
+ | `Icon` | `<span aria-hidden="true">` inside the trigger, such as a chevron. |
38
+ | `Content` | `<div popover="auto" data-side="…" data-align="…">` of links, anchored to its trigger. |
39
+ | `Link` | `<a>`; `aria-current="page"` and `data-active` when `active`. |
40
+
41
+ Give `Root` an `aria-label` ("Main") when the page has more than one `<nav>`. A `Root` inside a
42
+ `Content` is a nested menu: a `<div>`, inside its parent's landmark. Focus, the pointer or a click
43
+ in it keep its parent's content open.
44
+
45
+ ## API
46
+
47
+ ### NavigationMenu.Root
48
+
49
+ | Prop | Type | Default | |
50
+ | --------------- | --------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------- |
51
+ | `value` | `string \| null` | | The `value` of the item whose content is open, or `null`; opens or closes them when it changes, in the browser. |
52
+ | `defaultValue` | `string \| null` | `null` | The item open at first, in the browser, when `value` is left out. |
53
+ | `onValueChange` | `(value: string \| null) => void` | | Called after a content opens (its item's `value`) or the open one closes (`null`), whatever did it. |
54
+ | `bind:value` | `Signal<string \| null>` | | Stands in for `value` and `onValueChange`. |
55
+ | `delay` | `number` | `50` | How long the pointer rests on a trigger before its content opens, in milliseconds. |
56
+ | `closeDelay` | `number` | `50` | How long after the pointer leaves an item its content closes, in milliseconds (300 while it heads for the content). |
57
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Which way the items run: vertical moves with Arrow Up and Down and places contents at the inline end. |
58
+
59
+ Server HTML always starts closed: a popover only opens from its trigger or script.
60
+
61
+ ### NavigationMenu.Item
62
+
63
+ | Prop | Type | Default | |
64
+ | ------- | -------- | -------------- | --------------------------------------- |
65
+ | `value` | `string` | a generated id | The value `Root`'s `value` names it by. |
66
+
67
+ ### NavigationMenu.Content
68
+
69
+ | Prop | Type | Default | |
70
+ | ------------- | -------------------------------------------------------------------------- | ----------------------------------------- | ----------------------------------------------- |
71
+ | `side` | `"top" \| "bottom" \| "left" \| "right" \| "inline-start" \| "inline-end"` | `"bottom"` (`"inline-end"` when vertical) | The trigger's side it goes on; it flips to fit. |
72
+ | `align` | `"start" \| "center" \| "end"` | `"center"` (`"start"` when vertical) | How it aligns with the trigger along that side. |
73
+ | `sideOffset` | `number` | | The gap to the trigger, in CSS pixels. |
74
+ | `alignOffset` | `number` | | A shift along the side, in CSS pixels. |
75
+
76
+ ### NavigationMenu.Link
77
+
78
+ | Prop | Type | Default | |
79
+ | -------------- | --------- | ------- | ----------------------------------------------------------------------------- |
80
+ | `active` | `boolean` | `false` | The link to the current page: `aria-current="page"`. |
81
+ | `closeOnClick` | `boolean` | `false` | Whether following it closes the open content (every level, in a nested menu). |
82
+
83
+ ### The other parts
84
+
85
+ Each part takes the props of the element it renders (`class`, `style`, `aria-*`, `data-*`, event
86
+ handlers) and passes them through: `href` and `target` on a `Link`, `disabled` and `id` on a
87
+ `Trigger`. A `Content` keeps its own `id`, which its trigger points at (see [Ids](README.md#ids)).
88
+
89
+ **Differences from Base UI's NavigationMenu.** Each `Content` is its own popover in the top layer,
90
+ anchored to its trigger: there is no shared `Viewport`, `Popup`, `Positioner`, `Portal`,
91
+ `Backdrop` or `Arrow`, and so no `--popup-width` and `--popup-height` to animate one box between
92
+ contents. `data-activation-direction` lets a content slide in from where the previous one was. No
93
+ `actionsRef`, `onOpenChangeComplete`, `keepMounted` or `render`.
94
+
95
+ ## Example
96
+
97
+ A site's main navigation in a server-rendered page, with no island, styled by the default theme:
98
+
99
+ ```tsx check
100
+ import { NavigationMenu } from "@lumiose/ui";
101
+
102
+ export function MainNavigation() {
103
+ return (
104
+ <NavigationMenu.Root aria-label="Main" class="lu-navigation-menu">
105
+ <NavigationMenu.List>
106
+ <NavigationMenu.Item>
107
+ <NavigationMenu.Trigger>
108
+ Products <NavigationMenu.Icon>▾</NavigationMenu.Icon>
109
+ </NavigationMenu.Trigger>
110
+ <NavigationMenu.Content>
111
+ <ul>
112
+ <li>
113
+ <NavigationMenu.Link href="#analytics">Analytics</NavigationMenu.Link>
114
+ </li>
115
+ <li>
116
+ <NavigationMenu.Link href="#automation">Automation</NavigationMenu.Link>
117
+ </li>
118
+ </ul>
119
+ </NavigationMenu.Content>
120
+ </NavigationMenu.Item>
121
+ <NavigationMenu.Item>
122
+ <NavigationMenu.Trigger>
123
+ Learn <NavigationMenu.Icon>▾</NavigationMenu.Icon>
124
+ </NavigationMenu.Trigger>
125
+ <NavigationMenu.Content>
126
+ <ul>
127
+ <li>
128
+ <NavigationMenu.Link href="#guides">Guides</NavigationMenu.Link>
129
+ </li>
130
+ <li>
131
+ <NavigationMenu.Link href="#blog">Blog</NavigationMenu.Link>
132
+ </li>
133
+ </ul>
134
+ </NavigationMenu.Content>
135
+ </NavigationMenu.Item>
136
+ <NavigationMenu.Item>
137
+ <NavigationMenu.Link href="#pricing" active>
138
+ Pricing
139
+ </NavigationMenu.Link>
140
+ </NavigationMenu.Item>
141
+ </NavigationMenu.List>
142
+ </NavigationMenu.Root>
143
+ );
144
+ }
145
+ ```
146
+
147
+ In an island, the open item in a signal:
148
+
149
+ ```tsx check
150
+ import { NavigationMenu } from "@lumiose/ui";
151
+ import { signal } from "lumiose";
152
+
153
+ export function SiteNavigation() {
154
+ const open = signal<string | null>(null);
155
+ return (
156
+ <>
157
+ <NavigationMenu.Root aria-label="Site" bind:value={open} class="lu-navigation-menu">
158
+ <NavigationMenu.List>
159
+ <NavigationMenu.Item value="docs">
160
+ <NavigationMenu.Trigger>Docs</NavigationMenu.Trigger>
161
+ <NavigationMenu.Content>
162
+ <NavigationMenu.Link href="#start">Getting started</NavigationMenu.Link>
163
+ </NavigationMenu.Content>
164
+ </NavigationMenu.Item>
165
+ <NavigationMenu.Item value="community">
166
+ <NavigationMenu.Trigger>Community</NavigationMenu.Trigger>
167
+ <NavigationMenu.Content>
168
+ <NavigationMenu.Link href="#forum">Forum</NavigationMenu.Link>
169
+ </NavigationMenu.Content>
170
+ </NavigationMenu.Item>
171
+ </NavigationMenu.List>
172
+ </NavigationMenu.Root>
173
+ <p>Open: {open() ?? "none"}</p>
174
+ </>
175
+ );
176
+ }
177
+ ```
178
+
179
+ ## Styling
180
+
181
+ **With the default theme**, put `lu-navigation-menu` on `Root`: the list in a row (a column when
182
+ vertical), the triggers and links as 36px targets, the current page underlined, an open trigger
183
+ filled with its icon turned, and each content a surface of links that fades and slides in.
184
+
185
+ **With your own CSS**, style the parts through `class`, the triggers' `[aria-expanded="true"]`,
186
+ links' `[aria-current="page"]`, and the contents' `:popover-open`, `[data-side]` and
187
+ `[data-activation-direction]` (`left`, `right`, `up` or `down`: where the content's trigger is from
188
+ the trigger of the content that was open before; absent when none was):
189
+
190
+ ```css
191
+ .panel {
192
+ transition:
193
+ opacity 150ms,
194
+ translate 150ms,
195
+ overlay 150ms allow-discrete,
196
+ display 150ms allow-discrete;
197
+ }
198
+ .panel:not(:popover-open) {
199
+ opacity: 0;
200
+ }
201
+ @starting-style {
202
+ .panel[data-activation-direction="right"]:popover-open {
203
+ translate: 1rem 0;
204
+ }
205
+ }
206
+ ```
207
+
208
+ `@lumiose/ui/navigation-menu.css`, in the `lu.core` layer, holds the shared placement rules that put
209
+ each content next to its trigger, with the same flips as Popover and Menu, and gives
210
+ `<lu-navigation-menu>` no box.
211
+
212
+ ## Keyboard and focus
213
+
214
+ | Key | Does |
215
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------- |
216
+ | Tab, Shift+Tab | Every trigger and link is a tab stop; from an open trigger, Tab goes into its content. |
217
+ | Enter, Space | On a trigger: opens or closes its content. |
218
+ | Arrow Right, Arrow Left | Between the top-level triggers and links (Down and Up when vertical; swapped right to left). |
219
+ | Home, End | The first or last top-level trigger or link. |
220
+ | Arrow Down | On a trigger: opens its content on its first link (Arrow Right when vertical). In a content: the next link. |
221
+ | Arrow Up | In a content: the previous link. |
222
+ | Escape | Closes the open content and returns to its trigger. |
223
+
224
+ - **The pointer**: resting on a trigger opens its content after `delay`, at once while another one
225
+ is open; leaving the item closes it after `closeDelay`, unless a click kept it open. On the way
226
+ from a trigger to its open content, another trigger the pointer crosses waits 300ms before it
227
+ takes over, so a diagonal path into a wide panel keeps it. A click toggles a content; a click
228
+ outside closes it, but not a right-click or a press in a toast.
229
+ - **Focus** leaving an open item for elsewhere on the page closes its content. It is a navigation
230
+ landmark, not a menu: links keep their roles and the browser's link behaviour.
231
+
232
+ ## Without JavaScript, and the client script
233
+
234
+ The `<nav>`, its links and the triggers are HTML: each trigger opens its content with
235
+ `popovertarget`, one at a time (an auto popover closes the others), and Escape or a click outside
236
+ closes it. `value`, `onValueChange` and `bind:value` need the component to run in an island.
237
+
238
+ A page that renders a `NavigationMenu` loads a 3.5 kB (brotli) script (its dismissal module is
239
+ Menu's and Popover's too), without the Lumiose runtime; islands bundle it. `<lu-navigation-menu>`
240
+ takes over the trigger's click (so a click keeps a content opened by hover open), opens contents on
241
+ hover, adds the arrow keys, closes a content when focus leaves its item, dismisses contents as Menu
242
+ and Popover do (a press of the main button outside, an Escape, another layer opening), keeps
243
+ `aria-expanded` and `data-activation-direction` current, and places the contents where CSS anchor
244
+ positioning is missing.
245
+
246
+ ## Browser support
247
+
248
+ - **Popovers and `popovertarget`**: every current browser.
249
+ - **CSS anchor positioning**: Chrome and Edge 125, Firefox 147, Safari 26. Elsewhere the script
250
+ places the contents next to their triggers.
251
+ - **Exit animations** that keep a content in the top layer (`overlay`) are Chromium-only.
252
+
253
+ ## Related
254
+
255
+ - [Menubar](menubar.md): a bar of application menus, with the menu roles.
256
+ - [Popover](popover.md): one popup anchored to its trigger.
257
+ - [UI components](README.md): the approach, styling and the other components.
@@ -0,0 +1,181 @@
1
+ # Number Field
2
+
3
+ > A number input with buttons to step it, after Base UI's NumberField and the APG spinbutton. Without `format` it is a native `<input type="number">`, which steps, validates and submits without JavaScript; with `format` (`Intl.NumberFormat` options: a currency, a percentage, grouping) it shows the formatted number, reads what is typed back in the same locale, and a hidden input submits the number. `<lu-number-field>`'s 2.6 kB script adds Page Up and Page Down, Home and End, press-and-hold buttons, a scrub area, the wheel when asked, and commit-time clamping. `bind:value` keeps a signal in sync.
4
+
5
+ ```ts
6
+ import { NumberField } from "@lumiose/ui"; // or "@lumiose/ui/number-field"
7
+ import "@lumiose/ui/number-field.css"; // or "@lumiose/ui/styles.css" for every component
8
+ import "@lumiose/ui/themes/default.css"; // optional: the default theme
9
+ ```
10
+
11
+ ## Anatomy
12
+
13
+ ```tsx
14
+ <NumberField.Root>
15
+ <NumberField.ScrubArea />
16
+ <NumberField.Group>
17
+ <NumberField.Decrement />
18
+ <NumberField.Input />
19
+ <NumberField.Increment />
20
+ </NumberField.Group>
21
+ </NumberField.Root>
22
+ ```
23
+
24
+ | Part | Renders |
25
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
26
+ | `Root` | `<lu-number-field>` with the range and format as attributes, and an `<input type="hidden">` that submits the number with `format` |
27
+ | `ScrubArea` | `<span data-scrub>`: dragging across it steps the number (usually around the label) |
28
+ | `Group` | `<div>` around the input and the buttons |
29
+ | `Input` | `<input type="number" inputmode min max step>`, or with `format` `<input type="text" role="spinbutton" aria-value…>` |
30
+ | `Increment` | `<button type="button" tabindex="-1" aria-label="Increase" aria-controls="…">`, disabled at `max` |
31
+ | `Decrement` | `<button type="button" tabindex="-1" aria-label="Decrease" aria-controls="…">`, disabled at `min` |
32
+
33
+ Name the field with a [Field](field.md) (`Field.Label`), a `<label for>` the Root's `id`, or an
34
+ `aria-label` on the `Input`.
35
+
36
+ ## API
37
+
38
+ ### NumberField.Root
39
+
40
+ | Prop | Type | Default | |
41
+ | ---------------------------------- | --------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
+ | `value` | `number \| null` | | Shows this number (`null`: empty) when it changes. Pair it with `onValueChange`. |
43
+ | `defaultValue` | `number \| null` | `null` | The number at first; a form reset goes back to it. |
44
+ | `onValueChange` | `(value: number \| null) => void` | | Called as the number changes: typing, a key, a button, the wheel, a drag. |
45
+ | `onValueCommitted` | `(value: number \| null) => void` | | Called when a change is done: the input left, a key pressed, a button or a drag released. |
46
+ | `bind:value` | `Signal<number \| null>` | | Stands in for `value` and `onValueChange`. |
47
+ | `min`, `max` | `number` | | The range. A typed number outside it becomes its end when committed. |
48
+ | `step` | `number` | `1` | The step of the arrow keys, the buttons, the wheel and a drag. |
49
+ | `largeStep` | `number` | `10` | The step of Page Up, Page Down, and Shift with an arrow key or a button. |
50
+ | `smallStep` | `number` | `0.1` | The step of Alt with an arrow key or a button. |
51
+ | `snapOnStep` | `boolean` | `false` | A step lands on the next multiple of its size (from `min`, or 0): 1.3 steps up to 2, not 2.3. |
52
+ | `format` | `Intl.NumberFormatOptions` | | How the number is shown; with it, the input is a text field and a hidden input submits the number. Options or a `locale` the browser refuses fall back to its defaults. |
53
+ | `locale` | `string` | the browser's | The locale of the formatted number (the server's in server HTML). |
54
+ | `allowWheelScrub` | `boolean` | `false` | The wheel steps the number while the input has focus and the pointer is over it. |
55
+ | `allowOutOfRange` | `boolean` | `false` | Typed numbers outside the range stay as typed, for the browser's validation to report. |
56
+ | `name`, `form` | `string` | | The name the number is submitted under, and the id of its form when it is outside it. |
57
+ | `id` | `string` | generated | The input's id, for a `<label for>`. |
58
+ | `required`, `disabled`, `readOnly` | `boolean` | `false` | As on a native input; read-only and disabled fields' buttons are disabled. |
59
+
60
+ ### NumberField.ScrubArea
61
+
62
+ | Prop | Type | Default | |
63
+ | ------------------ | ---------------------------- | -------------- | ------------------------------------------------------------ |
64
+ | `direction` | `"horizontal" \| "vertical"` | `"horizontal"` | The drag that changes the number: right goes up, or up does. |
65
+ | `pixelSensitivity` | `number` | `2` | How many pixels of drag make one step. |
66
+
67
+ ### The other parts
68
+
69
+ `Group` takes `<div>` props. `Input` takes `<input>` props except the range's own (`type`, `value`,
70
+ `min`, `max`, `step`, `name`, `required`, `readonly`, `id`: the Root's). `Increment` and
71
+ `Decrement` take `<button>` props; give them an `aria-label` in the page's language. Inside a
72
+ [Field](field.md), the input takes the field's id, name, description and error.
73
+
74
+ **Native or formatted.** A native number input cannot show grouping, a currency or a percentage,
75
+ and reads a typed number in the browser's own way, so a formatted field is a text field with the
76
+ spinbutton role, and the number it submits sits in a hidden input. The native input is the default
77
+ because it needs no JavaScript: typing, the arrow keys, `min`, `max` and `step` validation and the
78
+ form value are the browser's.
79
+
80
+ With `snapOnStep`, each kind of step snaps to its own size: Shift steps 37 up to 40, Alt steps
81
+ 1.37 up to 1.4. A native field (no `format`) validates the value against `step` without
82
+ JavaScript; the script sets its `step` to `any`, so the values Alt's small step, Home, End and
83
+ clamping give always submit.
84
+
85
+ The keyboard: `inputmode="decimal"` when `min` is 0 or more, `"text"` otherwise, since iOS's
86
+ decimal keyboard has no minus sign. Digits of other scripts (full-width `1`, Arabic-Indic `٤`) are
87
+ read as numbers.
88
+
89
+ **Differences from Base UI's NumberField.** No `ScrubAreaCursor`: Base UI draws a cursor because
90
+ its scrub area locks the pointer (which hides the real one); this scrub area keeps the real cursor
91
+ (a resize cursor), so a drag stops at the screen's edge. No `inputRef`, no `render`. `locale` is a
92
+ string, written to the element for its script.
93
+
94
+ ## Example
95
+
96
+ ```tsx check
97
+ import { NumberField } from "@lumiose/ui";
98
+ import { signal } from "lumiose";
99
+
100
+ export function PriceField() {
101
+ const price = signal<number | null>(19.9);
102
+ return (
103
+ <NumberField.Root
104
+ id="price"
105
+ name="price"
106
+ bind:value={price}
107
+ min={0}
108
+ step={0.5}
109
+ format={{ style: "currency", currency: "EUR" }}
110
+ class="lu-number-field"
111
+ >
112
+ <NumberField.ScrubArea>
113
+ <label for="price">Price</label>
114
+ </NumberField.ScrubArea>
115
+ <NumberField.Group>
116
+ <NumberField.Decrement aria-label="Lower the price">−</NumberField.Decrement>
117
+ <NumberField.Input />
118
+ <NumberField.Increment aria-label="Raise the price">+</NumberField.Increment>
119
+ </NumberField.Group>
120
+ </NumberField.Root>
121
+ );
122
+ }
123
+ ```
124
+
125
+ ## Styling
126
+
127
+ **With the default theme**, put `lu-number-field` on `Root`: the label above one bordered group,
128
+ the input centred between two 36px buttons that dim at the ends of the range, and a focus ring
129
+ around the whole group.
130
+
131
+ **With your own CSS**, style the parts through `class`, the input's `:focus-visible`, the buttons'
132
+ `:disabled` and `[data-action="increment"]` / `[data-action="decrement"]`, the scrub area's
133
+ `[data-scrub]`, and `lu-number-field[data-scrubbing]` while a drag is on.
134
+
135
+ `@lumiose/ui/number-field.css` (0.2 kB brotli) hides the native spinners once the script runs
136
+ (`:defined`) and hides the buttons until then, keeps held buttons from selecting text, and gives a
137
+ scrub area its resize cursor and `touch-action: none`.
138
+
139
+ ## Keyboard and focus
140
+
141
+ | Key | Does |
142
+ | ----------------------- | ------------------------------------------------------------------------- |
143
+ | Arrow Up / Arrow Down | One step up or down. |
144
+ | Shift + Arrow Up / Down | `largeStep` up or down. |
145
+ | Alt + Arrow Up / Down | `smallStep` up or down. |
146
+ | Page Up / Page Down | `largeStep` up or down. |
147
+ | Home / End | `min` or `max`, when set. |
148
+ | Enter | Commits the typed number (clamped, formatted); a form submits. |
149
+ | Tab | Moves on: one tab stop, the input (the buttons are not in the tab order). |
150
+
151
+ Right to left, dragging the scrub area toward the left steps up, as a slider runs. A press on a
152
+ button steps once (by `largeStep` with Shift, `smallStep` with Alt) and, with a mouse, puts focus
153
+ in the input, so the arrow keys go on stepping (a touch leaves focus where it is, so no virtual
154
+ keyboard opens); holding it steps again every 60 ms after 400 ms. A click from a keyboard or a screen reader on a button steps once. Every drag (the
155
+ scrub area) has the arrow keys as its alternative (WCAG 2.5.7). The wheel steps the number only
156
+ with `allowWheelScrub`, while the input has focus and the pointer is over it; otherwise the wheel
157
+ scrolls the page, as over any field (Chromium's own stepping of a focused number input is turned
158
+ off). Ctrl with the wheel zooms the page, and a sideways swipe never steps. Text that is no number
159
+ (Firefox lets letters into a number input) goes on commit, and the last number comes back.
160
+
161
+ ## Without JavaScript, and the element's script
162
+
163
+ Without `format`, the native number input works and submits with no script; the buttons, which
164
+ need it, stay hidden and the native spinners show. With `format`, the text input submits its text
165
+ as typed until the script moves the name to the hidden input, so a form that must work without
166
+ JavaScript should leave `format` out.
167
+
168
+ `<lu-number-field>`'s script (2.6 kB brotli) adds the large step, Home and End, the buttons, the
169
+ scrub area and the wheel, clamps and formats on commit, keeps the hidden input, `aria-valuenow`
170
+ and `aria-valuetext` current, and follows a form reset.
171
+
172
+ ## Browser support
173
+
174
+ Native number inputs and `Intl.NumberFormat` (with `formatToParts`): every supported browser.
175
+ Scrubbing uses pointer capture, without pointer lock.
176
+
177
+ ## Related
178
+
179
+ - [Field](field.md): a label, a description and errors for a number field.
180
+ - [Slider](slider.md): a number picked by dragging along a range.
181
+ - [Input](input.md): any other text.