# Number Input

## Usage

`NumberInput` is a numeric field. People type a number; add `hasNumberSteppers` when they should also get up and down chevrons. `min` and `max` clamp the value, and `step` sets how much each click changes it.

Bind a number:

```vue
<script setup lang="ts">
import { NumberInput } from '@astryx-vue/core/NumberInput'
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <NumberInput v-model="count" :min="0" :max="100" />
</template>
```

Values outside `min` and `max` are pulled back into range. The stepper names are English unless the app calls `provideAstryxLocale('zh-CN')`. When `label` is set, it is included in those names.

Three props adjust the field to the number it holds. `isIntegerOnly` keeps decimals out of the committed value. `formatValue` shows a readable form while the field is idle, such as `1,234 items`, and switches back to the raw number on focus. `isWheelEnabled={false}` stops the wheel from stepping the value where page scrolling matters more.

### Anatomy

- **Label**: The label for the number input.
- **Description**: Additional description text below the label.
- **Icon**: An optional icon within the input.
- **Placeholder**: Placeholder text shown when the input is empty.
- **Number steppers**: Optional buttons that increment or decrement by the configured step.

## Best practices

| Guideline | Practice |
| --- | --- |
| Do | Set `min` and `max` when the value has a real range. |
| Do | Pick a `step` that matches the field: `1` for a count, `0.01` for a price. |
| Do | Put the unit in the label, such as "Quantity (pcs)", rather than inside the field. |
| Don't | Use a tiny step across a wide range. People will have to click the stepper many times. |

## Examples

### Clearable

Progress from 0 to 100, with a percent suffix and a clear button.

```vue
<script setup lang="ts">
import { NumberInput } from '@astryx-vue/core/NumberInput'
import { ref } from 'vue'

const value = ref<number | null>(75)
</script>

<template>
  <div style="width: 300px; max-width: 100%">
    <NumberInput v-model="value" label="Progress" units="%" :min="0" :max="100" has-clear />
  </div>
</template>
```

### Range

Team size from 1 to 50, with a description of what the number means.

```vue
<script setup lang="ts">
import { NumberInput } from '@astryx-vue/core/NumberInput'
import { ref } from 'vue'

const value = ref<number | null>(3)
</script>

<template>
  <div style="width: 300px; max-width: 100%">
    <NumberInput
      v-model="value"
      label="Team Size"
      placeholder="1–50"
      :min="1"
      :max="50"
      description="Number of people on the team"
    />
  </div>
</template>
```

### Status

Error, warning, and success on budget, headcount, and completion.

```vue
<script setup lang="ts">
import { NumberInput } from '@astryx-vue/core/NumberInput'
import { VStack } from '@astryx-vue/core/Stack'
import { ref } from 'vue'

const error = ref<number | null>(-5)
const warning = ref<number | null>(150)
const success = ref<number | null>(25)
</script>

<template>
  <div style="width: 300px; max-width: 100%">
    <VStack :gap="4">
      <NumberInput
        v-model="error"
        label="Budget"
        :status="{ type: 'error', message: 'Must be a positive amount' }"
      />
      <NumberInput
        v-model="warning"
        label="Headcount"
        :status="{ type: 'warning', message: 'Exceeds typical team size' }"
      />
      <NumberInput
        v-model="success"
        label="Completion"
        units="%"
        :status="{ type: 'success', message: 'On track' }"
      />
    </VStack>
  </div>
</template>
```

### Units

A discount with a percent suffix, limited to 0–100.

```vue
<script setup lang="ts">
import { NumberInput } from '@astryx-vue/core/NumberInput'
import { ref } from 'vue'

const value = ref<number | null>(50)
</script>

<template>
  <div style="width: 300px; max-width: 100%">
    <NumberInput
      v-model="value"
      label="Discount"
      placeholder="Enter discount"
      :min="0"
      :max="100"
      units="%"
    />
  </div>
</template>
```

