@12-apps/ui 6.101.0 → 6.102.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.
- package/NATIVE.md +1 -1
- package/components-catalog.md +1 -1
- package/dist/_internal/components/form/InputOTP/InputOTP.js +3 -3
- package/dist/_internal/components/form/InputOTP/InputOTP.metrics.js +11 -3
- package/dist/_internal/components/form/InputOTP/InputOTP.styles.js +2 -2
- package/dist/_internal/components/form/index.js +3 -3
- package/dist/_internal/components/index.js +3 -3
- package/dist/{chunk-645GGNTW.js → chunk-J3WJEUD2.js} +18 -4
- package/dist/chunk-J3WJEUD2.js.map +1 -0
- package/dist/{chunk-BAYCMSMI.js → chunk-VJ6ZLMQ3.js} +16 -7
- package/dist/chunk-VJ6ZLMQ3.js.map +1 -0
- package/dist/chunk-XHPL5VI2.js +31 -0
- package/dist/chunk-XHPL5VI2.js.map +1 -0
- package/dist/form/InputOTP.js +3 -3
- package/dist/native/form/InputOTP.js +75 -26
- package/dist/native/form/InputOTP.js.map +1 -1
- package/dist/types/components/form/InputOTP/InputOTP.base.d.ts +14 -0
- package/dist/types/components/form/InputOTP/InputOTP.base.d.ts.map +1 -1
- package/dist/types/components/form/InputOTP/InputOTP.d.ts.map +1 -1
- package/dist/types/components/form/InputOTP/InputOTP.metrics.d.ts +25 -0
- package/dist/types/components/form/InputOTP/InputOTP.metrics.d.ts.map +1 -1
- package/dist/types/components/form/InputOTP/InputOTP.styles.d.ts +2 -0
- package/dist/types/components/form/InputOTP/InputOTP.styles.d.ts.map +1 -1
- package/dist/types-native/components/form/InputOTP/InputOTP.base.d.ts +14 -0
- package/dist/types-native/components/form/InputOTP/InputOTP.base.d.ts.map +1 -1
- package/dist/types-native/components/form/InputOTP/InputOTP.metrics.d.ts +25 -0
- package/dist/types-native/components/form/InputOTP/InputOTP.metrics.d.ts.map +1 -1
- package/dist/types-native/components/form/InputOTP/InputOTP.native.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/form/InputOTP/InputOTP.base.ts +15 -0
- package/src/components/form/InputOTP/InputOTP.md +35 -0
- package/src/components/form/InputOTP/InputOTP.metrics.ts +32 -0
- package/src/components/form/InputOTP/InputOTP.native.tsx +70 -7
- package/src/components/form/InputOTP/InputOTP.styles.ts +32 -6
- package/src/components/form/InputOTP/InputOTP.tsx +17 -3
- package/dist/chunk-645GGNTW.js.map +0 -1
- package/dist/chunk-BAYCMSMI.js.map +0 -1
- package/dist/chunk-VAHJCRNW.js +0 -17
- package/dist/chunk-VAHJCRNW.js.map +0 -1
package/NATIVE.md
CHANGED
|
@@ -99,7 +99,7 @@ Ported: **29 of 154** public subpaths carry a `react-native` condition.
|
|
|
99
99
|
| `@12-apps/ui/form/Button` | 24 | 2 | — |
|
|
100
100
|
| `@12-apps/ui/form/Checkbox` | 26 | 0 | The control is a pressable carrying `role="checkbox"` and `aria-checked` — the state the web's hidden `<input type="checkbox">` announces — rather than a real form element. So it is a tab stop and a screen reader reads it correctly, but the `disabled` ATTRIBUTE a DOM assertion like `toBeDisabled()` looks for is `aria-disabled` here.; The box itself IS MUI's glyph: `@12-apps/ui/icons` generates `CheckBox`, `CheckBoxOutlineBlank` and `IndeterminateCheckBox` from the same `@mui/icons-material` package MUI's own `Checkbox` imports them from.; `glow` is a static shadow of MUI's default blue rather than one breathing between a 5px and a 20px blur, and `pulse` — an expanding ring drawn with `box-shadow` spread — is not drawn at all: React Native has neither keyframes on a shadow nor shadow spread.; Hover has no touch equivalent, so the 0.1 wash the web puts behind a hovered box is not drawn; `ripple` becomes Android's own ripple and is nothing on iOS.; `rounded` and `toggle` clip and scale the glyph's own box, as the web does — which on a transparent glyph shows only where the box would have clipped it. The web's `borderRadius` on an `<svg>` is the same near-invisible change.; Both renderers check in `primary.main` whatever `color` says: `Checkbox.tsx` overrides MUI's per-colour rule with primary and wins on source order. Fixing that is a web change.; The label and the helper text are set in MUI's own numbers (`body2` at 14px, `caption` at 12px, four spacing units of helper inset); a host that re-themes MUI's typography moves the web only. |
|
|
101
101
|
| `@12-apps/ui/form/Input` | 23 | 2 | The label sits ABOVE the field at its shrunk size (12px, aligned with the value's own left inset) instead of floating into a notch in the border: React Native can neither cut a notch in a border nor animate a label into one, so `floating` has nothing left to move.; `filled` keeps MUI's lopsided `25px 12px 8px` inset, which on the web is where the floating label sits inside the box — so a native filled field reads low in its box, with the label above it.; `glass` paints its 0.1 paper wash and hairline without the 20px backdrop blur (React Native has no backdrop filter), and draws ONE border where the web draws two — its own on the field root, plus the outlined fieldset's underneath.; `gradient` paints the first stop of its 135° fill flat and gives the 2px ring the gradient's first hue: React Native core has no gradient fill and no masked pseudo-element border.; Hover has no touch equivalent, so the web's hover border, its brighter glass wash and its deeper filled wash are not drawn; focus is, and it carries the same colours.; `pulse` grows by scaling the bar rather than spreading a box-shadow, which React Native does not have; `glow` is a shadow on iOS and an elevation on Android, so its halo is not symmetrical there.; `onClick` fires from the DOM click under react-native-web and from `onPressIn` on a device — react-native-web forwards only the first and React Native only the second, so exactly one runs and never both.; Enter keeps focus (`blurOnSubmit={false}`), as it does in a web `<input>`; React Native's own default for a single-line field is to dismiss the keyboard, and a caller wanting that passes its own.; An HTML `type` React Native has no keyboard for (`date`, `time`, `file`, `color`) renders a plain text field; `text`, `search`, `email`, `tel`, `url` and `number` pick the keyboard and `password` sets secure entry.; The value, label and helper text are set in MUI's own numbers (`body1` at 16px on a 23px line, the 0.75 shrunk label, `caption` at 12px); a host that re-themes MUI's typography variants moves the web only. |
|
|
102
|
-
| `@12-apps/ui/form/InputOTP` |
|
|
102
|
+
| `@12-apps/ui/form/InputOTP` | 52 | 0 | There is no paste event on React Native: a pasted or SMS-filled code arrives as a change of the first box (its `maxLength` is the code's length plus one), and is spread from there by the same rules the web's paste uses — for `numeric` and `alphanumeric`. A `masked` box shows a bullet, so a paste into it keeps one character: the web spreads it.; Hover has no touch equivalent, so the web's hover border is not drawn; focus is, in the same colour and 2px width.; `glass` and `gradient` are accepted and draw the plain box: React Native core has no backdrop blur and no gradient border.; Arrow keys do not move between boxes (a phone keypad has none); Backspace in an empty box steps back, as on the web. |
|
|
103
103
|
| `@12-apps/ui/form/Select` | 29 | 1 | The option list is an absolutely positioned panel under the field, not a portal over the screen: React Native's `Modal` traps focus and covers the viewport, which is right for a dialog and wrong for a dropdown. So the list is clipped by any ancestor that hides its overflow, it cannot escape a `ScrollView`, and there is no backdrop — the list closes on a pick, on Escape, or when the field is pressed again, never by a tap elsewhere.; Arrow keys open the list and Escape closes it, through react-native-web's DOM key events; `Enter` toggles it, because react-native-web's `Pressable` already answers `Enter` as a press. None of the three exists on a device, where the list is a touch surface — so there is no highlighted option to walk.; The label sits above the field rather than floating into a notch in its border, and the arrow is laid out in flow rather than absolutely at `right: 7`; the 1px difference that costs is taken out of the gap before the glyph, so the value still stops 32px from the edge. Both follow from React Native having no notched outline and no `position: absolute` inside a text run.; `glass` paints its 0.1 paper wash without the 20px backdrop blur (React Native has no backdrop filter), and `gradient` paints the first stop of its 135° fill flat inside a 2px ring of the gradient's first hue — React Native core has no gradient fill and no masked pseudo-element border.; Hover has no touch equivalent: the web's hover border, its brighter glass wash and the `action.hover` wash under a menu row are not drawn. `glow` is a shadow on iOS and an elevation on Android, so its halo is not symmetrical there, and it does not brighten on focus the way the web's does.; `defaultValue` selects an option here and does nothing on the web, where `Select.tsx` passes `value={value ?? ''}` and MUI's controlled value wins over it. Fixing that is a web change.; The list is rendered in full inside a `ScrollView` capped at the window height less 96px, where MUI's `Menu` measures the anchor and flips or shifts to stay on screen; a list opened near the bottom of a device screen runs off it.; The display slot, the option labels, the label and the helper text are set in MUI's own numbers (`body1` at 16px on a 23px line, the 0.75 shrunk label, `caption` at 12px); a host that re-themes MUI's typography variants moves the web only. |
|
|
104
104
|
| `@12-apps/ui/form/Switch` | 26 | 3 | The control is drawn here rather than handed to React Native's own `Switch`, which is the platform control at the platform's size: no size scale, no track geometry, no thumb radius per look, no room for an icon or a word inside the track. What that costs is the platform's own switch on a device — this one is a pressable carrying `role="checkbox"` and `aria-checked`, which is what the web's hidden `<input type="checkbox">` announces, but a `role="switch"` would announce better where nothing needs `getByRole('checkbox')` to find it.; Hover has no touch equivalent, so the thumb does not lift or take its halo, and `ripple` — a wash that expands from the thumb under the pointer on the web — is not drawn at all.; `glow` paints its halo and inner wash as a static shadow rather than breathing every two seconds, and `gradient` paints the gradient's first stop flat (`light` checked, `alpha(light, 0.3)` at rest): React Native core has no gradient fill, no keyframes on a shadow and no shimmer sweep.; `glass` washes the track and the thumb at MUI's alphas without the 20px and 10px backdrop blurs — React Native has no backdrop filter.; `loading` turns the platform's `ActivityIndicator` inside the thumb at MUI's 0.6 of the thumb's size, where the web draws its own 2px ring with one transparent quarter.; An `onIcon` or `offIcon` is rendered as given, centred in its box. The web sets `color` and `fontSize` on the wrapper and an MUI glyph inherits both; React Native has no inheritance, so pass an `<Icon size color>` that is already the size and colour you want.; The keyboard reaches the control only through react-native-web: `Enter` presses it (react-native-web's `Pressable` answers it), the space bar toggles it through a DOM key handler, and neither exists on a device.; `checked`, the label, the description and the helper text are set in MUI's own numbers (`body2` at 14px, `caption` at 12px, the 300ms `cubic-bezier(0.4, 0, 0.2, 1)`); a host that re-themes MUI's typography or its elevations moves the web only. |
|
|
105
105
|
| `@12-apps/ui/icons` | 8 | 0 | — |
|
package/components-catalog.md
CHANGED
|
@@ -445,7 +445,7 @@ Components for user input and form interactions.
|
|
|
445
445
|
### InputOTP
|
|
446
446
|
**Purpose**: One-time password input (web AND React Native)
|
|
447
447
|
**Use Cases**: 2FA, verification codes, security codes
|
|
448
|
-
**Features**: Auto-focus next field, paste support, validation
|
|
448
|
+
**Features**: Auto-focus next field, paste support, validation, `fit="width"` (boxes keep the size's height and type and narrow to share a narrow row, portrait rather than a smaller size)
|
|
449
449
|
**Location**: `src/components/form/InputOTP`
|
|
450
450
|
**Docs**: `src/components/form/InputOTP/InputOTP.md`
|
|
451
451
|
**Native**: yes — `@12-apps/ui/form/InputOTP` resolves to `InputOTP.native.tsx` under Metro: the same props, the number pad and the SMS one-time-code fill on the first box; gaps in `NATIVE-NOTES.md`.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
InputOTP
|
|
3
|
-
} from "../../../../chunk-
|
|
4
|
-
import "../../../../chunk-
|
|
5
|
-
import "../../../../chunk-
|
|
3
|
+
} from "../../../../chunk-J3WJEUD2.js";
|
|
4
|
+
import "../../../../chunk-VJ6ZLMQ3.js";
|
|
5
|
+
import "../../../../chunk-XHPL5VI2.js";
|
|
6
6
|
import "../../../../chunk-M6R32IBO.js";
|
|
7
7
|
import "../../../../chunk-IH6DZXTG.js";
|
|
8
8
|
import "../../../../chunk-HPK33ACY.js";
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
OTP_BORDER,
|
|
3
|
+
OTP_FIT_GAP_PX,
|
|
4
|
+
OTP_FIT_MIN_HEIGHT_PX,
|
|
5
|
+
OTP_FIT_MIN_WIDTH_PX,
|
|
3
6
|
OTP_FONT_PX,
|
|
4
|
-
OTP_GAP_PX
|
|
5
|
-
|
|
7
|
+
OTP_GAP_PX,
|
|
8
|
+
otpFitBox
|
|
9
|
+
} from "../../../../chunk-XHPL5VI2.js";
|
|
6
10
|
import "../../../../chunk-PZ5AY32C.js";
|
|
7
11
|
export {
|
|
8
12
|
OTP_BORDER,
|
|
13
|
+
OTP_FIT_GAP_PX,
|
|
14
|
+
OTP_FIT_MIN_HEIGHT_PX,
|
|
15
|
+
OTP_FIT_MIN_WIDTH_PX,
|
|
9
16
|
OTP_FONT_PX,
|
|
10
|
-
OTP_GAP_PX
|
|
17
|
+
OTP_GAP_PX,
|
|
18
|
+
otpFitBox
|
|
11
19
|
};
|
|
12
20
|
//# sourceMappingURL=InputOTP.metrics.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
otpSlotSx
|
|
3
|
-
} from "../../../../chunk-
|
|
4
|
-
import "../../../../chunk-
|
|
3
|
+
} from "../../../../chunk-VJ6ZLMQ3.js";
|
|
4
|
+
import "../../../../chunk-XHPL5VI2.js";
|
|
5
5
|
import "../../../../chunk-RGVNXES5.js";
|
|
6
6
|
import "../../../../chunk-GDN2LUTZ.js";
|
|
7
7
|
import "../../../../chunk-7ZADLRLP.js";
|
|
@@ -38,9 +38,9 @@ import "../../../chunk-UAFSEZ2E.js";
|
|
|
38
38
|
import "../../../chunk-3VMXOGGI.js";
|
|
39
39
|
import {
|
|
40
40
|
InputOTP
|
|
41
|
-
} from "../../../chunk-
|
|
42
|
-
import "../../../chunk-
|
|
43
|
-
import "../../../chunk-
|
|
41
|
+
} from "../../../chunk-J3WJEUD2.js";
|
|
42
|
+
import "../../../chunk-VJ6ZLMQ3.js";
|
|
43
|
+
import "../../../chunk-XHPL5VI2.js";
|
|
44
44
|
import "../../../chunk-M6R32IBO.js";
|
|
45
45
|
import "../../../chunk-IH6DZXTG.js";
|
|
46
46
|
import "../../../chunk-ZB4VIVRS.js";
|
|
@@ -264,9 +264,9 @@ import "../../chunk-UAFSEZ2E.js";
|
|
|
264
264
|
import "../../chunk-3VMXOGGI.js";
|
|
265
265
|
import {
|
|
266
266
|
InputOTP
|
|
267
|
-
} from "../../chunk-
|
|
268
|
-
import "../../chunk-
|
|
269
|
-
import "../../chunk-
|
|
267
|
+
} from "../../chunk-J3WJEUD2.js";
|
|
268
|
+
import "../../chunk-VJ6ZLMQ3.js";
|
|
269
|
+
import "../../chunk-XHPL5VI2.js";
|
|
270
270
|
import "../../chunk-M6R32IBO.js";
|
|
271
271
|
import "../../chunk-IH6DZXTG.js";
|
|
272
272
|
import "../../chunk-ZB4VIVRS.js";
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
otpSlotSx
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-VJ6ZLMQ3.js";
|
|
4
|
+
import {
|
|
5
|
+
OTP_FIT_GAP_PX
|
|
6
|
+
} from "./chunk-XHPL5VI2.js";
|
|
4
7
|
import {
|
|
5
8
|
useOtpDigits
|
|
6
9
|
} from "./chunk-M6R32IBO.js";
|
|
7
10
|
import {
|
|
8
11
|
withDefaults
|
|
9
12
|
} from "./chunk-HPK33ACY.js";
|
|
13
|
+
import {
|
|
14
|
+
rem
|
|
15
|
+
} from "./chunk-YKCAJ2TN.js";
|
|
10
16
|
|
|
11
17
|
// src/components/form/InputOTP/InputOTP.tsx
|
|
12
18
|
import Box from "@mui/material/Box/index.js";
|
|
@@ -15,7 +21,7 @@ import { styled } from "@mui/material/styles/index.js";
|
|
|
15
21
|
import { forwardRef } from "react";
|
|
16
22
|
import { jsx } from "react/jsx-runtime";
|
|
17
23
|
var StyledOTPInput = styled(TextField, {
|
|
18
|
-
shouldForwardProp: (prop) => !["customColor", "customSize", "glass", "gradient"].includes(prop)
|
|
24
|
+
shouldForwardProp: (prop) => !["customColor", "customSize", "glass", "gradient", "fitWidth"].includes(prop)
|
|
19
25
|
})(({ theme, ...flags }) => ({ ...otpSlotSx(theme, flags) }));
|
|
20
26
|
function keypadProps(variant, index) {
|
|
21
27
|
if (variant !== "numeric") return {};
|
|
@@ -26,6 +32,12 @@ function keypadProps(variant, index) {
|
|
|
26
32
|
};
|
|
27
33
|
}
|
|
28
34
|
var slotMaxLength = (variant, index, length) => variant === "numeric" && index === 0 ? length + 1 : 1;
|
|
35
|
+
var OTP_ROW_SX = { display: "flex", gap: 1, justifyContent: "center", alignItems: "center" };
|
|
36
|
+
var OTP_FIT_ROW_SX = {
|
|
37
|
+
...OTP_ROW_SX,
|
|
38
|
+
width: "100%",
|
|
39
|
+
gap: (theme) => rem(theme, OTP_FIT_GAP_PX)
|
|
40
|
+
};
|
|
29
41
|
var DEFAULTS = {
|
|
30
42
|
variant: "numeric",
|
|
31
43
|
color: "primary",
|
|
@@ -53,6 +65,7 @@ var InputOTP = forwardRef((props, ref) => {
|
|
|
53
65
|
error,
|
|
54
66
|
disabled,
|
|
55
67
|
dataTestId,
|
|
68
|
+
fit,
|
|
56
69
|
...rest
|
|
57
70
|
} = withDefaults(props, DEFAULTS);
|
|
58
71
|
const { digits, inputRefs, handleInputChange, handleKeyDown, handlePaste } = useOtpDigits({
|
|
@@ -68,7 +81,7 @@ var InputOTP = forwardRef((props, ref) => {
|
|
|
68
81
|
ref,
|
|
69
82
|
"data-testid": dataTestId,
|
|
70
83
|
role: "group",
|
|
71
|
-
sx:
|
|
84
|
+
sx: fit === "width" ? OTP_FIT_ROW_SX : OTP_ROW_SX,
|
|
72
85
|
...rest,
|
|
73
86
|
children: digits.map((digit, index) => /* @__PURE__ */ jsx(
|
|
74
87
|
StyledOTPInput,
|
|
@@ -80,6 +93,7 @@ var InputOTP = forwardRef((props, ref) => {
|
|
|
80
93
|
customSize: size,
|
|
81
94
|
glass,
|
|
82
95
|
gradient,
|
|
96
|
+
fitWidth: fit === "width",
|
|
83
97
|
value: variant === "masked" ? digit ? "\u2022" : "" : digit,
|
|
84
98
|
onChange: (e) => handleInputChange(index, e.target.value),
|
|
85
99
|
onKeyDown: (e) => handleKeyDown(index, e),
|
|
@@ -104,4 +118,4 @@ InputOTP.displayName = "InputOTP";
|
|
|
104
118
|
export {
|
|
105
119
|
InputOTP
|
|
106
120
|
};
|
|
107
|
-
//# sourceMappingURL=chunk-
|
|
121
|
+
//# sourceMappingURL=chunk-J3WJEUD2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/form/InputOTP/InputOTP.tsx"],"sourcesContent":["import Box from '@mui/material/Box/index.js';\nimport TextField from '@mui/material/TextField/index.js';\nimport { styled } from '@mui/material/styles/index.js';\nimport React, { forwardRef } from 'react';\n\nimport type { Theme } from '@mui/material/styles/index.js';\n\nimport { rem } from '../../../tokens/relative';\nimport { withDefaults } from '../../../utils/withDefaults';\n\nimport { useOtpDigits } from './InputOTP.hooks';\nimport { OTP_FIT_GAP_PX } from './InputOTP.metrics';\nimport { otpSlotSx } from './InputOTP.styles';\nimport type { OtpSlotFlags } from './InputOTP.styles';\nimport type { InputOTPProps } from './InputOTP.types';\n\nconst StyledOTPInput = styled(TextField, {\n shouldForwardProp: (prop) =>\n !['customColor', 'customSize', 'glass', 'gradient', 'fitWidth'].includes(prop as string),\n})<OtpSlotFlags>(({ theme, ...flags }) => ({ ...otpSlotSx(theme, flags) }));\n\n/**\n * What tells a phone that a `numeric` code is digits (FUT-4144).\n *\n * The hook already refused letters, but nothing reached the `<input>` to say\n * so, and the courier's six-digit join code opened the letter keyboard.\n * `inputMode` picks the keypad; `pattern` is what iOS reads for the same; and\n * `one-time-code` on the FIRST slot lets the phone offer a code from an SMS —\n * the first slot, because it is the one that takes a paste and spreads it.\n * `alphanumeric` and `masked` are left exactly as they were.\n */\nfunction keypadProps(variant: InputOTPProps['variant'], index: number): Record<string, string> {\n if (variant !== 'numeric') return {};\n return {\n inputMode: 'numeric',\n pattern: '[0-9]*',\n ...(index === 0 ? { autoComplete: 'one-time-code' } : {}),\n };\n}\n\n/**\n * One character per slot — except the numeric FIRST slot, which carries\n * `one-time-code`: the phone's SMS fill writes the whole code there in one\n * change, and a `maxLength` of 1 would cut it to one digit before the hook\n * could spread it.\n *\n * The code's length PLUS ONE: a phone that INSERTS the code at the caret keeps\n * the digit already there in front (`4` + `482913`), and the browser caps that\n * insertion at `maxlength` BEFORE the hook sees it. At the bare length it\n * arrived as `448291` — a wrong code that looks whole. One more character lets\n * the hook see the leading digit and drop it. A keystroke into the slot once\n * filled is still refused, by the hook, as `maxLength={1}` refused it.\n */\nconst slotMaxLength = (variant: InputOTPProps['variant'], index: number, length: number): number =>\n variant === 'numeric' && index === 0 ? length + 1 : 1;\n\nconst OTP_ROW_SX = { display: 'flex', gap: 1, justifyContent: 'center', alignItems: 'center' } as const;\n\n/** `fit=\"width\"`: the row takes its parent's width and the slots share it (`otpSlotSx`). */\nconst OTP_FIT_ROW_SX = {\n ...OTP_ROW_SX,\n width: '100%',\n gap: (theme: Theme) => rem(theme, OTP_FIT_GAP_PX),\n} as const;\n\nconst DEFAULTS = {\n variant: 'numeric',\n color: 'primary',\n size: 'md',\n length: 6,\n value: '',\n glass: false,\n gradient: false,\n autoFocus: false,\n error: false,\n disabled: false,\n} satisfies Partial<InputOTPProps>;\n\ntype ResolvedProps = InputOTPProps & Required<Pick<InputOTPProps, keyof typeof DEFAULTS>>;\n\nexport const InputOTP = forwardRef<HTMLDivElement, InputOTPProps>((props, ref) => {\n const {\n variant, color, size, length, value, onChange, onComplete,\n glass, gradient, autoFocus, error, disabled, dataTestId, fit,\n ...rest\n } = withDefaults(props, DEFAULTS) as ResolvedProps;\n\n const { digits, inputRefs, handleInputChange, handleKeyDown, handlePaste } = useOtpDigits({\n variant,\n length,\n value,\n onChange,\n onComplete,\n });\n\n return (\n <Box\n ref={ref}\n data-testid={dataTestId}\n // A group, so the `aria-label` a caller gives the code names the six\n // slots together: on a plain `div` it named nothing anyone could reach.\n role=\"group\"\n sx={fit === 'width' ? OTP_FIT_ROW_SX : OTP_ROW_SX}\n {...rest}\n >\n {digits.map((digit, index) => (\n <StyledOTPInput\n key={index}\n ref={(el: HTMLDivElement | null) => {\n inputRefs.current[index] = el?.querySelector('input') || null;\n }}\n customColor={color}\n customSize={size}\n glass={glass}\n gradient={gradient}\n fitWidth={fit === 'width'}\n // `masked` keeps the digit in state and shows a bullet in its place.\n value={variant === 'masked' ? (digit ? '•' : '') : digit}\n onChange={(e) => handleInputChange(index, e.target.value)}\n onKeyDown={(e) => handleKeyDown(index, e)}\n // Only the first slot takes a paste; it fills the rest itself.\n onPaste={index === 0 ? handlePaste : undefined}\n autoFocus={autoFocus && index === 0}\n error={error}\n disabled={disabled}\n inputProps={{\n maxLength: slotMaxLength(variant, index, length),\n style: { textAlign: 'center' },\n 'data-testid': dataTestId ? `${dataTestId}-slot-${index}` : undefined,\n ...keypadProps(variant, index),\n }}\n />\n ))}\n </Box>\n );\n});\n\nInputOTP.displayName = 'InputOTP';\n"],"mappings":";;;;;;;;;;;;;;;;;AAAA,OAAO,SAAS;AAChB,OAAO,eAAe;AACtB,SAAS,cAAc;AACvB,SAAgB,kBAAkB;AAuG1B;AA1FR,IAAM,iBAAiB,OAAO,WAAW;AAAA,EACvC,mBAAmB,CAAC,SAClB,CAAC,CAAC,eAAe,cAAc,SAAS,YAAY,UAAU,EAAE,SAAS,IAAc;AAC3F,CAAC,EAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,OAAO,EAAE,GAAG,UAAU,OAAO,KAAK,EAAE,EAAE;AAY1E,SAAS,YAAY,SAAmC,OAAuC;AAC7F,MAAI,YAAY,UAAW,QAAO,CAAC;AACnC,SAAO;AAAA,IACL,WAAW;AAAA,IACX,SAAS;AAAA,IACT,GAAI,UAAU,IAAI,EAAE,cAAc,gBAAgB,IAAI,CAAC;AAAA,EACzD;AACF;AAeA,IAAM,gBAAgB,CAAC,SAAmC,OAAe,WACvE,YAAY,aAAa,UAAU,IAAI,SAAS,IAAI;AAEtD,IAAM,aAAa,EAAE,SAAS,QAAQ,KAAK,GAAG,gBAAgB,UAAU,YAAY,SAAS;AAG7F,IAAM,iBAAiB;AAAA,EACrB,GAAG;AAAA,EACH,OAAO;AAAA,EACP,KAAK,CAAC,UAAiB,IAAI,OAAO,cAAc;AAClD;AAEA,IAAM,WAAW;AAAA,EACf,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,WAAW;AAAA,EACX,OAAO;AAAA,EACP,UAAU;AACZ;AAIO,IAAM,WAAW,WAA0C,CAAC,OAAO,QAAQ;AAChF,QAAM;AAAA,IACJ;AAAA,IAAS;AAAA,IAAO;AAAA,IAAM;AAAA,IAAQ;AAAA,IAAO;AAAA,IAAU;AAAA,IAC/C;AAAA,IAAO;AAAA,IAAU;AAAA,IAAW;AAAA,IAAO;AAAA,IAAU;AAAA,IAAY;AAAA,IACzD,GAAG;AAAA,EACL,IAAI,aAAa,OAAO,QAAQ;AAEhC,QAAM,EAAE,QAAQ,WAAW,mBAAmB,eAAe,YAAY,IAAI,aAAa;AAAA,IACxF;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,eAAa;AAAA,MAGb,MAAK;AAAA,MACL,IAAI,QAAQ,UAAU,iBAAiB;AAAA,MACtC,GAAG;AAAA,MAEH,iBAAO,IAAI,CAAC,OAAO,UAClB;AAAA,QAAC;AAAA;AAAA,UAEC,KAAK,CAAC,OAA8B;AAClC,sBAAU,QAAQ,KAAK,IAAI,IAAI,cAAc,OAAO,KAAK;AAAA,UAC3D;AAAA,UACA,aAAa;AAAA,UACb,YAAY;AAAA,UACZ;AAAA,UACA;AAAA,UACA,UAAU,QAAQ;AAAA,UAElB,OAAO,YAAY,WAAY,QAAQ,WAAM,KAAM;AAAA,UACnD,UAAU,CAAC,MAAM,kBAAkB,OAAO,EAAE,OAAO,KAAK;AAAA,UACxD,WAAW,CAAC,MAAM,cAAc,OAAO,CAAC;AAAA,UAExC,SAAS,UAAU,IAAI,cAAc;AAAA,UACrC,WAAW,aAAa,UAAU;AAAA,UAClC;AAAA,UACA;AAAA,UACA,YAAY;AAAA,YACV,WAAW,cAAc,SAAS,OAAO,MAAM;AAAA,YAC/C,OAAO,EAAE,WAAW,SAAS;AAAA,YAC7B,eAAe,aAAa,GAAG,UAAU,SAAS,KAAK,KAAK;AAAA,YAC5D,GAAG,YAAY,SAAS,KAAK;AAAA,UAC/B;AAAA;AAAA,QAvBK;AAAA,MAwBP,CACD;AAAA;AAAA,EACH;AAEJ,CAAC;AAED,SAAS,cAAc;","names":[]}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
OTP_FIT_MIN_HEIGHT_PX,
|
|
3
|
+
OTP_FIT_MIN_WIDTH_PX,
|
|
2
4
|
OTP_FONT_PX
|
|
3
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-XHPL5VI2.js";
|
|
4
6
|
import {
|
|
5
7
|
asFieldSize,
|
|
6
8
|
fieldHeight
|
|
@@ -46,17 +48,24 @@ var getColorFromTheme = (theme, color) => {
|
|
|
46
48
|
contrastText: palette.contrastText || absoluteInk(theme).white
|
|
47
49
|
};
|
|
48
50
|
};
|
|
51
|
+
var slotBox = (theme, side, fitWidth) => {
|
|
52
|
+
if (!fitWidth) return { outer: { width: side, height: side }, inner: { width: side, height: side } };
|
|
53
|
+
const height = `max(${side}, ${rem(theme, OTP_FIT_MIN_HEIGHT_PX)})`;
|
|
54
|
+
return {
|
|
55
|
+
outer: { flex: "1 1 0", minWidth: rem(theme, OTP_FIT_MIN_WIDTH_PX), maxWidth: height, height },
|
|
56
|
+
inner: { width: "100%", height }
|
|
57
|
+
};
|
|
58
|
+
};
|
|
49
59
|
var otpSlotSx = (theme, flags) => {
|
|
50
|
-
const { customColor = "primary", customSize = "md", glass, gradient } = flags;
|
|
60
|
+
const { customColor = "primary", customSize = "md", glass, gradient, fitWidth } = flags;
|
|
51
61
|
const palette = getColorFromTheme(theme, customColor);
|
|
52
62
|
const fontPx = OTP_FONT_PX[customSize] ?? OTP_FONT_PX.md;
|
|
53
63
|
const side = fieldHeight(theme, asFieldSize(customSize));
|
|
64
|
+
const box = slotBox(theme, side, fitWidth);
|
|
54
65
|
return {
|
|
55
|
-
|
|
56
|
-
height: side,
|
|
66
|
+
...box.outer,
|
|
57
67
|
"& .MuiOutlinedInput-root": {
|
|
58
|
-
|
|
59
|
-
height: side,
|
|
68
|
+
...box.inner,
|
|
60
69
|
fontSize: rem(theme, fontPx),
|
|
61
70
|
fontWeight: 600,
|
|
62
71
|
textAlign: "center",
|
|
@@ -83,4 +92,4 @@ var otpSlotSx = (theme, flags) => {
|
|
|
83
92
|
export {
|
|
84
93
|
otpSlotSx
|
|
85
94
|
};
|
|
86
|
-
//# sourceMappingURL=chunk-
|
|
95
|
+
//# sourceMappingURL=chunk-VJ6ZLMQ3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/form/InputOTP/InputOTP.styles.ts"],"sourcesContent":["import { alpha } from '@mui/material/styles/index.js';\nimport type { CSSObject, Theme } from '@mui/material/styles/index.js';\n\nimport { fieldEdge } from '../../../tokens/field-edge';\nimport { fieldRadius } from '../../../tokens/field-radius';\nimport { asFieldSize, fieldHeight } from '../../../tokens/field-height';\nimport { absoluteInk, controlNeutral } from '../../../tokens/ink';\nimport { rem } from '../../../tokens/relative';\n\nimport { OTP_FIT_MIN_HEIGHT_PX, OTP_FIT_MIN_WIDTH_PX, OTP_FONT_PX } from './InputOTP.metrics';\n\ninterface ColorPalette {\n main: string;\n dark?: string;\n light?: string;\n contrastText?: string;\n}\n\n/** `neutral` is not a MUI palette entry, so it is built from the grey ramp. */\nconst neutralPalette = (theme: Theme): ColorPalette => controlNeutral(theme);\n\n/** `danger` is this component's name for the error palette. */\nconst namedPalette = (theme: Theme, color: string): ColorPalette => {\n const colorMap: Record<string, ColorPalette> = {\n primary: theme.palette.primary,\n secondary: theme.palette.secondary,\n success: theme.palette.success,\n warning: theme.palette.warning,\n info: theme.palette.info,\n danger: theme.palette.error,\n };\n\n return colorMap[color] || theme.palette.primary;\n};\n\nconst getColorFromTheme = (theme: Theme, color: string): ColorPalette => {\n if (color === 'neutral') {\n return neutralPalette(theme);\n }\n\n const palette = namedPalette(theme, color);\n const { primary } = theme.palette;\n\n return {\n main: palette.main || primary.main,\n dark: palette.dark || palette.main || primary.dark,\n light: palette.light || palette.main || primary.light,\n contrastText: palette.contrastText || absoluteInk(theme).white,\n };\n};\n\ntype SizeKey = keyof typeof OTP_FONT_PX;\n\nexport interface OtpSlotFlags {\n customColor?: string;\n customSize?: string;\n glass?: boolean;\n gradient?: boolean;\n /** `fit=\"width\"`: share the row's width instead of drawing a square (FUT-4585). */\n fitWidth?: boolean;\n}\n\ninterface SlotBox {\n /** The slot's own footprint in the row. */\n outer: CSSObject;\n /** The outlined input inside it. */\n inner: CSSObject;\n}\n\n/**\n * A slot's box. By default a square of the field height, inside and out.\n *\n * Under `fit=\"width\"` (FUT-4585), the native `otpFitBox` in CSS: the slot keeps\n * the field's height (never under the 40px touch floor) and takes an equal\n * share of the row, never wider than it is tall, never under 32px; the input\n * fills it.\n */\nconst slotBox = (theme: Theme, side: string, fitWidth: boolean | undefined): SlotBox => {\n if (!fitWidth) return { outer: { width: side, height: side }, inner: { width: side, height: side } };\n const height = `max(${side}, ${rem(theme, OTP_FIT_MIN_HEIGHT_PX)})`;\n return {\n outer: { flex: '1 1 0', minWidth: rem(theme, OTP_FIT_MIN_WIDTH_PX), maxWidth: height, height },\n inner: { width: '100%', height },\n };\n};\n\nexport const otpSlotSx = (theme: Theme, flags: OtpSlotFlags): CSSObject => {\n const { customColor = 'primary', customSize = 'md', glass, gradient, fitWidth } = flags;\n const palette = getColorFromTheme(theme, customColor);\n const fontPx = OTP_FONT_PX[customSize as SizeKey] ?? OTP_FONT_PX.md;\n // A slot is a field: a square of the theme's field height for its size.\n const side = fieldHeight(theme, asFieldSize(customSize));\n\n const box = slotBox(theme, side, fitWidth);\n\n return {\n ...box.outer,\n '& .MuiOutlinedInput-root': {\n ...box.inner,\n fontSize: rem(theme, fontPx),\n fontWeight: 600,\n textAlign: 'center',\n borderRadius: fieldRadius(theme),\n ...(glass && {\n backgroundColor: alpha(theme.palette.background.paper, 0.1),\n backdropFilter: `blur(${rem(theme, 20)})`,\n '& fieldset': { border: `1px solid ${fieldEdge(theme)}` },\n }),\n ...(gradient && {\n '&.Mui-focused fieldset': {\n background: `linear-gradient(135deg, ${palette.main}, ${palette.light})`,\n borderWidth: rem(theme, 2),\n },\n }),\n '& input': { textAlign: 'center', padding: 0, fontWeight: 'inherit' },\n ...(!glass && { '& fieldset': { borderColor: fieldEdge(theme) } }),\n '&:hover fieldset': { borderColor: palette.main },\n '&.Mui-focused fieldset': { borderColor: palette.main, borderWidth: rem(theme, 2) },\n },\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,aAAa;AAmBtB,IAAM,iBAAiB,CAAC,UAA+B,eAAe,KAAK;AAG3E,IAAM,eAAe,CAAC,OAAc,UAAgC;AAClE,QAAM,WAAyC;AAAA,IAC7C,SAAS,MAAM,QAAQ;AAAA,IACvB,WAAW,MAAM,QAAQ;AAAA,IACzB,SAAS,MAAM,QAAQ;AAAA,IACvB,SAAS,MAAM,QAAQ;AAAA,IACvB,MAAM,MAAM,QAAQ;AAAA,IACpB,QAAQ,MAAM,QAAQ;AAAA,EACxB;AAEA,SAAO,SAAS,KAAK,KAAK,MAAM,QAAQ;AAC1C;AAEA,IAAM,oBAAoB,CAAC,OAAc,UAAgC;AACvE,MAAI,UAAU,WAAW;AACvB,WAAO,eAAe,KAAK;AAAA,EAC7B;AAEA,QAAM,UAAU,aAAa,OAAO,KAAK;AACzC,QAAM,EAAE,QAAQ,IAAI,MAAM;AAE1B,SAAO;AAAA,IACL,MAAM,QAAQ,QAAQ,QAAQ;AAAA,IAC9B,MAAM,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ;AAAA,IAC9C,OAAO,QAAQ,SAAS,QAAQ,QAAQ,QAAQ;AAAA,IAChD,cAAc,QAAQ,gBAAgB,YAAY,KAAK,EAAE;AAAA,EAC3D;AACF;AA4BA,IAAM,UAAU,CAAC,OAAc,MAAc,aAA2C;AACtF,MAAI,CAAC,SAAU,QAAO,EAAE,OAAO,EAAE,OAAO,MAAM,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,MAAM,QAAQ,KAAK,EAAE;AACnG,QAAM,SAAS,OAAO,IAAI,KAAK,IAAI,OAAO,qBAAqB,CAAC;AAChE,SAAO;AAAA,IACL,OAAO,EAAE,MAAM,SAAS,UAAU,IAAI,OAAO,oBAAoB,GAAG,UAAU,QAAQ,OAAO;AAAA,IAC7F,OAAO,EAAE,OAAO,QAAQ,OAAO;AAAA,EACjC;AACF;AAEO,IAAM,YAAY,CAAC,OAAc,UAAmC;AACzE,QAAM,EAAE,cAAc,WAAW,aAAa,MAAM,OAAO,UAAU,SAAS,IAAI;AAClF,QAAM,UAAU,kBAAkB,OAAO,WAAW;AACpD,QAAM,SAAS,YAAY,UAAqB,KAAK,YAAY;AAEjE,QAAM,OAAO,YAAY,OAAO,YAAY,UAAU,CAAC;AAEvD,QAAM,MAAM,QAAQ,OAAO,MAAM,QAAQ;AAEzC,SAAO;AAAA,IACL,GAAG,IAAI;AAAA,IACP,4BAA4B;AAAA,MAC1B,GAAG,IAAI;AAAA,MACP,UAAU,IAAI,OAAO,MAAM;AAAA,MAC3B,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,cAAc,YAAY,KAAK;AAAA,MAC/B,GAAI,SAAS;AAAA,QACX,iBAAiB,MAAM,MAAM,QAAQ,WAAW,OAAO,GAAG;AAAA,QAC1D,gBAAgB,QAAQ,IAAI,OAAO,EAAE,CAAC;AAAA,QACtC,cAAc,EAAE,QAAQ,aAAa,UAAU,KAAK,CAAC,GAAG;AAAA,MAC1D;AAAA,MACA,GAAI,YAAY;AAAA,QACd,0BAA0B;AAAA,UACxB,YAAY,2BAA2B,QAAQ,IAAI,KAAK,QAAQ,KAAK;AAAA,UACrE,aAAa,IAAI,OAAO,CAAC;AAAA,QAC3B;AAAA,MACF;AAAA,MACA,WAAW,EAAE,WAAW,UAAU,SAAS,GAAG,YAAY,UAAU;AAAA,MACpE,GAAI,CAAC,SAAS,EAAE,cAAc,EAAE,aAAa,UAAU,KAAK,EAAE,EAAE;AAAA,MAChE,oBAAoB,EAAE,aAAa,QAAQ,KAAK;AAAA,MAChD,0BAA0B,EAAE,aAAa,QAAQ,MAAM,aAAa,IAAI,OAAO,CAAC,EAAE;AAAA,IACpF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/components/form/InputOTP/InputOTP.metrics.ts
|
|
2
|
+
var OTP_FONT_PX = {
|
|
3
|
+
xs: 12,
|
|
4
|
+
sm: 14,
|
|
5
|
+
md: 16,
|
|
6
|
+
lg: 18,
|
|
7
|
+
xl: 20
|
|
8
|
+
};
|
|
9
|
+
var OTP_GAP_PX = 8;
|
|
10
|
+
var OTP_BORDER = { rest: 1, focused: 2 };
|
|
11
|
+
var OTP_FIT_GAP_PX = 6;
|
|
12
|
+
var OTP_FIT_MIN_WIDTH_PX = 32;
|
|
13
|
+
var OTP_FIT_MIN_HEIGHT_PX = 40;
|
|
14
|
+
function otpFitBox(rowWidth, side, length) {
|
|
15
|
+
const height = Math.max(side, OTP_FIT_MIN_HEIGHT_PX);
|
|
16
|
+
const count = Math.max(1, Math.floor(length));
|
|
17
|
+
const share = Math.floor((rowWidth - (count - 1) * OTP_FIT_GAP_PX) / count);
|
|
18
|
+
const width = Math.min(height, Math.max(OTP_FIT_MIN_WIDTH_PX, Number.isFinite(share) ? share : height));
|
|
19
|
+
return { width, height };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
OTP_FONT_PX,
|
|
24
|
+
OTP_GAP_PX,
|
|
25
|
+
OTP_BORDER,
|
|
26
|
+
OTP_FIT_GAP_PX,
|
|
27
|
+
OTP_FIT_MIN_WIDTH_PX,
|
|
28
|
+
OTP_FIT_MIN_HEIGHT_PX,
|
|
29
|
+
otpFitBox
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=chunk-XHPL5VI2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/form/InputOTP/InputOTP.metrics.ts"],"sourcesContent":["/**\n * The numbers both renderers read (see `NATIVE.md`, \"Numbers\").\n *\n * Each box's type size, in design px, per size step; its side is the theme's\n * field height for that step (a square), its weight 600.\n */\nexport const OTP_FONT_PX: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl', number> = {\n xs: 12,\n sm: 14,\n md: 16,\n lg: 18,\n xl: 20,\n};\n\n/** The gap between two boxes: MUI's `gap: 1`, one 8px spacing unit. */\nexport const OTP_GAP_PX = 8;\n\n/** The border: 1px at rest, 2px focused (MUI's focused outline). */\nexport const OTP_BORDER = { rest: 1, focused: 2 } as const;\n\n/**\n * `fit=\"width\"` (FUT-4585): the gap between two boxes when they share the row's\n * width — the approved prototype's 6px, a little tighter than the default 8px\n * so six portrait boxes stay at least 40 wide on a 320 phone.\n */\nexport const OTP_FIT_GAP_PX = 6;\n\n/** `fit=\"width\"`: no box is narrower than this, however narrow the row. */\nexport const OTP_FIT_MIN_WIDTH_PX = 32;\n\n/** `fit=\"width\"`: no box is shorter than the 40px touch floor, whatever its size. */\nexport const OTP_FIT_MIN_HEIGHT_PX = 40;\n\n/**\n * One box's size under `fit=\"width\"`, for a row `rowWidth` wide holding\n * `length` boxes whose size step draws a field `side` tall.\n *\n * The box keeps the field's height (never under {@link OTP_FIT_MIN_HEIGHT_PX})\n * and takes an equal share of the row, less the gaps, for its width: floored\n * to a whole pixel so six of them never overflow the row, never wider than it\n * is tall (a wide row keeps squares) and never under\n * {@link OTP_FIT_MIN_WIDTH_PX}. A narrow row therefore draws PORTRAIT boxes\n * rather than dropping to a smaller size and its smaller type.\n */\nexport function otpFitBox(rowWidth: number, side: number, length: number): { width: number; height: number } {\n const height = Math.max(side, OTP_FIT_MIN_HEIGHT_PX);\n const count = Math.max(1, Math.floor(length));\n const share = Math.floor((rowWidth - (count - 1) * OTP_FIT_GAP_PX) / count);\n const width = Math.min(height, Math.max(OTP_FIT_MIN_WIDTH_PX, Number.isFinite(share) ? share : height));\n return { width, height };\n}\n"],"mappings":";AAMO,IAAM,cAAgE;AAAA,EAC3E,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGO,IAAM,aAAa;AAGnB,IAAM,aAAa,EAAE,MAAM,GAAG,SAAS,EAAE;AAOzC,IAAM,iBAAiB;AAGvB,IAAM,uBAAuB;AAG7B,IAAM,wBAAwB;AAa9B,SAAS,UAAU,UAAkB,MAAc,QAAmD;AAC3G,QAAM,SAAS,KAAK,IAAI,MAAM,qBAAqB;AACnD,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,CAAC;AAC5C,QAAM,QAAQ,KAAK,OAAO,YAAY,QAAQ,KAAK,kBAAkB,KAAK;AAC1E,QAAM,QAAQ,KAAK,IAAI,QAAQ,KAAK,IAAI,sBAAsB,OAAO,SAAS,KAAK,IAAI,QAAQ,MAAM,CAAC;AACtG,SAAO,EAAE,OAAO,OAAO;AACzB;","names":[]}
|
package/dist/form/InputOTP.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import "../chunk-3VMXOGGI.js";
|
|
2
2
|
import {
|
|
3
3
|
InputOTP
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
6
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-J3WJEUD2.js";
|
|
5
|
+
import "../chunk-VJ6ZLMQ3.js";
|
|
6
|
+
import "../chunk-XHPL5VI2.js";
|
|
7
7
|
import "../chunk-M6R32IBO.js";
|
|
8
8
|
import "../chunk-IH6DZXTG.js";
|
|
9
9
|
import "../chunk-HPK33ACY.js";
|
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
|
|
15
15
|
// src/components/form/InputOTP/InputOTP.native.tsx
|
|
16
16
|
import * as React from "react";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
Platform,
|
|
19
|
+
StyleSheet,
|
|
20
|
+
TextInput,
|
|
21
|
+
View
|
|
22
|
+
} from "react-native";
|
|
18
23
|
|
|
19
24
|
// src/components/form/InputOTP/InputOTP.hooks.native.ts
|
|
20
25
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -90,6 +95,16 @@ var OTP_FONT_PX = {
|
|
|
90
95
|
};
|
|
91
96
|
var OTP_GAP_PX = 8;
|
|
92
97
|
var OTP_BORDER = { rest: 1, focused: 2 };
|
|
98
|
+
var OTP_FIT_GAP_PX = 6;
|
|
99
|
+
var OTP_FIT_MIN_WIDTH_PX = 32;
|
|
100
|
+
var OTP_FIT_MIN_HEIGHT_PX = 40;
|
|
101
|
+
function otpFitBox(rowWidth, side, length) {
|
|
102
|
+
const height = Math.max(side, OTP_FIT_MIN_HEIGHT_PX);
|
|
103
|
+
const count = Math.max(1, Math.floor(length));
|
|
104
|
+
const share = Math.floor((rowWidth - (count - 1) * OTP_FIT_GAP_PX) / count);
|
|
105
|
+
const width = Math.min(height, Math.max(OTP_FIT_MIN_WIDTH_PX, Number.isFinite(share) ? share : height));
|
|
106
|
+
return { width, height };
|
|
107
|
+
}
|
|
93
108
|
|
|
94
109
|
// src/components/form/InputOTP/InputOTP.native.tsx
|
|
95
110
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -108,13 +123,26 @@ function keypadProps(variant, index) {
|
|
|
108
123
|
return { keyboardType: "number-pad", inputMode: "numeric", ...fill };
|
|
109
124
|
}
|
|
110
125
|
var boxMaxLength = (variant, index, length) => variant !== "masked" && index === 0 ? length + 1 : 1;
|
|
111
|
-
function
|
|
126
|
+
function fitFootprint(side, length, rowWidth) {
|
|
127
|
+
if (rowWidth !== null) return otpFitBox(rowWidth, side, length);
|
|
128
|
+
const height = Math.max(side, OTP_FIT_MIN_HEIGHT_PX);
|
|
129
|
+
return { height, flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: OTP_FIT_MIN_WIDTH_PX, maxWidth: height };
|
|
130
|
+
}
|
|
131
|
+
function useFitWidth(fit, side, length) {
|
|
132
|
+
const [rowWidth, setRowWidth] = React.useState(null);
|
|
133
|
+
if (fit !== "width") return { footprint: void 0, rowLayout: {}, rowStyles: [] };
|
|
134
|
+
return {
|
|
135
|
+
footprint: fitFootprint(side, length, rowWidth),
|
|
136
|
+
rowLayout: { onLayout: (event) => setRowWidth(event.nativeEvent.layout.width) },
|
|
137
|
+
rowStyles: [styles.fitRow]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function boxStyle(theme, size, accent, state, footprint) {
|
|
112
141
|
const side = fieldHeightPx(theme.fieldHeight, size);
|
|
113
142
|
const edge = resolveFieldEdge(theme.palette.divider, theme.palette.background.paper);
|
|
114
143
|
const borderColor = state.error ? theme.palette.danger.main : state.disabled ? theme.palette.action.disabled : state.focused ? accent : edge;
|
|
115
144
|
return {
|
|
116
|
-
width: side,
|
|
117
|
-
height: side,
|
|
145
|
+
...footprint ?? { width: side, height: side },
|
|
118
146
|
borderRadius: theme.radius.field,
|
|
119
147
|
borderWidth: state.focused ? OTP_BORDER.focused : OTP_BORDER.rest,
|
|
120
148
|
borderColor,
|
|
@@ -140,12 +168,14 @@ var InputOTP = React.forwardRef((props, ref) => {
|
|
|
140
168
|
error = false,
|
|
141
169
|
disabled = false,
|
|
142
170
|
dataTestId,
|
|
171
|
+
fit,
|
|
143
172
|
style,
|
|
144
173
|
"aria-label": ariaLabel
|
|
145
174
|
} = props;
|
|
146
175
|
const theme = useUiTheme();
|
|
147
176
|
const accent = accentOf(theme, color);
|
|
148
177
|
const [focusedIndex, setFocusedIndex] = React.useState(null);
|
|
178
|
+
const { footprint, rowLayout, rowStyles } = useFitWidth(fit, fieldHeightPx(theme.fieldHeight, size), length);
|
|
149
179
|
const { digits, boxRefs, handleChangeText, handleKey } = useNativeOtpDigits({
|
|
150
180
|
variant,
|
|
151
181
|
length,
|
|
@@ -153,29 +183,40 @@ var InputOTP = React.forwardRef((props, ref) => {
|
|
|
153
183
|
onChange,
|
|
154
184
|
onComplete
|
|
155
185
|
});
|
|
156
|
-
return /* @__PURE__ */ jsx(
|
|
157
|
-
|
|
186
|
+
return /* @__PURE__ */ jsx(
|
|
187
|
+
View,
|
|
158
188
|
{
|
|
159
|
-
ref
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
189
|
+
ref,
|
|
190
|
+
testID: dataTestId,
|
|
191
|
+
role: "group",
|
|
192
|
+
"aria-label": ariaLabel,
|
|
193
|
+
...rowLayout,
|
|
194
|
+
style: [styles.row, ...rowStyles, style],
|
|
195
|
+
children: digits.map((digit, index) => /* @__PURE__ */ jsx(
|
|
196
|
+
TextInput,
|
|
197
|
+
{
|
|
198
|
+
ref: (element) => {
|
|
199
|
+
boxRefs.current[index] = element;
|
|
200
|
+
},
|
|
201
|
+
testID: dataTestId ? `${dataTestId}-slot-${index}` : void 0,
|
|
202
|
+
value: variant === "masked" ? digit ? "\u2022" : "" : digit,
|
|
203
|
+
onChangeText: (text) => handleChangeText(index, text),
|
|
204
|
+
onKeyPress: (event) => handleKey(index, event.nativeEvent.key),
|
|
205
|
+
onFocus: () => setFocusedIndex(index),
|
|
206
|
+
onBlur: () => setFocusedIndex((current) => current === index ? null : current),
|
|
207
|
+
autoFocus: autoFocus && index === 0,
|
|
208
|
+
editable: !disabled,
|
|
209
|
+
"aria-disabled": disabled,
|
|
210
|
+
...webAria({ "aria-invalid": error }),
|
|
211
|
+
maxLength: boxMaxLength(variant, index, length),
|
|
212
|
+
style: boxStyle(theme, size, accent, { focused: focusedIndex === index && !disabled, error, disabled }, footprint),
|
|
213
|
+
...webDisabled(disabled),
|
|
214
|
+
...keypadProps(variant, index)
|
|
215
|
+
},
|
|
216
|
+
index
|
|
217
|
+
))
|
|
218
|
+
}
|
|
219
|
+
);
|
|
179
220
|
});
|
|
180
221
|
InputOTP.displayName = "InputOTP";
|
|
181
222
|
var styles = StyleSheet.create({
|
|
@@ -184,6 +225,14 @@ var styles = StyleSheet.create({
|
|
|
184
225
|
justifyContent: "center",
|
|
185
226
|
alignItems: "center",
|
|
186
227
|
gap: OTP_GAP_PX
|
|
228
|
+
},
|
|
229
|
+
/** `fit="width"`: the parent's whole width, in a column or a row parent, shared at the prototype's gap. */
|
|
230
|
+
fitRow: {
|
|
231
|
+
// Both: `stretch` alone spans a column parent but not a row parent, where
|
|
232
|
+
// the row would shrink to its boxes and collapse them to their minimum.
|
|
233
|
+
alignSelf: "stretch",
|
|
234
|
+
width: "100%",
|
|
235
|
+
gap: OTP_FIT_GAP_PX
|
|
187
236
|
}
|
|
188
237
|
});
|
|
189
238
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/form/InputOTP/InputOTP.native.tsx","../../../src/components/form/InputOTP/InputOTP.hooks.native.ts","../../../src/components/form/InputOTP/InputOTP.core.ts","../../../src/components/form/InputOTP/InputOTP.metrics.ts"],"sourcesContent":["import * as React from 'react';\nimport { Platform, StyleSheet, TextInput, View, type TextInputProps, type TextStyle } from 'react-native';\n\nimport { useNativeOtpDigits } from './InputOTP.hooks.native';\nimport { OTP_BORDER, OTP_FONT_PX, OTP_GAP_PX } from './InputOTP.metrics';\nimport type { InputOTPProps } from './InputOTP.types.native';\nimport { webAria, webDisabled } from '../../../platform/aria';\nimport { resolveFieldEdge } from '../../../tokens/field-edge.core';\nimport { fieldHeightPx } from '../../../tokens/field-height.core';\nimport type { UiPaletteKey, UiTheme } from '../../../tokens/theme';\nimport type { SizeValue } from '../../../tokens/vocabulary';\nimport { useUiTheme } from '../../../provider/use-ui-theme.native';\n\nconst PALETTE_KEYS: readonly UiPaletteKey[] = ['primary', 'secondary', 'success', 'warning', 'info', 'danger', 'neutral'];\n\n/** The palette slot a `color` names; anything else is `primary`, as on the web. */\nfunction accentOf(theme: UiTheme, color: string): string {\n const key = PALETTE_KEYS.find((candidate) => candidate === color) ?? 'primary';\n return theme.palette[key].main;\n}\n\n/**\n * The keypad and the one-time-code fill, the web's `keypadProps` in React\n * Native's words: digits only for `numeric`, and on the FIRST box the hint that\n * lets the phone offer the code from an SMS — `oneTimeCode` on iOS,\n * `sms-otp` on Android.\n */\nfunction keypadProps(variant: string, index: number): Partial<TextInputProps> {\n // A code is not prose: no capital on the first letter, no autocorrect.\n if (variant !== 'numeric') return { autoCapitalize: 'none', autoCorrect: false, spellCheck: false };\n const fill: Partial<TextInputProps> =\n index === 0\n ? Platform.select<Partial<TextInputProps>>({\n ios: { textContentType: 'oneTimeCode', autoComplete: 'one-time-code' },\n android: { autoComplete: 'sms-otp' },\n default: { autoComplete: 'one-time-code' },\n })\n : {};\n return { keyboardType: 'number-pad', inputMode: 'numeric', ...fill };\n}\n\n/** The first box takes a whole code in one change (see the web's `slotMaxLength`), except `masked`'s. */\nconst boxMaxLength = (variant: string, index: number, length: number): number =>\n variant !== 'masked' && index === 0 ? length + 1 : 1;\n\ninterface BoxState {\n focused: boolean;\n error: boolean;\n disabled: boolean;\n}\n\n/** One box's paint: the web slot's square, border and type, from the same numbers. */\nfunction boxStyle(theme: UiTheme, size: SizeValue, accent: string, state: BoxState): TextStyle {\n const side = fieldHeightPx(theme.fieldHeight, size);\n const edge = resolveFieldEdge(theme.palette.divider, theme.palette.background.paper);\n // Error and disabled outrank focus, as in MUI's outlined input (and the native `Input`).\n const borderColor = state.error\n ? theme.palette.danger.main\n : state.disabled\n ? theme.palette.action.disabled\n : state.focused\n ? accent\n : edge;\n return {\n width: side,\n height: side,\n borderRadius: theme.radius.field,\n borderWidth: state.focused ? OTP_BORDER.focused : OTP_BORDER.rest,\n borderColor,\n backgroundColor: theme.palette.background.paper,\n color: state.disabled ? theme.palette.text.disabled : theme.palette.text.primary,\n fontFamily: theme.typography.fontFamily,\n fontSize: OTP_FONT_PX[size],\n fontWeight: '600',\n textAlign: 'center',\n padding: 0,\n };\n}\n\n/**\n * The native `InputOTP`: one row of boxes, one character each, that moves\n * forward as it is typed, steps back on Backspace in an empty box, and spreads\n * a code the phone fills (or a paste) across every box from the first.\n *\n * The same props as the web one; the row is a `group` named by `aria-label`,\n * each box is `${dataTestId}-slot-${index}`. Gaps against the web:\n * `NATIVE-NOTES.md`.\n */\nexport const InputOTP = React.forwardRef<View, InputOTPProps>((props, ref) => {\n const {\n variant = 'numeric',\n color = 'primary',\n size = 'md',\n length = 6,\n value = '',\n onChange,\n onComplete,\n autoFocus = false,\n error = false,\n disabled = false,\n dataTestId,\n style,\n 'aria-label': ariaLabel,\n } = props;\n\n const theme = useUiTheme();\n const accent = accentOf(theme, color);\n const [focusedIndex, setFocusedIndex] = React.useState<number | null>(null);\n const { digits, boxRefs, handleChangeText, handleKey } = useNativeOtpDigits({\n variant,\n length,\n value,\n onChange,\n onComplete,\n });\n\n return (\n <View ref={ref} testID={dataTestId} role=\"group\" aria-label={ariaLabel} style={[styles.row, style]}>\n {digits.map((digit, index) => (\n <TextInput\n key={index}\n ref={(element) => {\n boxRefs.current[index] = element;\n }}\n testID={dataTestId ? `${dataTestId}-slot-${index}` : undefined}\n value={variant === 'masked' ? (digit ? '•' : '') : digit}\n onChangeText={(text) => handleChangeText(index, text)}\n onKeyPress={(event) => handleKey(index, event.nativeEvent.key)}\n onFocus={() => setFocusedIndex(index)}\n onBlur={() => setFocusedIndex((current) => (current === index ? null : current))}\n autoFocus={autoFocus && index === 0}\n editable={!disabled}\n aria-disabled={disabled}\n {...webAria({ 'aria-invalid': error })}\n maxLength={boxMaxLength(variant, index, length)}\n style={boxStyle(theme, size, accent, { focused: focusedIndex === index && !disabled, error, disabled })}\n {...webDisabled(disabled)}\n {...keypadProps(variant, index)}\n />\n ))}\n </View>\n );\n});\n\nInputOTP.displayName = 'InputOTP';\n\nconst styles = StyleSheet.create({\n row: {\n flexDirection: 'row',\n justifyContent: 'center',\n alignItems: 'center',\n gap: OTP_GAP_PX,\n },\n});\n","import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { codeOfFill, firstSlotFill, isAllowed, padTo } from './InputOTP.core';\n\n/** What a box needs from the element behind it: to take the focus. */\ninterface Focusable {\n focus: () => void;\n}\n\ninterface NativeOtpInput {\n variant: string;\n length: number;\n value: string;\n onChange?: (value: string) => void;\n onComplete?: (value: string) => void;\n}\n\n/**\n * The native boxes' digits — the web hook's rules (`InputOTP.hooks.ts`) over\n * React Native's events: `onChangeText` carries the box's whole new text and\n * `onKeyPress` carries only the key's name, and there is no paste event at\n * all. A pasted or SMS-filled code arrives as a change of the FIRST box, whose\n * `maxLength` is the code's length plus one for exactly that reason, and is\n * spread across the boxes by the same `firstSlotFill` / `codeOfFill` the web\n * uses.\n */\nexport const useNativeOtpDigits = ({ variant, length, value, onChange, onComplete }: NativeOtpInput) => {\n const [digits, setDigits] = useState<string[]>(() => padTo([], length));\n const boxRefs = useRef<(Focusable | null)[]>([]);\n\n useEffect(() => {\n setDigits(padTo(value.split('').slice(0, length), length));\n }, [value, length]);\n\n const focusBox = useCallback((index: number) => boxRefs.current[index]?.focus(), []);\n\n const publish = useCallback(\n (next: string) => {\n onChange?.(next);\n if (next.length === length) onComplete?.(next);\n },\n [length, onChange, onComplete],\n );\n\n /** A whole code laid across the boxes from the first one. */\n const spread = useCallback(\n (text: string) => {\n const code = text.slice(0, length);\n if (!code || !isAllowed(variant, code)) return;\n setDigits(padTo(code.split(''), length));\n publish(code);\n focusBox(Math.min(code.length, length - 1));\n },\n [variant, length, publish, focusBox],\n );\n\n const handleChangeText = useCallback(\n (index: number, text: string) => {\n // The web's paste spreads a code for EVERY variant; React Native has no\n // paste event, so a code arrives as a change of the first box, read by\n // the numeric rule for `alphanumeric` too (`masked` shows a bullet, whose\n // text no longer holds the digit it hides: it keeps one character a box).\n const fill = firstSlotFill(variant === 'alphanumeric' ? 'numeric' : variant, index, text, digits[0] ?? '');\n if (fill) return fill === 'code' ? spread(codeOfFill(text, digits[0] ?? '', length)) : undefined;\n if (!isAllowed(variant, text)) return;\n const next = [...digits];\n next[index] = text.slice(-1);\n setDigits(next);\n publish(next.join(''));\n if (text && index < length - 1) focusBox(index + 1);\n },\n [variant, digits, length, publish, focusBox, spread],\n );\n\n /** Backspace in a box that is already empty steps back to the one before. */\n const handleKey = useCallback(\n (index: number, key: string) => {\n if (key === 'Backspace' && !digits[index] && index > 0) focusBox(index - 1);\n },\n [digits, focusBox],\n );\n\n return { digits, boxRefs, handleChangeText, handleKey };\n};\n","/**\n * The digits' arithmetic, shared by both renderers' hooks (`InputOTP.hooks.ts`\n * on the web, `InputOTP.hooks.native.ts` on React Native). Pure: no React, no\n * DOM, no react-native.\n */\n\n/**\n * What each variant accepts. `masked` constrains nothing — it only changes what\n * is drawn — so it has no pattern here.\n */\nexport const PATTERNS: Record<string, RegExp> = {\n numeric: /^\\d*$/,\n alphanumeric: /^[a-zA-Z0-9]*$/,\n};\n\n/** Whether `text` is something this variant will take. */\nexport const isAllowed = (variant: string, text: string) => (PATTERNS[variant] ?? /.*/).test(text);\n\n/** The digit array is always `length` long, blank-padded on the right. */\nexport const padTo = (digits: string[], length: number) =>\n Array.from({ length }, (_, i) => digits[i] ?? '');\n\n/**\n * What a change to the numeric FIRST slot is, now that its `maxLength` is the\n * code's length rather than 1 (FUT-4144).\n *\n * That slot carries `one-time-code`, and the phone's SMS fill (or a password\n * manager) writes the WHOLE code into it as one change, not a paste — kept to\n * one character, the code lost every digit but the last. So:\n *\n * - `overtype`: a keystroke into the already-filled slot (its old character\n * plus the new one). Ignored, exactly as `maxLength={1}` refused it before.\n * - `code`: anything else longer than one character, written in one go.\n * Spread across the slots like a paste.\n *\n * Every other slot and variant keeps `maxLength={1}` and never gets here.\n */\nexport function firstSlotFill(variant: string, index: number, inputValue: string, current: string) {\n if (variant !== 'numeric' || index !== 0 || inputValue.length < 2) return null;\n const overtype = inputValue.length === 2 && current !== '' && inputValue.includes(current);\n return overtype ? 'overtype' : 'code';\n}\n\n/**\n * The code inside a first-slot fill. A phone that INSERTS the SMS code at the\n * caret rather than replacing the slot leaves the digit already there in\n * front: `4` + `482913` arrives as `4482913`, which spread as-is would give\n * `448291`. Longer than the code and led by that digit: the digit is dropped.\n */\nexport const codeOfFill = (inputValue: string, current: string, length: number): string =>\n inputValue.length > length && current !== '' && inputValue.startsWith(current) ? inputValue.slice(1) : inputValue;\n","/**\n * The numbers both renderers read (see `NATIVE.md`, \"Numbers\").\n *\n * Each box's type size, in design px, per size step; its side is the theme's\n * field height for that step (a square), its weight 600.\n */\nexport const OTP_FONT_PX: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl', number> = {\n xs: 12,\n sm: 14,\n md: 16,\n lg: 18,\n xl: 20,\n};\n\n/** The gap between two boxes: MUI's `gap: 1`, one 8px spacing unit. */\nexport const OTP_GAP_PX = 8;\n\n/** The border: 1px at rest, 2px focused (MUI's focused outline). */\nexport const OTP_BORDER = { rest: 1, focused: 2 } as const;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/form/InputOTP/InputOTP.native.tsx","../../../src/components/form/InputOTP/InputOTP.hooks.native.ts","../../../src/components/form/InputOTP/InputOTP.core.ts","../../../src/components/form/InputOTP/InputOTP.metrics.ts"],"sourcesContent":["import * as React from 'react';\nimport {\n Platform,\n StyleSheet,\n TextInput,\n View,\n type LayoutChangeEvent,\n type TextInputProps,\n type TextStyle,\n type ViewStyle,\n} from 'react-native';\n\nimport { useNativeOtpDigits } from './InputOTP.hooks.native';\nimport {\n OTP_BORDER,\n OTP_FIT_GAP_PX,\n OTP_FIT_MIN_HEIGHT_PX,\n OTP_FIT_MIN_WIDTH_PX,\n OTP_FONT_PX,\n OTP_GAP_PX,\n otpFitBox,\n} from './InputOTP.metrics';\nimport type { InputOTPProps } from './InputOTP.types.native';\nimport { webAria, webDisabled } from '../../../platform/aria';\nimport { resolveFieldEdge } from '../../../tokens/field-edge.core';\nimport { fieldHeightPx } from '../../../tokens/field-height.core';\nimport type { UiPaletteKey, UiTheme } from '../../../tokens/theme';\nimport type { SizeValue } from '../../../tokens/vocabulary';\nimport { useUiTheme } from '../../../provider/use-ui-theme.native';\n\nconst PALETTE_KEYS: readonly UiPaletteKey[] = ['primary', 'secondary', 'success', 'warning', 'info', 'danger', 'neutral'];\n\n/** The palette slot a `color` names; anything else is `primary`, as on the web. */\nfunction accentOf(theme: UiTheme, color: string): string {\n const key = PALETTE_KEYS.find((candidate) => candidate === color) ?? 'primary';\n return theme.palette[key].main;\n}\n\n/**\n * The keypad and the one-time-code fill, the web's `keypadProps` in React\n * Native's words: digits only for `numeric`, and on the FIRST box the hint that\n * lets the phone offer the code from an SMS — `oneTimeCode` on iOS,\n * `sms-otp` on Android.\n */\nfunction keypadProps(variant: string, index: number): Partial<TextInputProps> {\n // A code is not prose: no capital on the first letter, no autocorrect.\n if (variant !== 'numeric') return { autoCapitalize: 'none', autoCorrect: false, spellCheck: false };\n const fill: Partial<TextInputProps> =\n index === 0\n ? Platform.select<Partial<TextInputProps>>({\n ios: { textContentType: 'oneTimeCode', autoComplete: 'one-time-code' },\n android: { autoComplete: 'sms-otp' },\n default: { autoComplete: 'one-time-code' },\n })\n : {};\n return { keyboardType: 'number-pad', inputMode: 'numeric', ...fill };\n}\n\n/** The first box takes a whole code in one change (see the web's `slotMaxLength`), except `masked`'s. */\nconst boxMaxLength = (variant: string, index: number, length: number): number =>\n variant !== 'masked' && index === 0 ? length + 1 : 1;\n\ninterface BoxState {\n focused: boolean;\n error: boolean;\n disabled: boolean;\n}\n\n/**\n * A box's footprint under `fit=\"width\"`: the measured share of the row once\n * the row has been laid out, and until then a flex share bounded by the same\n * limits, so the first frame already fits the row.\n */\nfunction fitFootprint(side: number, length: number, rowWidth: number | null): TextStyle {\n if (rowWidth !== null) return otpFitBox(rowWidth, side, length);\n const height = Math.max(side, OTP_FIT_MIN_HEIGHT_PX);\n return { height, flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: OTP_FIT_MIN_WIDTH_PX, maxWidth: height };\n}\n\n/**\n * `fit=\"width\"`'s state: the row's width once laid out (the row takes its\n * parent's whole width), the boxes' footprint from it, and what the row adds —\n * its `onLayout` and the fit style. Without the option, nothing: the boxes keep\n * their square and the row's props are exactly the default's.\n *\n * The share is of the row's OUTER width, so under `fit` the row's `style`\n * should set no horizontal padding and no gap: pad the parent instead.\n */\nfunction useFitWidth(fit: InputOTPProps['fit'], side: number, length: number) {\n const [rowWidth, setRowWidth] = React.useState<number | null>(null);\n if (fit !== 'width') return { footprint: undefined, rowLayout: {}, rowStyles: [] };\n return {\n footprint: fitFootprint(side, length, rowWidth),\n rowLayout: { onLayout: (event: LayoutChangeEvent) => setRowWidth(event.nativeEvent.layout.width) },\n rowStyles: [styles.fitRow],\n };\n}\n\n/** One box's paint: the web slot's square, border and type, from the same numbers. */\nfunction boxStyle(theme: UiTheme, size: SizeValue, accent: string, state: BoxState, footprint?: TextStyle): TextStyle {\n const side = fieldHeightPx(theme.fieldHeight, size);\n const edge = resolveFieldEdge(theme.palette.divider, theme.palette.background.paper);\n // Error and disabled outrank focus, as in MUI's outlined input (and the native `Input`).\n const borderColor = state.error\n ? theme.palette.danger.main\n : state.disabled\n ? theme.palette.action.disabled\n : state.focused\n ? accent\n : edge;\n return {\n ...(footprint ?? { width: side, height: side }),\n borderRadius: theme.radius.field,\n borderWidth: state.focused ? OTP_BORDER.focused : OTP_BORDER.rest,\n borderColor,\n backgroundColor: theme.palette.background.paper,\n color: state.disabled ? theme.palette.text.disabled : theme.palette.text.primary,\n fontFamily: theme.typography.fontFamily,\n fontSize: OTP_FONT_PX[size],\n fontWeight: '600',\n textAlign: 'center',\n padding: 0,\n };\n}\n\n/**\n * The native `InputOTP`: one row of boxes, one character each, that moves\n * forward as it is typed, steps back on Backspace in an empty box, and spreads\n * a code the phone fills (or a paste) across every box from the first.\n *\n * The same props as the web one; the row is a `group` named by `aria-label`,\n * each box is `${dataTestId}-slot-${index}`. Gaps against the web:\n * `NATIVE-NOTES.md`.\n */\nexport const InputOTP = React.forwardRef<View, InputOTPProps>((props, ref) => {\n const {\n variant = 'numeric',\n color = 'primary',\n size = 'md',\n length = 6,\n value = '',\n onChange,\n onComplete,\n autoFocus = false,\n error = false,\n disabled = false,\n dataTestId,\n fit,\n style,\n 'aria-label': ariaLabel,\n } = props;\n\n const theme = useUiTheme();\n const accent = accentOf(theme, color);\n const [focusedIndex, setFocusedIndex] = React.useState<number | null>(null);\n const { footprint, rowLayout, rowStyles } = useFitWidth(fit, fieldHeightPx(theme.fieldHeight, size), length);\n const { digits, boxRefs, handleChangeText, handleKey } = useNativeOtpDigits({\n variant,\n length,\n value,\n onChange,\n onComplete,\n });\n\n return (\n <View\n ref={ref}\n testID={dataTestId}\n role=\"group\"\n aria-label={ariaLabel}\n {...rowLayout}\n style={[styles.row, ...rowStyles, style]}\n >\n {digits.map((digit, index) => (\n <TextInput\n key={index}\n ref={(element) => {\n boxRefs.current[index] = element;\n }}\n testID={dataTestId ? `${dataTestId}-slot-${index}` : undefined}\n value={variant === 'masked' ? (digit ? '•' : '') : digit}\n onChangeText={(text) => handleChangeText(index, text)}\n onKeyPress={(event) => handleKey(index, event.nativeEvent.key)}\n onFocus={() => setFocusedIndex(index)}\n onBlur={() => setFocusedIndex((current) => (current === index ? null : current))}\n autoFocus={autoFocus && index === 0}\n editable={!disabled}\n aria-disabled={disabled}\n {...webAria({ 'aria-invalid': error })}\n maxLength={boxMaxLength(variant, index, length)}\n style={boxStyle(theme, size, accent, { focused: focusedIndex === index && !disabled, error, disabled }, footprint)}\n {...webDisabled(disabled)}\n {...keypadProps(variant, index)}\n />\n ))}\n </View>\n );\n});\n\nInputOTP.displayName = 'InputOTP';\n\nconst styles = StyleSheet.create({\n row: {\n flexDirection: 'row',\n justifyContent: 'center',\n alignItems: 'center',\n gap: OTP_GAP_PX,\n },\n /** `fit=\"width\"`: the parent's whole width, in a column or a row parent, shared at the prototype's gap. */\n fitRow: {\n // Both: `stretch` alone spans a column parent but not a row parent, where\n // the row would shrink to its boxes and collapse them to their minimum.\n alignSelf: 'stretch',\n width: '100%',\n gap: OTP_FIT_GAP_PX,\n } satisfies ViewStyle,\n});\n","import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { codeOfFill, firstSlotFill, isAllowed, padTo } from './InputOTP.core';\n\n/** What a box needs from the element behind it: to take the focus. */\ninterface Focusable {\n focus: () => void;\n}\n\ninterface NativeOtpInput {\n variant: string;\n length: number;\n value: string;\n onChange?: (value: string) => void;\n onComplete?: (value: string) => void;\n}\n\n/**\n * The native boxes' digits — the web hook's rules (`InputOTP.hooks.ts`) over\n * React Native's events: `onChangeText` carries the box's whole new text and\n * `onKeyPress` carries only the key's name, and there is no paste event at\n * all. A pasted or SMS-filled code arrives as a change of the FIRST box, whose\n * `maxLength` is the code's length plus one for exactly that reason, and is\n * spread across the boxes by the same `firstSlotFill` / `codeOfFill` the web\n * uses.\n */\nexport const useNativeOtpDigits = ({ variant, length, value, onChange, onComplete }: NativeOtpInput) => {\n const [digits, setDigits] = useState<string[]>(() => padTo([], length));\n const boxRefs = useRef<(Focusable | null)[]>([]);\n\n useEffect(() => {\n setDigits(padTo(value.split('').slice(0, length), length));\n }, [value, length]);\n\n const focusBox = useCallback((index: number) => boxRefs.current[index]?.focus(), []);\n\n const publish = useCallback(\n (next: string) => {\n onChange?.(next);\n if (next.length === length) onComplete?.(next);\n },\n [length, onChange, onComplete],\n );\n\n /** A whole code laid across the boxes from the first one. */\n const spread = useCallback(\n (text: string) => {\n const code = text.slice(0, length);\n if (!code || !isAllowed(variant, code)) return;\n setDigits(padTo(code.split(''), length));\n publish(code);\n focusBox(Math.min(code.length, length - 1));\n },\n [variant, length, publish, focusBox],\n );\n\n const handleChangeText = useCallback(\n (index: number, text: string) => {\n // The web's paste spreads a code for EVERY variant; React Native has no\n // paste event, so a code arrives as a change of the first box, read by\n // the numeric rule for `alphanumeric` too (`masked` shows a bullet, whose\n // text no longer holds the digit it hides: it keeps one character a box).\n const fill = firstSlotFill(variant === 'alphanumeric' ? 'numeric' : variant, index, text, digits[0] ?? '');\n if (fill) return fill === 'code' ? spread(codeOfFill(text, digits[0] ?? '', length)) : undefined;\n if (!isAllowed(variant, text)) return;\n const next = [...digits];\n next[index] = text.slice(-1);\n setDigits(next);\n publish(next.join(''));\n if (text && index < length - 1) focusBox(index + 1);\n },\n [variant, digits, length, publish, focusBox, spread],\n );\n\n /** Backspace in a box that is already empty steps back to the one before. */\n const handleKey = useCallback(\n (index: number, key: string) => {\n if (key === 'Backspace' && !digits[index] && index > 0) focusBox(index - 1);\n },\n [digits, focusBox],\n );\n\n return { digits, boxRefs, handleChangeText, handleKey };\n};\n","/**\n * The digits' arithmetic, shared by both renderers' hooks (`InputOTP.hooks.ts`\n * on the web, `InputOTP.hooks.native.ts` on React Native). Pure: no React, no\n * DOM, no react-native.\n */\n\n/**\n * What each variant accepts. `masked` constrains nothing — it only changes what\n * is drawn — so it has no pattern here.\n */\nexport const PATTERNS: Record<string, RegExp> = {\n numeric: /^\\d*$/,\n alphanumeric: /^[a-zA-Z0-9]*$/,\n};\n\n/** Whether `text` is something this variant will take. */\nexport const isAllowed = (variant: string, text: string) => (PATTERNS[variant] ?? /.*/).test(text);\n\n/** The digit array is always `length` long, blank-padded on the right. */\nexport const padTo = (digits: string[], length: number) =>\n Array.from({ length }, (_, i) => digits[i] ?? '');\n\n/**\n * What a change to the numeric FIRST slot is, now that its `maxLength` is the\n * code's length rather than 1 (FUT-4144).\n *\n * That slot carries `one-time-code`, and the phone's SMS fill (or a password\n * manager) writes the WHOLE code into it as one change, not a paste — kept to\n * one character, the code lost every digit but the last. So:\n *\n * - `overtype`: a keystroke into the already-filled slot (its old character\n * plus the new one). Ignored, exactly as `maxLength={1}` refused it before.\n * - `code`: anything else longer than one character, written in one go.\n * Spread across the slots like a paste.\n *\n * Every other slot and variant keeps `maxLength={1}` and never gets here.\n */\nexport function firstSlotFill(variant: string, index: number, inputValue: string, current: string) {\n if (variant !== 'numeric' || index !== 0 || inputValue.length < 2) return null;\n const overtype = inputValue.length === 2 && current !== '' && inputValue.includes(current);\n return overtype ? 'overtype' : 'code';\n}\n\n/**\n * The code inside a first-slot fill. A phone that INSERTS the SMS code at the\n * caret rather than replacing the slot leaves the digit already there in\n * front: `4` + `482913` arrives as `4482913`, which spread as-is would give\n * `448291`. Longer than the code and led by that digit: the digit is dropped.\n */\nexport const codeOfFill = (inputValue: string, current: string, length: number): string =>\n inputValue.length > length && current !== '' && inputValue.startsWith(current) ? inputValue.slice(1) : inputValue;\n","/**\n * The numbers both renderers read (see `NATIVE.md`, \"Numbers\").\n *\n * Each box's type size, in design px, per size step; its side is the theme's\n * field height for that step (a square), its weight 600.\n */\nexport const OTP_FONT_PX: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl', number> = {\n xs: 12,\n sm: 14,\n md: 16,\n lg: 18,\n xl: 20,\n};\n\n/** The gap between two boxes: MUI's `gap: 1`, one 8px spacing unit. */\nexport const OTP_GAP_PX = 8;\n\n/** The border: 1px at rest, 2px focused (MUI's focused outline). */\nexport const OTP_BORDER = { rest: 1, focused: 2 } as const;\n\n/**\n * `fit=\"width\"` (FUT-4585): the gap between two boxes when they share the row's\n * width — the approved prototype's 6px, a little tighter than the default 8px\n * so six portrait boxes stay at least 40 wide on a 320 phone.\n */\nexport const OTP_FIT_GAP_PX = 6;\n\n/** `fit=\"width\"`: no box is narrower than this, however narrow the row. */\nexport const OTP_FIT_MIN_WIDTH_PX = 32;\n\n/** `fit=\"width\"`: no box is shorter than the 40px touch floor, whatever its size. */\nexport const OTP_FIT_MIN_HEIGHT_PX = 40;\n\n/**\n * One box's size under `fit=\"width\"`, for a row `rowWidth` wide holding\n * `length` boxes whose size step draws a field `side` tall.\n *\n * The box keeps the field's height (never under {@link OTP_FIT_MIN_HEIGHT_PX})\n * and takes an equal share of the row, less the gaps, for its width: floored\n * to a whole pixel so six of them never overflow the row, never wider than it\n * is tall (a wide row keeps squares) and never under\n * {@link OTP_FIT_MIN_WIDTH_PX}. A narrow row therefore draws PORTRAIT boxes\n * rather than dropping to a smaller size and its smaller type.\n */\nexport function otpFitBox(rowWidth: number, side: number, length: number): { width: number; height: number } {\n const height = Math.max(side, OTP_FIT_MIN_HEIGHT_PX);\n const count = Math.max(1, Math.floor(length));\n const share = Math.floor((rowWidth - (count - 1) * OTP_FIT_GAP_PX) / count);\n const width = Math.min(height, Math.max(OTP_FIT_MIN_WIDTH_PX, Number.isFinite(share) ? share : height));\n return { width, height };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -18,6 +18,20 @@ export interface InputOTPBaseProps {
|
|
|
18
18
|
* The size of the OTP input
|
|
19
19
|
*/
|
|
20
20
|
size?: SizeValue;
|
|
21
|
+
/**
|
|
22
|
+
* How the boxes take the row's width (FUT-4585).
|
|
23
|
+
*
|
|
24
|
+
* Left out, each box is a square of the size's field height, and the row is
|
|
25
|
+
* as wide as its boxes — what every caller drew before this option.
|
|
26
|
+
*
|
|
27
|
+
* `'width'`: the row takes its parent's whole width and the boxes share it
|
|
28
|
+
* with a 6px gap. Each keeps the size's field height (never under 40) and
|
|
29
|
+
* its type, and narrows to fit: never wider than it is tall, never under
|
|
30
|
+
* 32. On a narrow phone that draws PORTRAIT boxes instead of a smaller size.
|
|
31
|
+
* The boxes share the row's outer width, so pad the parent, not the row: a
|
|
32
|
+
* `style` (or `sx`) setting the row's padding or gap is not accounted for.
|
|
33
|
+
*/
|
|
34
|
+
fit?: 'width';
|
|
21
35
|
/**
|
|
22
36
|
* Number of OTP digits
|
|
23
37
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputOTP.base.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.base.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAExE;;;;;GAKG;AAEH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,cAAc,GAAG,QAAQ,CAAC;IAEhD;;OAEG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IAEnB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAErC;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|
|
1
|
+
{"version":3,"file":"InputOTP.base.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.base.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAExE;;;;;GAKG;AAEH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,cAAc,GAAG,QAAQ,CAAC;IAEhD;;OAEG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IAEnB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB;;;;;;;;;;;;OAYG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IAEd;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAErC;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputOTP.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAqB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"InputOTP.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAqB,MAAM,OAAO,CAAC;AA6E1C,eAAO,MAAM,QAAQ,oHAuDnB,CAAC"}
|
|
@@ -12,4 +12,29 @@ export declare const OTP_BORDER: {
|
|
|
12
12
|
readonly rest: 1;
|
|
13
13
|
readonly focused: 2;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* `fit="width"` (FUT-4585): the gap between two boxes when they share the row's
|
|
17
|
+
* width — the approved prototype's 6px, a little tighter than the default 8px
|
|
18
|
+
* so six portrait boxes stay at least 40 wide on a 320 phone.
|
|
19
|
+
*/
|
|
20
|
+
export declare const OTP_FIT_GAP_PX = 6;
|
|
21
|
+
/** `fit="width"`: no box is narrower than this, however narrow the row. */
|
|
22
|
+
export declare const OTP_FIT_MIN_WIDTH_PX = 32;
|
|
23
|
+
/** `fit="width"`: no box is shorter than the 40px touch floor, whatever its size. */
|
|
24
|
+
export declare const OTP_FIT_MIN_HEIGHT_PX = 40;
|
|
25
|
+
/**
|
|
26
|
+
* One box's size under `fit="width"`, for a row `rowWidth` wide holding
|
|
27
|
+
* `length` boxes whose size step draws a field `side` tall.
|
|
28
|
+
*
|
|
29
|
+
* The box keeps the field's height (never under {@link OTP_FIT_MIN_HEIGHT_PX})
|
|
30
|
+
* and takes an equal share of the row, less the gaps, for its width: floored
|
|
31
|
+
* to a whole pixel so six of them never overflow the row, never wider than it
|
|
32
|
+
* is tall (a wide row keeps squares) and never under
|
|
33
|
+
* {@link OTP_FIT_MIN_WIDTH_PX}. A narrow row therefore draws PORTRAIT boxes
|
|
34
|
+
* rather than dropping to a smaller size and its smaller type.
|
|
35
|
+
*/
|
|
36
|
+
export declare function otpFitBox(rowWidth: number, side: number, length: number): {
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
};
|
|
15
40
|
//# sourceMappingURL=InputOTP.metrics.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputOTP.metrics.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.metrics.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,MAAM,CAMxE,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,oEAAoE;AACpE,eAAO,MAAM,UAAU;;;CAAmC,CAAC"}
|
|
1
|
+
{"version":3,"file":"InputOTP.metrics.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.metrics.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,MAAM,CAMxE,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,oEAAoE;AACpE,eAAO,MAAM,UAAU;;;CAAmC,CAAC;AAE3D;;;;GAIG;AACH,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,qFAAqF;AACrF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAM3G"}
|
|
@@ -4,6 +4,8 @@ export interface OtpSlotFlags {
|
|
|
4
4
|
customSize?: string;
|
|
5
5
|
glass?: boolean;
|
|
6
6
|
gradient?: boolean;
|
|
7
|
+
/** `fit="width"`: share the row's width instead of drawing a square (FUT-4585). */
|
|
8
|
+
fitWidth?: boolean;
|
|
7
9
|
}
|
|
8
10
|
export declare const otpSlotSx: (theme: Theme, flags: OtpSlotFlags) => CSSObject;
|
|
9
11
|
//# sourceMappingURL=InputOTP.styles.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputOTP.styles.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.styles.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AAoDtE,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"InputOTP.styles.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.styles.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AAoDtE,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mFAAmF;IACnF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA0BD,eAAO,MAAM,SAAS,GAAI,OAAO,KAAK,EAAE,OAAO,YAAY,KAAG,SAkC7D,CAAC"}
|
|
@@ -18,6 +18,20 @@ export interface InputOTPBaseProps {
|
|
|
18
18
|
* The size of the OTP input
|
|
19
19
|
*/
|
|
20
20
|
size?: SizeValue;
|
|
21
|
+
/**
|
|
22
|
+
* How the boxes take the row's width (FUT-4585).
|
|
23
|
+
*
|
|
24
|
+
* Left out, each box is a square of the size's field height, and the row is
|
|
25
|
+
* as wide as its boxes — what every caller drew before this option.
|
|
26
|
+
*
|
|
27
|
+
* `'width'`: the row takes its parent's whole width and the boxes share it
|
|
28
|
+
* with a 6px gap. Each keeps the size's field height (never under 40) and
|
|
29
|
+
* its type, and narrows to fit: never wider than it is tall, never under
|
|
30
|
+
* 32. On a narrow phone that draws PORTRAIT boxes instead of a smaller size.
|
|
31
|
+
* The boxes share the row's outer width, so pad the parent, not the row: a
|
|
32
|
+
* `style` (or `sx`) setting the row's padding or gap is not accounted for.
|
|
33
|
+
*/
|
|
34
|
+
fit?: 'width';
|
|
21
35
|
/**
|
|
22
36
|
* Number of OTP digits
|
|
23
37
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputOTP.base.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.base.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAExE;;;;;GAKG;AAEH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,cAAc,GAAG,QAAQ,CAAC;IAEhD;;OAEG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IAEnB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAErC;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|
|
1
|
+
{"version":3,"file":"InputOTP.base.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.base.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAExE;;;;;GAKG;AAEH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,cAAc,GAAG,QAAQ,CAAC;IAEhD;;OAEG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IAEnB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB;;;;;;;;;;;;OAYG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IAEd;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAErC;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|
|
@@ -12,4 +12,29 @@ export declare const OTP_BORDER: {
|
|
|
12
12
|
readonly rest: 1;
|
|
13
13
|
readonly focused: 2;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* `fit="width"` (FUT-4585): the gap between two boxes when they share the row's
|
|
17
|
+
* width — the approved prototype's 6px, a little tighter than the default 8px
|
|
18
|
+
* so six portrait boxes stay at least 40 wide on a 320 phone.
|
|
19
|
+
*/
|
|
20
|
+
export declare const OTP_FIT_GAP_PX = 6;
|
|
21
|
+
/** `fit="width"`: no box is narrower than this, however narrow the row. */
|
|
22
|
+
export declare const OTP_FIT_MIN_WIDTH_PX = 32;
|
|
23
|
+
/** `fit="width"`: no box is shorter than the 40px touch floor, whatever its size. */
|
|
24
|
+
export declare const OTP_FIT_MIN_HEIGHT_PX = 40;
|
|
25
|
+
/**
|
|
26
|
+
* One box's size under `fit="width"`, for a row `rowWidth` wide holding
|
|
27
|
+
* `length` boxes whose size step draws a field `side` tall.
|
|
28
|
+
*
|
|
29
|
+
* The box keeps the field's height (never under {@link OTP_FIT_MIN_HEIGHT_PX})
|
|
30
|
+
* and takes an equal share of the row, less the gaps, for its width: floored
|
|
31
|
+
* to a whole pixel so six of them never overflow the row, never wider than it
|
|
32
|
+
* is tall (a wide row keeps squares) and never under
|
|
33
|
+
* {@link OTP_FIT_MIN_WIDTH_PX}. A narrow row therefore draws PORTRAIT boxes
|
|
34
|
+
* rather than dropping to a smaller size and its smaller type.
|
|
35
|
+
*/
|
|
36
|
+
export declare function otpFitBox(rowWidth: number, side: number, length: number): {
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
};
|
|
15
40
|
//# sourceMappingURL=InputOTP.metrics.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputOTP.metrics.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.metrics.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,MAAM,CAMxE,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,oEAAoE;AACpE,eAAO,MAAM,UAAU;;;CAAmC,CAAC"}
|
|
1
|
+
{"version":3,"file":"InputOTP.metrics.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.metrics.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,MAAM,CAMxE,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,oEAAoE;AACpE,eAAO,MAAM,UAAU;;;CAAmC,CAAC;AAE3D;;;;GAIG;AACH,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,qFAAqF;AACrF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAM3G"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputOTP.native.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.native.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,
|
|
1
|
+
{"version":3,"file":"InputOTP.native.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/InputOTP/InputOTP.native.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAIL,IAAI,EAKL,MAAM,cAAc,CAAC;AAYtB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAuG7D;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,4EA+DnB,CAAC"}
|
package/package.json
CHANGED
|
@@ -23,6 +23,21 @@ export interface InputOTPBaseProps {
|
|
|
23
23
|
*/
|
|
24
24
|
size?: SizeValue;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* How the boxes take the row's width (FUT-4585).
|
|
28
|
+
*
|
|
29
|
+
* Left out, each box is a square of the size's field height, and the row is
|
|
30
|
+
* as wide as its boxes — what every caller drew before this option.
|
|
31
|
+
*
|
|
32
|
+
* `'width'`: the row takes its parent's whole width and the boxes share it
|
|
33
|
+
* with a 6px gap. Each keeps the size's field height (never under 40) and
|
|
34
|
+
* its type, and narrows to fit: never wider than it is tall, never under
|
|
35
|
+
* 32. On a narrow phone that draws PORTRAIT boxes instead of a smaller size.
|
|
36
|
+
* The boxes share the row's outer width, so pad the parent, not the row: a
|
|
37
|
+
* `style` (or `sx`) setting the row's padding or gap is not accounted for.
|
|
38
|
+
*/
|
|
39
|
+
fit?: 'width';
|
|
40
|
+
|
|
26
41
|
/**
|
|
27
42
|
* Number of OTP digits
|
|
28
43
|
*/
|
|
@@ -27,6 +27,7 @@ The InputOTP component provides a specialized input field for one-time passwords
|
|
|
27
27
|
| `onComplete` | `(value: string) => void` | - | Callback when all digits are filled |
|
|
28
28
|
| `color` | `'primary' \| 'secondary' \| 'success' \| 'warning' \| 'danger' \| 'neutral'` | `'primary'` | Theme color |
|
|
29
29
|
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Input size |
|
|
30
|
+
| `fit` | `'width'` | - | Share the row's width: each box keeps the size's height (never under 40) and type, and narrows to fit, never wider than tall nor under 32. Left out, each box is a square of the size's field height |
|
|
30
31
|
| `autoFocus` | `boolean` | `false` | Auto-focus first input on mount |
|
|
31
32
|
| `disabled` | `boolean` | `false` | Disable all inputs |
|
|
32
33
|
| `error` | `boolean` | `false` | Show error state |
|
|
@@ -191,6 +192,40 @@ The component supports various visual customizations:
|
|
|
191
192
|
- `lg`: Large (prominent forms)
|
|
192
193
|
- `xl`: Extra large (accessibility focus)
|
|
193
194
|
|
|
195
|
+
### Fitting a narrow row: `fit="width"`
|
|
196
|
+
|
|
197
|
+
A square box is as wide as its field is tall, so six `lg` boxes need 328px. On
|
|
198
|
+
a narrow phone the only way to fit them used to be a smaller size, whose boxes
|
|
199
|
+
drop under the 40px tap minimum and whose type shrinks with them. `fit="width"`
|
|
200
|
+
keeps the size and narrows the boxes instead (FUT-4585):
|
|
201
|
+
|
|
202
|
+
- the row takes its parent's whole width, with a 6px gap between boxes;
|
|
203
|
+
- each box keeps the size's field height, lifted to 40 if it is shorter, and
|
|
204
|
+
the size's own type;
|
|
205
|
+
- each box's width is the row less the gaps, divided by `length`, floored to a
|
|
206
|
+
whole pixel, never wider than the box is tall (a wide row keeps squares) and
|
|
207
|
+
never under 32.
|
|
208
|
+
|
|
209
|
+
At `size="lg"` (48 tall), a sheet with 24px of padding on each side draws:
|
|
210
|
+
|
|
211
|
+
| phone | row | box |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| 320 | 272 | 40 × 48 |
|
|
214
|
+
| 360 | 312 | 47 × 48 |
|
|
215
|
+
| 390 | 342 | 48 × 48 |
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
<InputOTP fit="width" size="lg" length={6} value={code} onChange={setCode} />
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The boxes share the row's **outer** width, and the row spans its parent in a
|
|
222
|
+
column or a row parent alike. Pad the parent, not the row: a `style` (or `sx`)
|
|
223
|
+
that sets the row's own padding or gap is not taken off the share.
|
|
224
|
+
|
|
225
|
+
The arithmetic is `otpFitBox` in `InputOTP.metrics.ts`, shared by both
|
|
226
|
+
renderers: React Native measures the row with `onLayout`, the web lets CSS flex
|
|
227
|
+
share it under the same bounds.
|
|
228
|
+
|
|
194
229
|
### Color Themes
|
|
195
230
|
- `primary`: Main brand color
|
|
196
231
|
- `secondary`: Alternative brand color
|
|
@@ -17,3 +17,35 @@ export const OTP_GAP_PX = 8;
|
|
|
17
17
|
|
|
18
18
|
/** The border: 1px at rest, 2px focused (MUI's focused outline). */
|
|
19
19
|
export const OTP_BORDER = { rest: 1, focused: 2 } as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `fit="width"` (FUT-4585): the gap between two boxes when they share the row's
|
|
23
|
+
* width — the approved prototype's 6px, a little tighter than the default 8px
|
|
24
|
+
* so six portrait boxes stay at least 40 wide on a 320 phone.
|
|
25
|
+
*/
|
|
26
|
+
export const OTP_FIT_GAP_PX = 6;
|
|
27
|
+
|
|
28
|
+
/** `fit="width"`: no box is narrower than this, however narrow the row. */
|
|
29
|
+
export const OTP_FIT_MIN_WIDTH_PX = 32;
|
|
30
|
+
|
|
31
|
+
/** `fit="width"`: no box is shorter than the 40px touch floor, whatever its size. */
|
|
32
|
+
export const OTP_FIT_MIN_HEIGHT_PX = 40;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One box's size under `fit="width"`, for a row `rowWidth` wide holding
|
|
36
|
+
* `length` boxes whose size step draws a field `side` tall.
|
|
37
|
+
*
|
|
38
|
+
* The box keeps the field's height (never under {@link OTP_FIT_MIN_HEIGHT_PX})
|
|
39
|
+
* and takes an equal share of the row, less the gaps, for its width: floored
|
|
40
|
+
* to a whole pixel so six of them never overflow the row, never wider than it
|
|
41
|
+
* is tall (a wide row keeps squares) and never under
|
|
42
|
+
* {@link OTP_FIT_MIN_WIDTH_PX}. A narrow row therefore draws PORTRAIT boxes
|
|
43
|
+
* rather than dropping to a smaller size and its smaller type.
|
|
44
|
+
*/
|
|
45
|
+
export function otpFitBox(rowWidth: number, side: number, length: number): { width: number; height: number } {
|
|
46
|
+
const height = Math.max(side, OTP_FIT_MIN_HEIGHT_PX);
|
|
47
|
+
const count = Math.max(1, Math.floor(length));
|
|
48
|
+
const share = Math.floor((rowWidth - (count - 1) * OTP_FIT_GAP_PX) / count);
|
|
49
|
+
const width = Math.min(height, Math.max(OTP_FIT_MIN_WIDTH_PX, Number.isFinite(share) ? share : height));
|
|
50
|
+
return { width, height };
|
|
51
|
+
}
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
TextInput,
|
|
6
|
+
View,
|
|
7
|
+
type LayoutChangeEvent,
|
|
8
|
+
type TextInputProps,
|
|
9
|
+
type TextStyle,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
} from 'react-native';
|
|
3
12
|
|
|
4
13
|
import { useNativeOtpDigits } from './InputOTP.hooks.native';
|
|
5
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
OTP_BORDER,
|
|
16
|
+
OTP_FIT_GAP_PX,
|
|
17
|
+
OTP_FIT_MIN_HEIGHT_PX,
|
|
18
|
+
OTP_FIT_MIN_WIDTH_PX,
|
|
19
|
+
OTP_FONT_PX,
|
|
20
|
+
OTP_GAP_PX,
|
|
21
|
+
otpFitBox,
|
|
22
|
+
} from './InputOTP.metrics';
|
|
6
23
|
import type { InputOTPProps } from './InputOTP.types.native';
|
|
7
24
|
import { webAria, webDisabled } from '../../../platform/aria';
|
|
8
25
|
import { resolveFieldEdge } from '../../../tokens/field-edge.core';
|
|
@@ -49,8 +66,38 @@ interface BoxState {
|
|
|
49
66
|
disabled: boolean;
|
|
50
67
|
}
|
|
51
68
|
|
|
69
|
+
/**
|
|
70
|
+
* A box's footprint under `fit="width"`: the measured share of the row once
|
|
71
|
+
* the row has been laid out, and until then a flex share bounded by the same
|
|
72
|
+
* limits, so the first frame already fits the row.
|
|
73
|
+
*/
|
|
74
|
+
function fitFootprint(side: number, length: number, rowWidth: number | null): TextStyle {
|
|
75
|
+
if (rowWidth !== null) return otpFitBox(rowWidth, side, length);
|
|
76
|
+
const height = Math.max(side, OTP_FIT_MIN_HEIGHT_PX);
|
|
77
|
+
return { height, flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: OTP_FIT_MIN_WIDTH_PX, maxWidth: height };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `fit="width"`'s state: the row's width once laid out (the row takes its
|
|
82
|
+
* parent's whole width), the boxes' footprint from it, and what the row adds —
|
|
83
|
+
* its `onLayout` and the fit style. Without the option, nothing: the boxes keep
|
|
84
|
+
* their square and the row's props are exactly the default's.
|
|
85
|
+
*
|
|
86
|
+
* The share is of the row's OUTER width, so under `fit` the row's `style`
|
|
87
|
+
* should set no horizontal padding and no gap: pad the parent instead.
|
|
88
|
+
*/
|
|
89
|
+
function useFitWidth(fit: InputOTPProps['fit'], side: number, length: number) {
|
|
90
|
+
const [rowWidth, setRowWidth] = React.useState<number | null>(null);
|
|
91
|
+
if (fit !== 'width') return { footprint: undefined, rowLayout: {}, rowStyles: [] };
|
|
92
|
+
return {
|
|
93
|
+
footprint: fitFootprint(side, length, rowWidth),
|
|
94
|
+
rowLayout: { onLayout: (event: LayoutChangeEvent) => setRowWidth(event.nativeEvent.layout.width) },
|
|
95
|
+
rowStyles: [styles.fitRow],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
52
99
|
/** One box's paint: the web slot's square, border and type, from the same numbers. */
|
|
53
|
-
function boxStyle(theme: UiTheme, size: SizeValue, accent: string, state: BoxState): TextStyle {
|
|
100
|
+
function boxStyle(theme: UiTheme, size: SizeValue, accent: string, state: BoxState, footprint?: TextStyle): TextStyle {
|
|
54
101
|
const side = fieldHeightPx(theme.fieldHeight, size);
|
|
55
102
|
const edge = resolveFieldEdge(theme.palette.divider, theme.palette.background.paper);
|
|
56
103
|
// Error and disabled outrank focus, as in MUI's outlined input (and the native `Input`).
|
|
@@ -62,8 +109,7 @@ function boxStyle(theme: UiTheme, size: SizeValue, accent: string, state: BoxSta
|
|
|
62
109
|
? accent
|
|
63
110
|
: edge;
|
|
64
111
|
return {
|
|
65
|
-
width: side,
|
|
66
|
-
height: side,
|
|
112
|
+
...(footprint ?? { width: side, height: side }),
|
|
67
113
|
borderRadius: theme.radius.field,
|
|
68
114
|
borderWidth: state.focused ? OTP_BORDER.focused : OTP_BORDER.rest,
|
|
69
115
|
borderColor,
|
|
@@ -99,6 +145,7 @@ export const InputOTP = React.forwardRef<View, InputOTPProps>((props, ref) => {
|
|
|
99
145
|
error = false,
|
|
100
146
|
disabled = false,
|
|
101
147
|
dataTestId,
|
|
148
|
+
fit,
|
|
102
149
|
style,
|
|
103
150
|
'aria-label': ariaLabel,
|
|
104
151
|
} = props;
|
|
@@ -106,6 +153,7 @@ export const InputOTP = React.forwardRef<View, InputOTPProps>((props, ref) => {
|
|
|
106
153
|
const theme = useUiTheme();
|
|
107
154
|
const accent = accentOf(theme, color);
|
|
108
155
|
const [focusedIndex, setFocusedIndex] = React.useState<number | null>(null);
|
|
156
|
+
const { footprint, rowLayout, rowStyles } = useFitWidth(fit, fieldHeightPx(theme.fieldHeight, size), length);
|
|
109
157
|
const { digits, boxRefs, handleChangeText, handleKey } = useNativeOtpDigits({
|
|
110
158
|
variant,
|
|
111
159
|
length,
|
|
@@ -115,7 +163,14 @@ export const InputOTP = React.forwardRef<View, InputOTPProps>((props, ref) => {
|
|
|
115
163
|
});
|
|
116
164
|
|
|
117
165
|
return (
|
|
118
|
-
<View
|
|
166
|
+
<View
|
|
167
|
+
ref={ref}
|
|
168
|
+
testID={dataTestId}
|
|
169
|
+
role="group"
|
|
170
|
+
aria-label={ariaLabel}
|
|
171
|
+
{...rowLayout}
|
|
172
|
+
style={[styles.row, ...rowStyles, style]}
|
|
173
|
+
>
|
|
119
174
|
{digits.map((digit, index) => (
|
|
120
175
|
<TextInput
|
|
121
176
|
key={index}
|
|
@@ -133,7 +188,7 @@ export const InputOTP = React.forwardRef<View, InputOTPProps>((props, ref) => {
|
|
|
133
188
|
aria-disabled={disabled}
|
|
134
189
|
{...webAria({ 'aria-invalid': error })}
|
|
135
190
|
maxLength={boxMaxLength(variant, index, length)}
|
|
136
|
-
style={boxStyle(theme, size, accent, { focused: focusedIndex === index && !disabled, error, disabled })}
|
|
191
|
+
style={boxStyle(theme, size, accent, { focused: focusedIndex === index && !disabled, error, disabled }, footprint)}
|
|
137
192
|
{...webDisabled(disabled)}
|
|
138
193
|
{...keypadProps(variant, index)}
|
|
139
194
|
/>
|
|
@@ -151,4 +206,12 @@ const styles = StyleSheet.create({
|
|
|
151
206
|
alignItems: 'center',
|
|
152
207
|
gap: OTP_GAP_PX,
|
|
153
208
|
},
|
|
209
|
+
/** `fit="width"`: the parent's whole width, in a column or a row parent, shared at the prototype's gap. */
|
|
210
|
+
fitRow: {
|
|
211
|
+
// Both: `stretch` alone spans a column parent but not a row parent, where
|
|
212
|
+
// the row would shrink to its boxes and collapse them to their minimum.
|
|
213
|
+
alignSelf: 'stretch',
|
|
214
|
+
width: '100%',
|
|
215
|
+
gap: OTP_FIT_GAP_PX,
|
|
216
|
+
} satisfies ViewStyle,
|
|
154
217
|
});
|
|
@@ -7,7 +7,7 @@ import { asFieldSize, fieldHeight } from '../../../tokens/field-height';
|
|
|
7
7
|
import { absoluteInk, controlNeutral } from '../../../tokens/ink';
|
|
8
8
|
import { rem } from '../../../tokens/relative';
|
|
9
9
|
|
|
10
|
-
import { OTP_FONT_PX } from './InputOTP.metrics';
|
|
10
|
+
import { OTP_FIT_MIN_HEIGHT_PX, OTP_FIT_MIN_WIDTH_PX, OTP_FONT_PX } from './InputOTP.metrics';
|
|
11
11
|
|
|
12
12
|
interface ColorPalette {
|
|
13
13
|
main: string;
|
|
@@ -56,21 +56,47 @@ export interface OtpSlotFlags {
|
|
|
56
56
|
customSize?: string;
|
|
57
57
|
glass?: boolean;
|
|
58
58
|
gradient?: boolean;
|
|
59
|
+
/** `fit="width"`: share the row's width instead of drawing a square (FUT-4585). */
|
|
60
|
+
fitWidth?: boolean;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
interface SlotBox {
|
|
64
|
+
/** The slot's own footprint in the row. */
|
|
65
|
+
outer: CSSObject;
|
|
66
|
+
/** The outlined input inside it. */
|
|
67
|
+
inner: CSSObject;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A slot's box. By default a square of the field height, inside and out.
|
|
72
|
+
*
|
|
73
|
+
* Under `fit="width"` (FUT-4585), the native `otpFitBox` in CSS: the slot keeps
|
|
74
|
+
* the field's height (never under the 40px touch floor) and takes an equal
|
|
75
|
+
* share of the row, never wider than it is tall, never under 32px; the input
|
|
76
|
+
* fills it.
|
|
77
|
+
*/
|
|
78
|
+
const slotBox = (theme: Theme, side: string, fitWidth: boolean | undefined): SlotBox => {
|
|
79
|
+
if (!fitWidth) return { outer: { width: side, height: side }, inner: { width: side, height: side } };
|
|
80
|
+
const height = `max(${side}, ${rem(theme, OTP_FIT_MIN_HEIGHT_PX)})`;
|
|
81
|
+
return {
|
|
82
|
+
outer: { flex: '1 1 0', minWidth: rem(theme, OTP_FIT_MIN_WIDTH_PX), maxWidth: height, height },
|
|
83
|
+
inner: { width: '100%', height },
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
61
87
|
export const otpSlotSx = (theme: Theme, flags: OtpSlotFlags): CSSObject => {
|
|
62
|
-
const { customColor = 'primary', customSize = 'md', glass, gradient } = flags;
|
|
88
|
+
const { customColor = 'primary', customSize = 'md', glass, gradient, fitWidth } = flags;
|
|
63
89
|
const palette = getColorFromTheme(theme, customColor);
|
|
64
90
|
const fontPx = OTP_FONT_PX[customSize as SizeKey] ?? OTP_FONT_PX.md;
|
|
65
91
|
// A slot is a field: a square of the theme's field height for its size.
|
|
66
92
|
const side = fieldHeight(theme, asFieldSize(customSize));
|
|
67
93
|
|
|
94
|
+
const box = slotBox(theme, side, fitWidth);
|
|
95
|
+
|
|
68
96
|
return {
|
|
69
|
-
|
|
70
|
-
height: side,
|
|
97
|
+
...box.outer,
|
|
71
98
|
'& .MuiOutlinedInput-root': {
|
|
72
|
-
|
|
73
|
-
height: side,
|
|
99
|
+
...box.inner,
|
|
74
100
|
fontSize: rem(theme, fontPx),
|
|
75
101
|
fontWeight: 600,
|
|
76
102
|
textAlign: 'center',
|
|
@@ -3,16 +3,20 @@ import TextField from '@mui/material/TextField/index.js';
|
|
|
3
3
|
import { styled } from '@mui/material/styles/index.js';
|
|
4
4
|
import React, { forwardRef } from 'react';
|
|
5
5
|
|
|
6
|
+
import type { Theme } from '@mui/material/styles/index.js';
|
|
7
|
+
|
|
8
|
+
import { rem } from '../../../tokens/relative';
|
|
6
9
|
import { withDefaults } from '../../../utils/withDefaults';
|
|
7
10
|
|
|
8
11
|
import { useOtpDigits } from './InputOTP.hooks';
|
|
12
|
+
import { OTP_FIT_GAP_PX } from './InputOTP.metrics';
|
|
9
13
|
import { otpSlotSx } from './InputOTP.styles';
|
|
10
14
|
import type { OtpSlotFlags } from './InputOTP.styles';
|
|
11
15
|
import type { InputOTPProps } from './InputOTP.types';
|
|
12
16
|
|
|
13
17
|
const StyledOTPInput = styled(TextField, {
|
|
14
18
|
shouldForwardProp: (prop) =>
|
|
15
|
-
!['customColor', 'customSize', 'glass', 'gradient'].includes(prop as string),
|
|
19
|
+
!['customColor', 'customSize', 'glass', 'gradient', 'fitWidth'].includes(prop as string),
|
|
16
20
|
})<OtpSlotFlags>(({ theme, ...flags }) => ({ ...otpSlotSx(theme, flags) }));
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -50,6 +54,15 @@ function keypadProps(variant: InputOTPProps['variant'], index: number): Record<s
|
|
|
50
54
|
const slotMaxLength = (variant: InputOTPProps['variant'], index: number, length: number): number =>
|
|
51
55
|
variant === 'numeric' && index === 0 ? length + 1 : 1;
|
|
52
56
|
|
|
57
|
+
const OTP_ROW_SX = { display: 'flex', gap: 1, justifyContent: 'center', alignItems: 'center' } as const;
|
|
58
|
+
|
|
59
|
+
/** `fit="width"`: the row takes its parent's width and the slots share it (`otpSlotSx`). */
|
|
60
|
+
const OTP_FIT_ROW_SX = {
|
|
61
|
+
...OTP_ROW_SX,
|
|
62
|
+
width: '100%',
|
|
63
|
+
gap: (theme: Theme) => rem(theme, OTP_FIT_GAP_PX),
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
53
66
|
const DEFAULTS = {
|
|
54
67
|
variant: 'numeric',
|
|
55
68
|
color: 'primary',
|
|
@@ -68,7 +81,7 @@ type ResolvedProps = InputOTPProps & Required<Pick<InputOTPProps, keyof typeof D
|
|
|
68
81
|
export const InputOTP = forwardRef<HTMLDivElement, InputOTPProps>((props, ref) => {
|
|
69
82
|
const {
|
|
70
83
|
variant, color, size, length, value, onChange, onComplete,
|
|
71
|
-
glass, gradient, autoFocus, error, disabled, dataTestId,
|
|
84
|
+
glass, gradient, autoFocus, error, disabled, dataTestId, fit,
|
|
72
85
|
...rest
|
|
73
86
|
} = withDefaults(props, DEFAULTS) as ResolvedProps;
|
|
74
87
|
|
|
@@ -87,7 +100,7 @@ export const InputOTP = forwardRef<HTMLDivElement, InputOTPProps>((props, ref) =
|
|
|
87
100
|
// A group, so the `aria-label` a caller gives the code names the six
|
|
88
101
|
// slots together: on a plain `div` it named nothing anyone could reach.
|
|
89
102
|
role="group"
|
|
90
|
-
sx={
|
|
103
|
+
sx={fit === 'width' ? OTP_FIT_ROW_SX : OTP_ROW_SX}
|
|
91
104
|
{...rest}
|
|
92
105
|
>
|
|
93
106
|
{digits.map((digit, index) => (
|
|
@@ -100,6 +113,7 @@ export const InputOTP = forwardRef<HTMLDivElement, InputOTPProps>((props, ref) =
|
|
|
100
113
|
customSize={size}
|
|
101
114
|
glass={glass}
|
|
102
115
|
gradient={gradient}
|
|
116
|
+
fitWidth={fit === 'width'}
|
|
103
117
|
// `masked` keeps the digit in state and shows a bullet in its place.
|
|
104
118
|
value={variant === 'masked' ? (digit ? '•' : '') : digit}
|
|
105
119
|
onChange={(e) => handleInputChange(index, e.target.value)}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/form/InputOTP/InputOTP.tsx"],"sourcesContent":["import Box from '@mui/material/Box/index.js';\nimport TextField from '@mui/material/TextField/index.js';\nimport { styled } from '@mui/material/styles/index.js';\nimport React, { forwardRef } from 'react';\n\nimport { withDefaults } from '../../../utils/withDefaults';\n\nimport { useOtpDigits } from './InputOTP.hooks';\nimport { otpSlotSx } from './InputOTP.styles';\nimport type { OtpSlotFlags } from './InputOTP.styles';\nimport type { InputOTPProps } from './InputOTP.types';\n\nconst StyledOTPInput = styled(TextField, {\n shouldForwardProp: (prop) =>\n !['customColor', 'customSize', 'glass', 'gradient'].includes(prop as string),\n})<OtpSlotFlags>(({ theme, ...flags }) => ({ ...otpSlotSx(theme, flags) }));\n\n/**\n * What tells a phone that a `numeric` code is digits (FUT-4144).\n *\n * The hook already refused letters, but nothing reached the `<input>` to say\n * so, and the courier's six-digit join code opened the letter keyboard.\n * `inputMode` picks the keypad; `pattern` is what iOS reads for the same; and\n * `one-time-code` on the FIRST slot lets the phone offer a code from an SMS —\n * the first slot, because it is the one that takes a paste and spreads it.\n * `alphanumeric` and `masked` are left exactly as they were.\n */\nfunction keypadProps(variant: InputOTPProps['variant'], index: number): Record<string, string> {\n if (variant !== 'numeric') return {};\n return {\n inputMode: 'numeric',\n pattern: '[0-9]*',\n ...(index === 0 ? { autoComplete: 'one-time-code' } : {}),\n };\n}\n\n/**\n * One character per slot — except the numeric FIRST slot, which carries\n * `one-time-code`: the phone's SMS fill writes the whole code there in one\n * change, and a `maxLength` of 1 would cut it to one digit before the hook\n * could spread it.\n *\n * The code's length PLUS ONE: a phone that INSERTS the code at the caret keeps\n * the digit already there in front (`4` + `482913`), and the browser caps that\n * insertion at `maxlength` BEFORE the hook sees it. At the bare length it\n * arrived as `448291` — a wrong code that looks whole. One more character lets\n * the hook see the leading digit and drop it. A keystroke into the slot once\n * filled is still refused, by the hook, as `maxLength={1}` refused it.\n */\nconst slotMaxLength = (variant: InputOTPProps['variant'], index: number, length: number): number =>\n variant === 'numeric' && index === 0 ? length + 1 : 1;\n\nconst DEFAULTS = {\n variant: 'numeric',\n color: 'primary',\n size: 'md',\n length: 6,\n value: '',\n glass: false,\n gradient: false,\n autoFocus: false,\n error: false,\n disabled: false,\n} satisfies Partial<InputOTPProps>;\n\ntype ResolvedProps = InputOTPProps & Required<Pick<InputOTPProps, keyof typeof DEFAULTS>>;\n\nexport const InputOTP = forwardRef<HTMLDivElement, InputOTPProps>((props, ref) => {\n const {\n variant, color, size, length, value, onChange, onComplete,\n glass, gradient, autoFocus, error, disabled, dataTestId,\n ...rest\n } = withDefaults(props, DEFAULTS) as ResolvedProps;\n\n const { digits, inputRefs, handleInputChange, handleKeyDown, handlePaste } = useOtpDigits({\n variant,\n length,\n value,\n onChange,\n onComplete,\n });\n\n return (\n <Box\n ref={ref}\n data-testid={dataTestId}\n // A group, so the `aria-label` a caller gives the code names the six\n // slots together: on a plain `div` it named nothing anyone could reach.\n role=\"group\"\n sx={{ display: 'flex', gap: 1, justifyContent: 'center', alignItems: 'center' }}\n {...rest}\n >\n {digits.map((digit, index) => (\n <StyledOTPInput\n key={index}\n ref={(el: HTMLDivElement | null) => {\n inputRefs.current[index] = el?.querySelector('input') || null;\n }}\n customColor={color}\n customSize={size}\n glass={glass}\n gradient={gradient}\n // `masked` keeps the digit in state and shows a bullet in its place.\n value={variant === 'masked' ? (digit ? '•' : '') : digit}\n onChange={(e) => handleInputChange(index, e.target.value)}\n onKeyDown={(e) => handleKeyDown(index, e)}\n // Only the first slot takes a paste; it fills the rest itself.\n onPaste={index === 0 ? handlePaste : undefined}\n autoFocus={autoFocus && index === 0}\n error={error}\n disabled={disabled}\n inputProps={{\n maxLength: slotMaxLength(variant, index, length),\n style: { textAlign: 'center' },\n 'data-testid': dataTestId ? `${dataTestId}-slot-${index}` : undefined,\n ...keypadProps(variant, index),\n }}\n />\n ))}\n </Box>\n );\n});\n\nInputOTP.displayName = 'InputOTP';\n"],"mappings":";;;;;;;;;;;AAAA,OAAO,SAAS;AAChB,OAAO,eAAe;AACtB,SAAS,cAAc;AACvB,SAAgB,kBAAkB;AA0F1B;AAjFR,IAAM,iBAAiB,OAAO,WAAW;AAAA,EACvC,mBAAmB,CAAC,SAClB,CAAC,CAAC,eAAe,cAAc,SAAS,UAAU,EAAE,SAAS,IAAc;AAC/E,CAAC,EAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,OAAO,EAAE,GAAG,UAAU,OAAO,KAAK,EAAE,EAAE;AAY1E,SAAS,YAAY,SAAmC,OAAuC;AAC7F,MAAI,YAAY,UAAW,QAAO,CAAC;AACnC,SAAO;AAAA,IACL,WAAW;AAAA,IACX,SAAS;AAAA,IACT,GAAI,UAAU,IAAI,EAAE,cAAc,gBAAgB,IAAI,CAAC;AAAA,EACzD;AACF;AAeA,IAAM,gBAAgB,CAAC,SAAmC,OAAe,WACvE,YAAY,aAAa,UAAU,IAAI,SAAS,IAAI;AAEtD,IAAM,WAAW;AAAA,EACf,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,WAAW;AAAA,EACX,OAAO;AAAA,EACP,UAAU;AACZ;AAIO,IAAM,WAAW,WAA0C,CAAC,OAAO,QAAQ;AAChF,QAAM;AAAA,IACJ;AAAA,IAAS;AAAA,IAAO;AAAA,IAAM;AAAA,IAAQ;AAAA,IAAO;AAAA,IAAU;AAAA,IAC/C;AAAA,IAAO;AAAA,IAAU;AAAA,IAAW;AAAA,IAAO;AAAA,IAAU;AAAA,IAC7C,GAAG;AAAA,EACL,IAAI,aAAa,OAAO,QAAQ;AAEhC,QAAM,EAAE,QAAQ,WAAW,mBAAmB,eAAe,YAAY,IAAI,aAAa;AAAA,IACxF;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,eAAa;AAAA,MAGb,MAAK;AAAA,MACL,IAAI,EAAE,SAAS,QAAQ,KAAK,GAAG,gBAAgB,UAAU,YAAY,SAAS;AAAA,MAC7E,GAAG;AAAA,MAEH,iBAAO,IAAI,CAAC,OAAO,UAClB;AAAA,QAAC;AAAA;AAAA,UAEC,KAAK,CAAC,OAA8B;AAClC,sBAAU,QAAQ,KAAK,IAAI,IAAI,cAAc,OAAO,KAAK;AAAA,UAC3D;AAAA,UACA,aAAa;AAAA,UACb,YAAY;AAAA,UACZ;AAAA,UACA;AAAA,UAEA,OAAO,YAAY,WAAY,QAAQ,WAAM,KAAM;AAAA,UACnD,UAAU,CAAC,MAAM,kBAAkB,OAAO,EAAE,OAAO,KAAK;AAAA,UACxD,WAAW,CAAC,MAAM,cAAc,OAAO,CAAC;AAAA,UAExC,SAAS,UAAU,IAAI,cAAc;AAAA,UACrC,WAAW,aAAa,UAAU;AAAA,UAClC;AAAA,UACA;AAAA,UACA,YAAY;AAAA,YACV,WAAW,cAAc,SAAS,OAAO,MAAM;AAAA,YAC/C,OAAO,EAAE,WAAW,SAAS;AAAA,YAC7B,eAAe,aAAa,GAAG,UAAU,SAAS,KAAK,KAAK;AAAA,YAC5D,GAAG,YAAY,SAAS,KAAK;AAAA,UAC/B;AAAA;AAAA,QAtBK;AAAA,MAuBP,CACD;AAAA;AAAA,EACH;AAEJ,CAAC;AAED,SAAS,cAAc;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/form/InputOTP/InputOTP.styles.ts"],"sourcesContent":["import { alpha } from '@mui/material/styles/index.js';\nimport type { CSSObject, Theme } from '@mui/material/styles/index.js';\n\nimport { fieldEdge } from '../../../tokens/field-edge';\nimport { fieldRadius } from '../../../tokens/field-radius';\nimport { asFieldSize, fieldHeight } from '../../../tokens/field-height';\nimport { absoluteInk, controlNeutral } from '../../../tokens/ink';\nimport { rem } from '../../../tokens/relative';\n\nimport { OTP_FONT_PX } from './InputOTP.metrics';\n\ninterface ColorPalette {\n main: string;\n dark?: string;\n light?: string;\n contrastText?: string;\n}\n\n/** `neutral` is not a MUI palette entry, so it is built from the grey ramp. */\nconst neutralPalette = (theme: Theme): ColorPalette => controlNeutral(theme);\n\n/** `danger` is this component's name for the error palette. */\nconst namedPalette = (theme: Theme, color: string): ColorPalette => {\n const colorMap: Record<string, ColorPalette> = {\n primary: theme.palette.primary,\n secondary: theme.palette.secondary,\n success: theme.palette.success,\n warning: theme.palette.warning,\n info: theme.palette.info,\n danger: theme.palette.error,\n };\n\n return colorMap[color] || theme.palette.primary;\n};\n\nconst getColorFromTheme = (theme: Theme, color: string): ColorPalette => {\n if (color === 'neutral') {\n return neutralPalette(theme);\n }\n\n const palette = namedPalette(theme, color);\n const { primary } = theme.palette;\n\n return {\n main: palette.main || primary.main,\n dark: palette.dark || palette.main || primary.dark,\n light: palette.light || palette.main || primary.light,\n contrastText: palette.contrastText || absoluteInk(theme).white,\n };\n};\n\ntype SizeKey = keyof typeof OTP_FONT_PX;\n\nexport interface OtpSlotFlags {\n customColor?: string;\n customSize?: string;\n glass?: boolean;\n gradient?: boolean;\n}\n\nexport const otpSlotSx = (theme: Theme, flags: OtpSlotFlags): CSSObject => {\n const { customColor = 'primary', customSize = 'md', glass, gradient } = flags;\n const palette = getColorFromTheme(theme, customColor);\n const fontPx = OTP_FONT_PX[customSize as SizeKey] ?? OTP_FONT_PX.md;\n // A slot is a field: a square of the theme's field height for its size.\n const side = fieldHeight(theme, asFieldSize(customSize));\n\n return {\n width: side,\n height: side,\n '& .MuiOutlinedInput-root': {\n width: side,\n height: side,\n fontSize: rem(theme, fontPx),\n fontWeight: 600,\n textAlign: 'center',\n borderRadius: fieldRadius(theme),\n ...(glass && {\n backgroundColor: alpha(theme.palette.background.paper, 0.1),\n backdropFilter: `blur(${rem(theme, 20)})`,\n '& fieldset': { border: `1px solid ${fieldEdge(theme)}` },\n }),\n ...(gradient && {\n '&.Mui-focused fieldset': {\n background: `linear-gradient(135deg, ${palette.main}, ${palette.light})`,\n borderWidth: rem(theme, 2),\n },\n }),\n '& input': { textAlign: 'center', padding: 0, fontWeight: 'inherit' },\n ...(!glass && { '& fieldset': { borderColor: fieldEdge(theme) } }),\n '&:hover fieldset': { borderColor: palette.main },\n '&.Mui-focused fieldset': { borderColor: palette.main, borderWidth: rem(theme, 2) },\n },\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,aAAa;AAmBtB,IAAM,iBAAiB,CAAC,UAA+B,eAAe,KAAK;AAG3E,IAAM,eAAe,CAAC,OAAc,UAAgC;AAClE,QAAM,WAAyC;AAAA,IAC7C,SAAS,MAAM,QAAQ;AAAA,IACvB,WAAW,MAAM,QAAQ;AAAA,IACzB,SAAS,MAAM,QAAQ;AAAA,IACvB,SAAS,MAAM,QAAQ;AAAA,IACvB,MAAM,MAAM,QAAQ;AAAA,IACpB,QAAQ,MAAM,QAAQ;AAAA,EACxB;AAEA,SAAO,SAAS,KAAK,KAAK,MAAM,QAAQ;AAC1C;AAEA,IAAM,oBAAoB,CAAC,OAAc,UAAgC;AACvE,MAAI,UAAU,WAAW;AACvB,WAAO,eAAe,KAAK;AAAA,EAC7B;AAEA,QAAM,UAAU,aAAa,OAAO,KAAK;AACzC,QAAM,EAAE,QAAQ,IAAI,MAAM;AAE1B,SAAO;AAAA,IACL,MAAM,QAAQ,QAAQ,QAAQ;AAAA,IAC9B,MAAM,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ;AAAA,IAC9C,OAAO,QAAQ,SAAS,QAAQ,QAAQ,QAAQ;AAAA,IAChD,cAAc,QAAQ,gBAAgB,YAAY,KAAK,EAAE;AAAA,EAC3D;AACF;AAWO,IAAM,YAAY,CAAC,OAAc,UAAmC;AACzE,QAAM,EAAE,cAAc,WAAW,aAAa,MAAM,OAAO,SAAS,IAAI;AACxE,QAAM,UAAU,kBAAkB,OAAO,WAAW;AACpD,QAAM,SAAS,YAAY,UAAqB,KAAK,YAAY;AAEjE,QAAM,OAAO,YAAY,OAAO,YAAY,UAAU,CAAC;AAEvD,SAAO;AAAA,IACL,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,4BAA4B;AAAA,MAC1B,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,UAAU,IAAI,OAAO,MAAM;AAAA,MAC3B,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,cAAc,YAAY,KAAK;AAAA,MAC/B,GAAI,SAAS;AAAA,QACX,iBAAiB,MAAM,MAAM,QAAQ,WAAW,OAAO,GAAG;AAAA,QAC1D,gBAAgB,QAAQ,IAAI,OAAO,EAAE,CAAC;AAAA,QACtC,cAAc,EAAE,QAAQ,aAAa,UAAU,KAAK,CAAC,GAAG;AAAA,MAC1D;AAAA,MACA,GAAI,YAAY;AAAA,QACd,0BAA0B;AAAA,UACxB,YAAY,2BAA2B,QAAQ,IAAI,KAAK,QAAQ,KAAK;AAAA,UACrE,aAAa,IAAI,OAAO,CAAC;AAAA,QAC3B;AAAA,MACF;AAAA,MACA,WAAW,EAAE,WAAW,UAAU,SAAS,GAAG,YAAY,UAAU;AAAA,MACpE,GAAI,CAAC,SAAS,EAAE,cAAc,EAAE,aAAa,UAAU,KAAK,EAAE,EAAE;AAAA,MAChE,oBAAoB,EAAE,aAAa,QAAQ,KAAK;AAAA,MAChD,0BAA0B,EAAE,aAAa,QAAQ,MAAM,aAAa,IAAI,OAAO,CAAC,EAAE;AAAA,IACpF;AAAA,EACF;AACF;","names":[]}
|
package/dist/chunk-VAHJCRNW.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// src/components/form/InputOTP/InputOTP.metrics.ts
|
|
2
|
-
var OTP_FONT_PX = {
|
|
3
|
-
xs: 12,
|
|
4
|
-
sm: 14,
|
|
5
|
-
md: 16,
|
|
6
|
-
lg: 18,
|
|
7
|
-
xl: 20
|
|
8
|
-
};
|
|
9
|
-
var OTP_GAP_PX = 8;
|
|
10
|
-
var OTP_BORDER = { rest: 1, focused: 2 };
|
|
11
|
-
|
|
12
|
-
export {
|
|
13
|
-
OTP_FONT_PX,
|
|
14
|
-
OTP_GAP_PX,
|
|
15
|
-
OTP_BORDER
|
|
16
|
-
};
|
|
17
|
-
//# sourceMappingURL=chunk-VAHJCRNW.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/form/InputOTP/InputOTP.metrics.ts"],"sourcesContent":["/**\n * The numbers both renderers read (see `NATIVE.md`, \"Numbers\").\n *\n * Each box's type size, in design px, per size step; its side is the theme's\n * field height for that step (a square), its weight 600.\n */\nexport const OTP_FONT_PX: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl', number> = {\n xs: 12,\n sm: 14,\n md: 16,\n lg: 18,\n xl: 20,\n};\n\n/** The gap between two boxes: MUI's `gap: 1`, one 8px spacing unit. */\nexport const OTP_GAP_PX = 8;\n\n/** The border: 1px at rest, 2px focused (MUI's focused outline). */\nexport const OTP_BORDER = { rest: 1, focused: 2 } as const;\n"],"mappings":";AAMO,IAAM,cAAgE;AAAA,EAC3E,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGO,IAAM,aAAa;AAGnB,IAAM,aAAa,EAAE,MAAM,GAAG,SAAS,EAAE;","names":[]}
|