@octanejs/base-ui 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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -0
  3. package/package.json +47 -0
  4. package/src/avatar.ts +308 -0
  5. package/src/checkbox-group.ts +183 -0
  6. package/src/checkbox.ts +578 -0
  7. package/src/field.ts +676 -0
  8. package/src/fieldset.ts +150 -0
  9. package/src/form.ts +183 -0
  10. package/src/index.ts +23 -0
  11. package/src/input.ts +9 -0
  12. package/src/internal.ts +42 -0
  13. package/src/merge-props.ts +2 -0
  14. package/src/meter.ts +204 -0
  15. package/src/number-field.ts +978 -0
  16. package/src/progress.ts +260 -0
  17. package/src/radio-group.ts +285 -0
  18. package/src/radio.ts +401 -0
  19. package/src/separator.ts +43 -0
  20. package/src/slider.ts +1617 -0
  21. package/src/switch.ts +374 -0
  22. package/src/toggle-group.ts +136 -0
  23. package/src/toggle.ts +145 -0
  24. package/src/use-render.ts +40 -0
  25. package/src/utils/CheckboxGroupContext.ts +30 -0
  26. package/src/utils/CompositeRootContext.ts +27 -0
  27. package/src/utils/DirectionContext.ts +15 -0
  28. package/src/utils/RadioGroupContext.ts +27 -0
  29. package/src/utils/ToggleGroupContext.ts +33 -0
  30. package/src/utils/addEventListener.ts +13 -0
  31. package/src/utils/areArraysEqual.ts +11 -0
  32. package/src/utils/clamp.ts +8 -0
  33. package/src/utils/composeRefs.ts +53 -0
  34. package/src/utils/composite/CompositeItem.ts +56 -0
  35. package/src/utils/composite/CompositeList.ts +190 -0
  36. package/src/utils/composite/CompositeListContext.ts +23 -0
  37. package/src/utils/composite/CompositeRoot.ts +132 -0
  38. package/src/utils/composite/keys.ts +179 -0
  39. package/src/utils/composite/list-utils.ts +78 -0
  40. package/src/utils/composite/useCompositeItem.ts +58 -0
  41. package/src/utils/composite/useCompositeListItem.ts +108 -0
  42. package/src/utils/composite/useCompositeRoot.ts +346 -0
  43. package/src/utils/contains.ts +22 -0
  44. package/src/utils/createChangeEventDetails.ts +81 -0
  45. package/src/utils/field/FieldItemContext.ts +13 -0
  46. package/src/utils/field/FieldRootContext.ts +98 -0
  47. package/src/utils/field/FormContext.ts +37 -0
  48. package/src/utils/field/LabelableContext.ts +32 -0
  49. package/src/utils/field/LabelableProvider.ts +108 -0
  50. package/src/utils/field/constants.ts +80 -0
  51. package/src/utils/field/getCombinedFieldValidityData.ts +16 -0
  52. package/src/utils/field/useAriaLabelledBy.ts +86 -0
  53. package/src/utils/field/useFieldControlRegistration.ts +179 -0
  54. package/src/utils/field/useFieldValidation.ts +311 -0
  55. package/src/utils/field/useLabel.ts +95 -0
  56. package/src/utils/field/useLabelableId.ts +99 -0
  57. package/src/utils/field/useRegisterFieldControl.ts +41 -0
  58. package/src/utils/formatNumber.ts +44 -0
  59. package/src/utils/getDefaultFormSubmitter.ts +19 -0
  60. package/src/utils/getElementRef.ts +13 -0
  61. package/src/utils/getStateAttributesProps.ts +34 -0
  62. package/src/utils/matchesFocusVisible.ts +16 -0
  63. package/src/utils/mergeObjects.ts +18 -0
  64. package/src/utils/mergeProps.ts +206 -0
  65. package/src/utils/noop.ts +4 -0
  66. package/src/utils/number/constants.ts +4 -0
  67. package/src/utils/number/parse.ts +227 -0
  68. package/src/utils/number/types.ts +25 -0
  69. package/src/utils/number/validate.ts +129 -0
  70. package/src/utils/owner.ts +9 -0
  71. package/src/utils/platform.ts +15 -0
  72. package/src/utils/resolveAriaLabelledBy.ts +11 -0
  73. package/src/utils/resolveClassName.ts +8 -0
  74. package/src/utils/resolveRef.ts +10 -0
  75. package/src/utils/resolveStyle.ts +10 -0
  76. package/src/utils/serializeValue.ts +15 -0
  77. package/src/utils/slider/asc.ts +4 -0
  78. package/src/utils/slider/getMidpoint.ts +14 -0
  79. package/src/utils/slider/getPushedThumbValues.ts +73 -0
  80. package/src/utils/slider/getSliderValue.ts +27 -0
  81. package/src/utils/slider/replaceArrayItemAtIndex.ts +8 -0
  82. package/src/utils/slider/resolveThumbCollision.ts +178 -0
  83. package/src/utils/slider/roundValueToStep.ts +22 -0
  84. package/src/utils/slider/validateMinimumDistance.ts +22 -0
  85. package/src/utils/slider/valueArrayToPercentages.ts +11 -0
  86. package/src/utils/stringifyLocale.ts +10 -0
  87. package/src/utils/useAnimationFrame.ts +59 -0
  88. package/src/utils/useAnimationsFinished.ts +100 -0
  89. package/src/utils/useBaseUiId.ts +19 -0
  90. package/src/utils/useButton.ts +217 -0
  91. package/src/utils/useCheckboxGroupParent.ts +113 -0
  92. package/src/utils/useControlled.ts +42 -0
  93. package/src/utils/useFocusableWhenDisabled.ts +84 -0
  94. package/src/utils/useForcedRerendering.ts +11 -0
  95. package/src/utils/useIsHydrating.ts +9 -0
  96. package/src/utils/useOpenChangeComplete.ts +46 -0
  97. package/src/utils/usePressAndHold.ts +35 -0
  98. package/src/utils/useRefWithInit.ts +23 -0
  99. package/src/utils/useRegisteredLabelId.ts +32 -0
  100. package/src/utils/useRenderElement.ts +165 -0
  101. package/src/utils/useStableCallback.ts +26 -0
  102. package/src/utils/useTimeout.ts +51 -0
  103. package/src/utils/useTransitionStatus.ts +115 -0
  104. package/src/utils/useValueAsRef.ts +25 -0
  105. package/src/utils/useValueChanged.ts +37 -0
  106. package/src/utils/valueToPercent.ts +4 -0
  107. package/src/utils/visuallyHidden.ts +24 -0
