@funstack/ui 0.1.0-alpha.3 → 0.1.0-alpha.5

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 (230) hide show
  1. package/dist/accordion/accordion-root.mjs +2 -1
  2. package/dist/accordion/accordion-root.mjs.map +1 -1
  3. package/dist/combobox/combobox-clear.mjs +2 -1
  4. package/dist/combobox/combobox-clear.mjs.map +1 -1
  5. package/dist/combobox/combobox-list.mjs +2 -1
  6. package/dist/combobox/combobox-list.mjs.map +1 -1
  7. package/dist/combobox/combobox-popup.d.mts.map +1 -1
  8. package/dist/combobox/combobox-popup.mjs +7 -2
  9. package/dist/combobox/combobox-popup.mjs.map +1 -1
  10. package/dist/combobox/combobox-root.mjs +1 -1
  11. package/dist/combobox/combobox-trigger.mjs +2 -1
  12. package/dist/combobox/combobox-trigger.mjs.map +1 -1
  13. package/dist/dialog/dialog-close.mjs +2 -1
  14. package/dist/dialog/dialog-close.mjs.map +1 -1
  15. package/dist/dialog/dialog-form.d.mts +14 -8
  16. package/dist/dialog/dialog-form.d.mts.map +1 -1
  17. package/dist/dialog/dialog-form.mjs +36 -5
  18. package/dist/dialog/dialog-form.mjs.map +1 -1
  19. package/dist/dialog/dialog-popup.d.mts.map +1 -1
  20. package/dist/dialog/dialog-popup.mjs +13 -5
  21. package/dist/dialog/dialog-popup.mjs.map +1 -1
  22. package/dist/dialog/dialog-root.mjs +1 -1
  23. package/dist/dialog/dialog-trigger.mjs +2 -1
  24. package/dist/dialog/dialog-trigger.mjs.map +1 -1
  25. package/dist/field/field-root.mjs +5 -3
  26. package/dist/field/field-root.mjs.map +1 -1
  27. package/dist/form/form-root.mjs +2 -1
  28. package/dist/form/form-root.mjs.map +1 -1
  29. package/dist/form/form-submit.mjs +2 -1
  30. package/dist/form/form-submit.mjs.map +1 -1
  31. package/dist/index.d.mts +27 -1
  32. package/dist/index.mjs +6 -1
  33. package/dist/internal/announce.mjs +53 -0
  34. package/dist/internal/announce.mjs.map +1 -0
  35. package/dist/internal/decimal.mjs +27 -0
  36. package/dist/internal/decimal.mjs.map +1 -0
  37. package/dist/internal/dev-warn.mjs +17 -0
  38. package/dist/internal/dev-warn.mjs.map +1 -0
  39. package/dist/internal/field-context.mjs.map +1 -1
  40. package/dist/internal/range-value.mjs +51 -0
  41. package/dist/internal/range-value.mjs.map +1 -0
  42. package/dist/internal/render-element.mjs +54 -0
  43. package/dist/internal/render-element.mjs.map +1 -0
  44. package/dist/internal/toolbar-context.mjs +17 -0
  45. package/dist/internal/toolbar-context.mjs.map +1 -0
  46. package/dist/internal/use-exit-animation.mjs +26 -6
  47. package/dist/internal/use-exit-animation.mjs.map +1 -1
  48. package/dist/internal/use-field-control.mjs +5 -3
  49. package/dist/internal/use-field-control.mjs.map +1 -1
  50. package/dist/internal/use-popover-popup.mjs +3 -2
  51. package/dist/internal/use-popover-popup.mjs.map +1 -1
  52. package/dist/internal/use-popover-trigger.mjs +1 -1
  53. package/dist/internal/use-render.mjs +11 -34
  54. package/dist/internal/use-render.mjs.map +1 -1
  55. package/dist/internal/use-roving-focus.mjs +301 -0
  56. package/dist/internal/use-roving-focus.mjs.map +1 -0
  57. package/dist/menu/menu-checkbox-item.mjs +2 -1
  58. package/dist/menu/menu-checkbox-item.mjs.map +1 -1
  59. package/dist/menu/menu-item.mjs +2 -1
  60. package/dist/menu/menu-item.mjs.map +1 -1
  61. package/dist/menu/menu-popup.d.mts.map +1 -1
  62. package/dist/menu/menu-popup.mjs +7 -2
  63. package/dist/menu/menu-popup.mjs.map +1 -1
  64. package/dist/menu/menu-radio-item.mjs +2 -1
  65. package/dist/menu/menu-radio-item.mjs.map +1 -1
  66. package/dist/menu/menu-root.mjs +1 -1
  67. package/dist/menu/menu-trigger.d.mts +2 -1
  68. package/dist/menu/menu-trigger.d.mts.map +1 -1
  69. package/dist/menu/menu-trigger.mjs +15 -4
  70. package/dist/menu/menu-trigger.mjs.map +1 -1
  71. package/dist/meter/index.d.mts +24 -0
  72. package/dist/meter/index.d.mts.map +1 -0
  73. package/dist/meter/index.mjs +23 -0
  74. package/dist/meter/index.mjs.map +1 -0
  75. package/dist/meter/meter-context.mjs +13 -0
  76. package/dist/meter/meter-context.mjs.map +1 -0
  77. package/dist/meter/meter-indicator.d.mts +19 -0
  78. package/dist/meter/meter-indicator.d.mts.map +1 -0
  79. package/dist/meter/meter-indicator.mjs +25 -0
  80. package/dist/meter/meter-indicator.mjs.map +1 -0
  81. package/dist/meter/meter-label.d.mts +19 -0
  82. package/dist/meter/meter-label.d.mts.map +1 -0
  83. package/dist/meter/meter-label.mjs +28 -0
  84. package/dist/meter/meter-label.mjs.map +1 -0
  85. package/dist/meter/meter-region.d.mts +4 -0
  86. package/dist/meter/meter-region.d.mts.map +1 -0
  87. package/dist/meter/meter-region.mjs +39 -0
  88. package/dist/meter/meter-region.mjs.map +1 -0
  89. package/dist/meter/meter-root.d.mts +71 -0
  90. package/dist/meter/meter-root.d.mts.map +1 -0
  91. package/dist/meter/meter-root.mjs +83 -0
  92. package/dist/meter/meter-root.mjs.map +1 -0
  93. package/dist/meter/meter-track.d.mts +19 -0
  94. package/dist/meter/meter-track.d.mts.map +1 -0
  95. package/dist/meter/meter-track.mjs +25 -0
  96. package/dist/meter/meter-track.mjs.map +1 -0
  97. package/dist/meter/meter-value.d.mts +20 -0
  98. package/dist/meter/meter-value.d.mts.map +1 -0
  99. package/dist/meter/meter-value.mjs +29 -0
  100. package/dist/meter/meter-value.mjs.map +1 -0
  101. package/dist/number-field/index.d.mts +26 -0
  102. package/dist/number-field/index.d.mts.map +1 -0
  103. package/dist/number-field/index.mjs +26 -0
  104. package/dist/number-field/index.mjs.map +1 -0
  105. package/dist/number-field/number-field-arithmetic.mjs +78 -0
  106. package/dist/number-field/number-field-arithmetic.mjs.map +1 -0
  107. package/dist/number-field/number-field-context.d.mts +2 -0
  108. package/dist/number-field/number-field-context.mjs +13 -0
  109. package/dist/number-field/number-field-context.mjs.map +1 -0
  110. package/dist/number-field/number-field-decrement.d.mts +16 -0
  111. package/dist/number-field/number-field-decrement.d.mts.map +1 -0
  112. package/dist/number-field/number-field-decrement.mjs +19 -0
  113. package/dist/number-field/number-field-decrement.mjs.map +1 -0
  114. package/dist/number-field/number-field-format.mjs +279 -0
  115. package/dist/number-field/number-field-format.mjs.map +1 -0
  116. package/dist/number-field/number-field-increment.d.mts +32 -0
  117. package/dist/number-field/number-field-increment.d.mts.map +1 -0
  118. package/dist/number-field/number-field-increment.mjs +68 -0
  119. package/dist/number-field/number-field-increment.mjs.map +1 -0
  120. package/dist/number-field/number-field-input.d.mts +29 -0
  121. package/dist/number-field/number-field-input.d.mts.map +1 -0
  122. package/dist/number-field/number-field-input.mjs +163 -0
  123. package/dist/number-field/number-field-input.mjs.map +1 -0
  124. package/dist/number-field/number-field-label.d.mts +23 -0
  125. package/dist/number-field/number-field-label.d.mts.map +1 -0
  126. package/dist/number-field/number-field-label.mjs +47 -0
  127. package/dist/number-field/number-field-label.mjs.map +1 -0
  128. package/dist/number-field/number-field-root.d.mts +108 -0
  129. package/dist/number-field/number-field-root.d.mts.map +1 -0
  130. package/dist/number-field/number-field-root.mjs +523 -0
  131. package/dist/number-field/number-field-root.mjs.map +1 -0
  132. package/dist/number-field/use-step-button.mjs +107 -0
  133. package/dist/number-field/use-step-button.mjs.map +1 -0
  134. package/dist/popover/popover-close.mjs +2 -1
  135. package/dist/popover/popover-close.mjs.map +1 -1
  136. package/dist/popover/popover-popup.d.mts.map +1 -1
  137. package/dist/popover/popover-popup.mjs +7 -2
  138. package/dist/popover/popover-popup.mjs.map +1 -1
  139. package/dist/popover/popover-root.mjs +1 -1
  140. package/dist/progress/index.d.mts +24 -0
  141. package/dist/progress/index.d.mts.map +1 -0
  142. package/dist/progress/index.mjs +24 -0
  143. package/dist/progress/index.mjs.map +1 -0
  144. package/dist/progress/progress-context.mjs +13 -0
  145. package/dist/progress/progress-context.mjs.map +1 -0
  146. package/dist/progress/progress-indicator.d.mts +19 -0
  147. package/dist/progress/progress-indicator.d.mts.map +1 -0
  148. package/dist/progress/progress-indicator.mjs +29 -0
  149. package/dist/progress/progress-indicator.mjs.map +1 -0
  150. package/dist/progress/progress-label.d.mts +19 -0
  151. package/dist/progress/progress-label.d.mts.map +1 -0
  152. package/dist/progress/progress-label.mjs +30 -0
  153. package/dist/progress/progress-label.mjs.map +1 -0
  154. package/dist/progress/progress-root.d.mts +68 -0
  155. package/dist/progress/progress-root.d.mts.map +1 -0
  156. package/dist/progress/progress-root.mjs +80 -0
  157. package/dist/progress/progress-root.mjs.map +1 -0
  158. package/dist/progress/progress-track.d.mts +19 -0
  159. package/dist/progress/progress-track.d.mts.map +1 -0
  160. package/dist/progress/progress-track.mjs +29 -0
  161. package/dist/progress/progress-track.mjs.map +1 -0
  162. package/dist/progress/progress-value.d.mts +20 -0
  163. package/dist/progress/progress-value.d.mts.map +1 -0
  164. package/dist/progress/progress-value.mjs +33 -0
  165. package/dist/progress/progress-value.mjs.map +1 -0
  166. package/dist/slider/slider-arithmetic.mjs +1 -17
  167. package/dist/slider/slider-arithmetic.mjs.map +1 -1
  168. package/dist/slider/slider-root.mjs +3 -2
  169. package/dist/slider/slider-root.mjs.map +1 -1
  170. package/dist/slider/slider-thumb.mjs +2 -1
  171. package/dist/slider/slider-thumb.mjs.map +1 -1
  172. package/dist/slider/slider-track.mjs +2 -1
  173. package/dist/slider/slider-track.mjs.map +1 -1
  174. package/dist/tabs/tabs-indicator.mjs +2 -1
  175. package/dist/tabs/tabs-indicator.mjs.map +1 -1
  176. package/dist/tabs/tabs-tab.mjs +2 -1
  177. package/dist/tabs/tabs-tab.mjs.map +1 -1
  178. package/dist/toast/toast-action.mjs +2 -1
  179. package/dist/toast/toast-action.mjs.map +1 -1
  180. package/dist/toast/toast-close.mjs +2 -1
  181. package/dist/toast/toast-close.mjs.map +1 -1
  182. package/dist/toast/toast-root.mjs +3 -2
  183. package/dist/toast/toast-root.mjs.map +1 -1
  184. package/dist/toast/toast-viewport.d.mts.map +1 -1
  185. package/dist/toast/toast-viewport.mjs +30 -7
  186. package/dist/toast/toast-viewport.mjs.map +1 -1
  187. package/dist/toast/toast.d.mts.map +1 -1
  188. package/dist/toast/toast.mjs +5 -1
  189. package/dist/toast/toast.mjs.map +1 -1
  190. package/dist/toggle/toggle.mjs +2 -1
  191. package/dist/toggle/toggle.mjs.map +1 -1
  192. package/dist/toggle-group/index.d.mts +16 -0
  193. package/dist/toggle-group/index.d.mts.map +1 -0
  194. package/dist/toggle-group/index.mjs +15 -0
  195. package/dist/toggle-group/index.mjs.map +1 -0
  196. package/dist/toggle-group/toggle-group-context.d.mts +4 -0
  197. package/dist/toggle-group/toggle-group-context.d.mts.map +1 -0
  198. package/dist/toggle-group/toggle-group-context.mjs +13 -0
  199. package/dist/toggle-group/toggle-group-context.mjs.map +1 -0
  200. package/dist/toggle-group/toggle-group-item.d.mts +47 -0
  201. package/dist/toggle-group/toggle-group-item.d.mts.map +1 -0
  202. package/dist/toggle-group/toggle-group-item.mjs +63 -0
  203. package/dist/toggle-group/toggle-group-item.mjs.map +1 -0
  204. package/dist/toggle-group/toggle-group-root.d.mts +95 -0
  205. package/dist/toggle-group/toggle-group-root.d.mts.map +1 -0
  206. package/dist/toggle-group/toggle-group-root.mjs +211 -0
  207. package/dist/toggle-group/toggle-group-root.mjs.map +1 -0
  208. package/dist/toolbar/index.d.mts +18 -0
  209. package/dist/toolbar/index.d.mts.map +1 -0
  210. package/dist/toolbar/index.mjs +18 -0
  211. package/dist/toolbar/index.mjs.map +1 -0
  212. package/dist/toolbar/toolbar-button.d.mts +42 -0
  213. package/dist/toolbar/toolbar-button.d.mts.map +1 -0
  214. package/dist/toolbar/toolbar-button.mjs +50 -0
  215. package/dist/toolbar/toolbar-button.mjs.map +1 -0
  216. package/dist/toolbar/toolbar-root.d.mts +38 -0
  217. package/dist/toolbar/toolbar-root.d.mts.map +1 -0
  218. package/dist/toolbar/toolbar-root.mjs +66 -0
  219. package/dist/toolbar/toolbar-root.mjs.map +1 -0
  220. package/dist/toolbar/toolbar-separator.d.mts +25 -0
  221. package/dist/toolbar/toolbar-separator.d.mts.map +1 -0
  222. package/dist/toolbar/toolbar-separator.mjs +31 -0
  223. package/dist/toolbar/toolbar-separator.mjs.map +1 -0
  224. package/dist/tooltip/tooltip-popup.mjs +2 -1
  225. package/dist/tooltip/tooltip-popup.mjs.map +1 -1
  226. package/dist/tooltip/tooltip-root.mjs +18 -2
  227. package/dist/tooltip/tooltip-root.mjs.map +1 -1
  228. package/dist/tooltip/tooltip-trigger.mjs +2 -1
  229. package/dist/tooltip/tooltip-trigger.mjs.map +1 -1
  230. package/package.json +6 -1
