GrDatePicker

Пакет: @feugene/granularity-chronoспутникГруппа: Прочее

Берут, когда дата — значение поля формы.

Когда брать

  • дата — значение поля формы — контракт форм-контрола ядра работает целиком: ошибка, обязательность и подпись приезжают из GrFormField без единой правки;
  • дату можно и набрать, и выбратьeditable включает ввод с клавиатуры, разбор идёт по локали, а не по одному жёсткому шаблону;
  • часть дат недоступнаdisabledDates принимает и список, и предикат: выходные, занятые слоты, «не раньше сегодня» через min/max;
  • на бэкенд уходит не DatevalueAdapter меняет тип модели на строку ISO, timestamp или свой формат, и это видно в типах.

Когда взять другое

НужноБерите
Период «с — по»GrDateRangePicker
Дата вместе со временемGrDateTimePicker
Только времяGrTimePicker
Сетка месяца сама по себе, без поляGrCalendar
Показать момент, а не выбрать егоGrRelativeTime
Выбирают только из готовых значений, произвольная дата не нужнаGrSelect

Готовые даты живут внутри панели

presets рисует ряд шорткатов в подвале: «Сегодня», «Завтра», «Понедельник». Дату можно задать функцией — «сегодня» считается в момент показа, а не в момент объявления пропа.

Шорткат идёт мимо сетки, поэтому проверяет запреты сам: дата вне min/max или из disabledDates приходит выключенной — ровно как её ячейка в сетке. Кнопка, которая ничего не делает, обманывает.

Свой подвал — слот footer; он заменяет ряд целиком и получает select, canSelect и close.

Значение — локальная полночь, а не момент

Дата без времени собирается как локальная полночь: пользователь выбирает день в своём календаре, а не точку на мировой линии. Кому нужен именно момент — тот сериализует его своим адаптером; молча подставлять UTC пакет не станет, потому что «12 августа» в двух часовых поясах это два разных момента и один и тот же день.

Не только дни: неделя, месяц, квартал, год

mode пробрасывается в календарь как есть, и поле показывает выбранное обычным форматом даты:

<GrDatePicker v-model="reportPeriod" mode="quarter" />

Неделя кладёт в модель своё начало, квартал и месяц — первое число, год — первое января. Форма значения от режима не зависит: в поле по-прежнему одна дата, и valueAdapter работает как работал.

Разбор режимов — ./GrCalendar.md; там же о том, почему неделя рисуется сеткой дней, а не сеткой периодов, и почему номер недели не показывается.

Набор дат — не диапазон

multiple собирает произвольное множество: расписание занятий, даты-исключения в графике, забронированные дни. Модель становится массивом.

<GrDatePicker v-model="lessons" multiple />

Отличие от GrDateRangePicker — в существе, а не в удобстве: там непрерывный отрезок с двумя краями и правилами длины, здесь — множество, где соседство ничего не значит.

Клик по выбранной дате её снимает. Набор — переключатель, а не накопитель: иначе снять ошибочно взятую дату было бы нечем.

Панель не закрывается после выбора. Набор набирают, а одиночную дату выбирают однажды — и закрытие после первого клика превратило бы набор из десяти дат в десять открытий панели.

Порядок всегда по возрастанию, куда бы ни кликнули. Модель обязана быть сравнима: перестановка элементов не должна читаться как изменение — иначе «есть несохранённые правки» срабатывает на пустом месте.

Ручной ввод в этом режиме выключен, даже при editable. Строка, описывающая N дат, требует своего разбора: сколько их набрано, из строки не следует, а неполный ввод не отличить от короткого набора. Диапазон и дата со временем описывают ровно два значения, и потому набираются (GrDateRangePicker, GrDateTimePicker); произвольный набор — нет.

В поле видны первые три даты и остаток числом («и ещё 2»): без потолка подпись переполняется уже на пятой. В форму уходит по скрытому полю на дату — с одним именем, как читает FormData.getAll.

min, max и disabledDates действуют на каждую дату по отдельности, теми же правилами, что и при одиночном выборе.

`applyOnBlur` включён

Выбор в календаре применяется сразу, а уход фокуса из поля фиксирует набранное руками. Иначе набранная, но не подтверждённая дата исчезала бы при переходе к следующему полю — а пользователь считает, что он её ввёл.

Редактируемое поле показывает то, что принимает обратно

С editable значение выводится цифрами (08/12/2026), а не Aug 12, 2026. Иначе правка числа прямо в поле оставляла бы разбору две группы цифр вместо трёх: набранное молча откатывалось бы, хотя пользователь ничего не нарушил.

Свой format это правило перебивает — тогда за читаемость набранного отвечает потребитель. Без editable показ остаётся прежним.

