Uživatelská příručka

Návod, jak Gregory nasadit do reálného projektu. Čte se odshora dolů, ale klidně skoč rovnou do kuchařky — recepty jsou celé a fungují samostatně. Kompletní výčet všeho, co se dá nastavit, je v API dokumentaci.

Co Gregory je a co není

Je to komponenta pro výběr data, rozsahu dat, času a období. Pracuje výhradně v místním čase prohlížeče a časové zóny neřeší — datum, na které uživatel klikne, je to datum, které dostaneš. Není to knihovna na počítání s daty; na to si vezmi cokoli jiného, Gregory ti do toho mluvit nebude.

Instalace

Se sestavovacím nástrojem (Vite, Webpack, Rollup). Instaluje se rovnou z GitHubu — do npm registru se knihovna vydávat nebude. npm si ji po naklonování sestaví sám, nic se nemusí buildit ručně:

npm install github:svatekr70/gregory        # poslední main
npm install github:svatekr70/gregory#v0.3.0 # konkrétní verze
import { Gregory } from '@svatekr70/gregory'
import '@svatekr70/gregory/style.css'

Jméno balíčku je @svatekr70/gregory i při instalaci z GitHubu, takže importy vypadají obvykle.

Bez buildu, přímo do stránky — sestavená knihovna se publikuje spolu s touhle dokumentací, takže jde načíst z URL. UMD podoba vystaví globální Gregory:

<link rel="stylesheet" href="https://svatekr70.github.io/gregory/dist/gregory.css">
<script src="https://svatekr70.github.io/gregory/dist/gregory.umd.js"></script>
<script>
  new Gregory.Gregory('#termin', { mode: 'date', locale: 'cs' })
</script>

Nebo jako ES modul, bez globální proměnné:

<script type="module">
  import { Gregory } from 'https://svatekr70.github.io/gregory/dist/gregory.js'
  new Gregory('#termin', { mode: 'date', locale: 'cs' })
</script>
Na produkci raději k sobě

Adresy výše ukazují na poslední verzi na main a visí na cizím serveru. Pro ostrý provoz si stáhni přílohu vydání z GitHub Releases a soubory si nahraj k sobě.

Styly se importují jednou za aplikaci, ne u každého pickeru. Motivy jsou v samostatném souboru a jsou nepovinné:

import '@svatekr70/gregory/themes.css'

První picker

Gregory se věší na existující prvek. Nejčastěji na textový input:

<input type="text" id="termin" placeholder="Vyber datum…">
const picker = new Gregory('#termin', {
  mode: 'date',
  locale: 'cs',
})

To je všechno. Klik do pole otevře kalendář, klik na den ho zavře a do pole napíše datum. Hodnotu si vyzvedneš kdykoli:

picker.getValue()   // Date, nebo null
Proč ne <input type="date">

Nativní pole vypadá v každém prohlížeči jinak, neumí rozsah ani presety a jeho vyskakovací kalendář se nedá stylovat. Gregory je naopak všude stejný a řídí se tvým designem. Když ti nativní pole stačí, používej ho — tahle knihovna začíná dávat smysl u rozsahů, omezení a vlastního vzhledu.

Který režim zvolit

Volba mode je první a nejdůležitější rozhodnutí: určuje tvar hodnoty i výchozí chování zbytku.

Potřebuji…modeDostanu
jeden den (narozeniny, splatnost)'date'Date | null
období od–do (pobyt, filtr)'range'{ from, to }
den i hodinu (schůzka)'datetime'Date | null
od kdy do kdy včetně času (směna)'datetime-range'{ from, to }
několik samostatných dnů (dovolená po dnech)'multiple'Date[]
celý měsíc (uzávěrka)'month'Date = 1. den měsíce
čtvrtletí (report)'quarter'Date = 1. den čtvrtletí
rok (ročenka)'year'Date = 1. leden

Režim se dá měnit i za běhu, ale je to spíš pro konfigurátory než pro běžné stránky:

picker.setOptions({ mode: 'range' })

Hodnota se přitom nezahazuje, jen se čte podle nového tvaru: z rozsahu zůstane po přepnutí na date jeho začátek a v režimu multiple se neprojeví vůbec, protože seznam dnů je prázdný. Po návratu zpátky na range je celý rozsah zase na místě. Když má hodnota při přepnutí opravdu zmizet, zavolej clear().

Období vs. rozsah