@@ -0,0 +1,279 @@
1
+ import { roundTo } from "../internal/decimal.mjs";
2
+ //#region src/number-field/number-field-format.ts
3
+ const BIDI = /[؜‎‏‪-‮⁦-⁩]/g;
4
+ function normalize(text) {
5
+ return text.normalize("NFKC").replace(BIDI, "").replace(/\s/g, " ").toLowerCase();
6
+ }
7
+ /** Every string `format` may emit, with the plural samples that change a unit or currency name. */
8
+ const SAMPLES = [
9
+ -1234567.891,
10
+ 0,
11
+ 1,
12
+ 2,
13
+ 3,
14
+ 5,
15
+ 11,
16
+ 21,
17
+ .5,
18
+ 1.5,
19
+ 100
20
+ ];
21
+ function collect(locale, options, type, into) {
22
+ let formatter;
23
+ try {
24
+ formatter = new Intl.NumberFormat(locale, options);
25
+ } catch {
26
+ return;
27
+ }
28
+ for (const sample of SAMPLES) for (const part of formatter.formatToParts(sample)) {
29
+ if (part.type !== type) continue;
30
+ const value = normalize(part.value).trim();
31
+ if (value !== "") into.add(value);
32
+ }
33
+ }
34
+ function deriveSymbols(locale, options, numberingSystem) {
35
+ const loc = locale;
36
+ const probeOptions = {
37
+ ...options,
38
+ notation: "standard",
39
+ useGrouping: true,
40
+ minimumFractionDigits: 1,
41
+ maximumFractionDigits: 3,
42
+ minimumSignificantDigits: void 0,
43
+ maximumSignificantDigits: void 0,
44
+ signDisplay: "auto",
45
+ numberingSystem
46
+ };
47
+ let probe;
48
+ try {
49
+ probe = new Intl.NumberFormat(loc, probeOptions);
50
+ } catch {
51
+ probe = new Intl.NumberFormat(loc, {
52
+ useGrouping: true,
53
+ minimumFractionDigits: 1,
54
+ numberingSystem
55
+ });
56
+ }
57
+ const parts = probe.formatToParts(-1234567890.5);
58
+ let decimal = ".";
59
+ let group = ",";
60
+ const minus = /* @__PURE__ */ new Set(["-", "−"]);
61
+ const plus = /* @__PURE__ */ new Set(["+"]);
62
+ const integers = [];
63
+ for (const part of parts) {
64
+ const value = normalize(part.value);
65
+ if (part.type === "decimal") decimal = value;
66
+ else if (part.type === "group") group = value;
67
+ else if (part.type === "minusSign" && value.trim() !== "") minus.add(value.trim());
68
+ else if (part.type === "integer") integers.push(part.value);
69
+ }
70
+ try {
71
+ for (const part of new Intl.NumberFormat(loc, {
72
+ ...probeOptions,
73
+ signDisplay: "always"
74
+ }).formatToParts(1)) if (part.type === "plusSign") plus.add(normalize(part.value).trim());
75
+ } catch {}
76
+ const primaryGroup = integers.at(-1)?.length ?? 3;
77
+ const secondaryGroup = integers.length > 2 ? integers.at(-2)?.length ?? 3 : primaryGroup;
78
+ const groups = [group.trim() === "" ? " " : group];
79
+ if ([
80
+ "'",
81
+ "’",
82
+ "ʼ"
83
+ ].includes(group)) groups.push("'", "’", "ʼ");
84
+ const affixes = /* @__PURE__ */ new Set();
85
+ const style = options.style ?? "decimal";
86
+ if (style === "percent") {
87
+ collect(loc, options, "percentSign", affixes);
88
+ affixes.add("%");
89
+ } else if (style === "currency") {
90
+ for (const currencyDisplay of [
91
+ "symbol",
92
+ "narrowSymbol",
93
+ "code",
94
+ "name"
95
+ ]) collect(loc, {
96
+ ...options,
97
+ currencyDisplay
98
+ }, "currency", affixes);
99
+ if (options.currency !== void 0) affixes.add(options.currency.toLowerCase());
100
+ } else if (style === "unit") for (const unitDisplay of [
101
+ "short",
102
+ "long",
103
+ "narrow"
104
+ ]) collect(loc, {
105
+ ...options,
106
+ unitDisplay
107
+ }, "unit", affixes);
108
+ const literals = /* @__PURE__ */ new Set();
109
+ collect(loc, options, "literal", literals);
110
+ for (const ch of ["(", ")"]) literals.delete(ch);
111
+ const literalList = [...literals].filter((l) => l !== "").sort((a, b) => b.length - a.length);
112
+ const digitFormat = new Intl.NumberFormat(loc, {
113
+ useGrouping: false,
114
+ numberingSystem
115
+ });
116
+ const digits = Array.from({ length: 10 }, (_, d) => normalize(digitFormat.format(d)));
117
+ return {
118
+ decimal,
119
+ groups,
120
+ minus: [...minus],
121
+ plus: [...plus],
122
+ affixes: [...affixes].sort((a, b) => b.length - a.length),
123
+ literals: literalList,
124
+ edges: [...affixes, ...literalList].sort((a, b) => b.length - a.length),
125
+ digits,
126
+ primaryGroup,
127
+ secondaryGroup
128
+ };
129
+ }
130
+ function stripEdges(text, tokens) {
131
+ let s = text.trim();
132
+ let changed = true;
133
+ while (changed && s !== "") {
134
+ changed = false;
135
+ for (const token of tokens) {
136
+ if (s.startsWith(token)) {
137
+ s = s.slice(token.length).trim();
138
+ changed = true;
139
+ }
140
+ if (s.endsWith(token)) {
141
+ s = s.slice(0, s.length - token.length).trim();
142
+ changed = true;
143
+ }
144
+ }
145
+ }
146
+ return s;
147
+ }
148
+ function startsWithAny(s, tokens) {
149
+ for (const token of tokens) if (s.startsWith(token)) return token;
150
+ return null;
151
+ }
152
+ function endsWithAny(s, tokens) {
153
+ for (const token of tokens) if (s.endsWith(token)) return token;
154
+ return null;
155
+ }
156
+ /** Strict grouping (5.3 step 5): the integer part, with group separators replaced by nothing. */
157
+ function ungroup(integer, symbols) {
158
+ const separators = symbols.groups;
159
+ const chunks = [];
160
+ let current = "";
161
+ for (const ch of integer) if (separators.includes(ch)) {
162
+ chunks.push(current);
163
+ current = "";
164
+ } else current += ch;
165
+ chunks.push(current);
166
+ if (chunks.length === 1) return integer;
167
+ const { primaryGroup, secondaryGroup } = symbols;
168
+ const last = chunks.length - 1;
169
+ for (let i = 0; i <= last; i += 1) {
170
+ const length = chunks[i].length;
171
+ if (i === last) {
172
+ if (length !== primaryGroup) return void 0;
173
+ } else if (i === 0) {
174
+ if (length < 1 || length > secondaryGroup) return void 0;
175
+ } else if (length !== secondaryGroup) return;
176
+ }
177
+ return chunks.join("");
178
+ }
179
+ function parseWith(text, symbols, percent, round) {
180
+ let s = normalize(text).trim();
181
+ if (s === "") return null;
182
+ const edges = symbols.edges;
183
+ s = stripEdges(s, edges);
184
+ if (s === "") return void 0;
185
+ let negative = false;
186
+ if (s.startsWith("(") && s.endsWith(")")) {
187
+ negative = true;
188
+ s = stripEdges(s.slice(1, -1), edges);
189
+ } else {
190
+ const leadMinus = startsWithAny(s, symbols.minus);
191
+ const leadPlus = leadMinus === null ? startsWithAny(s, symbols.plus) : null;
192
+ if (leadMinus !== null || leadPlus !== null) {
193
+ negative = leadMinus !== null;
194
+ s = s.slice((leadMinus ?? leadPlus).length);
195
+ } else {
196
+ const trailMinus = endsWithAny(s, symbols.minus);
197
+ const trailPlus = trailMinus === null ? endsWithAny(s, symbols.plus) : null;
198
+ if (trailMinus !== null || trailPlus !== null) {
199
+ negative = trailMinus !== null;
200
+ s = s.slice(0, s.length - (trailMinus ?? trailPlus).length);
201
+ }
202
+ }
203
+ s = stripEdges(s, edges);
204
+ }
205
+ if (s === "") return void 0;
206
+ const decimalAliases = [symbols.decimal];
207
+ if (symbols.decimal === "٫" && !symbols.groups.includes(".")) decimalAliases.push(".");
208
+ let integer = "";
209
+ let fraction = null;
210
+ for (const ch of s) {
211
+ const localDigit = symbols.digits.indexOf(ch);
212
+ const digit = localDigit !== -1 ? String(localDigit) : /[0-9]/.test(ch) ? ch : null;
213
+ if (digit !== null) {
214
+ if (fraction === null) integer += digit;
215
+ else fraction += digit;
216
+ } else if (decimalAliases.includes(ch)) {
217
+ if (fraction !== null) return void 0;
218
+ fraction = "";
219
+ } else if (symbols.groups.includes(ch)) {
220
+ if (fraction !== null) return void 0;
221
+ integer += ch;
222
+ } else return;
223
+ }
224
+ const digitsOnly = ungroup(integer, symbols);
225
+ if (digitsOnly === void 0) return void 0;
226
+ if (digitsOnly === "" && (fraction === null || fraction === "")) return void 0;
227
+ let value = Number(`${digitsOnly === "" ? "0" : digitsOnly}.${fraction ?? ""}`.replace(/\.$/, ""));
228
+ if (!Number.isFinite(value)) return void 0;
229
+ if (negative) value = -value;
230
+ const result = round(percent ? value / 100 : value);
231
+ return result === 0 ? 0 : result;
232
+ }
233
+ const cache = /* @__PURE__ */ new Map();
234
+ function cacheKey(locale, format) {
235
+ const entries = Object.entries(format).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
236
+ return JSON.stringify([locale ?? null, entries]);
237
+ }
238
+ /** A stable serialisation of `format`, for memoising on its content rather than its identity. */
239
+ function formatKey(format) {
240
+ return cacheKey(void 0, format ?? {});
241
+ }
242
+ /**
243
+ * The formatter and parser for `(locale, format)`, cached per pair at module level (construction
244
+ * is expensive). `locale` `undefined` is the runtime's locale.
245
+ */
246
+ function createNumberFormatter(locale, format = {}) {
247
+ const key = cacheKey(locale, format);
248
+ const cached = cache.get(key);
249
+ if (cached !== void 0) return cached;
250
+ const intl = new Intl.NumberFormat(locale, format);
251
+ const resolved = intl.resolvedOptions();
252
+ const percent = resolved.style === "percent";
253
+ const significant = format.maximumSignificantDigits !== void 0 || format.minimumSignificantDigits !== void 0;
254
+ const fractionDigits = significant ? void 0 : resolved.maximumFractionDigits;
255
+ const notation = format.notation ?? "standard";
256
+ const warning = notation !== "standard" ? `\`notation: "${notation}"\` cannot be parsed back; typed text in this format reverts.` : significant ? "`maximumSignificantDigits` makes the value's rounding impossible to state; the value may differ from the text." : null;
257
+ const round = (value) => {
258
+ if (fractionDigits === void 0) return value;
259
+ if (percent) return roundTo(roundTo(value * 100, fractionDigits) / 100, fractionDigits + 2);
260
+ return roundTo(value, fractionDigits);
261
+ };
262
+ const symbols = deriveSymbols(locale, format, resolved.numberingSystem);
263
+ const formatter = {
264
+ locale: resolved.locale,
265
+ format: (value) => value === null ? "" : intl.format(value),
266
+ parse: (text) => parseWith(text, symbols, percent, round),
267
+ round,
268
+ fractionDigits,
269
+ allowsFractions: percent || fractionDigits === void 0 || fractionDigits > 0,
270
+ percent,
271
+ warning
272
+ };
273
+ cache.set(key, formatter);
274
+ return formatter;
275
+ }
276
+ //#endregion
277
+ export { createNumberFormatter, formatKey };
278
+
279
+ //# sourceMappingURL=number-field-format.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-field-format.mjs","names":[],"sources":["../../src/number-field/number-field-format.ts"],"sourcesContent":["// Locale-aware formatting and parsing of NumberField (design/components/number-field.md 5). No\n// directive: no React, no client-only API; usable on the server.\n//\n// ECMA-402 has no parser, so the locale's symbols are derived from `formatToParts` once per\n// `(locale, format)` and the parser accepts exactly the shapes the formatter produces (plus\n// full-width digits, ASCII signs and the other spellings of the currency or unit), with **strict\n// grouping**: `1,5` under `en` is unparsable, never `15`.\nimport { roundTo } from \"../internal/decimal.js\";\n\nexport interface NumberFormatter {\n /** The resolved locale (`resolvedOptions().locale`). */\n readonly locale: string;\n /** The text for `value`; `\"\"` for `null`. */\n readonly format: (value: number | null) => string;\n /**\n * The value `text` stands for, rounded (5.4): `null` for an empty text, `undefined` for an\n * unparsable one.\n */\n readonly parse: (text: string) => number | null | undefined;\n /** `value` rounded to what the text can show (5.4); unchanged with significant digits. */\n readonly round: (value: number) => number;\n /** The resolved `maximumFractionDigits`, or `undefined` when significant digits rule. */\n readonly fractionDigits: number | undefined;\n /** The format can show a fraction (`inputmode=\"decimal\"`, 8.5). */\n readonly allowsFractions: boolean;\n /** `style: \"percent\"`: the text shows the value × 100. */\n readonly percent: boolean;\n /** Development: why this format cannot round-trip (`compact` notation, significant digits). */\n readonly warning: string | null;\n}\n\n// Bidirectional controls that some locales put around numbers and signs (`ar`: U+061C).\nconst BIDI = /[؜‎‏‪-‮⁦-⁩]/g;\n\nfunction normalize(text: string): string {\n return text.normalize(\"NFKC\").replace(BIDI, \"\").replace(/\\s/g, \" \").toLowerCase();\n}\n\n/** Every string `format` may emit, with the plural samples that change a unit or currency name. */\nconst SAMPLES = [-1234567.891, 0, 1, 2, 3, 5, 11, 21, 0.5, 1.5, 100];\n\nfunction collect(\n locale: string | readonly string[] | undefined,\n options: Intl.NumberFormatOptions,\n type: Intl.NumberFormatPartTypes,\n into: Set<string>,\n): void {\n let formatter: Intl.NumberFormat;\n try {\n formatter = new Intl.NumberFormat(locale as string | string[] | undefined, options);\n } catch {\n return;\n }\n for (const sample of SAMPLES) {\n for (const part of formatter.formatToParts(sample)) {\n if (part.type !== type) continue;\n const value = normalize(part.value).trim();\n if (value !== \"\") into.add(value);\n }\n }\n}\n\ninterface Symbols {\n readonly decimal: string;\n /** Group separators accepted (normalised). `\" \"` stands for any whitespace. */\n readonly groups: readonly string[];\n readonly minus: readonly string[];\n readonly plus: readonly string[];\n /** Currency, unit and percent strings, longest first. */\n readonly affixes: readonly string[];\n /** Literal strings the formatter puts around the number (never parentheses). */\n readonly literals: readonly string[];\n /** Affixes and literals, longest first: what may be stripped from the edges. */\n readonly edges: readonly string[];\n /** The numbering system's ten digits, normalised. */\n readonly digits: readonly string[];\n readonly primaryGroup: number;\n readonly secondaryGroup: number;\n}\n\nfunction deriveSymbols(\n locale: string | readonly string[] | undefined,\n options: Intl.NumberFormatOptions,\n numberingSystem: string,\n): Symbols {\n const loc = locale as string | string[] | undefined;\n // A probe that always shows a fraction and groups, without rounding by significant digits.\n const probeOptions: Intl.NumberFormatOptions = {\n ...options,\n notation: \"standard\",\n useGrouping: true,\n minimumFractionDigits: 1,\n maximumFractionDigits: 3,\n minimumSignificantDigits: undefined,\n maximumSignificantDigits: undefined,\n signDisplay: \"auto\",\n numberingSystem,\n };\n let probe: Intl.NumberFormat;\n try {\n probe = new Intl.NumberFormat(loc, probeOptions);\n } catch {\n probe = new Intl.NumberFormat(loc, {\n useGrouping: true,\n minimumFractionDigits: 1,\n numberingSystem,\n });\n }\n const parts = probe.formatToParts(-1234567890.5);\n let decimal = \".\";\n let group = \",\";\n const minus = new Set([\"-\", \"−\"]);\n const plus = new Set([\"+\"]);\n const integers: string[] = [];\n for (const part of parts) {\n const value = normalize(part.value);\n if (part.type === \"decimal\") decimal = value;\n else if (part.type === \"group\") group = value;\n else if (part.type === \"minusSign\" && value.trim() !== \"\") minus.add(value.trim());\n else if (part.type === \"integer\") integers.push(part.value);\n }\n try {\n for (const part of new Intl.NumberFormat(loc, {\n ...probeOptions,\n signDisplay: \"always\",\n }).formatToParts(1)) {\n if (part.type === \"plusSign\") plus.add(normalize(part.value).trim());\n }\n } catch {\n // The plus sign stays ASCII.\n }\n const primaryGroup = integers.at(-1)?.length ?? 3;\n const secondaryGroup = integers.length > 2 ? (integers.at(-2)?.length ?? 3) : primaryGroup;\n\n const groups = [group.trim() === \"\" ? \" \" : group];\n // An apostrophe group (`de-CH`) is typed with whichever apostrophe the keyboard has.\n if ([\"'\", \"’\", \"ʼ\"].includes(group)) groups.push(\"'\", \"’\", \"ʼ\");\n\n const affixes = new Set<string>();\n const style = options.style ?? \"decimal\";\n if (style === \"percent\") {\n collect(loc, options, \"percentSign\", affixes);\n affixes.add(\"%\");\n } else if (style === \"currency\") {\n for (const currencyDisplay of [\"symbol\", \"narrowSymbol\", \"code\", \"name\"] as const) {\n collect(loc, { ...options, currencyDisplay }, \"currency\", affixes);\n }\n if (options.currency !== undefined) affixes.add(options.currency.toLowerCase());\n } else if (style === \"unit\") {\n for (const unitDisplay of [\"short\", \"long\", \"narrow\"] as const) {\n collect(loc, { ...options, unitDisplay }, \"unit\", affixes);\n }\n }\n const literals = new Set<string>();\n collect(loc, options, \"literal\", literals);\n for (const ch of [\"(\", \")\"]) literals.delete(ch);\n\n const literalList = [...literals].filter((l) => l !== \"\").sort((a, b) => b.length - a.length);\n\n const digitFormat = new Intl.NumberFormat(loc, { useGrouping: false, numberingSystem });\n const digits = Array.from({ length: 10 }, (_, d) => normalize(digitFormat.format(d)));\n\n return {\n decimal,\n groups,\n minus: [...minus],\n plus: [...plus],\n affixes: [...affixes].sort((a, b) => b.length - a.length),\n literals: literalList,\n edges: [...affixes, ...literalList].sort((a, b) => b.length - a.length),\n digits,\n primaryGroup,\n secondaryGroup,\n };\n}\n\nfunction stripEdges(text: string, tokens: readonly string[]): string {\n let s = text.trim();\n let changed = true;\n while (changed && s !== \"\") {\n changed = false;\n for (const token of tokens) {\n if (s.startsWith(token)) {\n s = s.slice(token.length).trim();\n changed = true;\n }\n if (s.endsWith(token)) {\n s = s.slice(0, s.length - token.length).trim();\n changed = true;\n }\n }\n }\n return s;\n}\n\nfunction startsWithAny(s: string, tokens: readonly string[]): string | null {\n for (const token of tokens) if (s.startsWith(token)) return token;\n return null;\n}\n\nfunction endsWithAny(s: string, tokens: readonly string[]): string | null {\n for (const token of tokens) if (s.endsWith(token)) return token;\n return null;\n}\n\n/** Strict grouping (5.3 step 5): the integer part, with group separators replaced by nothing. */\nfunction ungroup(integer: string, symbols: Symbols): string | undefined {\n const separators = symbols.groups;\n const chunks: string[] = [];\n let current = \"\";\n for (const ch of integer) {\n if (separators.includes(ch)) {\n chunks.push(current);\n current = \"\";\n } else {\n current += ch;\n }\n }\n chunks.push(current);\n if (chunks.length === 1) return integer;\n const { primaryGroup, secondaryGroup } = symbols;\n const last = chunks.length - 1;\n for (let i = 0; i <= last; i += 1) {\n const length = chunks[i]!.length;\n if (i === last) {\n if (length !== primaryGroup) return undefined;\n } else if (i === 0) {\n if (length < 1 || length > secondaryGroup) return undefined;\n } else if (length !== secondaryGroup) {\n return undefined;\n }\n }\n return chunks.join(\"\");\n}\n\nfunction parseWith(\n text: string,\n symbols: Symbols,\n percent: boolean,\n round: (value: number) => number,\n): number | null | undefined {\n let s = normalize(text).trim();\n if (s === \"\") return null;\n\n // 2. Currency, unit and percent strings and literals, at the edges only (and again inside the\n // sign below): in the middle of the number they are garbage, never a group separator.\n const edges = symbols.edges;\n s = stripEdges(s, edges);\n if (s === \"\") return undefined;\n\n // 3. One sign: parentheses (accounting), a leading or trailing minus or plus.\n let negative = false;\n if (s.startsWith(\"(\") && s.endsWith(\")\")) {\n negative = true;\n s = stripEdges(s.slice(1, -1), edges);\n } else {\n const leadMinus = startsWithAny(s, symbols.minus);\n const leadPlus = leadMinus === null ? startsWithAny(s, symbols.plus) : null;\n if (leadMinus !== null || leadPlus !== null) {\n negative = leadMinus !== null;\n s = s.slice((leadMinus ?? leadPlus)!.length);\n } else {\n const trailMinus = endsWithAny(s, symbols.minus);\n const trailPlus = trailMinus === null ? endsWithAny(s, symbols.plus) : null;\n if (trailMinus !== null || trailPlus !== null) {\n negative = trailMinus !== null;\n s = s.slice(0, s.length - (trailMinus ?? trailPlus)!.length);\n }\n }\n s = stripEdges(s, edges);\n }\n if (s === \"\") return undefined;\n\n // 4. Digits to ASCII; anything but a digit, the decimal or a group separator is garbage.\n const decimalAliases = [symbols.decimal];\n // An Arabic decimal separator is often typed as the ASCII period.\n if (symbols.decimal === \"٫\" && !symbols.groups.includes(\".\")) decimalAliases.push(\".\");\n let integer = \"\";\n let fraction: string | null = null;\n for (const ch of s) {\n const localDigit = symbols.digits.indexOf(ch);\n const digit = localDigit !== -1 ? String(localDigit) : /[0-9]/.test(ch) ? ch : null;\n if (digit !== null) {\n if (fraction === null) integer += digit;\n else fraction += digit;\n } else if (decimalAliases.includes(ch)) {\n if (fraction !== null) return undefined;\n fraction = \"\";\n } else if (symbols.groups.includes(ch)) {\n // 5. Group separators only before the decimal separator.\n if (fraction !== null) return undefined;\n integer += ch;\n } else {\n return undefined;\n }\n }\n const digitsOnly = ungroup(integer, symbols);\n if (digitsOnly === undefined) return undefined;\n if (digitsOnly === \"\" && (fraction === null || fraction === \"\")) return undefined;\n\n // 6. The number; percent divides by 100.\n let value = Number(\n `${digitsOnly === \"\" ? \"0\" : digitsOnly}.${fraction ?? \"\"}`.replace(/\\.$/, \"\"),\n );\n if (!Number.isFinite(value)) return undefined;\n if (negative) value = -value;\n // 7. Rounded to what the text shows.\n const result = round(percent ? value / 100 : value);\n return result === 0 ? 0 : result;\n}\n\nconst cache = new Map<string, NumberFormatter>();\n\nfunction cacheKey(locale: string | readonly string[] | undefined, format: object): string {\n const entries = Object.entries(format)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return JSON.stringify([locale ?? null, entries]);\n}\n\n/** A stable serialisation of `format`, for memoising on its content rather than its identity. */\nexport function formatKey(format: Intl.NumberFormatOptions | undefined): string {\n return cacheKey(undefined, format ?? {});\n}\n\n/**\n * The formatter and parser for `(locale, format)`, cached per pair at module level (construction\n * is expensive). `locale` `undefined` is the runtime's locale.\n */\nexport function createNumberFormatter(\n locale: string | readonly string[] | undefined,\n format: Intl.NumberFormatOptions = {},\n): NumberFormatter {\n const key = cacheKey(locale, format);\n const cached = cache.get(key);\n if (cached !== undefined) return cached;\n\n const intl = new Intl.NumberFormat(locale as string | string[] | undefined, format);\n const resolved = intl.resolvedOptions() as Intl.ResolvedNumberFormatOptions & {\n roundingPriority?: string;\n };\n const percent = resolved.style === \"percent\";\n const significant =\n format.maximumSignificantDigits !== undefined || format.minimumSignificantDigits !== undefined;\n const fractionDigits = significant ? undefined : resolved.maximumFractionDigits;\n const notation = format.notation ?? \"standard\";\n const warning =\n notation !== \"standard\"\n ? `\\`notation: \"${notation}\"\\` cannot be parsed back; typed text in this format reverts.`\n : significant\n ? \"`maximumSignificantDigits` makes the value's rounding impossible to state; the value \" +\n \"may differ from the text.\"\n : null;\n\n const round = (value: number): number => {\n if (fractionDigits === undefined) return value;\n if (percent) return roundTo(roundTo(value * 100, fractionDigits) / 100, fractionDigits + 2);\n return roundTo(value, fractionDigits);\n };\n\n const symbols = deriveSymbols(locale, format, resolved.numberingSystem);\n const formatter: NumberFormatter = {\n locale: resolved.locale,\n format: (value) => (value === null ? \"\" : intl.format(value)),\n parse: (text) => parseWith(text, symbols, percent, round),\n round,\n fractionDigits,\n allowsFractions: percent || fractionDigits === undefined || fractionDigits > 0,\n percent,\n warning,\n };\n cache.set(key, formatter);\n return formatter;\n}\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { RenderProp, StateValue } from "../internal/types.mjs";
2
+ import "./number-field-context.mjs";
3
+ import { CSSProperties, ComponentPropsWithRef, ReactNode } from "react";
4
+ //#region src/number-field/number-field-increment.d.ts
5
+ export interface NumberFieldButtonState {
6
+ /** The Root is pending and the latest step or commit came from this button. */
7
+ readonly pending: boolean;
8
+ /** The prop, the Root's `disabled` or `readOnly`, or the next step would change nothing. */
9
+ readonly disabled: boolean;
10
+ }
11
+ export interface NumberFieldIncrementProps extends Omit<ComponentPropsWithRef<"button">, "type" | "tabIndex" | "disabled" | "onPointerDown" | "onClick" | "onContextMenu" | "aria-controls" | "className" | "style" | "children"> {
12
+ disabled?: boolean;
13
+ className?: StateValue<string | undefined, NumberFieldButtonState>;
14
+ style?: StateValue<CSSProperties | undefined, NumberFieldButtonState>;
15
+ children?: StateValue<ReactNode, NumberFieldButtonState>;
16
+ render?: RenderProp<ComponentPropsWithRef<"button">, NumberFieldButtonState>;
17
+ }
18
+ export type NumberFieldDecrementProps = NumberFieldIncrementProps;
19
+ /**
20
+ * Steps the value up: `button[type=button][tabindex=-1]` with `aria-controls` → the input and the
21
+ * default `aria-label` `"Increase"`; one step at the press, then repeated while held, one commit
22
+ * at the release. Natively `disabled` when the next step would change nothing.
23
+ *
24
+ * Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `onPointerDown`, `onClick`,
25
+ * `onContextMenu`, `data-pending`, `data-disabled`.
26
+ */
27
+ export declare function NumberFieldIncrement(props: NumberFieldIncrementProps): ReactNode;
28
+ export declare namespace NumberFieldIncrement {
29
+ var displayName: string;
30
+ }
31
+ //#endregion
32
+ //# sourceMappingURL=number-field-increment.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-field-increment.d.mts","names":[],"sources":["../../src/number-field/number-field-increment.tsx"],"mappings":";;;;iBAYiB;;WAEN;;WAEA;;iBAGM,kCAAkC,KACjD;EAYA;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,iCAAiC;;YAG3C,4BAA4B;;;;;;;;;wBA6DxB,qBAAqB,OAAO,4BAAyB;yBAArD"}
@@ -0,0 +1,68 @@
1
+ "use client";
2
+ import { dataAttr } from "../internal/data-attr.mjs";
3
+ import { rendersButton } from "../internal/render-element.mjs";
4
+ import { useRender } from "../internal/use-render.mjs";
5
+ import { useDevWarning } from "../internal/use-dev-warning.mjs";
6
+ import { isAtBound } from "./number-field-arithmetic.mjs";
7
+ import { useNumberFieldContext } from "./number-field-context.mjs";
8
+ import { useStepButton } from "./use-step-button.mjs";
9
+ //#region src/number-field/number-field-increment.tsx
10
+ const LABELS = {
11
+ increment: "Increase",
12
+ decrement: "Decrease"
13
+ };
14
+ /** Shared by `NumberField.Increment` and `NumberField.Decrement` (design 3.3, 4.3). */
15
+ function useStepButtonElement(direction, part, props) {
16
+ const { disabled: disabledProp = false, className, style, children, render, ...rest } = props;
17
+ const ctx = useNumberFieldContext(part);
18
+ const { state } = ctx;
19
+ const atBound = isAtBound(ctx.stepBase, direction === "increment" ? 1 : -1, ctx.bounds);
20
+ const disabled = disabledProp || state.disabled || state.readOnly || atBound;
21
+ const pending = ctx.pendingSource === direction;
22
+ const buttonState = {
23
+ pending,
24
+ disabled
25
+ };
26
+ const handlers = useStepButton(ctx, direction, disabled);
27
+ useDevWarning(rest["aria-label"] === "", `${part}: \`aria-label=""\` leaves the button without a name. Pass the label text instead (the default is "${LABELS[direction]}").`);
28
+ const props$ = {
29
+ ...rest["aria-labelledby"] === void 0 ? { "aria-label": LABELS[direction] } : {},
30
+ ...rest
31
+ };
32
+ return useRender({
33
+ defaultElement: "button",
34
+ render,
35
+ state: buttonState,
36
+ className,
37
+ style,
38
+ children,
39
+ props: props$,
40
+ libraryProps: {
41
+ ...rendersButton(render) ? { type: "button" } : {},
42
+ tabIndex: -1,
43
+ "aria-controls": ctx.inputId,
44
+ disabled,
45
+ onPointerDown: handlers.onPointerDown,
46
+ onClick: handlers.onClick,
47
+ onContextMenu: handlers.onContextMenu,
48
+ "data-pending": dataAttr(pending),
49
+ "data-disabled": dataAttr(disabled)
50
+ }
51
+ });
52
+ }
53
+ /**
54
+ * Steps the value up: `button[type=button][tabindex=-1]` with `aria-controls` → the input and the
55
+ * default `aria-label` `"Increase"`; one step at the press, then repeated while held, one commit
56
+ * at the release. Natively `disabled` when the next step would change nothing.
57
+ *
58
+ * Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `onPointerDown`, `onClick`,
59
+ * `onContextMenu`, `data-pending`, `data-disabled`.
60
+ */
61
+ function NumberFieldIncrement(props) {
62
+ return useStepButtonElement("increment", "NumberField.Increment", props);
63
+ }
64
+ NumberFieldIncrement.displayName = "NumberFieldIncrement";
65
+ //#endregion
66
+ export { NumberFieldIncrement, useStepButtonElement };
67
+
68
+ //# sourceMappingURL=number-field-increment.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-field-increment.mjs","names":[],"sources":["../../src/number-field/number-field-increment.tsx"],"sourcesContent":["\"use client\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { rendersButton, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { isAtBound } from \"./number-field-arithmetic.js\";\nimport { useNumberFieldContext } from \"./number-field-context.js\";\nimport type { StepDirection } from \"./number-field-context.js\";\nimport { useStepButton } from \"./use-step-button.js\";\n\nexport interface NumberFieldButtonState {\n /** The Root is pending and the latest step or commit came from this button. */\n readonly pending: boolean;\n /** The prop, the Root's `disabled` or `readOnly`, or the next step would change nothing. */\n readonly disabled: boolean;\n}\n\nexport interface NumberFieldIncrementProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n | \"type\"\n | \"tabIndex\"\n | \"disabled\"\n | \"onPointerDown\"\n | \"onClick\"\n | \"onContextMenu\"\n | \"aria-controls\"\n | \"className\"\n | \"style\"\n | \"children\"\n> {\n disabled?: boolean;\n className?: StateValue<string | undefined, NumberFieldButtonState>;\n style?: StateValue<CSSProperties | undefined, NumberFieldButtonState>;\n children?: StateValue<ReactNode, NumberFieldButtonState>;\n render?: RenderProp<ComponentPropsWithRef<\"button\">, NumberFieldButtonState>;\n}\n\nexport type NumberFieldDecrementProps = NumberFieldIncrementProps;\n\nconst LABELS: Record<StepDirection, string> = { increment: \"Increase\", decrement: \"Decrease\" };\n\n/** Shared by `NumberField.Increment` and `NumberField.Decrement` (design 3.3, 4.3). */\nexport function useStepButtonElement(\n direction: StepDirection,\n part: string,\n props: NumberFieldIncrementProps,\n): ReactNode {\n const { disabled: disabledProp = false, className, style, children, render, ...rest } = props;\n const ctx = useNumberFieldContext(part);\n const { state } = ctx;\n const atBound = isAtBound(ctx.stepBase, direction === \"increment\" ? 1 : -1, ctx.bounds);\n const disabled = disabledProp || state.disabled || state.readOnly || atBound;\n const pending = ctx.pendingSource === direction;\n const buttonState: NumberFieldButtonState = { pending, disabled };\n const handlers = useStepButton(ctx, direction, disabled);\n\n useDevWarning(\n rest[\"aria-label\"] === \"\",\n `${part}: \\`aria-label=\"\"\\` leaves the button without a name. Pass the label text instead ` +\n `(the default is \"${LABELS[direction]}\").`,\n );\n\n // The built-in name is a default the consumer's `aria-label` or `aria-labelledby` replaces.\n const props$ = {\n ...(rest[\"aria-labelledby\"] === undefined ? { \"aria-label\": LABELS[direction] } : {}),\n ...rest,\n };\n\n return useRender<ComponentPropsWithRef<\"button\">, NumberFieldButtonState>({\n defaultElement: \"button\",\n render,\n state: buttonState,\n className,\n style,\n children,\n props: props$,\n libraryProps: {\n ...(rendersButton(render) ? { type: \"button\" as const } : {}),\n tabIndex: -1,\n \"aria-controls\": ctx.inputId,\n disabled,\n onPointerDown: handlers.onPointerDown,\n onClick: handlers.onClick,\n onContextMenu: handlers.onContextMenu,\n \"data-pending\": dataAttr(pending),\n \"data-disabled\": dataAttr(disabled),\n } as Partial<ComponentPropsWithRef<\"button\">> & DataAttributes,\n });\n}\n\n/**\n * Steps the value up: `button[type=button][tabindex=-1]` with `aria-controls` → the input and the\n * default `aria-label` `\"Increase\"`; one step at the press, then repeated while held, one commit\n * at the release. Natively `disabled` when the next step would change nothing.\n *\n * Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `onPointerDown`, `onClick`,\n * `onContextMenu`, `data-pending`, `data-disabled`.\n */\nexport function NumberFieldIncrement(props: NumberFieldIncrementProps) {\n return useStepButtonElement(\"increment\", \"NumberField.Increment\", props);\n}\n\nNumberFieldIncrement.displayName = \"NumberFieldIncrement\";\n"],"mappings":";;;;;;;;;AAyCA,MAAM,SAAwC;CAAE,WAAW;CAAY,WAAW;AAAW;;AAG7F,SAAgB,qBACd,WACA,MACA,OACW;CACX,MAAM,EAAE,UAAU,eAAe,OAAO,WAAW,OAAO,UAAU,QAAQ,GAAG,SAAS;CACxF,MAAM,MAAM,sBAAsB,IAAI;CACtC,MAAM,EAAE,UAAU;CAClB,MAAM,UAAU,UAAU,IAAI,UAAU,cAAc,cAAc,IAAI,IAAI,IAAI,MAAM;CACtF,MAAM,WAAW,gBAAgB,MAAM,YAAY,MAAM,YAAY;CACrE,MAAM,UAAU,IAAI,kBAAkB;CACtC,MAAM,cAAsC;EAAE;EAAS;CAAS;CAChE,MAAM,WAAW,cAAc,KAAK,WAAW,QAAQ;CAEvD,cACE,KAAK,kBAAkB,IACvB,GAAG,KAAK,qGACc,OAAO,WAAW,IAC1C;CAGA,MAAM,SAAS;EACb,GAAI,KAAK,uBAAuB,KAAA,IAAY,EAAE,cAAc,OAAO,WAAW,IAAI,CAAC;EACnF,GAAG;CACL;CAEA,OAAO,UAAmE;EACxE,gBAAgB;EAChB;EACA,OAAO;EACP;EACA;EACA;EACA,OAAO;EACP,cAAc;GACZ,GAAI,cAAc,MAAM,IAAI,EAAE,MAAM,SAAkB,IAAI,CAAC;GAC3D,UAAU;GACV,iBAAiB,IAAI;GACrB;GACA,eAAe,SAAS;GACxB,SAAS,SAAS;GAClB,eAAe,SAAS;GACxB,gBAAgB,SAAS,OAAO;GAChC,iBAAiB,SAAS,QAAQ;EACpC;CACF,CAAC;AACH;;;;;;;;;AAUA,SAAgB,qBAAqB,OAAkC;CACrE,OAAO,qBAAqB,aAAa,yBAAyB,KAAK;AACzE;AAEA,qBAAqB,cAAc"}
@@ -0,0 +1,29 @@
1
+ import { RenderProp, StateValue } from "../internal/types.mjs";
2
+ import { NumberFieldState } from "./number-field-root.mjs";
3
+ import { CSSProperties, ComponentPropsWithRef, Ref } from "react";
4
+ //#region src/number-field/number-field-input.d.ts
5
+ export interface NumberFieldInputProps extends Omit<ComponentPropsWithRef<"input">, "value" | "defaultValue" | "type" | "onChange" | "onKeyDown" | "onBlur" | "disabled" | "readOnly" | "required" | "name" | "form" | "min" | "max" | "step" | "pattern" | "aria-invalid" | "className" | "style" | "children"> {
6
+ /** The hidden native `<input type="number">` rendered after the visible input. */
7
+ hiddenInputRef?: Ref<HTMLInputElement>;
8
+ className?: StateValue<string | undefined, NumberFieldState>;
9
+ style?: StateValue<CSSProperties | undefined, NumberFieldState>;
10
+ /** Must render an `<input>` of type `text`, `search` or `tel`; the hidden input is rendered after it regardless. */
11
+ render?: RenderProp<ComponentPropsWithRef<"input">, NumberFieldState>;
12
+ }
13
+ /**
14
+ * The visible text field (`input[type=text]`, the user's surface) **followed by** the visually
15
+ * hidden native `input[type=number]` that carries `name`, `form`, `min`, `max`, `step`,
16
+ * `required` and the canonical value (design/components/number-field.md 6). `ref` goes to the
17
+ * visible input, `hiddenInputRef` to the hidden one.
18
+ *
19
+ * Owned keys on the visible input: `id`, `type`, `value`, `disabled`, `readOnly`, `onChange`,
20
+ * `onKeyDown`, `onBlur`, `aria-invalid` and `aria-describedby` (Field, token-merged),
21
+ * `data-pending`, `data-disabled`, `data-invalid`. Defaults the consumer's own prop replaces:
22
+ * `inputMode`, `autoComplete="off"`, `autoCorrect="off"`, `spellCheck={false}`.
23
+ */
24
+ export declare function NumberFieldInput({ hiddenInputRef, className, style, render, ...rest }: NumberFieldInputProps): import("react").JSX.Element;
25
+ export declare namespace NumberFieldInput {
26
+ var displayName: string;
27
+ }
28
+ //#endregion
29
+ //# sourceMappingURL=number-field-input.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-field-input.d.mts","names":[],"sources":["../../src/number-field/number-field-input.tsx"],"mappings":";;;;iBA4BiB,8BAA8B,KAC7C;;EAsBA,iBAAiB,IAAI;EACrB,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;;EAE9C,SAAS,WAAW,gCAAgC;;;;;;;;;;;;;wBAgCtC,mBACd,gBACA,WACA,OACA,WACG,QACF,wCAAqB,IAAA;yBANR"}
@@ -0,0 +1,163 @@
1
+ "use client";
2
+ import { dataAttr } from "../internal/data-attr.mjs";
3
+ import { mergeRefs } from "../internal/merge-props.mjs";
4
+ import { useRender } from "../internal/use-render.mjs";
5
+ import { useDevWarning } from "../internal/use-dev-warning.mjs";
6
+ import { useFieldControl, useMergedRef } from "../internal/use-field-control.mjs";
7
+ import { useNumberFieldContext } from "./number-field-context.mjs";
8
+ import { hiddenText } from "./number-field-root.mjs";
9
+ import { isValidElement, useCallback, useEffectEvent, useLayoutEffect, useMemo, useRef } from "react";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ //#region src/number-field/number-field-input.tsx
12
+ const isDev = process.env.NODE_ENV !== "production";
13
+ /** Visually hidden, unfocusable, out of the accessibility tree: the form's control (6.1). */
14
+ const HIDDEN_STYLE = {
15
+ position: "absolute",
16
+ width: "1px",
17
+ height: "1px",
18
+ margin: "-1px",
19
+ padding: 0,
20
+ border: 0,
21
+ opacity: 0,
22
+ overflow: "hidden",
23
+ clipPath: "inset(50%)",
24
+ pointerEvents: "none"
25
+ };
26
+ const TEXT_TYPES = [
27
+ "text",
28
+ "search",
29
+ "tel"
30
+ ];
31
+ /**
32
+ * The visible text field (`input[type=text]`, the user's surface) **followed by** the visually
33
+ * hidden native `input[type=number]` that carries `name`, `form`, `min`, `max`, `step`,
34
+ * `required` and the canonical value (design/components/number-field.md 6). `ref` goes to the
35
+ * visible input, `hiddenInputRef` to the hidden one.
36
+ *
37
+ * Owned keys on the visible input: `id`, `type`, `value`, `disabled`, `readOnly`, `onChange`,
38
+ * `onKeyDown`, `onBlur`, `aria-invalid` and `aria-describedby` (Field, token-merged),
39
+ * `data-pending`, `data-disabled`, `data-invalid`. Defaults the consumer's own prop replaces:
40
+ * `inputMode`, `autoComplete="off"`, `autoCorrect="off"`, `spellCheck={false}`.
41
+ */
42
+ function NumberFieldInput({ hiddenInputRef, className, style, render, ...rest }) {
43
+ const ctx = useNumberFieldContext("NumberField.Input");
44
+ const { state, visibleRef, hiddenRef, mirror, onInputMounted } = ctx;
45
+ const validityElement = useCallback(() => hiddenRef.current, [hiddenRef]);
46
+ const field = useFieldControl({
47
+ labelable: true,
48
+ name: ctx.name,
49
+ disabled: state.disabled,
50
+ required: state.required,
51
+ component: "NumberField.Input",
52
+ validityElement
53
+ });
54
+ const restName = rest.name;
55
+ useDevWarning(restName !== void 0, "NumberField.Input: `name` belongs on NumberField.Root (it goes to the hidden number input); on the visible input it would post the formatted text.");
56
+ const libraryRef = useCallback((element) => {
57
+ if (isDev) {
58
+ const type = element.tagName === "INPUT" ? element.type : "";
59
+ if (!TEXT_TYPES.includes(type)) console.error(`NumberField.Input: rendered <${element.tagName.toLowerCase()}${type === "" ? "" : ` type="${type}"`}>. Render an <input> of type "text", "search" or "tel": a number input would re-introduce the native parser.`);
60
+ }
61
+ visibleRef.current = element;
62
+ return () => {
63
+ if (visibleRef.current === element) visibleRef.current = null;
64
+ };
65
+ }, [visibleRef]);
66
+ const mergedRef = useMergedRef(libraryRef, field.ref);
67
+ const hiddenMergedRef = useMemo(() => mergeRefs(hiddenRef, hiddenInputRef), [hiddenRef, hiddenInputRef]);
68
+ useLayoutEffect(() => {
69
+ const hidden = hiddenRef.current;
70
+ if (hidden !== null) {
71
+ const text = hiddenText(state.value);
72
+ if (hidden.value !== text) hidden.value = text;
73
+ }
74
+ mirror();
75
+ });
76
+ useLayoutEffect(() => {
77
+ const hidden = hiddenRef.current;
78
+ if (hidden === null) return;
79
+ const onInvalid = (event) => event.preventDefault();
80
+ hidden.addEventListener("invalid", onInvalid);
81
+ return () => hidden.removeEventListener("invalid", onInvalid);
82
+ }, [hiddenRef]);
83
+ const onWheel = useEffectEvent((event) => {
84
+ ctx.wheel(event);
85
+ });
86
+ const { stepOnWheel } = ctx;
87
+ useLayoutEffect(() => {
88
+ const visible = visibleRef.current;
89
+ if (!stepOnWheel || visible === null) return;
90
+ visible.addEventListener("wheel", onWheel, { passive: false });
91
+ return () => visible.removeEventListener("wheel", onWheel);
92
+ }, [stepOnWheel, visibleRef]);
93
+ const mounted = useRef(false);
94
+ useLayoutEffect(() => {
95
+ if (mounted.current) return;
96
+ mounted.current = true;
97
+ onInputMounted();
98
+ }, [onInputMounted]);
99
+ const renderType = isValidElement(render) && typeof render.props.type === "string" ? render.props.type : void 0;
100
+ const type = renderType !== void 0 && TEXT_TYPES.includes(renderType) ? renderType : "text";
101
+ useDevWarning(renderType !== void 0 && !TEXT_TYPES.includes(renderType), `NumberField.Input: \`render\` has type="${renderType ?? ""}"; the visible input is rendered as type "text". A number input would re-introduce the native parser the NumberField exists to avoid.`);
102
+ const dataInvalid = ctx.fieldInvalid === void 0 ? {} : { "data-invalid": dataAttr(ctx.fieldInvalid) };
103
+ const libraryProps = {
104
+ ...field.attributes,
105
+ id: ctx.inputId,
106
+ type,
107
+ value: state.text,
108
+ disabled: state.disabled,
109
+ readOnly: state.readOnly,
110
+ onChange: (event) => {
111
+ ctx.typed(event.currentTarget.value);
112
+ },
113
+ onKeyDown: (event) => {
114
+ ctx.keyDown(event.nativeEvent);
115
+ },
116
+ onBlur: (event) => {
117
+ ctx.commitDraft(event.nativeEvent);
118
+ },
119
+ "data-pending": dataAttr(state.pending),
120
+ "data-disabled": dataAttr(state.disabled),
121
+ ...dataInvalid
122
+ };
123
+ const visible = useRender({
124
+ defaultElement: "input",
125
+ render,
126
+ state,
127
+ className,
128
+ style,
129
+ children: void 0,
130
+ props: {
131
+ inputMode: ctx.allowsFractions ? "decimal" : "numeric",
132
+ autoComplete: "off",
133
+ autoCorrect: "off",
134
+ spellCheck: false,
135
+ ...rest
136
+ },
137
+ libraryProps,
138
+ ref: mergedRef
139
+ });
140
+ const { min, max, step } = ctx.bounds;
141
+ return /* @__PURE__ */ jsxs(Fragment, { children: [visible, /* @__PURE__ */ jsx("input", {
142
+ ref: hiddenMergedRef,
143
+ type: "number",
144
+ name: field.name,
145
+ form: ctx.form,
146
+ min,
147
+ max,
148
+ step,
149
+ required: field.required,
150
+ disabled: field.disabled,
151
+ readOnly: state.readOnly,
152
+ defaultValue: ctx.resetTargetText,
153
+ autoComplete: "off",
154
+ "aria-hidden": "true",
155
+ tabIndex: -1,
156
+ style: HIDDEN_STYLE
157
+ })] });
158
+ }
159
+ NumberFieldInput.displayName = "NumberFieldInput";
160
+ //#endregion
161
+ export { NumberFieldInput };
162
+
163
+ //# sourceMappingURL=number-field-input.mjs.map