base-ui-vue 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/button/ToolbarButton.cjs +6 -0
  2. package/dist/button/ToolbarButton.js +1 -1
  3. package/dist/content/ScrollAreaContent.cjs +168 -0
  4. package/dist/content/ScrollAreaContent.cjs.map +1 -0
  5. package/dist/content/ScrollAreaContent.js +133 -0
  6. package/dist/content/ScrollAreaContent.js.map +1 -0
  7. package/dist/control/SliderControl.js +2 -2
  8. package/dist/corner/ScrollAreaCorner.cjs +77 -0
  9. package/dist/corner/ScrollAreaCorner.cjs.map +1 -0
  10. package/dist/corner/ScrollAreaCorner.js +72 -0
  11. package/dist/corner/ScrollAreaCorner.js.map +1 -0
  12. package/dist/decrement/NumberFieldDecrement.cjs +861 -0
  13. package/dist/decrement/NumberFieldDecrement.cjs.map +1 -0
  14. package/dist/decrement/NumberFieldDecrement.js +700 -0
  15. package/dist/decrement/NumberFieldDecrement.js.map +1 -0
  16. package/dist/fallback/AvatarFallback.cjs +2 -46
  17. package/dist/fallback/AvatarFallback.cjs.map +1 -1
  18. package/dist/fallback/AvatarFallback.js +3 -41
  19. package/dist/fallback/AvatarFallback.js.map +1 -1
  20. package/dist/group/NumberFieldGroup.cjs +72 -0
  21. package/dist/group/NumberFieldGroup.cjs.map +1 -0
  22. package/dist/group/NumberFieldGroup.js +67 -0
  23. package/dist/group/NumberFieldGroup.js.map +1 -0
  24. package/dist/increment/NumberFieldIncrement.cjs +112 -0
  25. package/dist/increment/NumberFieldIncrement.cjs.map +1 -0
  26. package/dist/increment/NumberFieldIncrement.js +107 -0
  27. package/dist/increment/NumberFieldIncrement.js.map +1 -0
  28. package/dist/index.cjs +52 -0
  29. package/dist/index.d.cts +1761 -430
  30. package/dist/index.d.cts.map +1 -1
  31. package/dist/index.d.ts +1761 -430
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +7 -2
  34. package/dist/index2.cjs +4065 -60
  35. package/dist/index2.cjs.map +1 -1
  36. package/dist/index2.js +3955 -184
  37. package/dist/index2.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/index.ts +6 -0
  40. package/src/input/Input.vue +37 -0
  41. package/src/input/InputDataAttributes.ts +30 -0
  42. package/src/input/index.ts +4 -0
  43. package/src/meter/index.ts +16 -0
  44. package/src/meter/indicator/MeterIndicator.vue +65 -0
  45. package/src/meter/label/MeterLabel.vue +63 -0
  46. package/src/meter/root/MeterRoot.vue +131 -0
  47. package/src/meter/root/MeterRootContext.ts +41 -0
  48. package/src/meter/track/MeterTrack.vue +46 -0
  49. package/src/meter/value/MeterValue.vue +85 -0
  50. package/src/number-field/decrement/NumberFieldDecrement.vue +109 -0
  51. package/src/number-field/group/NumberFieldGroup.vue +47 -0
  52. package/src/number-field/increment/NumberFieldIncrement.vue +109 -0
  53. package/src/number-field/index.ts +42 -0
  54. package/src/number-field/input/NumberFieldInput.vue +455 -0
  55. package/src/number-field/root/NumberFieldRoot.vue +626 -0
  56. package/src/number-field/root/NumberFieldRootContext.ts +94 -0
  57. package/src/number-field/root/useNumberFieldButton.ts +171 -0
  58. package/src/number-field/scrub-area/NumberFieldScrubArea.vue +359 -0
  59. package/src/number-field/scrub-area/NumberFieldScrubAreaContext.ts +26 -0
  60. package/src/number-field/scrub-area-cursor/NumberFieldScrubAreaCursor.vue +75 -0
  61. package/src/number-field/utils/constants.ts +4 -0
  62. package/src/number-field/utils/getViewportRect.ts +34 -0
  63. package/src/number-field/utils/parse.ts +248 -0
  64. package/src/number-field/utils/stateAttributesMapping.ts +9 -0
  65. package/src/number-field/utils/subscribeToVisualViewportResize.ts +27 -0
  66. package/src/number-field/utils/types.ts +24 -0
  67. package/src/number-field/utils/validate.ts +120 -0
  68. package/src/otp-field/index.ts +22 -0
  69. package/src/otp-field/input/OtpFieldInput.vue +336 -0
  70. package/src/otp-field/root/OtpFieldRoot.vue +583 -0
  71. package/src/otp-field/root/OtpFieldRootContext.ts +81 -0
  72. package/src/otp-field/utils/otp.ts +135 -0
  73. package/src/otp-field/utils/stateAttributesMapping.ts +16 -0
  74. package/src/progress/index.ts +23 -0
  75. package/src/progress/indicator/ProgressIndicator.vue +74 -0
  76. package/src/progress/label/ProgressLabel.vue +63 -0
  77. package/src/progress/root/ProgressRoot.vue +160 -0
  78. package/src/progress/root/ProgressRootContext.ts +51 -0
  79. package/src/progress/root/ProgressRootDataAttributes.ts +14 -0
  80. package/src/progress/root/stateAttributesMapping.ts +18 -0
  81. package/src/progress/track/ProgressTrack.vue +48 -0
  82. package/src/progress/value/ProgressValue.vue +92 -0
  83. package/src/scroll-area/constants.ts +2 -0
  84. package/src/scroll-area/content/ScrollAreaContent.vue +87 -0
  85. package/src/scroll-area/corner/ScrollAreaCorner.vue +64 -0
  86. package/src/scroll-area/index.ts +25 -0
  87. package/src/scroll-area/root/ScrollAreaRoot.vue +297 -0
  88. package/src/scroll-area/root/ScrollAreaRootContext.ts +89 -0
  89. package/src/scroll-area/root/ScrollAreaRootCssVars.ts +4 -0
  90. package/src/scroll-area/root/ScrollAreaRootDataAttributes.ts +9 -0
  91. package/src/scroll-area/root/stateAttributes.ts +14 -0
  92. package/src/scroll-area/scrollbar/ScrollAreaScrollbar.vue +263 -0
  93. package/src/scroll-area/scrollbar/ScrollAreaScrollbarContext.ts +20 -0
  94. package/src/scroll-area/scrollbar/ScrollAreaScrollbarCssVars.ts +4 -0
  95. package/src/scroll-area/scrollbar/ScrollAreaScrollbarDataAttributes.ts +11 -0
  96. package/src/scroll-area/thumb/ScrollAreaThumb.vue +120 -0
  97. package/src/scroll-area/thumb/ScrollAreaThumbDataAttributes.ts +3 -0
  98. package/src/scroll-area/utils/getOffset.ts +34 -0
  99. package/src/scroll-area/viewport/ScrollAreaViewport.vue +379 -0
  100. package/src/scroll-area/viewport/ScrollAreaViewportContext.ts +20 -0
  101. package/src/scroll-area/viewport/ScrollAreaViewportCssVars.ts +6 -0
  102. package/src/scroll-area/viewport/ScrollAreaViewportDataAttributes.ts +9 -0
  103. package/src/utils/detectBrowser.ts +15 -0
  104. package/src/utils/formatNumber.ts +60 -2
  105. package/src/utils/scrollEdges.ts +33 -0
  106. package/src/utils/styles.ts +28 -0
  107. package/src/utils/useInterval.ts +45 -0
  108. package/src/utils/usePressAndHold.ts +260 -0
  109. package/src/utils/useValueChanged.ts +21 -0