Na „celý srpen" jsou dvě cesty. mode: 'month' vrátí jeden Date (1. 8.) a je to volba filtru — server si sám dopočítá, že jde o celý měsíc. mode: 'range' s presetem „Tento měsíc" vrátí { from: 1. 8., to: 31. 8. } a je to skutečný rozsah, který uživatel může ještě upravit. Ptej se, jestli má jít hranice měnit: když ne, stačí období.

Čtení a nastavení hodnoty

Picker rozlišuje dva stavy: potvrzenou hodnotu a rozpracovaný výběr. Uživatel s otevřeným panelem klikl na první konec rozsahu — výběr se změnil, hodnota ještě ne.

picker.getValue()      // potvrzené: to, co je v poli
picker.getSelection()  // rozpracované: i poloviční rozsah

Na změny se dá poslouchat. Pro ukládání chceš skoro vždycky apply, ne change:

picker.on('apply', ({ value }) => ulozit(value))
picker.on('change', ({ value, complete }) => {
  // Padá při každém kliknutí. complete je false, dokud rozsah nemá oba konce.
})

Nastavit hodnotu můžeš čímkoli, co jde přečíst jako datum — ISO řetězec, Date, timestamp:

picker.setValue('2026-08-13')
picker.setValue({ from: '2026-08-10', to: '2026-08-16' })
picker.setValue(['2026-08-10', '2026-08-16'])
picker.clear()
Pozor na new Date('2026-08-13')

JavaScript takový řetězec čte jako půlnoc v UTC, takže v Praze z něj v srpnu vyjde 13. srpna 02:00 (v zimě 01:00) a v New Yorku 12. srpna 20:00. Gregory si ISO řetězce parsuje sám a v místním čase, takže setValue('2026-08-13') je vždycky 13. srpna. Když si datum skládáš ručně, použij new Date(2026, 7, 13) nebo utilitu createDate(2026, 7, 13).

Text v poli a hodnota pro server

Do pole se píše datum pro lidi — 13. 8. 2026, podle jazyka. Server to spolehlivě nepřečte, tak mu posílej ISO tvar:

import { formatISODate } from '@svatekr70/gregory'

const den = picker.getValue()   // v režimu date: Date | null
if (den) odeslat(formatISODate(den))   // '2026-08-13'

V TypeScriptu getValue() vrací sjednocení všech tvarů (Date | DateRange | Date[] | null), protože jeden picker umí všechny režimy. Když víš, který máš, zúž si typ — picker.getValue() as Date.

U formulářů to za tebe udělá submitName — viz dál.

Odeslání formuláře

Nejjednodušší cesta: nech Gregory vyrobit skryté pole s ISO hodnotou. Vznikne hned vedle viditelného vstupu, takže spadne do stejného formuláře.

new Gregory('#termin', {
  mode: 'date',
  submitName: 'termin',
})
// odejde termin=2026-08-13

U rozsahu vzniknou pole dvě, s příponami _from a _to. Vlastní jména se dají zadat dvojicí:

new Gregory('#pobyt', {
  mode: 'range',
  submitName: { from: 'prijezd', to: 'odjezd' },
})

Validace

Picker sám nepustí do hodnoty nic, co odporuje nastaveným mezím — ani přes setValue(), ani rukou napsaným textem. Odmítnutí ohlásí událostí invalid, takže si formulář může říct své:

picker.on('invalid', ({ reason }) => {
  chyba.textContent = reason === 'unreadable'
    ? 'Datum se nepodařilo přečíst.'
    : 'Tenhle termín nejde vybrat.'
})

Odmítnuté pole zároveň dostane aria-invalid="true", což se dá obarvit jedním pravidlem:

input[aria-invalid] { border-color: #d64545; }

Povinné pole

Vestavěné required na viditelném poli funguje: dokud je prázdné, prohlížeč formulář neodešle. Se skrytým polem z submitName ale required nesahá — na to si napiš kontrolu při odeslání:

form.addEventListener('submit', (e) => {
  if (!picker.getValue()) { e.preventDefault(); chyba.textContent = 'Vyber termín.' }
})

Omezení výběru

Omezení se skládají — platí všechna najednou a zakázaný den je zakázaný i pro klávesnici.

Chci…Volba
jen budoucnostmin: today()
nejvýš rok dopředumax: addDays(today(), 365)
bez víkendů a svátkůisDisabled: (d) => …
pobyt nejvýš na 14 nocímaxSpan: 15
pobyt aspoň na dvě nociminSpan: 3
rozsah nesmí přeskočit obsazený denstopAtDisabled: true
jen v otevírací doběminTime, maxTime, timeWindow
nejvýš pět dnů v multiplemaxSelected: 5
Rozsahy se počítají včetně obou konců

Rozsah 10.–12. srpna jsou tři dny, ale jen dvě noci. Pro ubytování na nejvýš 14 nocí je tedy správně maxSpan: 15. Stejné pravidlo platí pro minSpan i pro počet dnů v informačním řádku.

Zakázané dny

isDisabled dostane Date nastavené na půlnoc daného dne a vrací true pro dny, které nejdou vybrat. Volá se pro každou vykreslenou buňku, takže ať je rychlá — hledání v Setu ano, dotaz na server ne:

let obsazeno = new Set(['2026-08-15', '2026-08-16'])

const picker = new Gregory('#pobyt', {
  mode: 'range',
  isDisabled: (d) => obsazeno.has(formatISODate(d)),
})

Když se seznam obsazených dnů dotáhne později, funkce si nová data vezme sama — jen jí musíš dát vědět, ať se panel překreslí:

obsazeno = new Set(await nactiObsazene())
picker.setOptions({})   // překreslí panel s aktuálními daty

Porovnání období

Filtr v přehledu skoro nikdy nekončí u jednoho období — hned za ním přijde otázka „a jak to bylo předtím?“. compare to období dopočítá samo. Nevybírá se; v kalendáři se jen vyznačí pruhem pod dny a přijde s hodnotou:

const filtr = new Gregory('#obdobi', {
  mode: 'range',
  locale: 'cs',
  summary: true,
  compare: 'previous',
})

filtr.on('apply', ({ value, compare }) => {
  nacistData(value.from, value.to)
  if (compare) nacistPorovnani(compare.from, compare.to)
})

Na výběr jsou tři způsoby, jak období odvodit, plus vlastní funkce:

  • 'previous' — stejně dlouhý úsek těsně před začátkem. To je to, co člověk myslí „minulým obdobím“.
  • 'year' — stejná data o rok zpět. Sedí datum, ne dny v týdnu.
  • 'year-weekday' — posun o 364 dní. Sedí dny v týdnu, ne datum. Pro týdenní a prodejní reporty je tohle obvykle to správné: víkend se porovnává s víkendem.
Celý měsíc se porovnává s celým měsícem

Kdyby se počítalo jen po dnech, vyšlo by „předchozí období“ k celému únoru na 4.–31. ledna — únor je o tři dny kratší. Výběr, který přesně odpovídá kalendářnímu měsíci, čtvrtletí nebo roku, se proto porovnává s celým předchozím celkem.

Když má porovnání vycházet z něčeho vlastního — z fiskálního roku, z minulé sezóny, z období uloženého v nastavení — dej místo názvu funkci. Dostane vybraný rozsah setříděný a vrací dvojici dat, nebo null, když se pro ten výběr porovnávat nemá:

compare: ({ from, to }) => [
  minulaSezona(from),
  minulaSezona(to),
]

Období se dá dopočítat i bez pickeru — stejnou funkcí, jakou používá knihovna uvnitř. To se hodí, když stejný výpočet potřebuje i dotaz na serveru:

import { comparePeriod } from '@svatekr70/gregory'

comparePeriod({ from, to }, 'year-weekday')

Aktuální období vrací i filtr.getCompare(). Bez volby compare je to vždycky null — porovnávání je ve výchozím stavu vypnuté.

Kam picker pověsit

Na jeden input

Výchozí případ. Rozsah se do jednoho pole zapíše jako 10.–16. 8. 2026.

Na dvě pole

Když má formulář zvlášť „od" a zvlášť „do", drží je jeden picker pohromadě — panel otevře kterékoli z nich a výběr se rozdělí do obou:

new Gregory('#od', {
  mode: 'range',
  endInput: '#do',
})

Na cokoli jiného

Datum bývá i mimo formulář — v odznaku, v buňce tabulky. Picker umí viset na jakémkoli prvku: klik ho otevře, potvrzená hodnota se vypíše dovnitř. Když má prvek potomka s data-gr-value, píše se text do něj, takže ikona vedle zůstane:

<span class="badge" id="splatnost" data-value="2026-08-13">
  📅 <span data-gr-value>13. 8. 2026</span>
</span>
const picker = new Gregory('#splatnost', { mode: 'date' })
splatnost.addEventListener('gregory:change', (e) => ulozit(e.detail.value))

Natrvalo rozbalený

inline: true vykreslí panel na místo cílového prvku místo popoveru. Hodí se do postranních filtrů; s summary: true má i řádek, který slovy říká, co je vybrané.

Deklarativně, v HTML

Custom element se nastavuje atributy; JavaScript je potřeba jen jednou, na jeho registraci:

<gregory-picker mode="range" locale="cs" months="2"></gregory-picker>
import { defineElement } from '@svatekr70/gregory'
defineElement()

Jazyk a formáty

Bez nastavení si Gregory vezme jazyk prohlížeče. Explicitně stačí BCP 47 tag:

new Gregory('#termin', { locale: 'cs' })

Názvy měsíců, dnů, formát data i skloňování počtu dnů řeší Intl, takže fungují pro jakýkoli jazyk. Vlastní překlad mají popisky tlačítek — hotové jsou pro češtinu, slovenštinu, angličtinu, němčinu, polštinu, španělštinu, francouzštinu a italštinu. Ostatní jazyky dostanou anglické popisky, dokud jim nějaké nedodáš:

import { registerTranslation } from '@svatekr70/gregory'

registerTranslation('nl', { labels: { apply: 'Toepassen', … }, presets: { … }, days: { … } })

Přejmenovat jedno tlačítko jde i bez překladu — locale bere i objekt, který se přiloží na vyřešený jazyk:

new Gregory('#termin', {
  locale: { code: 'cs', labels: { apply: 'Rezervovat' } },
})

Text v poli si můžeš napsat úplně sám:

new Gregory('#termin', {
  format: (value) => value ? `termín ${formatISODate(value)}` : '',
})

Vzhled

Panel nemá stínový DOM, takže se dá stylovat běžným CSS. Skoro všechno ale jde přenastavit proměnnými, což je odolnější než přepisovat jednotlivá pravidla:

.gr {
  --gr-accent: #0e7c86;
  --gr-radius-day: 999px;
  --gr-day-size: 26px;
}

Šířku panelu určuje velikost políčka dne, ne dostupné místo. Hustotu tedy měň přes --gr-day-size, --gr-gap a --gr-pad, ne pevnou šířkou.

Den je ve výchozím stavu čtverec, protože --gr-day-height vychází z --gr-day-size. Když se pod čísla vykreslují značky (vytížené dny), zvedni jen výšku — sloupce se řídí pořád šířkou, takže panel nenaroste do stran:

.gr {
  --gr-day-height: 38px;
  --gr-day-badge-size: 11px;
}

Panel popoveru žije na <body>, takže se k němu selektorem přes rodiče nedostaneš. Vlastní třídu mu dá className — tudy se nasazují i hotové motivy:

new Gregory('#termin', { className: 'gr-theme-clinic gr-density-compact' })

Motivy (blueprint, riso, clinic, nocturne) a stupně hustoty jsou v themes.css. Všechny jsou jen sadou proměnných, takže si vlastní motiv uděláš zkopírováním kteréhokoli z nich.

Tmavý režim

Výchozí motiv reaguje na prefers-color-scheme sám. Když má aplikace vlastní přepínač, přenastav proměnné pod svou třídou: .dark .gr { --gr-bg: #12141a; --gr-fg: #e8eaed; … }

Klávesnice

Panel je ovladatelný bez myši a to je součástí veřejného chování — nemusíš nic zapínat.

KlávesaCo udělá
v poliotevře panel a skočí do mřížky
šipkyposun po dnech a týdnech
PageUp / PageDowno měsíc zpět a vpřed
Entervybere den; v poli přečte napsané datum a zavře
Esczahodí rozpracovaný výběr, zavře panel a vrátí fokus do pole

Datum se dá i prostě napsat: 13. 8. 2026, 13.8. (doplní se letošní rok) i 2026-08-13. Nesrozumitelný text se vrátí na poslední platnou hodnotu. Když psaní nechceš, vypni ho přes allowTyping: false. Samotné readonly na poli pickeru nevadí — otevře se a datum do pole zapíše. Zamknout ho i tam jde volbou lockOnReadonly: true.

Recept: rezervace ubytování

Příjezd a odjezd, obsazené termíny, nejméně dvě noci a nejvýš čtrnáct, rozsah nesmí přeskočit obsazený den:

import { Gregory, formatISODate, today } from '@svatekr70/gregory'

const obsazeno = new Set(await nactiObsazene())

const picker = new Gregory('#prijezd', {
  mode: 'range',
  locale: 'cs',
  endInput: '#odjezd',
  min: today(),
  minSpan: 3,          // dvě noci
  maxSpan: 15,         // čtrnáct nocí
  isDisabled: (d) => obsazeno.has(formatISODate(d)),
  stopAtDisabled: true,
  submitName: { from: 'prijezd', to: 'odjezd' },
  summary: true,
})

Po prvním kliknutí se zašedí všechno, co by rozsah protáhlo přes obsazený den nebo mimo povolenou délku — uživatel tedy nemá jak vybrat termín, který mu stejně zamítneš.

Recept: filtr v přehledu

Rozbalený panel v postranním sloupci, zkratky typu „Posledních 30 dní" a okamžité potvrzení bez tlačítek:

const filtr = new Gregory('#filtr', {
  mode: 'range',
  locale: 'cs',
  inline: true,
  autoApply: true,
  summary: true,
  presets: true,
  months: 1,
})

filtr.on('apply', ({ value }) => nacistData(value))

Vlastní zkratky nahradí vestavěné. Rozsah se vyhodnocuje až při kliknutí, takže „dnes" zůstane dnešek i po půlnoci:

presets: [
  { label: 'Toto čtvrtletí', range: () => [zacatekCtvrtleti(), today()] },
  { label: 'Loni', range: () => ['2025-01-01', '2025-12-31'] },
]

Když má filtr vracet celé období a ne konkrétní hranice, sáhni rovnou po mode: 'month' nebo 'quarter'. A když se má vybrané období s něčím srovnávat, přidej compare — viz Porovnání období.

Recept: objednání na hodinu

Termín i čas, po půlhodinách, v otevírací době — a ta je v sobotu jiná než ve všední den:

new Gregory('#objednani', {
  mode: 'datetime',
  locale: 'cs',
  min: today(),
  timeStep: 30,
  minTime: '08:00',
  maxTime: '18:00',
  // V sobotu jen dopoledne, v neděli zavřeno.
  timeWindow: (d) => d.getDay() === 6 ? { min: '09:00', max: '12:00' } : null,
  isDisabled: (d) => d.getDay() === 0,
  submitName: 'objednani',
})

Vybraný čas, který by se přesunem na jiný den dostal mimo okno, se srovná dovnitř — z pátečních 17:00 tak v sobotu vyjde 12:00, ne nabídka, kterou nemůžeš splnit.

V patičce je místo Dnes odkaz Nyní: nastaví dnešek i aktuální čas, srovnaný po timeStep do stejného okna. Popisek se dá přejmenovat přes labels.now.

Recept: vytížené dny

Kalendář, ve kterém je pod každým dnem vidět, kolik už je na něj naplánováno — typicky u plánování kontaktů, směn nebo návštěv. Počty vrací dayBadge, barvu podle vytížení nasadí dayClass:

const hovory = new Map([['2026-08-27', 3], ['2026-08-28', 12]])
const pocet = (d) => hovory.get(formatISODate(d)) ?? 0

new Gregory('#plan', {
  mode: 'date',
  locale: 'cs',
  dayBadge: (d) => String(pocet(d)),
  dayClass: (d) => pocet(d) > 8 ? 'je-plno' : pocet(d) > 3 ? 'je-rusno' : null,
  className: 'plan-kontaktu',
})
.plan-kontaktu {
  /* Vyšší buňka, ale stejně široký kalendář. */
  --gr-day-height: 38px;
  --gr-day-badge-size: 11px;
  --gr-day-badge-gap: 3px;
}

.gr-day.je-rusno .gr-day-badge { color: #b45309; }
.gr-day.je-plno .gr-day-badge { color: #b91c1c; }

Řádek pro značku si drží každý den, i ten, kterému dayBadge vrátí null — čísla dnů tak zůstanou na jedné lince a řádek se nerozklíží. Prázdný řetězec místo počtu vykreslí jen tečku, na „tady něco je" bez čísla.

Značka nereaguje na myš, takže den zůstává klikatelný celý. Pro dny přesahující ze sousedních měsíců se dayBadge volá stejně jako dayClass — obchodník plánující přes přelom měsíce tak vidí, že prvního je už nabito. Když o cizí měsíc nestojíš, vrať pro něj null:

dayBadge: (d) => d.getMonth() === mesic ? String(pocet(d)) : null

Recept: migrace ze starého pickeru

Z <input type="date">

Změň typ na text — nativní kalendář by se otevíral přes ten náš — a hodnotu čti z pickeru, ne z pole:

// bylo
<input type="date" name="termin">

// je
<input type="text" id="termin">
new Gregory('#termin', { mode: 'date', locale: 'cs', submitName: 'termin' })

Skryté pole ze submitName posílá stejný ISO tvar, jaký posílalo nativní pole, takže server se měnit nemusí.

Z jQuery daterangepicker

daterangepickerGregory
startDate, endDatevalue: { from, to }
minDate, maxDatemin, max
dateLimitmaxSpan
singleDatePickermode: 'date'
timePickermode: 'datetime' / 'datetime-range'
timePickerIncrementtimeStep
rangespresets
autoApplyautoApply
locale.formatřeší locale, jinak format
on('apply.daterangepicker')picker.on('apply', …)

Největší rozdíl: Gregory nevrací momenty ani řetězce, ale Date. A nepotřebuje jQuery ani moment.js, takže s ním z projektu často zmizí dvě další závislosti.

Recept: React, Vue, Svelte

Gregory je obyčejná třída, takže do komponenty patří tam, kde vzniká DOM, a v úklidu se musí zavolat destroy(). V Reactu:

function DateField({ value, onChange }) {
  const ref = useRef(null)
  const picker = useRef(null)

  useEffect(() => {
    picker.current = new Gregory(ref.current, { mode: 'date', locale: 'cs' })
    picker.current.on('apply', (e) => onChange(e.value))
    return () => picker.current.destroy()
  }, [])

  // Hodnota shora dovnitř, bez události zpátky — jinak vznikne smyčka.
  useEffect(() => picker.current?.setValue(value, { silent: true }), [value])

  return <input ref={ref} type="text" />
}

Dvě věci, na kterých to obvykle padá: efekt s prázdným polem závislostí (jinak se picker přestavuje při každém renderu) a { silent: true } při zápisu hodnoty shora. Ve Vue je to onMounted / onBeforeUnmount, ve Svelte onMount s návratovou funkcí — princip je stejný.

Renderování na serveru

Balíček definuje custom element, takže samotný import potřebuje DOM — v Node spadne na HTMLElement is not defined. V Next.js, Nuxtu a spol. si ho proto natáhni až v prohlížeči: dynamickým importem uvnitř efektu, nebo komponentou vyloučenou ze serverového renderu (next/dynamic s ssr: false).

Řešení potíží

Panel se neotevře

Pole je disabled, nebo má picker disabled: true či lockOnReadonly: true nad readonly polem. Když chceš zabránit jen psaní, použij allowTyping: false.

Panel vypadá rozsypaně

Nejspíš chybí gregory.css. Druhá možnost: stránka má globální pravidlo na značky, ze kterých je panel poskládaný. Gregory se proti tomu brání resetem s nulovou specificitou, ale section { padding: 52px 0 !important } ani ten reset nepřebije.

Poslední sloupec dnů není vidět

Panel je v užším místě, než je jeden měsíc. Neořízne se — dá se odrolovat do strany — ale lepší je dát mu místo: zmenši --gr-day-size, sniž months na 1 nebo vypni presets.

Značky pod dny se do políčka nevejdou

Výchozí den je čtverec 24 × 24 px a číslo se značkou se do něj vejde jen tak tak. Zvedni --gr-day-height — šířku sloupců drží --gr-day-size, takže se kalendář nerozšíří. Písmo značky má vlastní proměnnou --gr-day-badge-size.

Datum se posunulo o den

Skoro jistě někde v kódu je new Date('2026-08-13'), což je půlnoc v UTC. Používej createDate(), nebo ISO řetězec předej rovnou pickeru, který ho čte v místním čase.

Hodnota v poli je jiná než v databázi

V poli je datum pro lidi, podle jazyka. Pro server ber picker.getValue() a formatISODate(), nebo nech pracovat submitName.

change se ozývá pořád dokola

change padá při každém kliknutí, i uprostřed rozsahu. Na ukládání je apply. A hodnotu nastavovanou zvenčí posílej se { silent: true }, jinak si posluchače vyvoláš sám.

Picker zůstal viset po přepnutí stránky

Panel popoveru žije na <body>, takže odstranění pole ho samo neuklidí. V SPA vždycky volej picker.destroy().

Kam dál

Demo má konfigurátor, ve kterém si každou volbu naklikáš a rovnou vidíš kód. API dokumentace je úplný výčet voleb, metod, událostí, CSS proměnných a typů. Repozitář bere hlášení chyb i nápady.