Запрещённая дата не принимается и текстом. disabledDates, min и max действуют одинаково на клик и на Enter; поле, обходящее ограничения панели, означало бы, что ограничений нет.

Установка

npm i @feugene/granularity-chrono

Импорт

import { GrDatePicker } from '@feugene/granularity-chrono/components/GrDatePicker'

API

API этого компонента ещё не посчитан: генератор витрины пока обходит только ядро. Пока его нет, справочник — в документации пакета.

Примеры 7

Basic

Basic
<script setup lang="ts">
import { ref } from 'vue'

// `GrDatePicker` подставляется авто-импортом (`unplugin-vue-components`).
const value = ref<Date | null>(new Date(2026, 7, 12))
const iso = ref<string | null>('2026-08-20')
</script>

<template>
  <div class="grid max-w-[320px] gap-4">
    <GrDatePicker
      v-model="value"
      clearable
      placeholder="Pick a date"
      aria-label="Pick a date"
    />

    <!-- Тип модели задаёт адаптер, а не строковый проп формата: значение
         остаётся `string` и в типах, и в рантайме. -->
    <GrDatePicker
      v-model="iso"
      value-adapter="isoDate"
      :format="{ dateStyle: 'full' }"
      clearable
      placeholder="ISO model"
      aria-label="ISO model"
    />

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">Date=</span><code>{{ value?.toDateString() ?? '—' }}</code>
      <span class="opacity-70"> · isoDate=</span><code>{{ iso ?? '—' }}</code>
    </p>
  </div>
</template>

In Dialog

In Dialog
<script setup lang="ts">
import { ref } from 'vue'

// `GrDatePicker`, `GrDialog`, `GrButton` и `GrFormField` подставляются
// авто-импортом (`unplugin-vue-components`).
const open = ref(false)
const value = ref<string | null>('2026-08-12')
</script>

<template>
  <div class="grid gap-3">
    <GrButton class="justify-self-start" @click="open = true">
      Schedule delivery
    </GrButton>

    <GrDialog v-model="open" title="Schedule delivery" size="sm">
      <!-- Панель пикера встаёт в общий стек слоёв поверх окна: Esc закрывает
           сначала её, и только следующий — само окно. -->
      <GrFormField label="Delivery date">
        <GrDatePicker
          v-model="value"
          value-adapter="isoDate"
          clearable
          placeholder="Pick a date"
        />
      </GrFormField>

      <template #footer>
        <div class="flex items-center justify-between gap-3">
          <span class="showcase-demo-text text-sm">
            <span class="opacity-70">value=</span><code>{{ value ?? '—' }}</code>
          </span>
          <GrButton @click="open = false">
            Done
          </GrButton>
        </div>
      </template>
    </GrDialog>
  </div>
</template>

Form

Form
<script setup lang="ts">
import { computed, ref } from 'vue'

// `GrDatePicker`, `GrFormField` и `GrButton` подставляются авто-импортом.
// Модель — строка `2026-08-20`: её задаёт адаптер, а не проп формата.
const departure = ref<string | null>(null)
const attempted = ref(false)
const submitted = ref('')

const today = new Date(2026, 7, 12)

const error = computed(() => (attempted.value && !departure.value ? 'Choose a departure date' : ''))

function submit(event: Event): void {
  attempted.value = true

  // Форме уходит `2026-08-20`, а не «Aug 20, 2026»: показ локале-зависим и на
  // сервере не разбирается.
  const data = new FormData(event.target as HTMLFormElement)
  submitted.value = String(data.get('departure') ?? '')
}
</script>

<template>
  <form class="grid max-w-[320px] gap-4" @submit.prevent="submit">
    <!-- Поле пикера — обычный форм-контрол: подпись через `<label for>`,
         ошибка через `aria-describedby`, значение уходит по `name`. -->
    <GrFormField label="Departure" :error="error" required>
      <GrDatePicker
        v-model="departure"
        name="departure"
        value-adapter="isoDate"
        :min="today"
        clearable
        placeholder="Pick a date"
      />
    </GrFormField>

    <GrButton type="submit" size="sm">
      Submit
    </GrButton>

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">form data=</span><code>{{ submitted || '—' }}</code>
    </p>
  </form>
</template>

Inline

Inline
<script setup lang="ts">
import { ref } from 'vue'

// `GrDatePicker` подставляется авто-импортом (`unplugin-vue-components`).
const value = ref<string | null>('2026-08-12')
</script>

<template>
  <div class="grid gap-4 justify-items-start">
    <!-- Панель на месте, поля нет — но модель, адаптер и `name` остаются
         пикеровскими: этим `inline` и отличается от голого `GrCalendar`. -->
    <GrDatePicker
      v-model="value"
      inline
      name="due"
      value-adapter="isoDate"
      aria-label="Due date"
    />

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">value=</span><code>{{ value ?? '—' }}</code>
    </p>
  </div>