@@ -0,0 +1,206 @@
1
+ // Ported from .base-ui/packages/react/src/merge-props/mergeProps.ts (v1.6.0).
2
+ //
3
+ // Merges sets of props left-to-right, Object.assign-style (rightmost wins) EXCEPT:
4
+ // - event handlers (`onXxx`) CHAIN — the external (rightmost) handler runs first,
5
+ // then ours, unless the external handler called `event.preventBaseUIHandler()`;
6
+ // - `className` strings concatenate (external first);
7
+ // - `style` objects merge (external keys win).
8
+ // A prop entry may be a getter function `(mergedSoFar) => props`; then it is
9
+ // responsible for its own chaining. `ref` is never merged here.
10
+ //
11
+ // octane adaptation: React makes an event preventable only when it is a SYNTHETIC event
12
+ // (`nativeEvent in event`). octane dispatches NATIVE DOM events, so we attach the
13
+ // `preventBaseUIHandler` shim to ANY event object — the analogue of the radix port using
14
+ // `event.cancelBubble` — so a Base UI part's `preventBaseUIHandler()` coordination works.
15
+ import { mergeObjects } from './mergeObjects';
16
+
17
+ const EMPTY_PROPS = {};
18
+
19
+ type AnyProps = Record<string, any>;
20
+ type InputProps = AnyProps | ((otherProps: AnyProps) => AnyProps) | undefined;
21
+
22
+ export function mergeProps(a: InputProps, b: InputProps): AnyProps;
23
+ export function mergeProps(a: InputProps, b: InputProps, c: InputProps): AnyProps;
24
+ export function mergeProps(a: InputProps, b: InputProps, c: InputProps, d: InputProps): AnyProps;
25
+ export function mergeProps(
26
+ a: InputProps,
27
+ b: InputProps,
28
+ c: InputProps,
29
+ d: InputProps,
30
+ e: InputProps,
31
+ ): AnyProps;
32
+ export function mergeProps(a: any, b: any, c?: any, d?: any, e?: any): AnyProps {
33
+ if (!c && !d && !e && !a) {
34
+ return createInitialMergedProps(b);
35
+ }
36
+ let merged = createInitialMergedProps(a);
37
+ if (b) {
38
+ merged = mergeInto(merged, b);
39
+ }
40
+ if (c) {
41
+ merged = mergeInto(merged, c);
42
+ }
43
+ if (d) {
44
+ merged = mergeInto(merged, d);
45
+ }
46
+ if (e) {
47
+ merged = mergeInto(merged, e);
48
+ }
49
+ return merged;
50
+ }
51
+
52
+ export function mergePropsN(props: InputProps[]): AnyProps {
53
+ if (props.length === 0) {
54
+ return EMPTY_PROPS as AnyProps;
55
+ }
56
+ if (props.length === 1) {
57
+ return createInitialMergedProps(props[0]) as AnyProps;
58
+ }
59
+ let merged = createInitialMergedProps(props[0]);
60
+ for (let i = 1; i < props.length; i += 1) {
61
+ merged = mergeInto(merged, props[i]);
62
+ }
63
+ return merged;
64
+ }
65
+
66
+ function createInitialMergedProps(inputProps: InputProps): AnyProps {
67
+ if (isPropsGetter(inputProps)) {
68
+ return { ...resolvePropsGetter(inputProps, EMPTY_PROPS) };
69
+ }
70
+ return copyInitialProps(inputProps);
71
+ }
72
+
73
+ function mergeInto(merged: AnyProps, inputProps: InputProps): AnyProps {
74
+ if (isPropsGetter(inputProps)) {
75
+ return resolvePropsGetter(inputProps, merged);
76
+ }
77
+ return mutablyMergeInto(merged, inputProps);
78
+ }
79
+
80
+ function copyInitialProps(inputProps: AnyProps | undefined): AnyProps {
81
+ const copiedProps: AnyProps = { ...inputProps };
82
+ for (const propName in copiedProps) {
83
+ const propValue = copiedProps[propName];
84
+ if (isEventHandler(propName, propValue)) {
85
+ copiedProps[propName] = wrapEventHandler(propValue);
86
+ }
87
+ }
88
+ return copiedProps;
89
+ }
90
+
91
+ function mutablyMergeInto(mergedProps: AnyProps, externalProps: AnyProps | undefined): AnyProps {
92
+ if (!externalProps) {
93
+ return mergedProps;
94
+ }
95
+ for (const propName in externalProps) {
96
+ const externalPropValue = externalProps[propName];
97
+ switch (propName) {
98
+ case 'style': {
99
+ mergedProps[propName] = mergeObjects(mergedProps.style, externalPropValue);
100
+ break;
101
+ }
102
+ case 'className': {
103
+ mergedProps[propName] = mergeClassNames(mergedProps.className, externalPropValue);
104
+ break;
105
+ }
106
+ default: {
107
+ if (isEventHandler(propName, externalPropValue)) {
108
+ mergedProps[propName] = mergeEventHandlers(mergedProps[propName], externalPropValue);
109
+ } else {
110
+ mergedProps[propName] = externalPropValue;
111
+ }
112
+ }
113
+ }
114
+ }
115
+ return mergedProps;
116
+ }
117
+
118
+ function isEventHandler(key: string, value: unknown): boolean {
119
+ // `onX` where X is uppercase; value is a function or undefined. Char-code check
120
+ // (matches Base UI — faster than a regex).
121
+ const code0 = key.charCodeAt(0);
122
+ const code1 = key.charCodeAt(1);
123
+ const code2 = key.charCodeAt(2);
124
+ return (
125
+ code0 === 111 /* o */ &&
126
+ code1 === 110 /* n */ &&
127
+ code2 >= 65 /* A */ &&
128
+ code2 <= 90 /* Z */ &&
129
+ (typeof value === 'function' || typeof value === 'undefined')
130
+ );
131
+ }
132
+
133
+ function isPropsGetter(inputProps: InputProps): inputProps is (props: AnyProps) => AnyProps {
134
+ return typeof inputProps === 'function';
135
+ }
136
+
137
+ function resolvePropsGetter(inputProps: InputProps, previousProps: AnyProps): AnyProps {
138
+ if (isPropsGetter(inputProps)) {
139
+ return inputProps(previousProps);
140
+ }
141
+ return inputProps ?? (EMPTY_PROPS as AnyProps);
142
+ }
143
+
144
+ // octane: any event object is preventable (octane events are native, never synthetic).
145
+ function isPreventableEvent(event: unknown): event is object {
146
+ return event != null && typeof event === 'object';
147
+ }
148
+
149
+ function mergeEventHandlers(
150
+ ourHandler: Function | undefined,
151
+ theirHandler: Function | undefined,
152
+ ): Function | undefined {
153
+ if (!theirHandler) {
154
+ return ourHandler;
155
+ }
156
+ if (!ourHandler) {
157
+ return wrapEventHandler(theirHandler);
158
+ }
159
+ return (...args: unknown[]) => {
160
+ const event = args[0];
161
+ if (isPreventableEvent(event)) {
162
+ makeEventPreventable(event);
163
+ const result = theirHandler(...args);
164
+ if (!(event as any).baseUIHandlerPrevented) {
165
+ ourHandler(...args);
166
+ }
167
+ return result;
168
+ }
169
+ const result = theirHandler(...args);
170
+ ourHandler(...args);
171
+ return result;
172
+ };
173
+ }
174
+
175
+ function wrapEventHandler(handler: Function | undefined): Function | undefined {
176
+ if (!handler) {
177
+ return handler;
178
+ }
179
+ return (...args: unknown[]) => {
180
+ const event = args[0];
181
+ if (isPreventableEvent(event)) {
182
+ makeEventPreventable(event);
183
+ }
184
+ return handler(...args);
185
+ };
186
+ }
187
+
188
+ export function makeEventPreventable<T extends object>(event: T): T {
189
+ (event as any).preventBaseUIHandler = () => {
190
+ (event as any).baseUIHandlerPrevented = true;
191
+ };
192
+ return event;
193
+ }
194
+
195
+ export function mergeClassNames(
196
+ ourClassName: string | undefined,
197
+ theirClassName: string | undefined,
198
+ ): string | undefined {
199
+ if (theirClassName) {
200
+ if (ourClassName) {
201
+ return theirClassName + ' ' + ourClassName;
202
+ }
203
+ return theirClassName;
204
+ }
205
+ return ourClassName;
206
+ }
@@ -0,0 +1,4 @@
1
+ // Ported from .base-ui/packages/react/src/internals/noop.ts (@base-ui/utils/empty NOOP).
2
+ // A single shared no-op — context hooks compare against it BY IDENTITY to detect "no
3
+ // provider" (e.g. `context.setValidityData === NOOP`), so it must be one module-level value.
4
+ export function NOOP(): void {}
@@ -0,0 +1,4 @@
1
+ // Ported verbatim from .base-ui/packages/react/src/number-field/utils/constants.ts.
2
+ export const CHANGE_VALUE_TICK_DELAY = 60;
3
+ export const START_AUTO_CHANGE_DELAY = 400;
4
+ export const SCROLLING_POINTER_MOVE_DISTANCE = 8;
@@ -0,0 +1,227 @@
1
+ // Ported verbatim from .base-ui/packages/react/src/number-field/utils/parse.ts. Locale-aware
2
+ // number parsing (multiple numeral systems, currency/unit/percent/permille stripping). Pure —
3
+ // only the `getFormatter` import path changes.
4
+ import { getFormatter } from '../formatNumber';
5
+
6
+ export const HAN_NUMERALS = ['零', '〇', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
7
+ export const HAN_NUMERAL_TO_DIGIT: Record<string, string> = {
8
+ 零: '0',
9
+ 〇: '0',
10
+ 一: '1',
11
+ 二: '2',
12
+ 三: '3',
13
+ 四: '4',
14
+ 五: '5',
15
+ 六: '6',
16
+ 七: '7',
17
+ 八: '8',
18
+ 九: '9',
19
+ };
20
+ export const ARABIC_NUMERALS = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
21
+ export const PERSIAN_NUMERALS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
22
+ export const FULLWIDTH_NUMERALS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
23
+
24
+ export const PERCENTAGES = ['%', '٪', '%', '﹪'];
25
+ export const PERMILLE = ['‰', '؉'];
26
+
27
+ export const UNICODE_MINUS_SIGNS = ['−', '-', '‒', '–', '—', '﹣'];
28
+ export const UNICODE_PLUS_SIGNS = ['+', '﹢'];
29
+
30
+ export const FULLWIDTH_DECIMAL = '.';
31
+ export const FULLWIDTH_GROUP = ',';
32
+
33
+ export const ARABIC_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`, 'g');
34
+ export const PERSIAN_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`, 'g');
35
+ export const FULLWIDTH_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`, 'g');
36
+ export const HAN_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`, 'g');
37
+ export const PERCENT_RE = new RegExp(`[${PERCENTAGES.join('')}]`);
38
+ export const PERMILLE_RE = new RegExp(`[${PERMILLE.join('')}]`);
39
+ const PERCENT_GLOBAL_RE = new RegExp(PERCENT_RE.source, 'g');
40
+ const PERMILLE_GLOBAL_RE = new RegExp(PERMILLE_RE.source, 'g');
41
+
42
+ export const ARABIC_DETECT_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`);
43
+ export const PERSIAN_DETECT_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`);
44
+ export const HAN_DETECT_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`);
45
+ export const FULLWIDTH_DETECT_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`);
46
+
47
+ export function isNumeralChar(char: string): boolean {
48
+ return (
49
+ (char >= '0' && char <= '9') ||
50
+ ARABIC_DETECT_RE.test(char) ||
51
+ PERSIAN_DETECT_RE.test(char) ||
52
+ HAN_DETECT_RE.test(char) ||
53
+ FULLWIDTH_DETECT_RE.test(char)
54
+ );
55
+ }
56
+
57
+ export const BASE_NON_NUMERIC_SYMBOLS = [
58
+ '.',
59
+ ',',
60
+ FULLWIDTH_DECIMAL,
61
+ FULLWIDTH_GROUP,
62
+ '٫',
63
+ '٬',
64
+ ] as const;
65
+ export const SPACE_SEPARATOR_RE = /\p{Zs}/u;
66
+ export const PLUS_SIGNS_WITH_ASCII = ['+', ...UNICODE_PLUS_SIGNS];
67
+ export const MINUS_SIGNS_WITH_ASCII = ['-', ...UNICODE_MINUS_SIGNS];
68
+
69
+ const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
70
+ const escapeClassChar = (s: string) => s.replace(/[-\\\]^]/g, (m) => `\\${m}`);
71
+
72
+ function shiftDecimal(value: number, exponentDelta: number): number {
73
+ const [coefficient, exponent = '0'] = String(value).split('e');
74
+ return Number(`${coefficient}e${Number(exponent) + exponentDelta}`);
75
+ }
76
+
77
+ const charClassFrom = (chars: string[]) => `[${chars.map(escapeClassChar).join('')}]`;
78
+
79
+ const ANY_MINUS_CLASS = charClassFrom(['-'].concat(UNICODE_MINUS_SIGNS));
80
+ const ANY_PLUS_CLASS = charClassFrom(['+'].concat(UNICODE_PLUS_SIGNS));
81
+
82
+ export const ANY_MINUS_RE = new RegExp(ANY_MINUS_CLASS, 'gu');
83
+ export const ANY_PLUS_RE = new RegExp(ANY_PLUS_CLASS, 'gu');
84
+ export const ANY_MINUS_DETECT_RE = new RegExp(ANY_MINUS_CLASS);
85
+ export const ANY_PLUS_DETECT_RE = new RegExp(ANY_PLUS_CLASS);
86
+
87
+ export function getNumberLocaleDetails(
88
+ locale?: Intl.LocalesArgument,
89
+ options?: Intl.NumberFormatOptions,
90
+ ): Partial<Record<Intl.NumberFormatPartTypes, string | undefined>> {
91
+ const parts = getFormatter(locale, options).formatToParts(11111.1);
92
+ const result: Partial<Record<Intl.NumberFormatPartTypes, string | undefined>> = {};
93
+ parts.forEach((part) => {
94
+ result[part.type] = part.value;
95
+ });
96
+ getFormatter(locale)
97
+ .formatToParts(0.1)
98
+ .forEach((part) => {
99
+ if (part.type === 'decimal') {
100
+ result[part.type] = part.value;
101
+ }
102
+ });
103
+ return result;
104
+ }
105
+
106
+ export function parseNumber(
107
+ formattedNumber: string,
108
+ locale?: Intl.LocalesArgument,
109
+ options?: Intl.NumberFormatOptions,
110
+ ): number | null {
111
+ if (formattedNumber == null) {
112
+ return null;
113
+ }
114
+
115
+ let input = String(formattedNumber)
116
+ .replace(/\p{Cf}/gu, '')
117
+ .trim();
118
+
119
+ input = input.replace(ANY_MINUS_RE, '-').replace(ANY_PLUS_RE, '+');
120
+
121
+ let isNegative = false;
122
+
123
+ const trailing = input.match(/([+-])\s*$/);
124
+ if (trailing) {
125
+ if (trailing[1] === '-') {
126
+ isNegative = true;
127
+ }
128
+ input = input.replace(/([+-])\s*$/, '');
129
+ }
130
+ const leading = input.match(/^\s*([+-])/);
131
+ if (leading) {
132
+ if (leading[1] === '-') {
133
+ isNegative = true;
134
+ }
135
+ input = input.replace(/^\s*[+-]/, '');
136
+ }
137
+
138
+ let computedLocale = locale;
139
+ if (computedLocale === undefined) {
140
+ if (ARABIC_DETECT_RE.test(input) || PERSIAN_DETECT_RE.test(input)) {
141
+ computedLocale = 'ar';
142
+ } else if (HAN_DETECT_RE.test(input)) {
143
+ computedLocale = 'zh';
144
+ }
145
+ }
146
+
147
+ const { group, decimal, currency, exponentSeparator } = getNumberLocaleDetails(
148
+ computedLocale,
149
+ options,
150
+ );
151
+
152
+ const unitParts = getFormatter(computedLocale, options)
153
+ .formatToParts(1)
154
+ .filter((p) => p.type === 'unit')
155
+ .map((p) => escapeRegExp(p.value));
156
+ const unitRegex = unitParts.length ? new RegExp(unitParts.join('|'), 'g') : null;
157
+
158
+ let groupRegex: RegExp | null = null;
159
+ if (group) {
160
+ const isSpaceGroup = /\p{Zs}/u.test(group);
161
+ const isApostropheGroup = group === "'" || group === '’';
162
+ if (isSpaceGroup) {
163
+ groupRegex = /\p{Zs}/gu;
164
+ } else if (isApostropheGroup) {
165
+ groupRegex = /['’]/g;
166
+ } else {
167
+ groupRegex = new RegExp(escapeRegExp(group), 'g');
168
+ }
169
+ }
170
+
171
+ const replacements: Array<{
172
+ regex: RegExp | null;
173
+ replacement: string | ((m: string) => string);
174
+ }> = [
175
+ { regex: groupRegex, replacement: '' },
176
+ { regex: decimal ? new RegExp(escapeRegExp(decimal), 'g') : null, replacement: '.' },
177
+ { regex: /./g, replacement: '.' },
178
+ { regex: /,/g, replacement: '' },
179
+ { regex: /٫/g, replacement: '.' },
180
+ { regex: /٬/g, replacement: '' },
181
+ { regex: currency ? new RegExp(escapeRegExp(currency), 'g') : null, replacement: '' },
182
+ { regex: unitRegex, replacement: '' },
183
+ { regex: PERCENT_GLOBAL_RE, replacement: '' },
184
+ { regex: PERMILLE_GLOBAL_RE, replacement: '' },
185
+ {
186
+ regex: exponentSeparator ? new RegExp(escapeRegExp(exponentSeparator), 'g') : null,
187
+ replacement: 'e',
188
+ },
189
+ { regex: ARABIC_RE, replacement: (ch) => String(ARABIC_NUMERALS.indexOf(ch)) },
190
+ { regex: PERSIAN_RE, replacement: (ch) => String(PERSIAN_NUMERALS.indexOf(ch)) },
191
+ { regex: FULLWIDTH_RE, replacement: (ch) => String(FULLWIDTH_NUMERALS.indexOf(ch)) },
192
+ { regex: HAN_RE, replacement: (ch) => HAN_NUMERAL_TO_DIGIT[ch] },
193
+ ];
194
+
195
+ let unformatted = replacements.reduce((acc, { regex, replacement }) => {
196
+ return regex ? acc.replace(regex, replacement as any) : acc;
197
+ }, input);
198
+
199
+ const lastDot = unformatted.lastIndexOf('.');
200
+ if (lastDot !== -1) {
201
+ unformatted = `${unformatted.slice(0, lastDot).replace(/\./g, '')}.${unformatted.slice(lastDot + 1).replace(/\./g, '')}`;
202
+ }
203
+
204
+ if (/^[-+]?Infinity$/i.test(input) || input.includes('∞')) {
205
+ return null;
206
+ }
207
+
208
+ const parseTarget = (isNegative ? '-' : '') + unformatted;
209
+ let num = parseFloat(parseTarget);
210
+
211
+ const style = options?.style;
212
+ const isUnitPercent = style === 'unit' && options?.unit === 'percent';
213
+ const hasPercentSymbol = PERCENT_RE.test(formattedNumber) || style === 'percent';
214
+ const hasPermilleSymbol = PERMILLE_RE.test(formattedNumber);
215
+
216
+ if (hasPermilleSymbol) {
217
+ num = shiftDecimal(num, -3);
218
+ } else if (!isUnitPercent && hasPercentSymbol) {
219
+ num = shiftDecimal(num, -2);
220
+ }
221
+
222
+ if (!Number.isFinite(num)) {
223
+ return null;
224
+ }
225
+
226
+ return num;
227
+ }
@@ -0,0 +1,25 @@
1
+ // Ported from .base-ui/packages/react/src/number-field/utils/types.ts.
2
+ export type Direction = -1 | 1;
3
+
4
+ export type DirectionalChangeReason =
5
+ | 'increment-press'
6
+ | 'decrement-press'
7
+ | 'wheel'
8
+ | 'scrub'
9
+ | 'keyboard';
10
+
11
+ export interface ChangeEventCustomProperties {
12
+ direction?: Direction;
13
+ }
14
+
15
+ export interface IncrementValueParameters {
16
+ direction: Direction;
17
+ event?: any;
18
+ reason: DirectionalChangeReason;
19
+ currentValue?: number | null;
20
+ }
21
+
22
+ export interface EventWithOptionalKeyState {
23
+ altKey?: boolean;
24
+ shiftKey?: boolean;
25
+ }
@@ -0,0 +1,129 @@
1
+ // Ported verbatim from .base-ui/packages/react/src/number-field/utils/validate.ts. Snapping,
2
+ // clamping, and floating-point cleanup for stepped values. Pure.
3
+ import { clamp } from '../clamp';
4
+ import { getFormatter } from '../formatNumber';
5
+ import { parseNumber } from './parse';
6
+
7
+ const STEP_EPSILON_FACTOR = 1e-10;
8
+ const MAX_FLOATING_POINT_CLEANUP_DELTA = 1e-10;
9
+
10
+ type NumberFormatOptionsWithRounding = Intl.NumberFormatOptions & {
11
+ roundingIncrement?: number;
12
+ roundingMode?: string;
13
+ roundingPriority?: string;
14
+ };
15
+
16
+ export function hasNumberFormatRoundingOptions(
17
+ format?: NumberFormatOptionsWithRounding,
18
+ ): format is NumberFormatOptionsWithRounding {
19
+ return (
20
+ format?.maximumFractionDigits != null ||
21
+ format?.minimumFractionDigits != null ||
22
+ format?.maximumSignificantDigits != null ||
23
+ format?.minimumSignificantDigits != null ||
24
+ format?.roundingIncrement != null ||
25
+ format?.roundingMode != null ||
26
+ format?.roundingPriority != null
27
+ );
28
+ }
29
+
30
+ export function removeFloatingPointErrors(
31
+ value: number,
32
+ format?: NumberFormatOptionsWithRounding,
33
+ ): number {
34
+ if (!Number.isFinite(value)) {
35
+ return value;
36
+ }
37
+
38
+ if (!hasNumberFormatRoundingOptions(format)) {
39
+ const roundedValue = parseFloat(value.toPrecision(15));
40
+ const cleanupDelta = Math.abs(roundedValue - value);
41
+ const cleanupTolerance = Math.min(
42
+ Number.EPSILON * Math.max(1, Math.abs(value)),
43
+ MAX_FLOATING_POINT_CLEANUP_DELTA,
44
+ );
45
+ return cleanupDelta <= cleanupTolerance ? roundedValue : value;
46
+ }
47
+
48
+ const formatter = getFormatter('en-US', {
49
+ ...format,
50
+ signDisplay: 'auto',
51
+ currencySign: 'standard',
52
+ notation: format.notation === 'compact' ? 'standard' : format.notation,
53
+ useGrouping: false,
54
+ } as NumberFormatOptionsWithRounding);
55
+ const roundedText = formatter.format(value);
56
+ const roundedValue = parseNumber(roundedText, 'en-US', format);
57
+
58
+ if (roundedValue === null) {
59
+ return value;
60
+ }
61
+
62
+ return formatter.format(roundedValue) === roundedText ? roundedValue : value;
63
+ }
64
+
65
+ function snapToStep(
66
+ value: number,
67
+ base: number,
68
+ step: number,
69
+ mode: 'directional' | 'nearest' = 'directional',
70
+ ): number {
71
+ const stepSize = Math.abs(step);
72
+ const direction = Math.sign(step);
73
+ const tolerance = stepSize * STEP_EPSILON_FACTOR * direction;
74
+ const rawSteps = value - base + tolerance;
75
+
76
+ if (mode === 'nearest') {
77
+ return base + Math.round(rawSteps / step) * step;
78
+ }
79
+
80
+ const snappedSteps =
81
+ direction > 0 ? Math.floor(rawSteps / stepSize) : Math.ceil(rawSteps / stepSize);
82
+ return base + snappedSteps * stepSize;
83
+ }
84
+
85
+ export function toValidatedNumber(
86
+ value: number | null,
87
+ {
88
+ step,
89
+ minWithDefault,
90
+ maxWithDefault,
91
+ minWithZeroDefault,
92
+ format,
93
+ snapOnStep,
94
+ small,
95
+ clamp: shouldClamp,
96
+ }: {
97
+ step: number | undefined;
98
+ minWithDefault: number;
99
+ maxWithDefault: number;
100
+ minWithZeroDefault: number;
101
+ format: NumberFormatOptionsWithRounding | undefined;
102
+ snapOnStep: boolean;
103
+ small: boolean;
104
+ clamp: boolean;
105
+ },
106
+ ): number | null {
107
+ if (value === null) {
108
+ return value;
109
+ }
110
+
111
+ let nextValue = value;
112
+
113
+ if (step != null && snapOnStep && step !== 0) {
114
+ const base =
115
+ small || minWithDefault === Number.MIN_SAFE_INTEGER ? minWithZeroDefault : minWithDefault;
116
+ nextValue = snapToStep(nextValue, base, step, small ? 'nearest' : 'directional');
117
+ }
118
+
119
+ if (shouldClamp) {
120
+ nextValue = clamp(nextValue, minWithDefault, maxWithDefault);
121
+ }
122
+
123
+ if (step == null && !hasNumberFormatRoundingOptions(format)) {
124
+ return nextValue;
125
+ }
126
+
127
+ const roundedValue = removeFloatingPointErrors(nextValue, format);
128
+ return shouldClamp ? clamp(roundedValue, minWithDefault, maxWithDefault) : roundedValue;
129
+ }
@@ -0,0 +1,9 @@
1
+ // Ported from .base-ui/packages/utils/src/owner.ts. `ownerWindow` resolves the window that
2
+ // owns a node (Base UI re-exports `@floating-ui/utils/dom`'s `getWindow`).
3
+ export function ownerWindow(node: Node | null | undefined): Window & typeof globalThis {
4
+ return ((node as any)?.ownerDocument?.defaultView ?? window) as Window & typeof globalThis;
5
+ }
6
+
7
+ export function ownerDocument(node: Element | null): Document {
8
+ return node?.ownerDocument || document;
9
+ }
@@ -0,0 +1,15 @@
1
+ // Minimal port of @base-ui/utils/platform — only the `os.ios` check NumberField needs.
2
+ // (jsdom is not iOS, so the iOS input-mode branch is inert in tests.)
3
+ function isIOS(): boolean {
4
+ if (typeof navigator === 'undefined') {
5
+ return false;
6
+ }
7
+ return /iP(ad|hone|od)/.test(navigator.platform ?? '');
8
+ }
9
+ export const platform = {
10
+ os: {
11
+ get ios() {
12
+ return isIOS();
13
+ },
14
+ },
15
+ };
@@ -0,0 +1,11 @@
1
+ // Ported verbatim from .base-ui/packages/react/src/utils/resolveAriaLabelledBy.ts.
2
+ export function getDefaultLabelId(id: string | null | undefined) {
3
+ return id == null ? undefined : `${id}-label`;
4
+ }
5
+
6
+ export function resolveAriaLabelledBy(
7
+ fieldLabelId: string | undefined,
8
+ localLabelId: string | undefined,
9
+ ) {
10
+ return fieldLabelId ?? localLabelId;
11
+ }
@@ -0,0 +1,8 @@
1
+ // Ported from .base-ui/packages/react/src/utils/resolveClassName.ts.
2
+ // A className that is a function is called with the component state.
3
+ export function resolveClassName<State>(
4
+ className: string | ((state: State) => string | undefined) | undefined,
5
+ state: State,
6
+ ): string | undefined {
7
+ return typeof className === 'function' ? className(state) : className;
8
+ }
@@ -0,0 +1,10 @@
1
+ // Ported from .base-ui/packages/react/src/utils/resolveRef.ts. Returns a ref object's
2
+ // `.current`, or the value itself if it's already an element.
3
+ export function resolveRef<T extends HTMLElement | null | undefined>(
4
+ maybeRef: T | { current: T },
5
+ ): T {
6
+ if (maybeRef == null) {
7
+ return maybeRef as T;
8
+ }
9
+ return 'current' in maybeRef ? maybeRef.current : maybeRef;
10
+ }
@@ -0,0 +1,10 @@
1
+ // Ported from .base-ui/packages/react/src/utils/resolveStyle.ts.
2
+ // A style that is a function is called with the component state.
3
+ type CSSProperties = Record<string, any>;
4
+
5
+ export function resolveStyle<State>(
6
+ style: CSSProperties | ((state: State) => CSSProperties | undefined) | undefined,
7
+ state: State,
8
+ ): CSSProperties | undefined {
9
+ return typeof style === 'function' ? style(state) : style;
10
+ }
@@ -0,0 +1,15 @@
1
+ // Ported verbatim from .base-ui/packages/react/src/internals/serializeValue.ts. Coerces a
2
+ // control value to the string a hidden `<input value>` needs.
3
+ export function serializeValue(value: unknown): string {
4
+ if (value == null) {
5
+ return '';
6
+ }
7
+ if (typeof value === 'string') {
8
+ return value;
9
+ }
10
+ try {
11
+ return JSON.stringify(value);
12
+ } catch {
13
+ return String(value);
14
+ }
15
+ }
@@ -0,0 +1,4 @@
1
+ // Ported verbatim from .base-ui/packages/react/src/slider/utils/asc.ts.
2
+ export function asc(a: number, b: number) {
3
+ return a - b;
4
+ }