Picker data, rozsahu dat, času a období pro obyčejné weby. Vznikl z otravné
situace, kterou zná asi každý, kdo spravuje víc než jeden projekt: tady jQuery
daterangepicker, tam nativní <input type="date">, jinde něco třetího. Každý
vypadá jinak, jinak se ovládá a jinak vrací hodnotu. Gregory je jedna
komponenta, která to všechno zvládne — bez jediné runtime závislosti a bez
toho, aby si diktovala, jakým frameworkem je stránka postavená.
const picker = new Gregory('#termin', { mode: 'range', locale: 'cs' })
picker.on('apply', ({ value }) => console.log(value.from, value.to))Do pole se napíše 10.–16. 8. 2026, ven vypadnou dva obyčejné Date. Žádné
momenty, žádné řetězce, které se pak musí luštit.
- Osm režimů, jedno API. Jedno datum, rozsah, datum s časem, rozsah
s časem, seznam samostatných dnů, měsíc, čtvrtletí, rok. Přepíná se jedinou
volbou
mode, zbytek zůstává stejný. - ~17 kB gzip JavaScriptu a 2,6 kB stylů. Žádné závislosti — ani jQuery, ani knihovna na práci s daty.
- Funguje všude. Ve staré jQuery aplikaci stejně jako v Reactu; vedle třídy
je i custom element
<gregory-picker>pro deklarativní použití. - Omezení, která dávají smysl v praxi.
min/max, zakázané dny, nejkratší i nejdelší rozsah, zákaz přeskočit obsazený termín, časové okno zvlášť pro každý den. Co picker nepustí do výběru, to nepustí ani do hodnoty. - Lokalizace přes
Intl. Názvy měsíců, formáty a skloňování počtu dnů fungují pro jakýkoli jazyk; popisky tlačítek jsou hotové pro osm z nich. - Vzhled přes CSS proměnné. Motivy i hustota jsou jen sady
--gr-*, takže se nikdy nemusíš prát o specificitu. Tmavý režim automaticky. - Ovládání klávesnicí. Šipky, PageUp/PageDown, Enter, Escape — a datum jde do pole i prostě napsat rukou.
- Místní čas, žádná magie. Datum, na které uživatel klikne, je to datum, které dostaneš. Časové zóny knihovna vědomě neřeší.
- 300+ testů ve Vitestu nad jádrem i nad DOM, pokrytí přes 90 % příkazů.
Dokumentace: svatekr70.github.io/gregory —
demo s konfigurátorem,
uživatelská příručka na nasazení
krok za krokem a API reference
s úplným výčtem voleb, metod, událostí a CSS proměnných. Web se dá spustit
i lokálně přes npm run site:dev.
Knihovna se instaluje rovnou z GitHubu — do npm registru se vydávat nebude.
npm si ji po naklonování sestaví sám (skript prepare), nic se nemusí
buildit ručně:
npm install github:svatekr70/gregory # poslední main
npm install github:svatekr70/gregory#v0.3.0 # konkrétní verzeJméno balíčku je @svatekr70/gregory, takže importy vypadají obvykle:
import { Gregory } from '@svatekr70/gregory'
import '@svatekr70/gregory/style.css'
import '@svatekr70/gregory/themes.css' // nepovinné, jen hotové motivySestavená knihovna se publikuje s dokumentací, takže jde načíst z URL. Soubory
odpovídají poslední verzi na main:
<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:
<link rel="stylesheet" href="https://svatekr70.github.io/gregory/dist/gregory.css">
<script type="module">
import { Gregory } from 'https://svatekr70.github.io/gregory/dist/gregory.js'
new Gregory('#termin', { mode: 'date', locale: 'cs' })
</script>Pro produkci, kde nechceš viset na cizí adrese ani na posledním commitu, si stáhni přílohu vydání a soubory si nahraj k sobě.
const picker = new Gregory('#input', {
mode: 'range',
locale: 'cs',
maxSpan: 31,
})
picker.on('apply', ({ value }) => {
console.log(value.from, value.to)
})<span id="termin" data-value="2026-08-13" data-placeholder="Nezadáno">
📅 <b data-gr-value>13. 8. 2026</b>
</span>new Gregory('#termin', { mode: 'date' })Klik nebo Enter otevře panel, potvrzená hodnota se vypíše do [data-gr-value]
(nebo do prvku, když takový potomek není) a strojová podoba do data-value.
dayBadge vrací krátký text (nebo prázdný řetězec pro tečku), který se vykreslí
pod číslo dne — kolik je ten den rezervací, hovorů, směn:
const CALLS = new Map([['2026-08-27', 3], ['2026-08-28', 12]])
new Gregory('#termin', {
mode: 'date',
locale: 'cs',
dayBadge: (date) => {
const count = CALLS.get(formatISODate(date))
return count ? String(count) : null
},
// Vyšší buňka dá značce vzduch. Šířku sloupců drží --gr-day-size, kalendář
// tedy zůstane stejně široký.
className: 'kalendar-s-pocty',
}).kalendar-s-pocty {
--gr-day-height: 38px;
--gr-day-badge-size: 11px;
}Barvu značky si obarvíš podle vytížení přes dayClass:
dayClass: (date) => ((CALLS.get(formatISODate(date)) ?? 0) > 8 ? 'je-plno' : null).gr-day.je-plno .gr-day-badge { color: #b91c1c; }dayBadge se volá i pro dny přesahující ze sousedních měsíců — stejně jako
dayClass — aby konec měsíce nebyl obarvený, ale bez čísla. Značka v nich zdědí
ztlumení .is-outside. Když značky mimo zobrazený měsíc nechceš, vrať pro ně
null.
Analytické reporty skoro vždycky potřebují ke zvolenému období ještě to
předchozí. compare ho dopočítá z hlavního rozsahu — nevybírá se, jen se
v mřížce vyznačí pruhem pod dny a je k dispozici v hodnotě:
const picker = new Gregory('#obdobi', {
mode: 'range',
locale: 'cs',
summary: true,
compare: 'previous',
})
picker.on('apply', ({ value, compare }) => {
nacti(value.from, value.to)
if (compare) nacti(compare.from, compare.to) // { from: Date, to: Date }
})| hodnota | co spočítá |
|---|---|
'previous' (nebo true) |
stejně dlouhé období těsně před začátkem |
'year' |
stejná data o rok zpět |
'year-weekday' |
posun o 364 dní, takže sedí dny v týdnu |
(range) => [from, to] |
vlastní výpočet; null znamená „neporovnávat" |
Dvě věci, které dělá jinak, než by čekal prostý odečet dnů:
- Celý kalendářní celek se porovnává s celým předchozím celkem. Únor je kratší než leden, takže „předchozí období" k 1.–28. 2. by po dnech vyšlo na 4.–31. 1. Vybraný celý měsíc, čtvrtletí i rok proto vrací celý předchozí měsíc, čtvrtletí, rok.
'year-weekday'posouvá o 52 týdnů, ne o rok. Data nesedí, ale pondělí padne na pondělí — což je to, co potřebují týdenní a prodejní reporty.'year'naopak drží data a konce měsíců: 29. 2. 2024 vyjde na 28. 2. 2023.
Matematika je čistá funkce, takže se dá použít i mimo picker — třeba když stejný výpočet potřebuje i dotaz na serveru:
import { comparePeriod } from '@svatekr70/gregory'
comparePeriod({ from: new Date(2026, 1, 1), to: new Date(2026, 1, 28) }, 'previous')
// → { from: 1. 1. 2026, to: 31. 1. 2026 }Barvu pruhu drží --gr-compare, jeho tloušťku --gr-compare-bar. Porovnávat
jde jen v režimech rozsahu; jinde je compare bez efektu.
import { defineElement } from '@svatekr70/gregory'
defineElement()<gregory-picker mode="range" locale="cs" months="2" value="2026-08-01/2026-08-13">
</gregory-picker>Element vypisuje gregory:change, gregory:apply, gregory:open a
gregory:close jako bublající CustomEvent, hodnota je v event.detail.value.
Atribut compare zapne srovnávací období — compare bez hodnoty znamená
previous.
| volba | výchozí | popis |
|---|---|---|
mode |
'date' |
date, range, datetime, datetime-range, multiple, month, quarter, year |
className |
— | vlastní třídy pro kořen panelu (takhle se aplikují motivy) |
value |
null |
Date, ISO string, {from,to} nebo [from, to] |
locale |
jazyk prohlížeče | BCP 47 tag nebo částečný objekt Locale |
min / max |
null |
hranice výběru |
firstDayOfWeek |
podle locale | 0 = neděle … 6 = sobota |
months |
2 v range módu, jinak 1 |
počet panelů vedle sebe |
linkedCalendars |
false |
listovat všemi panely najednou místo každým zvlášť |
weekNumbers |
false |
sloupec s ISO čísly týdnů |
showOutsideDays |
true |
zobrazovat dny přesahující ze sousedních měsíců |
weekSelection |
'off' |
výběr celého týdne: 'number' klikem na číslo týdne, 'day' klikem na kterýkoli den, 'both' obojí |
dropdowns |
false |
výběr měsíce a roku: true nativní <select>, 'menu' seznam po kliknutí na caption |
endInput |
— | druhé pole pro konec rozsahu (from do prvního, to do druhého) |
allowTyping |
true |
číst datum napsané rukou do pole |
submitName |
— | skrytá pole s ISO hodnotou pro odeslání formuláře |
disabled |
false |
zamkne picker; totéž udělá disabled na poli |
lockOnReadonly |
false |
zamknout i nad polem s readonly (jinak se nad ním picker normálně otevře) |
inline |
false |
vykreslit na místo místo popoveru |
autoApply |
true jen v módu date |
potvrdit hned, bez tlačítek Apply/Cancel |
presets |
vestavěné v range módu | postranní zkratky, false je skryje |
compare |
false |
srovnávací období: 'previous', 'year', 'year-weekday' nebo vlastní funkce (viz Srovnávací období) |
maxSpan |
null |
nejdelší povolený rozsah ve dnech |
minSpan |
null |
nejkratší povolený rozsah ve dnech |
stopAtDisabled |
false |
rozsah nesmí přeskočit den zakázaný přes isDisabled |
allowOpenRange |
false |
povolí rozsah otevřený na jednom konci ({ from, to: null }) |
maxSelected |
null |
nejvíc dnů v režimu multiple |
timeStep |
5 |
krok minut v časových režimech |
timeUi |
'select' |
ovládání času: 'select' selecty, 'slider' posuvníky, 'input' nativní pole |
minTime / maxTime |
null |
okno dne, 'HH:MM', včetně obou hranic |
timeWindow |
— | (date) => { min, max } — okno dne pro konkrétní den |
fullscreenBelow |
480 |
pod touto šířkou okna se panel otevře přes celou obrazovku |
opens / drops |
'right' / 'auto' |
umístění popoveru |
isDisabled |
— | (date) => boolean |
dayClass |
— | (date) => string | null, např. svátky |
dayBadge |
— | (date) => string | null — značka pod číslem dne (viz Vzhled) |
format |
— | (value, locale) => string pro text v inputu |
summary |
false |
řádek v panelu s právě vybranými daty (true nebo vlastní funkce) |
picker.getValue() // Date | DateRange | Date[] | null — potvrzená hodnota
picker.getSelection() // rozpracovaný výběr (v range módu i poloviční)
picker.getCompare() // { from, to } | null — srovnávací období k hodnotě
picker.setValue(value, { silent })
picker.clear()
picker.setOptions(patch)
picker.goTo('2026-12-01')
picker.openPanel() / picker.close() / picker.toggle()
picker.apply() / picker.cancel()
picker.on(event, listener) // vrací odhlašovací funkci
picker.destroy()Události: change (s příznakem complete), apply, cancel, open,
close, invalid (hodnota neprošla omezeními), month-change. change
a apply nesou i compare — srovnávací období, nebo null.
Názvy měsíců, zkratky dnů, první den v týdnu i formát data řeší Intl,
takže fungují pro jakýkoli jazyk. Popisky tlačítek a presetů knihovna nese
sama — pro cs, sk, de, pl, en, es, fr, it. Rozhoduje jazyk,
ne region, takže de-AT dostane němčinu. Ostatní jazyky mají popisky
anglicky.
Chybějící jazyk se dodá zvenčí:
import { registerTranslation } from '@svatekr70/gregory'
registerTranslation('ja', {
labels: { apply: '適用', cancel: 'キャンセル', today: '今日', now: '現在', /* … */ },
presets: { today: '今日', yesterday: '昨日', /* … */ },
days: { other: '日' },
})Všechno jsou CSS proměnné na .gr, není potřeba přebíjet selektory:
.gr {
--gr-accent: #0f766e;
--gr-range-bg: #ccfbf1;
--gr-radius: 14px;
}Panel je light DOM — schválně, protože přes shadow DOM by se barvení
proměnnými spíš komplikovalo. Kalendář uvnitř používá běžné značky
(<header>, <section>, <footer>, <aside>, <button>…), takže by ho
chytly i holé elementové selektory hostitelské stránky. Knihovna je proto
hned na začátku nuluje pravidlem přes :where() — typografii i rozvržení,
včetně flex-direction a rozměrů:
/* Tohle panel nerozhodí. */
header { display: flex; flex-direction: column; gap: 1rem; }
section { padding: 52px 0; }:where() má nulovou specificitu, takže vlastní úpravy přes .gr-* mají dál
přednost bez souboje o specificitu. Nenulují se schválně dvě věci:
display (přepsat ho pro všechny značky najednou by rozbilo tlačítka
i inputy) a text-align (čísla dnů jsou vycentrovaná od prohlížeče).
Hustotu drží čtyři proměnné — velikost dne, mezera, odsazení a písmo.
Ostatní odsazení se z --gr-pad dopočítává:
.gr {
--gr-day-size: 21px;
--gr-gap: 0;
--gr-pad: 7px;
--gr-font-size: 13px;
/* Čísla dnů se odvozují z velikosti políčka (výchozí 50 %), ne ze základního
písma — prázdno okolo číslice je poměr, ne odsazení. */
--gr-day-font-size: calc(var(--gr-day-size) * 0.6);
}Hotové stupně jsou v themes.css jako gr-density-compact
a gr-density-comfortable; barvy neřeší, takže se s motivy kombinují.
Den je čtverec o hraně --gr-day-size — ta zároveň určuje šířku sloupců,
a tím i celého panelu. Když je potřeba vyšší buňka (typicky kvůli dayBadge),
zvedni jen výšku; šířka kalendáře zůstane stejná:
.gr {
--gr-day-height: 38px; /* výchozí je var(--gr-day-size) */
--gr-day-badge-gap: 3px; /* mezera mezi číslem a značkou */
--gr-day-badge-size: 11px; /* písmo značky a výška jejího řádku */
}Řádek pro značku si v takové mřížce drží i dny, které žádnou nedostaly, takže čísla v řádku sedí na jedné lince.
Srovnávací období (compare) se kreslí pruhem při spodní hraně dne, aby se
nepralo s výplní výběru — barva je --gr-compare, tloušťka --gr-compare-bar.
Tmavý režim se aktivuje sám podle prefers-color-scheme, nebo natvrdo přes
data-theme="dark" / data-theme="light" na kořenovém prvku pickeru.
import '@svatekr70/gregory/style.css'
import '@svatekr70/gregory/themes.css' // vždy až po style.css
new Gregory('#vstup', { className: 'gr-theme-riso' })| Třída | Charakter |
|---|---|
gr-theme-blueprint |
technický výkres — tmavě modrá, monospace, hustá mřížka |
gr-theme-riso |
dvoubarevný tisk — papír, fluorescentní růžová, posunutý stín |
gr-theme-clinic |
objednávkový systém — vzdušná bílá, modrozelená, dny jako pilulky |
gr-theme-nocturne |
noční provoz — skoro černá s teplým jantarem |
Žádný z nich nepřepisuje selektor komponenty, mění jen proměnné --gr-*.
npm run dev # vývojový playground
npm test # vitest
npm run test:coverage
npm run build # typecheck + ESM/UMD/d.ts do dist/
npm run site:dev # projektový web (úvod, demo, příručka, API dokumentace)
npm run site:build # statický web do dist-site/Web v site/ importuje knihovnu přímo ze src/, takže demo vždy ukazuje
aktuální kód. dist-site/ je čistě statický — nahraje se kamkoli.
Moderní evergreen prohlížeče — Chrome a Edge 90+, Firefox 88+, Safari 15+.
Knihovna se sestavuje na ES2022 a nepoužívá polyfilly. Intl.Locale#getWeekInfo()
(první den v týdnu) zatím neumí každý prohlížeč, takže na něj existuje záložní
tabulka.
MIT © Rudolf Svátek