</template>

Modes

Modes
<script setup lang="ts">
import { ref } from 'vue'

// `GrDatePicker` подставляется авто-импортом (`unplugin-vue-components`).
const day = ref<string | null>('2026-08-12')
const month = ref<string | null>('2026-08-01')
const year = ref<string | null>('2026-01-01')
</script>

<template>
  <div class="grid max-w-[320px] gap-4">
    <!-- Режим меняет и панель, и вид значения в поле: подставлять свой
         `format` для этого не нужно. -->
    <GrDatePicker v-model="day" value-adapter="isoDate" aria-label="Day" />
    <GrDatePicker v-model="month" mode="month" value-adapter="isoDate" aria-label="Month" />
    <GrDatePicker v-model="year" mode="year" value-adapter="isoDate" aria-label="Year" />

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">day=</span><code>{{ day ?? '—' }}</code>
      <span class="opacity-70"> · month=</span><code>{{ month ?? '—' }}</code>
      <span class="opacity-70"> · year=</span><code>{{ year ?? '—' }}</code>
    </p>
  </div>
</template>

Multiple

Multiple
<script setup lang="ts">
import { computed, ref } from 'vue'

// `GrDatePicker` и `GrChip` подставляются авто-импортом.

/**
 * Набор дат — не диапазон.
 *
 * Демо намеренно ставит рядом список выбранного: на нём видно, что модель это
 * массив, что он всегда отсортирован и что снять дату можно двумя путями —
 * повторным кликом в сетке и крестиком в списке.
 */
const TODAY = new Date(2026, 7, 12)

const lessons = ref<Date[]>([new Date(2026, 7, 12), new Date(2026, 7, 14)])

const formatter = new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: 'short' })

const chips = computed(() => lessons.value.map(date => ({
  key: date.toISOString().slice(0, 10),
  label: formatter.format(date),
  date,
})))

function remove(key: string): void {
  lessons.value = lessons.value.filter(date => date.toISOString().slice(0, 10) !== key)
}
</script>

<template>
  <div class="grid gap-4 justify-items-start">
    <GrDatePicker
      v-model="lessons"
      multiple
      :today="TODAY"
      locale="ru-RU"
      placeholder="Выберите занятия"
      aria-label="Даты занятий"
      class="w-80"
    />

    <div v-if="chips.length > 0" class="flex flex-wrap items-center gap-2">
      <!-- Удаление у чипа своё: `closable` плюс `remove`, свой крестик был бы копией. -->
      <GrChip
        v-for="chip in chips"
        :key="chip.key"
        size="sm"
        closable
        :remove-label="`Убрать ${chip.label}`"
        @remove="remove(chip.key)"
      >
        {{ chip.label }}
      </GrChip>
    </div>

    <p v-else class="showcase-demo-text text-sm opacity-70">
      Пока ничего не выбрано.
    </p>

    <p class="showcase-demo-text text-sm opacity-70">
      Наберите несколько дат: панель <strong>не закрывается</strong> — набор набирают, а одиночную
      дату выбирают однажды. Клик по выбранной снимает её: набор это переключатель, а не накопитель,
      иначе снять ошибочно взятую дату было бы нечем.
    </p>

    <p class="showcase-demo-text text-sm opacity-70">
      Порядок в модели всегда по возрастанию, куда бы вы ни кликнули — модель обязана быть сравнима,
      иначе перестановка читалась бы как изменение. В поле видны первые три даты и остаток числом:
      без потолка подпись переполняется уже на пятой. Отличие от диапазона — в существе: там
      непрерывный отрезок с двумя краями, здесь множество, где соседство ничего не значит.
    </p>
  </div>
</template>

Typed

Typed
<script setup lang="ts">
import { ref } from 'vue'

// `GrDatePicker` подставляется авто-импортом (`unplugin-vue-components`).
const value = ref<string | null>('2026-08-12')
const russian = ref<string | null>('2026-08-12')
</script>

<template>
  <div class="grid max-w-[320px] gap-4">
    <!-- Порядок частей и разделитель берутся из локали, поэтому маска и
         подсказка формата у полей разные, а проп — один. -->
    <GrDatePicker
      v-model="value"
      editable
      value-adapter="isoDate"
      locale="en-US"
      clearable
      aria-label="US format"
    />

    <GrDatePicker
      v-model="russian"
      editable
      value-adapter="isoDate"
      locale="ru-RU"
      clearable
      aria-label="RU format"
    />

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">en=</span><code>{{ value ?? '—' }}</code>
      <span class="opacity-70"> · ru=</span><code>{{ russian ?? '—' }}</code>
    </p>
  </div>
</template>

Документация компонентаВсе компоненты