# Date Range Input

## Usage

`DateRangeInput` selects a start date and an end date. The value is `{ start, end }` with ISO dates, or `null`.

```vue
<script setup lang="ts">
import type { DateRange } from '@astryx-vue/core/DateRangeInput'
import { DateRangeInput } from '@astryx-vue/core/DateRangeInput'
import { shallowRef } from 'vue'

const range = shallowRef<DateRange | null>(null)
</script>

<template>
  <DateRangeInput v-model="range" label="Date range" />
</template>
```

The popover shows two months under one header. The first click sets the start. The second click commits the range when the span is allowed. Clicking the start again commits a single day when the shortest span is one day, and cancels the selection otherwise. The closed field omits the year when both ends are in the current year. Presets stay beside the calendars. When the two months no longer fit side by side, they wrap onto separate rows. A constraint can still turn individual days off. While a change is saving, the field cannot be edited.

### Anatomy

- **Label**: Text above the trigger describing what date range is expected.
- **Field surface**: Bordered control containing the calendar toggle, range trigger, and end affordances.
- **Trigger button**: A button showing the formatted range or placeholder. Clicking opens the popover.
- **Calendar icon**: A trailing icon that also opens the popover.
- **Calendar popover**: A dual-month calendar grid with range selection and hover preview.
- **Preset sidebar**: A list of preset range options beside the calendar.
- **Preset button**: A quick-select action for one preset range, reflecting current and disabled states.
- **Clear button**: A × button that resets the range to null.
- **Status message**: An error, warning, or success message below the trigger.

## Best practices

| Guideline | Practice |
| --- | --- |
| Do | Offer presets for the ranges people pick most often. |
| Do | Set `minRangeSpan` and `maxRangeSpan` when a one-day or a very long range is invalid. Counts include both ends. |
| Don't | Use it for one date. DateInput is the control for that. |

## Examples

### Validation

Error, warning, and success on a filled range.

```vue
<script setup lang="ts">
import type { DateRange } from '@astryx-vue/core/DateRangeInput'
import { DateRangeInput } from '@astryx-vue/core/DateRangeInput'
import { VStack } from '@astryx-vue/core/Stack'
import { shallowRef } from 'vue'

const errorRange = shallowRef<DateRange | null>({ start: '2026-01-01', end: '2026-01-31' })
const warningRange = shallowRef<DateRange | null>({ start: '2026-06-01', end: '2026-06-30' })
const successRange = shallowRef<DateRange | null>({ start: '2026-03-01', end: '2026-03-31' })
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <DateRangeInput v-model="errorRange" label="Booking period" :status="{ type: 'error', message: 'Selected dates are no longer available' }" />
    <DateRangeInput v-model="warningRange" label="Preferred period" :status="{ type: 'warning', message: 'High demand — limited availability' }" />
    <DateRangeInput v-model="successRange" label="Confirmed period" :status="{ type: 'success', message: 'Dates confirmed and available' }" />
  </VStack>
</template>
```
### With presets

Presets for the last 7, 14, 30, and 90 days, or a custom range.

```vue
<script setup lang="ts">
import type { DateRange } from '@astryx-vue/core/DateRangeInput'
import { DateRangeInput } from '@astryx-vue/core/DateRangeInput'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { shallowRef } from 'vue'

function isoDaysAgo(days: number): string {
  const date = new Date()
  date.setDate(date.getDate() - days)
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${date.getFullYear()}-${month}-${day}`
}

const range = shallowRef<DateRange | null>(null)
const presets = [
  { label: 'Last 7 days', getRange: () => ({ start: isoDaysAgo(7), end: isoDaysAgo(0) }) },
  { label: 'Last 14 days', getRange: () => ({ start: isoDaysAgo(14), end: isoDaysAgo(0) }) },
  { label: 'Last 30 days', getRange: () => ({ start: isoDaysAgo(30), end: isoDaysAgo(0) }) },
  { label: 'Last 90 days', getRange: () => ({ start: isoDaysAgo(90), end: isoDaysAgo(0) }) },
]
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <Text type="supporting" color="secondary">
      {{ range ? `${range.start} → ${range.end}` : 'No range selected' }}
    </Text>
    <DateRangeInput
      v-model="range"
      label="Report period"
      description="Use a preset or pick a custom range"
      :presets="presets"
    />
  </VStack>
</template>
```

## DateRangeInput Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `v-model` | `DateRange \| null` | `null` | `{ start, end }` ISO dates. |
| `label` | `string` | — | Required accessible name. |
| `presets` | `{ label, getRange }[]` | — | Shortcut buttons. `getRange` returns a range. |
| `monthCount` | `number` | `2` | How many months the popover shows. |
| `numberOfMonths` | `1 \| 2` | — | Months side by side. This wins over monthCount. |
| `dateConstraints` | `Array<(date: Date) => boolean>` | — | A day is disabled when any function returns false. |
| `min` / `max` | `string` | — | ISO dates that cannot be selected outside. |
| `minRangeSpan` | `number` | `1` | Shortest span in days, including both ends. |
| `maxRangeSpan` | `number` | — | Longest span in days, including both ends. |
| `weekStartsOn` | `0-6 \| 'sun' \| 'mon' \| ...` | Sunday, Monday for zh-CN | First column of the calendar. |
| `hasClear` | `boolean` | `true` | Shows a clear button. Keyboard activation returns focus to the trigger without scrolling the page. |
| `isLabelHidden` | `boolean` | `false` | Hides the label visually while keeping it accessible. `labelHidden` is the older name; either one hides it. Playground templates use `is-label-hidden`. |
| `containerClass` | `string \| object \| array` | — | Class for the field container. Has no effect, and warns in development, inside InputGroup and in a horizontal-labels FormLayout. |
| `containerStyle` | `StyleValue` | — | Inline style for the field container. Same targeting as containerClass. |
| `controlClass` | `string \| object \| array` | — | Class for the bordered control box. A class written on the component lands here too, not on the trigger button. |
| `controlStyle` | `StyleValue` | — | Inline style for the bordered control box. |
| `placeholder` | `string` | `Select date range` | Empty-state text. Follows the locale. |
| `status` | `FieldStatusInput` | — | Error, warning, or success. |
| `statusVariant` | `'attached' \| 'detached' \| 'tooltip'` | `'attached'` | Where the status message sits. |
| `isDisabled` | `boolean` | `false` | Disables the field. |
| `disabledMessage` | `string` | — | Explains why the field is disabled. With `isDisabled`, the message shows in a tooltip on hover or keyboard focus and the trigger stays focusable with `aria-disabled`; the calendar still cannot open. |
| `isLoading` | `boolean` | `false` | The field cannot be edited while a change is saving. |
| `changeAction` | `(value) => void \| Promise<void>` | — | Runs after the range is committed. A promise keeps the field busy. |
| `isOptional` | `boolean` | — | Marks the label as optional. |
| `isRequired` | `boolean` | — | Marks the field required. |
| `labelTooltip` | `string` | — | Hint shown from the info icon at the end of the label. |
