@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.
Files changed (39) hide show
  1. package/NATIVE.md +1 -1
  2. package/components-catalog.md +1 -1
  3. package/dist/_internal/components/form/InputOTP/InputOTP.js +3 -3
  4. package/dist/_internal/components/form/InputOTP/InputOTP.metrics.js +11 -3
  5. package/dist/_internal/components/form/InputOTP/InputOTP.styles.js +2 -2
  6. package/dist/_internal/components/form/index.js +3 -3
  7. package/dist/_internal/components/index.js +3 -3
  8. package/dist/{chunk-645GGNTW.js → chunk-J3WJEUD2.js} +18 -4
  9. package/dist/chunk-J3WJEUD2.js.map +1 -0
  10. package/dist/{chunk-BAYCMSMI.js → chunk-VJ6ZLMQ3.js} +16 -7
  11. package/dist/chunk-VJ6ZLMQ3.js.map +1 -0
  12. package/dist/chunk-XHPL5VI2.js +31 -0
  13. package/dist/chunk-XHPL5VI2.js.map +1 -0
  14. package/dist/form/InputOTP.js +3 -3
  15. package/dist/native/form/InputOTP.js +75 -26
  16. package/dist/native/form/InputOTP.js.map +1 -1
  17. package/dist/types/components/form/InputOTP/InputOTP.base.d.ts +14 -0
  18. package/dist/types/components/form/InputOTP/InputOTP.base.d.ts.map +1 -1
  19. package/dist/types/components/form/InputOTP/InputOTP.d.ts.map +1 -1
  20. package/dist/types/components/form/InputOTP/InputOTP.metrics.d.ts +25 -0
  21. package/dist/types/components/form/InputOTP/InputOTP.metrics.d.ts.map +1 -1
  22. package/dist/types/components/form/InputOTP/InputOTP.styles.d.ts +2 -0
  23. package/dist/types/components/form/InputOTP/InputOTP.styles.d.ts.map +1 -1
  24. package/dist/types-native/components/form/InputOTP/InputOTP.base.d.ts +14 -0
  25. package/dist/types-native/components/form/InputOTP/InputOTP.base.d.ts.map +1 -1
  26. package/dist/types-native/components/form/InputOTP/InputOTP.metrics.d.ts +25 -0
  27. package/dist/types-native/components/form/InputOTP/InputOTP.metrics.d.ts.map +1 -1
  28. package/dist/types-native/components/form/InputOTP/InputOTP.native.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/src/components/form/InputOTP/InputOTP.base.ts +15 -0
  31. package/src/components/form/InputOTP/InputOTP.md +35 -0
  32. package/src/components/form/InputOTP/InputOTP.metrics.ts +32 -0
  33. package/src/components/form/InputOTP/InputOTP.native.tsx +70 -7
  34. package/src/components/form/InputOTP/InputOTP.styles.ts +32 -6
  35. package/src/components/form/InputOTP/InputOTP.tsx +17 -3
  36. package/dist/chunk-645GGNTW.js.map +0 -1
  37. package/dist/chunk-BAYCMSMI.js.map +0 -1
  38. package/dist/chunk-VAHJCRNW.js +0 -17
  39. 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` | 51 | 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. |
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 | — |
@@ -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-645GGNTW.js";
4
- import "../../../../chunk-BAYCMSMI.js";
5
- import "../../../../chunk-VAHJCRNW.js";
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
- } from "../../../../chunk-VAHJCRNW.js";
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-BAYCMSMI.js";
4
- import "../../../../chunk-VAHJCRNW.js";
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-645GGNTW.js";
42
- import "../../../chunk-BAYCMSMI.js";
43
- import "../../../chunk-VAHJCRNW.js";
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-645GGNTW.js";
268
- import "../../chunk-BAYCMSMI.js";
269
- import "../../chunk-VAHJCRNW.js";
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-BAYCMSMI.js";
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: { display: "flex", gap: 1, justifyContent: "center", alignItems: "center" },
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-645GGNTW.js.map
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-VAHJCRNW.js";
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
- width: side,
56
- height: side,
66
+ ...box.outer,
57
67
  "& .MuiOutlinedInput-root": {
58
- width: side,
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-BAYCMSMI.js.map
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":[]}
@@ -1,9 +1,9 @@
1
1
  import "../chunk-3VMXOGGI.js";
2
2
  import {
3
3
  InputOTP
4
- } from "../chunk-645GGNTW.js";
5
- import "../chunk-BAYCMSMI.js";
6
- import "../chunk-VAHJCRNW.js";
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 { Platform, StyleSheet, TextInput, View } from "react-native";
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 boxStyle(theme, size, accent, state) {
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(View, { ref, testID: dataTestId, role: "group", "aria-label": ariaLabel, style: [styles.row, style], children: digits.map((digit, index) => /* @__PURE__ */ jsx(
157
- TextInput,
186
+ return /* @__PURE__ */ jsx(
187
+ View,
158
188
  {
159
- ref: (element) => {
160
- boxRefs.current[index] = element;
161
- },
162
- testID: dataTestId ? `${dataTestId}-slot-${index}` : void 0,
163
- value: variant === "masked" ? digit ? "\u2022" : "" : digit,
164
- onChangeText: (text) => handleChangeText(index, text),
165
- onKeyPress: (event) => handleKey(index, event.nativeEvent.key),
166
- onFocus: () => setFocusedIndex(index),
167
- onBlur: () => setFocusedIndex((current) => current === index ? null : current),
168
- autoFocus: autoFocus && index === 0,
169
- editable: !disabled,
170
- "aria-disabled": disabled,
171
- ...webAria({ "aria-invalid": error }),
172
- maxLength: boxMaxLength(variant, index, length),
173
- style: boxStyle(theme, size, accent, { focused: focusedIndex === index && !disabled, error, disabled }),
174
- ...webDisabled(disabled),
175
- ...keypadProps(variant, index)
176
- },
177
- index
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;AAgE1C,eAAO,MAAM,QAAQ,oHAsDnB,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;AAED,eAAO,MAAM,SAAS,GAAI,OAAO,KAAK,EAAE,OAAO,YAAY,KAAG,SAkC7D,CAAC"}
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,EAAmC,IAAI,EAAuC,MAAM,cAAc,CAAC;AAI1G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AA0E7D;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,4EAsDnB,CAAC"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/ui",
3
- "version": "6.101.0",
3
+ "version": "6.102.0",
4
4
  "license": "MIT",
5
5
  "packageManager": "pnpm@10.34.5",
6
6
  "type": "module",
@@ -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 { Platform, StyleSheet, TextInput, View, type TextInputProps, type TextStyle } from 'react-native';
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 { OTP_BORDER, OTP_FONT_PX, OTP_GAP_PX } from './InputOTP.metrics';
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 ref={ref} testID={dataTestId} role="group" aria-label={ariaLabel} style={[styles.row, style]}>
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
- width: side,
70
- height: side,
97
+ ...box.outer,
71
98
  '& .MuiOutlinedInput-root': {
72
- width: side,
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={{ display: 'flex', gap: 1, justifyContent: 'center', alignItems: 'center' }}
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":[]}
@@ -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":[]}