{"version":3,"file":"otp-input.cjs","names":[],"sources":["../src/inputs/otp-input/otp-input.ts"],"sourcesContent":["import { bind, define, getHost, html, onCleanup, onMounted, prop, useEmit, useField } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\nimport { createListControl, lifecycleSignal } from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { colorThemeMixin, fieldVariantMixin, forcedColorsFocusMixin, sizeVariantMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor, VisualVariant } from '../../types';\nimport { defineFieldValue, dispatchNativeFieldEvent, setFieldValue } from '../shared/native-field-event';\nimport componentStyles from './otp-input.css?inline';\n\nexport type OreOtpInputEvents = {\n  change: Event;\n  complete: { originalEvent?: Event; value: string };\n  input: Event;\n};\n\n/** OTP Input props */\nexport type OreOtpInputProps = {\n  /** Theme color */\n  color?: ThemeColor;\n  /** Make inputs disabled */\n  disabled?: boolean;\n  /** Accessible label */\n  label?: string;\n  /** Number of input cells */\n  length?: number;\n  /** Mask input (show dots instead of characters) */\n  masked?: boolean;\n  /** Form field name */\n  name?: string;\n  /** Show a separator in the middle (e.g. \"–\") */\n  separator?: string;\n  /** Component size */\n  size?: ComponentSize;\n  /** Input type: 'numeric' (digits only) or 'alphanumeric' */\n  type?: 'numeric' | 'alphanumeric';\n  /** Current value */\n  value?: string;\n  /** Visual variant */\n  variant?: Exclude<VisualVariant, 'text' | 'frost'>;\n};\n\n/**\n * A segmented OTP (One-Time Password) input with N individual cells.\n * Auto-advances focus on input, auto-backs on Backspace, handles paste.\n *\n * @element ore-otp-input\n *\n * @attr {number} length - Number of cells (default: 6)\n * @attr {string} type - 'numeric' (default) | 'alphanumeric'\n * @attr {string} value - Current code value\n * @attr {boolean} disabled - Disable all cells\n * @attr {boolean} masked - Show as password\n * @attr {string} size - 'sm' | 'md' | 'lg'\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} name - Form field name\n * @attr {string} label - Group aria-label\n * @attr {string} separator - Optional separator character shown in the middle\n *\n * @fires input - Emitted whenever a cell changes.\n * @fires change - Emitted whenever a cell changes.\n * @fires complete - Emitted when all cells are filled. detail: { value: string, originalEvent?: Event }\n *\n * @slot label - Custom label above the OTP input\n * @slot helper - Custom helper text below the OTP input\n * @slot error - Custom error content below the OTP input\n *\n * @cssprop --otp-cell-size - Width and height of each cell\n * @cssprop --otp-cell-gap - Gap between cells\n * @cssprop --otp-cell-font-size - Font size inside cells\n * @cssprop --otp-cell-radius - Border radius of cells\n * @cssprop --otp-cell-bg - Cell background color\n * @cssprop --otp-cell-border-color - Default border color\n * @cssprop --otp-cell-focus-border - Focused border/caret color\n * @cssprop --otp-cell-hover-bg - Cell background on hover (flat/ghost variants)\n * @cssprop --otp-cell-hover-border-color - Cell border on hover (flat/bordered variants)\n * @cssprop --otp-cell-focus-bg - Cell background when focused (flat variant)\n * @cssprop --otp-cell-focus-border-color - Cell border when focused (flat variant)\n *\n * @part group - Group container.\n * @part cell - Shadow part for the `cell` element.\n * @example\n * ```html\n * <ore-otp-input length=\"6\" color=\"primary\"></ore-otp-input>\n * ```\n */\nexport const OTP_INPUT_TAG = 'ore-otp-input' as const;\ndefine<OreOtpInputProps>(OTP_INPUT_TAG, {\n  formAssociated: true,\n  props: {\n    ...themableBundle,\n    ...sizableBundle,\n    ...disablableBundle,\n    label: prop.string('One-time password'),\n    length: prop.number(6),\n    masked: prop.bool(false),\n    name: prop.string(),\n    separator: prop.string(),\n    type: prop.oneOf(['numeric', 'alphanumeric'] as const, 'numeric'),\n    value: prop.string(),\n    variant: prop.string<Exclude<VisualVariant, 'text' | 'frost'>>(),\n  },\n  setup(props) {\n    const el = getHost();\n    const emit = useEmit<OreOtpInputEvents>();\n\n    const lengthValue = computed(() => Number(props.length.value) || 6);\n    const isDisabled = computed(() => Boolean(props.disabled.value));\n    const cells = computed(() => Array.from({ length: lengthValue.value }, (_, i) => i));\n    const otpValue = signal(String(props.value.value || ''));\n    const normalizedPropValue = () => String(props.value.value || '');\n\n    // Form association: the joined cell value is the single form value for `name`,\n    // since individual per-cell <input> elements inside the shadow root cannot\n    // participate in an ancestor <form>'s submission on their own.\n    useField<string>({ disabled: isDisabled, toFormValue: (v) => v, value: otpValue });\n\n    bind({\n      attr: {\n        size: props.size,\n        value: () => otpValue.value || null,\n        variant: props.variant,\n      },\n    });\n\n    function getInputs(): HTMLInputElement[] {\n      return [...(el.shadowRoot?.querySelectorAll<HTMLInputElement>('input.cell') ?? [])];\n    }\n\n    const listControl = createListControl({\n      getItems: () => getInputs(),\n      keys: { next: ['ArrowRight'], prev: ['ArrowLeft'] },\n      loop: false,\n      onNavigate: ({ item }) => {\n        item.focus();\n      },\n      signal: lifecycleSignal(onCleanup),\n    });\n\n    function getValue(): string {\n      return getInputs()\n        .map((i) => i.value)\n        .join('');\n    }\n    function syncInputsFromValue(raw: string) {\n      const allInputs = getInputs();\n      const normalized = String(raw || '').slice(0, allInputs.length);\n\n      for (const input of allInputs) input.value = '';\n\n      normalized.split('').forEach((c, i) => {\n        if (allInputs[i]) allInputs[i].value = c;\n      });\n\n      otpValue.value = getValue();\n    }\n    defineFieldValue(\n      el,\n      () => otpValue.value,\n      (value) => {\n        syncInputsFromValue(value);\n      },\n    );\n    function emitOtpState(originalEvent?: Event, forceComplete?: boolean): void {\n      const allInputs = getInputs();\n      const full = getValue();\n      const complete = forceComplete ?? (full.length === allInputs.length && full.split('').every(Boolean));\n\n      otpValue.value = full;\n      setFieldValue(el, full);\n      dispatchNativeFieldEvent(el, 'input');\n      dispatchNativeFieldEvent(el, 'change');\n\n      if (complete) emit('complete', { originalEvent, value: full });\n    }\n    function isAllowed(char: string): boolean {\n      if (props.type.value === 'numeric') return /^\\d$/.test(char);\n\n      return /^[a-z\\d]$/i.test(char);\n    }\n    function handleInput(e: Event, index: number) {\n      const input = e.target as HTMLInputElement;\n      let val = input.value;\n\n      // Keep only last character if multiple were typed somehow\n      if (val.length > 1) val = val.slice(-1);\n\n      // Validate character\n      if (val && !isAllowed(val)) {\n        input.value = '';\n\n        return;\n      }\n\n      input.value = val;\n\n      const allInputs = getInputs();\n\n      emitOtpState(e);\n\n      // Auto-advance\n      if (val && index < allInputs.length - 1) {\n        allInputs[index + 1].focus();\n        allInputs[index + 1].select();\n      }\n    }\n    function handleKeydown(e: KeyboardEvent, index: number) {\n      const input = e.target as HTMLInputElement;\n      const allInputs = getInputs();\n\n      listControl.set(index);\n\n      if (e.key === 'Backspace') {\n        if (input.value) {\n          input.value = '';\n        } else if (index > 0) {\n          const prevIndex = listControl.navigate('prev');\n          const prevInput = allInputs[prevIndex];\n\n          if (prevInput) {\n            prevInput.select();\n            prevInput.value = '';\n          }\n        }\n\n        emitOtpState(e, false);\n        e.preventDefault();\n      } else {\n        listControl.handleKeydown(e);\n      }\n    }\n    function handlePaste(e: ClipboardEvent) {\n      e.preventDefault();\n\n      const pasted = e.clipboardData?.getData('text') ?? '';\n      const chars = pasted\n        .split('')\n        .filter((c) => isAllowed(c))\n        .slice(0, lengthValue.value);\n      const allInputs = getInputs();\n\n      chars.forEach((char, i) => {\n        if (allInputs[i]) allInputs[i].value = char;\n      });\n\n      emitOtpState(e);\n\n      // Focus the cell after last pasted char\n      const focusIdx = Math.min(chars.length, allInputs.length - 1);\n\n      allInputs[focusIdx]?.focus();\n    }\n\n    watch(props.value, (value) => {\n      syncInputsFromValue(String(value || ''));\n    });\n\n    watch(props.length, () => {\n      // Cells re-render when length changes; sync after DOM updates.\n      requestAnimationFrame(() => syncInputsFromValue(normalizedPropValue()));\n    });\n\n    const separatorIdx = computed(() => {\n      const len = Number(props.length.value) || 6;\n\n      return props.separator.value != null ? Math.floor(len / 2) : -1;\n    });\n\n    onMounted(() => {\n      // Populate cells from value prop on mount\n      syncInputsFromValue(normalizedPropValue());\n    });\n\n    return html`\n      <div class=\"otp-group\" part=\"group\" role=\"group\" aria-label=\"${props.label}\">\n        ${() =>\n          cells.value.map(\n            (i) => html`\n              ${() =>\n                separatorIdx.value > 0 && i === separatorIdx.value\n                  ? html`\n                      <span class=\"separator\" aria-hidden=\"true\">${() => props.separator.value || '-'}</span>\n                    `\n                  : ''}\n              <input\n                class=\"cell\"\n                part=\"cell\"\n                type=\"${() => (props.masked.value ? 'password' : 'text')}\"\n                inputmode=\"${() => (props.type.value === 'numeric' ? 'numeric' : 'text')}\"\n                maxlength=\"1\"\n                autocomplete=\"${() => (i === 0 ? 'one-time-code' : 'off')}\"\n                aria-label=\"${() => `Digit ${i + 1} of ${lengthValue.value}`}\"\n                disabled=\"${() => (isDisabled.value ? true : null)}\"\n                @input=\"${(e: Event) => handleInput(e, i)}\"\n                @keydown=\"${(e: KeyboardEvent) => handleKeydown(e, i)}\"\n                @paste=\"${(e: ClipboardEvent) => (i === 0 ? handlePaste(e) : e.preventDefault())}\"\n                @focus=\"${(e: FocusEvent) => (e.target as HTMLInputElement).select()}\" />\n            `,\n          )}\n      </div>\n    `;\n  },\n  styles: [\n    colorThemeMixin,\n    sizeVariantMixin({}),\n    forcedColorsFocusMixin('.cell'),\n    componentStyles,\n    // Must come after `componentStyles` — see `ore-input`'s identical ordering note for why.\n    fieldVariantMixin({ accentVar: '--_cell-focus-border', container: '.cell', tokenPrefix: 'otp-cell' }),\n  ],\n});\n"],"mappings":"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"}