{"version":3,"file":"number-input.cjs","names":[],"sources":["../src/inputs/number-input/number-input.ts"],"sourcesContent":["import { clamp } from '@vielzeug/arsenal/math';\nimport { bind, define, getHost, html, onElement, prop, ref, watchEffect } from '@vielzeug/ore';\nimport { computed, watch as rippleWatch, signal } from '@vielzeug/ripple';\nimport { createSpinnerControl } from '../../core';\nimport type { ComponentSize, ThemeColor, VisualVariant } from '../../types';\nimport '../../content/icon/icon';\nimport '../input/input';\nimport { disablableBundle, roundableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { disabledLoadingMixin } from '../../styles';\nimport { defineFieldValue, dispatchNativeFieldEvent, setFieldValue } from '../shared/native-field-event';\nimport componentStyles from './number-input.css?inline';\n\nexport type OreNumberInputEvents = {\n  change: Event;\n  input: InputEvent;\n};\n\n/** Number Input props */\nexport type OreNumberInputProps = {\n  /** Theme color */\n  color?: ThemeColor;\n  /** Disable interaction */\n  disabled?: boolean;\n  /** Error message */\n  error?: string;\n  /** Stretch to full width of container */\n  fullwidth?: boolean;\n  /** Helper text */\n  helper?: string;\n  /** Visible label */\n  label?: string;\n  /** Label placement: 'inset' renders the label inside the control box, 'outside' renders it above */\n  'label-placement'?: 'inset' | 'outside';\n  /** Large step (for Page Up/Down, default: 10 × step) */\n  'large-step'?: number;\n  /**\n   * Shows an inline spinner inside the field and forces the control into `disabled` for the\n   * duration — use while an async validation/submission request is in flight to prevent\n   * double-submits.\n   */\n  loading?: boolean;\n  /** Maximum allowed value */\n  max?: number;\n  /** Minimum allowed value */\n  min?: number;\n  /** Form field name */\n  name?: string;\n  /** Placeholder text */\n  placeholder?: string;\n  /** Make the input read-only */\n  readonly?: boolean;\n  /**\n   * JS-only callback fired with the inner `<input>` element when it mounts,\n   * and with `null` when it unmounts.\n   * Set as a JS property: `bitNumberInput.ref = (el) => { ... }`.\n   */\n  ref?: ((el: HTMLInputElement | null) => void) | null;\n  /** Border radius */\n  rounded?: string;\n  /** Component size */\n  size?: ComponentSize;\n  /** Step size for increment/decrement */\n  step?: number;\n  /**\n   * Shows an inline green check icon inside the field to confirm the value has\n   * passed validation. Ignored while `error` is set — an error always wins.\n   */\n  success?: boolean;\n  /** Current numeric value */\n  value?: number;\n  /** Visual variant */\n  variant?: VisualVariant;\n};\n\n/**\n * A numeric spin-button input with +/− controls, min/max clamping, and full keyboard support.\n *\n * @element ore-number-input\n *\n * @attr {number} value - Current value\n * @attr {number} min - Minimum value\n * @attr {number} max - Maximum value\n * @attr {number} step - Increment/decrement step (default: 1)\n * @attr {number} large-step - Step for Page Up/Down (default: 10)\n * @attr {boolean} disabled - Disables the control\n * @attr {boolean} readonly - Read-only mode\n * @attr {boolean} loading - Show an inline spinner and force the control disabled\n * @attr {boolean} success - Show an inline success check icon (suppressed while `error` is set)\n * @attr {string} label - Visible label\n * @attr {string} name - Form field name\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} size - 'sm' | 'md' | 'lg'\n * @attr {string} placeholder - Input placeholder\n *\n * @fires input - On every keystroke.\n * @fires change - On committed value change.\n *\n * @slot prefix - Content before the input (e.g. icon)\n * @slot suffix - Content after the input (e.g. unit label)\n * @slot label - Custom label content\n * @slot helper - Custom helper text content\n * @slot error - Custom error content\n *\n * @cssprop --number-input-height - Control height\n * @cssprop --number-input-border-color - Border color\n * @cssprop --number-input-radius - Border radius\n * @cssprop --number-input-bg - Background\n * @cssprop --number-input-btn-bg - Spin button background\n *\n * @part control - Control container.\n * @part decrement-btn - Decrement stepper button.\n * @part input - Input element.\n * @part increment-btn - Increment stepper button.\n * @example\n * ```html\n * <ore-number-input label=\"Quantity\" value=\"1\" min=\"1\" max=\"99\" step=\"1\"></ore-number-input>\n * <ore-number-input label=\"Quantity\" success></ore-number-input>\n * <ore-number-input label=\"Quantity\" loading></ore-number-input>\n * ```\n */\nexport const NUMBER_INPUT_TAG = 'ore-number-input' as const;\ndefine<OreNumberInputProps>(NUMBER_INPUT_TAG, {\n  formAssociated: true,\n  props: {\n    ...themableBundle,\n    ...sizableBundle,\n    ...disablableBundle,\n    ...roundableBundle,\n    error: prop.string(),\n    fullwidth: prop.bool(false),\n    helper: prop.string(),\n    label: prop.string(),\n    'label-placement': prop.oneOf(['inset', 'outside'] as const, 'inset'),\n    'large-step': prop.json(undefined as number | undefined),\n    loading: prop.bool(false),\n    max: prop.json(undefined as number | undefined),\n    min: prop.json(undefined as number | undefined),\n    name: prop.string(),\n    placeholder: prop.string(),\n    readonly: prop.bool(false),\n    ref: prop.data<((el: HTMLInputElement | null) => void) | null>(),\n    step: prop.number(1),\n    success: prop.bool(false),\n    value: prop.json(undefined as number | undefined),\n    variant: prop.string<VisualVariant>(),\n  },\n  setup(props) {\n    const el = getHost();\n    const watch = watchEffect;\n\n    // `loading` behaves like a temporary `disabled` — see ore-input's identical computation.\n    const isDisabled = computed(() => props.disabled.value || props.loading.value);\n    const isReadonly = computed(() => Boolean(props.readonly.value));\n\n    // Internal numeric value signal (string representation for the input)\n    const fieldValue = signal(props.value.value != null ? String(props.value.value) : '');\n\n    defineFieldValue(\n      el,\n      () => fieldValue.value,\n      (value) => {\n        fieldValue.value = value;\n      },\n    );\n\n    // Keep fieldValue in sync when props.value changes externally\n    rippleWatch(props.value, (v) => {\n      const next = v != null ? String(v) : '';\n\n      if (fieldValue.value !== next) fieldValue.value = next;\n    });\n\n    function parseValue(): number | null {\n      const v = fieldValue.value.trim();\n\n      if (!v) return null;\n\n      const n = Number.parseFloat(v);\n\n      return Number.isNaN(n) ? null : n;\n    }\n\n    function commit(val: number | null, _originalEvent?: Event) {\n      const min = props.min.value != null ? Number(props.min.value) : undefined;\n      const max = props.max.value != null ? Number(props.max.value) : undefined;\n      const clamped = val != null ? clamp(val, min, max) : null;\n      const nextValue = clamped != null ? String(clamped) : '';\n\n      if (fieldValue.value !== nextValue) fieldValue.value = nextValue;\n\n      setFieldValue(el, nextValue);\n      dispatchNativeFieldEvent(el, 'change');\n    }\n\n    const spinner = createSpinnerControl({\n      commit,\n      disabled: isDisabled,\n      largeStep: props['large-step'],\n      max: props.max,\n      min: props.min,\n      parse: parseValue,\n      readonly: isReadonly,\n      step: props.step,\n    });\n\n    // Composition uses ore-input's own documented `ref` prop (fired with the raw\n    // <input> on mount, `null` on unmount) instead of reaching into its shadow DOM —\n    // see `packages/refine/AGENTS.md` / input.ts's `ref` JSDoc for the supported contract.\n    // Set imperatively (rather than declared in the template) because ore's template\n    // engine treats a function-valued attr binding as a reactive getter to invoke, not\n    // a literal value to assign — a plain property assignment avoids that entirely.\n    const bitInputRef = ref<HTMLElementTagNameMap['ore-input']>();\n    let stopAriaWatch: (() => void) | null = null;\n    let detachListeners: (() => void) | null = null;\n    let refSub: { dispose(): void } | null = null;\n\n    const handleFieldRef = (rawInput: HTMLInputElement | null): void => {\n      if (!rawInput) {\n        stopAriaWatch?.();\n        detachListeners?.();\n        refSub?.dispose();\n        stopAriaWatch = null;\n        detachListeners = null;\n        refSub = null;\n        props.ref.value?.(null);\n\n        return;\n      }\n\n      rawInput.setAttribute('inputmode', 'decimal');\n      // The wrapper div is a plain layout container — WAI-ARIA spinbutton semantics\n      // live on the actually-focusable element (the raw <input> rendered by ore-input).\n      rawInput.setAttribute('role', 'spinbutton');\n\n      const handleChange = (e: Event) => {\n        const val = (e.target as HTMLInputElement).value;\n        const n = val !== '' ? Number.parseFloat(val) : null;\n\n        commit(Number.isNaN(n ?? NaN) ? null : n, e);\n      };\n\n      const handleInput = (e: Event) => {\n        const val = (e.target as HTMLInputElement).value;\n\n        fieldValue.value = val;\n        setFieldValue(el, val);\n        dispatchNativeFieldEvent(el, 'input');\n      };\n\n      rawInput.addEventListener('change', handleChange);\n      rawInput.addEventListener('input', handleInput);\n      detachListeners = () => {\n        rawInput.removeEventListener('change', handleChange);\n        rawInput.removeEventListener('input', handleInput);\n      };\n\n      stopAriaWatch = watch(() => {\n        const now = parseValue();\n\n        if (now == null) rawInput.removeAttribute('aria-valuenow');\n        else rawInput.setAttribute('aria-valuenow', String(now));\n\n        if (props.min.value != null) rawInput.setAttribute('aria-valuemin', String(props.min.value));\n        else rawInput.removeAttribute('aria-valuemin');\n\n        if (props.max.value != null) rawInput.setAttribute('aria-valuemax', String(props.max.value));\n        else rawInput.removeAttribute('aria-valuemax');\n\n        if (isReadonly.value) rawInput.setAttribute('aria-readonly', 'true');\n        else rawInput.removeAttribute('aria-readonly');\n      });\n\n      // Fire user ref callback\n      props.ref.value?.(rawInput);\n\n      refSub = rippleWatch(props.ref, (cb) => {\n        cb?.(rawInput);\n      });\n    };\n\n    // ore-input's own onElement/ref lifecycle calls this back with `null` when its\n    // inner <input> unmounts, so no explicit teardown is needed here.\n    onElement(bitInputRef, (bitInputEl) => {\n      bitInputEl.ref = handleFieldRef;\n    });\n\n    bind({\n      attr: {\n        size: props.size,\n        value: () => fieldValue.value || null,\n        variant: props.variant,\n      },\n    });\n\n    const isNonInteractive = computed(() => isDisabled.value || isReadonly.value);\n\n    // The stepper buttons are slotted *into* ore-input's own `prefix`/`suffix` slots instead of\n    // sitting outside it in a separate wrapper — they render inside ore-input's own bordered\n    // box (same as its built-in clear/password-toggle buttons), matching every other field's\n    // single-box look instead of floating as two detached icon buttons either side of a\n    // narrow field. `@keydown` moves to ore-input itself: native keyboard events are\n    // `composed: true` and bubble out past a shadow boundary, so it still sees keydowns\n    // originating from ore-input's internal <input> as well as from these slotted buttons.\n    return html`\n      <ore-input\n        class=\"field\"\n        part=\"field control\"\n        ref=\"${bitInputRef}\"\n        value=\"${() => fieldValue.value ?? 0}\"\n        label=\"${() => props.label.value ?? ''}\"\n        label-placement=\"${() => props['label-placement'].value ?? 'inset'}\"\n        placeholder=\"${() => props.placeholder.value ?? ''}\"\n        name=\"${() => props.name.value ?? ''}\"\n        helper=\"${() => props.helper.value ?? ''}\"\n        error=\"${() => props.error.value ?? ''}\"\n        size=\"${props.size}\"\n        color=\"${() => props.color.value ?? ''}\"\n        variant=\"${props.variant}\"\n        rounded=\"${() => props.rounded.value}\"\n        ?disabled=\"${props.disabled}\"\n        ?readonly=\"${isReadonly}\"\n        ?fullwidth=\"${() => props.fullwidth.value}\"\n        ?loading=\"${() => props.loading.value}\"\n        ?success=\"${() => props.success.value}\"\n        @keydown=\"${(e: KeyboardEvent) => spinner.handleKeydown(e)}\">\n        <button\n          slot=\"prefix\"\n          type=\"button\"\n          part=\"decrement-btn\"\n          aria-label=\"Decrease\"\n          ?disabled=\"${() => isNonInteractive.value || spinner.atMin()}\"\n          @click=\"${(e: Event) => spinner.incrementBy(-(Number(props.step.value) || 1), e)}\">\n          <ore-icon name=\"minus\" size=\"14\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n        </button>\n        <button\n          slot=\"suffix\"\n          type=\"button\"\n          part=\"increment-btn\"\n          aria-label=\"Increase\"\n          ?disabled=\"${() => isNonInteractive.value || spinner.atMax()}\"\n          @click=\"${(e: Event) => spinner.incrementBy(Number(props.step.value) || 1, e)}\">\n          <ore-icon name=\"plus\" size=\"14\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n        </button>\n      </ore-input>\n    `;\n  },\n  shadow: { delegatesFocus: true },\n  styles: [disabledLoadingMixin, componentStyles],\n});\n"],"mappings":"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"}