## NumberInput Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `modelValue` | `number \| null` | `null` | Current value. An empty field is `null`. |
| `label` | `string` | — | Field label, also used as the accessible name. |
| `description` | `string` | — | Helper text under the label. |
| `placeholder` | `string` | `''` | Placeholder text. |
| `min` | `number \| null` | — | Minimum value. On blur or Enter, a smaller entry commits at this value. While typing, it is marked invalid. |
| `max` | `number \| null` | — | Maximum value. On blur or Enter, a larger entry commits at this value. While typing, it is marked invalid. |
| `step` | `number \| null` | `1` | Amount added or removed on each step. |
| `hasNumberSteppers` | `boolean` | `false` | Shows increment and decrement buttons at the end of the input. A button is disabled when its step would not change the value. |
| `isWheelEnabled` | `boolean` | `true` | Whether scrolling the wheel over the focused input steps the value. Turn it off where page scrolling should always win. |
| `isIntegerOnly` | `boolean` | `false` | Only whole numbers commit. Decimal entries stay in the field while editing and are dropped when the field loses focus. The keyboard also switches to the numeric keypad. |
| `formatValue` | `(value: number) => string` | — | Formats the value shown while the field is not focused, for example `(v) => `${v} items``. Focus shows the raw number so it stays editable, and the formatted text is exposed through `aria-valuetext`. |
| `units` | `string \| null` | — | Unit shown at the end of the input, such as %. |
| `hasClear` | `boolean` | `false` | Shows a clear button when the field has a value. The tooltip only says “Clear”; the accessible name includes the field label. Clearing sets the value to `null`. An emptied draft commits `null` on blur only when this is on; otherwise it reverts. |
| `isDisabled` | `boolean` | `false` | Disables the field. |
| `disabledMessage` | `string` | — | Explains why the field is disabled. With `isDisabled`, the reason shows in a tooltip on hover and keyboard focus, and the input stays focusable through `aria-disabled` instead of being removed from the tab order. |
| `isReadOnly` | `boolean` | `false` | Shows the value at full opacity and still submits it, but blocks edits. Arrow keys, the wheel, and the stepper buttons are all off while read-only. `isDisabled` wins when both are set. |
| `isOptional` | `boolean` | `false` | Marks the field optional. Mutually exclusive with `isRequired`. |
| `isRequired` | `boolean` | `false` | Marks the field required. Mutually exclusive with `isOptional`. |
| `isLabelHidden` | `boolean` | `false` | Keeps the label for screen readers and hides it visually. |
| `labelTooltip` | `string` | — | Tooltip text behind an info icon at the end of the label. |
| `labelIcon` | `Component` | — | Icon shown before the label text. The `labelIcon` slot works too. |
| `startIcon` | `Component` | — | Icon shown at the start of the input. The `icon` slot works too. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Field size. |
| `width` | `number \| string` | — | Width of the whole field, label and status included. Numbers are pixels, strings are used as-is, such as `'100%'`. |
| `status` | `'default' \| 'error' \| 'warning' \| 'success' \| { type, message? }` | — | Validation status. An object with `message` shows that text under the field. |
| `statusVariant` | `'attached' \| 'detached' \| 'tooltip'` | `'attached'` | How the status message is placed. `'tooltip'` hides the message box and reveals it from the status icon instead. |
| `htmlName` | `string` | — | Form field name. |
| `autoComplete` | `string` | — | Native `autocomplete` attribute. |
| `hasAutoFocus` | `boolean` | `false` | Focuses the input after it mounts. |
| `id` | `string` | — | Id for the input element. Generated when omitted. |
| `disabled` | `boolean` | `false` | Same as `isDisabled`. |
| `disabledReason` | `string` | — | Same as `disabledMessage`. |
| `clearable` | `boolean` | `false` | Same as `hasClear`. |
| `name` | `string` | — | Same as `htmlName`. |
| `containerClass` | `string \| object \| array` | — | Class for the field container. Has no effect, and warns in development, when no Field box renders, including inside InputGroup and a horizontal-labels FormLayout. |
| `containerStyle` | `StyleValue` | — | Inline style for the field container. |
| `controlClass` | `string \| object \| array` | — | Class for the number field box. A class written on the component lands here too, with no warning. |
| `controlStyle` | `StyleValue` | — | Inline style for the number field box. A style written on the component lands here too. |

## NumberInput events

| Name | Payload | Description |
| --- | --- | --- |
| `update:modelValue` | `value: number \| null` | Emitted when the value changes. |
| `change` | `value: number \| null` | Emitted when the value changes. `null` means the field was cleared. |
| `focus` | `event: FocusEvent` | Emitted when the input receives focus. |
| `blur` | `event: FocusEvent` | Emitted when the input loses focus. |
| `enter` | `event: KeyboardEvent` | Emitted when Enter is pressed in the input. |
| `keydown` | `event: KeyboardEvent` | Emitted on keydown in the input, before the arrow keys step the value. Calling `preventDefault()` keeps the value unchanged. |

## NumberInput slots

| Name | Description |
| --- | --- |
| `icon` | Icon at the start of the input. |
| `labelIcon` | Icon before the label text. |