@@ -0,0 +1,248 @@
1
+ import { getFormatter } from '../../utils/formatNumber'
2
+
3
+ export const HAN_NUMERALS = ['零', '〇', '一', '二', '三', '四', '五', '六', '七', '八', '九']
4
+ // Map Han numeral characters to ASCII digits. Includes both forms of zero.
5
+ export const HAN_NUMERAL_TO_DIGIT: Record<string, string> = {
6
+ 零: '0',
7
+ 〇: '0',
8
+ 一: '1',
9
+ 二: '2',
10
+ 三: '3',
11
+ 四: '4',
12
+ 五: '5',
13
+ 六: '6',
14
+ 七: '7',
15
+ 八: '8',
16
+ 九: '9',
17
+ }
18
+ export const ARABIC_NUMERALS = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']
19
+ export const PERSIAN_NUMERALS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']
20
+ export const FULLWIDTH_NUMERALS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
21
+
22
+ export const PERCENTAGES = ['%', '٪', '%', '﹪']
23
+ export const PERMILLE = ['‰', '؉']
24
+
25
+ export const UNICODE_MINUS_SIGNS = ['−', '-', '‒', '–', '—', '﹣']
26
+ export const UNICODE_PLUS_SIGNS = ['+', '﹢']
27
+
28
+ // Fullwidth punctuation common in CJK inputs
29
+ export const FULLWIDTH_DECIMAL = '.' // U+FF0E
30
+ export const FULLWIDTH_GROUP = ',' // U+FF0C
31
+
32
+ export const ARABIC_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`, 'g')
33
+ export const PERSIAN_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`, 'g')
34
+ export const FULLWIDTH_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`, 'g')
35
+ export const HAN_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`, 'g')
36
+ export const PERCENT_RE = new RegExp(`[${PERCENTAGES.join('')}]`)
37
+ export const PERMILLE_RE = new RegExp(`[${PERMILLE.join('')}]`)
38
+ const PERCENT_GLOBAL_RE = new RegExp(PERCENT_RE.source, 'g')
39
+ const PERMILLE_GLOBAL_RE = new RegExp(PERMILLE_RE.source, 'g')
40
+
41
+ // Detection regexes (non-global to avoid lastIndex side effects)
42
+ export const ARABIC_DETECT_RE = /[٠١٢٣٤٥٦٧٨٩]/
43
+ export const PERSIAN_DETECT_RE = /[۰۱۲۳۴۵۶۷۸۹]/
44
+ export const HAN_DETECT_RE = /[零〇一二三四五六七八九]/
45
+ export const FULLWIDTH_DETECT_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`)
46
+
47
+ export const BASE_NON_NUMERIC_SYMBOLS = [
48
+ '.',
49
+ ',',
50
+ FULLWIDTH_DECIMAL,
51
+ FULLWIDTH_GROUP,
52
+ '٫',
53
+ '٬',
54
+ ] as const
55
+ export const SPACE_SEPARATOR_RE = /\p{Zs}/u
56
+ export const PLUS_SIGNS_WITH_ASCII = ['+', ...UNICODE_PLUS_SIGNS]
57
+ export const MINUS_SIGNS_WITH_ASCII = ['-', ...UNICODE_MINUS_SIGNS]
58
+
59
+ function escapeRegExp(s: string) {
60
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
61
+ }
62
+ function escapeClassChar(s: string) {
63
+ return s.replace(/[-\\\]^]/g, m => `\\${m}`) // escape for use inside [...]
64
+ }
65
+
66
+ function shiftDecimal(value: number, exponentDelta: number) {
67
+ const [coefficient, exponent = '0'] = String(value).split('e')
68
+ return Number(`${coefficient}e${Number(exponent) + exponentDelta}`)
69
+ }
70
+
71
+ function charClassFrom(chars: string[]) {
72
+ return `[${chars.map(escapeClassChar).join('')}]`
73
+ }
74
+
75
+ const ANY_MINUS_CLASS = charClassFrom(['-'].concat(UNICODE_MINUS_SIGNS))
76
+ const ANY_PLUS_CLASS = charClassFrom(['+'].concat(UNICODE_PLUS_SIGNS))
77
+
78
+ export const ANY_MINUS_RE = new RegExp(ANY_MINUS_CLASS, 'gu')
79
+ export const ANY_PLUS_RE = new RegExp(ANY_PLUS_CLASS, 'gu')
80
+ export const ANY_MINUS_DETECT_RE = new RegExp(ANY_MINUS_CLASS)
81
+ export const ANY_PLUS_DETECT_RE = new RegExp(ANY_PLUS_CLASS)
82
+
83
+ export function getNumberLocaleDetails(
84
+ locale?: Intl.LocalesArgument,
85
+ options?: Intl.NumberFormatOptions,
86
+ ) {
87
+ const parts = getFormatter(locale, options).formatToParts(11111.1)
88
+ const result: Partial<Record<Intl.NumberFormatPartTypes, string | undefined>> = {}
89
+
90
+ parts.forEach((part) => {
91
+ result[part.type] = part.value
92
+ })
93
+
94
+ // The formatting options may result in not returning a decimal.
95
+ getFormatter(locale)
96
+ .formatToParts(0.1)
97
+ .forEach((part) => {
98
+ if (part.type === 'decimal') {
99
+ result[part.type] = part.value
100
+ }
101
+ })
102
+
103
+ return result
104
+ }
105
+
106
+ export function parseNumber(
107
+ formattedNumber: string,
108
+ locale?: Intl.LocalesArgument,
109
+ options?: Intl.NumberFormatOptions,
110
+ ) {
111
+ if (formattedNumber == null) {
112
+ return null
113
+ }
114
+
115
+ // Normalize control characters and whitespace; remove bidi/format controls
116
+ let input = String(formattedNumber)
117
+ .replace(/\p{Cf}/gu, '')
118
+ .trim()
119
+
120
+ // Normalize unicode minus/plus to ASCII, handle leading/trailing signs
121
+ input = input.replace(ANY_MINUS_RE, '-').replace(ANY_PLUS_RE, '+')
122
+
123
+ let isNegative = false
124
+
125
+ // Trailing sign, e.g. "1234-" / "1234+"
126
+ const trailing = input.match(/([+-])\s*$/)
127
+ if (trailing) {
128
+ if (trailing[1] === '-') {
129
+ isNegative = true
130
+ }
131
+ input = input.replace(/([+-])\s*$/, '')
132
+ }
133
+ // Leading sign
134
+ const leading = input.match(/^\s*([+-])/)
135
+ if (leading) {
136
+ if (leading[1] === '-') {
137
+ isNegative = true
138
+ }
139
+ input = input.replace(/^\s*[+-]/, '')
140
+ }
141
+
142
+ // Heuristic locale detection
143
+ let computedLocale = locale
144
+ if (computedLocale === undefined) {
145
+ if (ARABIC_DETECT_RE.test(input) || PERSIAN_DETECT_RE.test(input)) {
146
+ computedLocale = 'ar'
147
+ }
148
+ else if (HAN_DETECT_RE.test(input)) {
149
+ computedLocale = 'zh'
150
+ }
151
+ }
152
+
153
+ const { group, decimal, currency, exponentSeparator } = getNumberLocaleDetails(
154
+ computedLocale,
155
+ options,
156
+ )
157
+
158
+ // Build robust unit regex from all unit parts (such as "km/h")
159
+ const unitParts = getFormatter(computedLocale, options)
160
+ .formatToParts(1)
161
+ .filter(p => p.type === 'unit')
162
+ .map(p => escapeRegExp(p.value))
163
+ const unitRegex = unitParts.length ? new RegExp(unitParts.join('|'), 'g') : null
164
+
165
+ let groupRegex: RegExp | null = null
166
+ if (group) {
167
+ const isSpaceGroup = /\p{Zs}/u.test(group)
168
+ const isApostropheGroup = group === '\'' || group === '’'
169
+
170
+ // Check if the group separator is a space-like character.
171
+ // If so, we'll replace all such characters with an empty string.
172
+ if (isSpaceGroup) {
173
+ groupRegex = /\p{Zs}/gu
174
+ }
175
+ else if (isApostropheGroup) {
176
+ // Some environments format numbers with ASCII apostrophe and others with a curly apostrophe.
177
+ groupRegex = /['’]/g
178
+ }
179
+ else {
180
+ groupRegex = new RegExp(escapeRegExp(group), 'g')
181
+ }
182
+ }
183
+
184
+ const replacements: Array<{
185
+ regex: RegExp | null
186
+ replacement: string | ((m: string) => string)
187
+ }> = [
188
+ { regex: group ? groupRegex : null, replacement: '' },
189
+ { regex: decimal ? new RegExp(escapeRegExp(decimal), 'g') : null, replacement: '.' },
190
+ // Fullwidth punctuation
191
+ { regex: /./g, replacement: '.' }, // FULLWIDTH_DECIMAL
192
+ { regex: /,/g, replacement: '' }, // FULLWIDTH_GROUP
193
+ // Arabic punctuation
194
+ { regex: /٫/g, replacement: '.' }, // ARABIC DECIMAL SEPARATOR (U+066B)
195
+ { regex: /٬/g, replacement: '' }, // ARABIC THOUSANDS SEPARATOR (U+066C)
196
+ // Currency & unit labels
197
+ { regex: currency ? new RegExp(escapeRegExp(currency), 'g') : null, replacement: '' },
198
+ { regex: unitRegex, replacement: '' },
199
+ { regex: PERCENT_GLOBAL_RE, replacement: '' },
200
+ { regex: PERMILLE_GLOBAL_RE, replacement: '' },
201
+ {
202
+ regex: exponentSeparator ? new RegExp(escapeRegExp(exponentSeparator), 'g') : null,
203
+ replacement: 'e',
204
+ },
205
+ // Numeral systems to ASCII digits
206
+ { regex: ARABIC_RE, replacement: ch => String(ARABIC_NUMERALS.indexOf(ch)) },
207
+ { regex: PERSIAN_RE, replacement: ch => String(PERSIAN_NUMERALS.indexOf(ch)) },
208
+ { regex: FULLWIDTH_RE, replacement: ch => String(FULLWIDTH_NUMERALS.indexOf(ch)) },
209
+ { regex: HAN_RE, replacement: ch => HAN_NUMERAL_TO_DIGIT[ch] },
210
+ ]
211
+
212
+ let unformatted = replacements.reduce((acc, { regex, replacement }) => {
213
+ return regex ? acc.replace(regex, replacement as any) : acc
214
+ }, input)
215
+
216
+ // Mixed-locale safety: keep only the last '.' as decimal
217
+ const lastDot = unformatted.lastIndexOf('.')
218
+ if (lastDot !== -1) {
219
+ unformatted = `${unformatted.slice(0, lastDot).replace(/\./g, '')}.${unformatted.slice(lastDot + 1).replace(/\./g, '')}`
220
+ }
221
+
222
+ // Guard against Infinity inputs (ASCII and symbol)
223
+ if (/^[+-]?Infinity$/i.test(input) || /∞/.test(input)) {
224
+ return null
225
+ }
226
+
227
+ const parseTarget = (isNegative ? '-' : '') + unformatted
228
+
229
+ let num = Number.parseFloat(parseTarget)
230
+
231
+ const style = options?.style
232
+ const isUnitPercent = style === 'unit' && options?.unit === 'percent'
233
+ const hasPercentSymbol = PERCENT_RE.test(formattedNumber) || style === 'percent'
234
+ const hasPermilleSymbol = PERMILLE_RE.test(formattedNumber)
235
+
236
+ if (hasPermilleSymbol) {
237
+ num = shiftDecimal(num, -3)
238
+ }
239
+ else if (!isUnitPercent && hasPercentSymbol) {
240
+ num = shiftDecimal(num, -2)
241
+ }
242
+
243
+ if (!Number.isFinite(num)) {
244
+ return null
245
+ }
246
+
247
+ return num
248
+ }
@@ -0,0 +1,9 @@
1
+ import type { StateAttributesMapping } from '../../utils/getStateAttributesProps'
2
+ import type { NumberFieldRootState } from '../root/NumberFieldRoot.vue'
3
+ import { fieldValidityMapping } from '../../field/utils/constants'
4
+
5
+ export const stateAttributesMapping: StateAttributesMapping<NumberFieldRootState> = {
6
+ inputValue: () => null,
7
+ value: () => null,
8
+ ...fieldValidityMapping,
9
+ }
@@ -0,0 +1,27 @@
1
+ import type { Ref } from 'vue'
2
+ import { ownerWindow } from '../../utils/owner'
3
+
4
+ // This lets us invert the scale of the cursor to match the OS scale, in which the cursor doesn't
5
+ // scale with the content on pinch-zoom.
6
+ export function subscribeToVisualViewportResize(
7
+ element: Element,
8
+ visualScaleRef: Ref<number>,
9
+ ) {
10
+ const vV = ownerWindow(element).visualViewport
11
+
12
+ if (!vV) {
13
+ return () => {}
14
+ }
15
+
16
+ function handleVisualResize() {
17
+ if (vV) {
18
+ visualScaleRef.value = vV.scale
19
+ }
20
+ }
21
+
22
+ handleVisualResize()
23
+ vV.addEventListener('resize', handleVisualResize)
24
+ return () => {
25
+ vV.removeEventListener('resize', handleVisualResize)
26
+ }
27
+ }
@@ -0,0 +1,24 @@
1
+ export type Direction = -1 | 1
2
+
3
+ export type DirectionalChangeReason
4
+ = | 'increment-press'
5
+ | 'decrement-press'
6
+ | 'wheel'
7
+ | 'scrub'
8
+ | 'keyboard'
9
+
10
+ export interface ChangeEventCustomProperties {
11
+ direction?: Direction | undefined
12
+ }
13
+
14
+ export interface IncrementValueParameters {
15
+ direction: Direction
16
+ event?: Event | undefined
17
+ reason: DirectionalChangeReason
18
+ currentValue?: number | null | undefined
19
+ }
20
+
21
+ export interface EventWithOptionalKeyState {
22
+ altKey?: boolean | undefined
23
+ shiftKey?: boolean | undefined
24
+ }
@@ -0,0 +1,120 @@
1
+ import { clamp } from '../../utils/clamp'
2
+ import { getFormatter } from '../../utils/formatNumber'
3
+ import { parseNumber } from './parse'
4
+
5
+ const STEP_EPSILON_FACTOR = 1e-10
6
+ // Matches Intl.NumberFormat's decimal maximumFractionDigits default.
7
+ const DEFAULT_DIGITS = 3
8
+
9
+ // The repo compiles against es2022 Intl types, so model NumberFormat v3 options locally.
10
+ // Delete this once tsconfig includes es2023.
11
+ type NumberFormatOptionsWithRounding = Intl.NumberFormatOptions & {
12
+ roundingIncrement?: number | undefined
13
+ roundingMode?: string | undefined
14
+ roundingPriority?: string | undefined
15
+ }
16
+
17
+ export function hasNumberFormatRoundingOptions(
18
+ format?: NumberFormatOptionsWithRounding,
19
+ ): format is NumberFormatOptionsWithRounding {
20
+ return (
21
+ format?.maximumFractionDigits != null
22
+ || format?.minimumFractionDigits != null
23
+ || format?.maximumSignificantDigits != null
24
+ || format?.minimumSignificantDigits != null
25
+ || format?.roundingIncrement != null
26
+ || format?.roundingMode != null
27
+ || format?.roundingPriority != null
28
+ )
29
+ }
30
+
31
+ export function removeFloatingPointErrors(value: number, format?: NumberFormatOptionsWithRounding) {
32
+ if (!Number.isFinite(value)) {
33
+ return value
34
+ }
35
+
36
+ if (!hasNumberFormatRoundingOptions(format)) {
37
+ return Number(value.toFixed(DEFAULT_DIGITS))
38
+ }
39
+
40
+ const formatter = getFormatter('en-US', {
41
+ ...format,
42
+ // These options alter only display decoration, not numeric rounding.
43
+ signDisplay: 'auto',
44
+ currencySign: 'standard',
45
+ notation: format.notation === 'compact' ? 'standard' : format.notation,
46
+ useGrouping: false,
47
+ } as NumberFormatOptionsWithRounding)
48
+ const roundedText = formatter.format(value)
49
+ const roundedValue = parseNumber(roundedText, 'en-US', format)
50
+
51
+ if (roundedValue === null) {
52
+ return value
53
+ }
54
+
55
+ return formatter.format(roundedValue) === roundedText ? roundedValue : value
56
+ }
57
+
58
+ function snapToStep(
59
+ value: number,
60
+ base: number,
61
+ step: number,
62
+ mode: 'directional' | 'nearest' = 'directional',
63
+ ) {
64
+ const stepSize = Math.abs(step)
65
+ const direction = Math.sign(step)
66
+ const tolerance = stepSize * STEP_EPSILON_FACTOR * direction
67
+ const rawSteps = value - base + tolerance
68
+
69
+ if (mode === 'nearest') {
70
+ return base + Math.round(rawSteps / step) * step
71
+ }
72
+
73
+ const snappedSteps
74
+ = direction > 0 ? Math.floor(rawSteps / stepSize) : Math.ceil(rawSteps / stepSize)
75
+ return base + snappedSteps * stepSize
76
+ }
77
+
78
+ export function toValidatedNumber(
79
+ value: number | null,
80
+ {
81
+ step,
82
+ minWithDefault,
83
+ maxWithDefault,
84
+ minWithZeroDefault,
85
+ format,
86
+ snapOnStep,
87
+ small,
88
+ clamp: shouldClamp,
89
+ }: {
90
+ step: number | undefined
91
+ minWithDefault: number
92
+ maxWithDefault: number
93
+ minWithZeroDefault: number
94
+ format: NumberFormatOptionsWithRounding | undefined
95
+ snapOnStep: boolean
96
+ small: boolean
97
+ clamp: boolean
98
+ },
99
+ ) {
100
+ if (value === null) {
101
+ return value
102
+ }
103
+
104
+ let nextValue = value
105
+
106
+ if (step != null && snapOnStep && step !== 0) {
107
+ const base
108
+ = small || minWithDefault === Number.MIN_SAFE_INTEGER ? minWithZeroDefault : minWithDefault
109
+
110
+ // Snap before clamping so non-step-aligned boundaries stay reachable.
111
+ nextValue = snapToStep(nextValue, base, step, small ? 'nearest' : 'directional')
112
+ }
113
+
114
+ if (shouldClamp) {
115
+ nextValue = clamp(nextValue, minWithDefault, maxWithDefault)
116
+ }
117
+
118
+ const roundedValue = removeFloatingPointErrors(nextValue, format)
119
+ return shouldClamp ? clamp(roundedValue, minWithDefault, maxWithDefault) : roundedValue
120
+ }
@@ -0,0 +1,22 @@
1
+ export { default as OtpFieldInput } from './input/OtpFieldInput.vue'
2
+ export type { OtpFieldInputProps, OtpFieldInputState } from './input/OtpFieldInput.vue'
3
+
4
+ export { default as OtpFieldRoot } from './root/OtpFieldRoot.vue'
5
+ export type { OtpFieldRootProps, OtpFieldRootState } from './root/OtpFieldRoot.vue'
6
+
7
+ export {
8
+ getOtpFieldInputState,
9
+ otpFieldRootContextKey,
10
+ useOtpFieldRootContext,
11
+ } from './root/OtpFieldRootContext'
12
+ export type {
13
+ OtpFieldRootChangeEventDetails,
14
+ OtpFieldRootChangeEventReason,
15
+ OtpFieldRootCompleteEventDetails,
16
+ OtpFieldRootCompleteEventReason,
17
+ OtpFieldRootContext,
18
+ OtpFieldRootInvalidEventDetails,
19
+ OtpFieldRootInvalidEventReason,
20
+ } from './root/OtpFieldRootContext'
21
+
22
+ export type { OtpValidationType } from './utils/otp'