@konce-pt/datetime 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 konce.pt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ <p align="center">
2
+ <img src="https://ui.konce.pt/images/koncept_ui_lib.png" alt="Koncept UI — Angular component library" width="100%" />
3
+ </p>
4
+
5
+ # @konce-pt/datetime
6
+
7
+ Rdzeń kalendarza i zegara dla kontrolek daty [Koncept UI](https://gitlab.com/konce-pt/koncept-ui).
8
+ Czysty TypeScript, **zero zależności runtime**, bez wiązania z frameworkiem — siatki dni, miesięcy,
9
+ lat i dekad, pasma zakresów oraz znaczniki tarczy zegara.
10
+
11
+ Zwykle nie instalujesz go samodzielnie: `@konce-pt/react` używa go pod `KptDatepicker`,
12
+ `KptDateRange` i `KptClock`. Sięgnij po niego wprost, gdy budujesz własną kontrolkę daty
13
+ na tym samym zachowaniu.
14
+
15
+ ```bash
16
+ npm i @konce-pt/datetime
17
+ ```
18
+
19
+ ```ts
20
+ import { buildDayGrid, pickRangeDay, formatDate } from '@konce-pt/datetime';
21
+
22
+ // Sześć pełnych tygodni od poniedziałku — panel nie zmienia wysokości przy przewijaniu.
23
+ const weeks = buildDayGrid({
24
+ page: { y: 2026, m: 8 },
25
+ selectedIso: '2026-09-10',
26
+ todayIso: '2026-09-06',
27
+ minDate: '2026-09-01',
28
+ });
29
+
30
+ // Protokół klikania zakresu: start → koniec → od nowa, z normalizacją kolejności.
31
+ pickRangeDay({ start: '2026-09-10', end: '' }, '2026-09-05');
32
+ // { start: '2026-09-05', end: '2026-09-10' }
33
+
34
+ formatDate('2026-09-06', 'pl'); // '06.09.2026'
35
+ ```
36
+
37
+ ## Wartości to stringi, nie `Date`
38
+
39
+ Każdy format jest tekstowy: `'RRRR-MM-DD'`, `'RRRR-MM'`, `'RRRR'`, `'MM'`, `'RRRR-MM-DDTHH:mm'`,
40
+ `'HH:mm'`. To one trafiają do formularza i na serwer, i nie niosą strefy, którą kalendarz mógłby
41
+ zgubić. `Date` służy wyłącznie do arytmetyki lokalnej — `toIso()` czyta datę lokalną, bo
42
+ `toISOString()` przesunąłby dzień o offset strefy.
43
+
44
+ ## API
45
+
46
+ - **Daty i godziny** — `pad`, `toIso`, `parseDate`, `parseTime`, `parseDateTime`, `datePart`,
47
+ `timePart`, `formatDate`, `formatTime`, `from12h`
48
+ - **Stos widoków** — `KPT_CALENDAR_LEVELS`, `commitLevelFor`, `topLevelFor`, `resolveStartView`,
49
+ `initialPage`, `pageBy`, `canPageBack`, `canPageForward`, `buildDayGrid`, `buildMonthCells`,
50
+ `buildYearCells`, `buildDecadeCells`, `selectedYearMonth`, `isYearDisabled`,
51
+ `isMonthYearDisabled`, `decadeStart`, `centuryStart`
52
+ - **Zakresy** — `buildRangeGrid`, `pickRangeDay`, `clampVisibleMonths`
53
+ - **Tarcza zegara** — `hourMarks`, `minuteMarks`, `hourFromAngle`, `minuteFromAngle`, `handAngle`,
54
+ `handIsInner`, `markPosition`
55
+
56
+ Pełny opis: [`llms.txt`](./llms.txt) (EN) i [`llms-pl.txt`](./llms-pl.txt) (PL).
57
+
58
+ ## Testy
59
+
60
+ ```bash
61
+ pnpm --filter @konce-pt/datetime exec node --test "src/**/*.test.ts"
62
+ ```
63
+
64
+ ## Licencja
65
+
66
+ MIT © [konce.pt](https://konce.pt)
@@ -0,0 +1,97 @@
1
+ /** Granulacja wyboru w kontrolce daty. */
2
+ export type KptDateSelectionMode = 'date' | 'monthYear' | 'year' | 'month' | 'datetime' | 'time';
3
+ /** Poziom panelu: dni → miesiące → lata → dekady. */
4
+ export type KptCalendarView = 'day' | 'month' | 'year' | 'decade';
5
+ /** Kolejność poziomów panelu, od najdrobniejszego. Indeks = „wysokość" widoku. */
6
+ export declare const KPT_CALENDAR_LEVELS: readonly ["day", "month", "year", "decade"];
7
+ /** Widoczna strona kalendarza: rok + miesiąc (0–11). */
8
+ export interface KptCalendarPage {
9
+ y: number;
10
+ m: number;
11
+ }
12
+ export interface KptDayCell {
13
+ day: number;
14
+ iso: string;
15
+ inMonth: boolean;
16
+ today: boolean;
17
+ selected: boolean;
18
+ disabled: boolean;
19
+ }
20
+ /** Komórka siatki miesięcy/lat/dekad — niesie cel nawigacji (rok + ewentualny miesiąc). */
21
+ export interface KptGridCell {
22
+ label: string;
23
+ y: number;
24
+ m: number;
25
+ selected: boolean;
26
+ /** Zawiera dzisiejszą datę — obrys jak „dziś" w siatce dni. */
27
+ current: boolean;
28
+ disabled: boolean;
29
+ }
30
+ /** Poziom, na którym klik w komórkę zatwierdza wartość zamiast schodzić niżej. */
31
+ export declare function commitLevelFor(mode: KptDateSelectionMode): KptCalendarView;
32
+ /** Tryb 'month' nie ma roku ani dekady — wartością jest samo 'MM'. */
33
+ export declare function topLevelFor(mode: KptDateSelectionMode): KptCalendarView;
34
+ /**
35
+ * Poziom startowy panelu, przycięty do zakresu trybu: nigdy poniżej poziomu zatwierdzania
36
+ * (`startView="day"` w trybie 'year' nie ma sensu) i nigdy powyżej szczytu stosu.
37
+ */
38
+ export declare function resolveStartView(mode: KptDateSelectionMode, wanted: KptCalendarView | null): KptCalendarView;
39
+ /** Początek dekady widocznej w siatce lat (1979 → 1970). */
40
+ export declare function decadeStart(year: number): number;
41
+ /** Początek strony dekad: 12 dekad, czyli 120 lat (1979 → 1920, strona 1920–2039). */
42
+ export declare function centuryStart(year: number): number;
43
+ /** Strzałki ‹ › — krok zależny od poziomu: miesiąc, rok, dekada, strona dekad. */
44
+ export declare function pageBy(page: KptCalendarPage, level: KptCalendarView, dir: 1 | -1): KptCalendarPage;
45
+ /** Strzałka wstecz gaśnie, gdy cała poprzednia strona wypada przed `minDate`. */
46
+ export declare function canPageBack(page: KptCalendarPage, level: KptCalendarView, minDate: string | null): boolean;
47
+ /** Strzałka w przód gaśnie, gdy cała następna strona wypada za `maxDate`. */
48
+ export declare function canPageForward(page: KptCalendarPage, level: KptCalendarView, maxDate: string | null): boolean;
49
+ export declare function isYearDisabled(year: number, minDate: string | null, maxDate: string | null): boolean;
50
+ export declare function isMonthYearDisabled(year: number, monthIndex: number, minDate: string | null, maxDate: string | null): boolean;
51
+ export interface DayGridOptions {
52
+ page: KptCalendarPage;
53
+ selectedIso: string;
54
+ todayIso: string;
55
+ minDate?: string | null;
56
+ maxDate?: string | null;
57
+ }
58
+ /**
59
+ * Siatka dni — zawsze sześć pełnych tygodni od poniedziałku, także gdy część wypada poza miesiąc.
60
+ * Stała liczba wierszy trzyma wysokość panelu, więc przewijanie miesięcy nie skacze.
61
+ */
62
+ export declare function buildDayGrid({ page, selectedIso, todayIso, minDate, maxDate, }: DayGridOptions): KptDayCell[][];
63
+ /** Rok i miesiąc bieżącej wartości — zaznaczenie w siatkach wyższych poziomów. */
64
+ export declare function selectedYearMonth(value: string, mode: KptDateSelectionMode): {
65
+ y: number | null;
66
+ m: number | null;
67
+ };
68
+ export interface MonthCellsOptions {
69
+ year: number;
70
+ monthNames: string[];
71
+ selected: {
72
+ y: number | null;
73
+ m: number | null;
74
+ };
75
+ today: Date;
76
+ monthOnly?: boolean;
77
+ numeric?: boolean;
78
+ minDate?: string | null;
79
+ maxDate?: string | null;
80
+ }
81
+ export declare function buildMonthCells({ year, monthNames, selected, today, monthOnly, numeric, minDate, maxDate, }: MonthCellsOptions): KptGridCell[];
82
+ export interface YearCellsOptions {
83
+ year: number;
84
+ selected: {
85
+ y: number | null;
86
+ m: number | null;
87
+ };
88
+ today: Date;
89
+ minDate?: string | null;
90
+ maxDate?: string | null;
91
+ }
92
+ /** Strona lat to pełna dekada: 10 komórek, 1970–1979. */
93
+ export declare function buildYearCells({ year, selected, today, minDate, maxDate, }: YearCellsOptions): KptGridCell[];
94
+ /** Strona dekad to 12 dekad, czyli 120 lat. */
95
+ export declare function buildDecadeCells({ year, selected, today, minDate, maxDate, }: YearCellsOptions): KptGridCell[];
96
+ /** Strona kalendarza, od której otwiera się panel dla danej wartości i trybu. */
97
+ export declare function initialPage(value: string, mode: KptDateSelectionMode, today: Date): KptCalendarPage;
@@ -0,0 +1,223 @@
1
+ import { pad, parseDate, toIso } from "./iso.js";
2
+ /** Kolejność poziomów panelu, od najdrobniejszego. Indeks = „wysokość" widoku. */
3
+ export const KPT_CALENDAR_LEVELS = ['day', 'month', 'year', 'decade'];
4
+ /** Poziom, na którym klik w komórkę zatwierdza wartość zamiast schodzić niżej. */
5
+ export function commitLevelFor(mode) {
6
+ switch (mode) {
7
+ case 'monthYear':
8
+ case 'month':
9
+ return 'month';
10
+ case 'year':
11
+ return 'year';
12
+ default:
13
+ return 'day';
14
+ }
15
+ }
16
+ /** Tryb 'month' nie ma roku ani dekady — wartością jest samo 'MM'. */
17
+ export function topLevelFor(mode) {
18
+ return mode === 'month' ? 'month' : 'decade';
19
+ }
20
+ /**
21
+ * Poziom startowy panelu, przycięty do zakresu trybu: nigdy poniżej poziomu zatwierdzania
22
+ * (`startView="day"` w trybie 'year' nie ma sensu) i nigdy powyżej szczytu stosu.
23
+ */
24
+ export function resolveStartView(mode, wanted) {
25
+ const commit = KPT_CALENDAR_LEVELS.indexOf(commitLevelFor(mode));
26
+ const top = KPT_CALENDAR_LEVELS.indexOf(topLevelFor(mode));
27
+ const index = wanted ? KPT_CALENDAR_LEVELS.indexOf(wanted) : commit;
28
+ return KPT_CALENDAR_LEVELS[Math.min(Math.max(index, commit), top)];
29
+ }
30
+ /** Początek dekady widocznej w siatce lat (1979 → 1970). */
31
+ export function decadeStart(year) {
32
+ return Math.floor(year / 10) * 10;
33
+ }
34
+ /** Początek strony dekad: 12 dekad, czyli 120 lat (1979 → 1920, strona 1920–2039). */
35
+ export function centuryStart(year) {
36
+ return Math.floor(year / 120) * 120;
37
+ }
38
+ /** Strzałki ‹ › — krok zależny od poziomu: miesiąc, rok, dekada, strona dekad. */
39
+ export function pageBy(page, level, dir) {
40
+ const { y, m } = page;
41
+ switch (level) {
42
+ case 'month':
43
+ return { y: y + dir, m };
44
+ case 'year':
45
+ return { y: y + dir * 10, m };
46
+ case 'decade':
47
+ return { y: y + dir * 120, m };
48
+ default: {
49
+ const next = m + dir;
50
+ if (next < 0)
51
+ return { y: y - 1, m: 11 };
52
+ if (next > 11)
53
+ return { y: y + 1, m: 0 };
54
+ return { y, m: next };
55
+ }
56
+ }
57
+ }
58
+ /** Strzałka wstecz gaśnie, gdy cała poprzednia strona wypada przed `minDate`. */
59
+ export function canPageBack(page, level, minDate) {
60
+ if (!minDate)
61
+ return true;
62
+ const { y, m } = page;
63
+ switch (level) {
64
+ case 'month':
65
+ return `${y - 1}-12` >= minDate.slice(0, 7);
66
+ case 'year':
67
+ return decadeStart(y) - 1 >= +minDate.slice(0, 4);
68
+ case 'decade':
69
+ return centuryStart(y) - 1 >= +minDate.slice(0, 4);
70
+ default:
71
+ // Ostatni dzień poprzedniego miesiąca.
72
+ return toIso(new Date(y, m, 0)) >= minDate;
73
+ }
74
+ }
75
+ /** Strzałka w przód gaśnie, gdy cała następna strona wypada za `maxDate`. */
76
+ export function canPageForward(page, level, maxDate) {
77
+ if (!maxDate)
78
+ return true;
79
+ const { y, m } = page;
80
+ switch (level) {
81
+ case 'month':
82
+ return `${y + 1}-01` <= maxDate.slice(0, 7);
83
+ case 'year':
84
+ return decadeStart(y) + 10 <= +maxDate.slice(0, 4);
85
+ case 'decade':
86
+ return centuryStart(y) + 120 <= +maxDate.slice(0, 4);
87
+ default:
88
+ return toIso(new Date(y, m + 1, 1)) <= maxDate;
89
+ }
90
+ }
91
+ export function isYearDisabled(year, minDate, maxDate) {
92
+ if (minDate && year < +minDate.slice(0, 4))
93
+ return true;
94
+ if (maxDate && year > +maxDate.slice(0, 4))
95
+ return true;
96
+ return false;
97
+ }
98
+ export function isMonthYearDisabled(year, monthIndex, minDate, maxDate) {
99
+ const ym = `${year}-${pad(monthIndex + 1)}`;
100
+ if (minDate && ym < minDate.slice(0, 7))
101
+ return true;
102
+ if (maxDate && ym > maxDate.slice(0, 7))
103
+ return true;
104
+ return false;
105
+ }
106
+ /**
107
+ * Siatka dni — zawsze sześć pełnych tygodni od poniedziałku, także gdy część wypada poza miesiąc.
108
+ * Stała liczba wierszy trzyma wysokość panelu, więc przewijanie miesięcy nie skacze.
109
+ */
110
+ export function buildDayGrid({ page, selectedIso, todayIso, minDate = null, maxDate = null, }) {
111
+ const { y, m } = page;
112
+ const first = new Date(y, m, 1);
113
+ const startOffset = (first.getDay() + 6) % 7; // poniedziałek = 0
114
+ const gridStart = new Date(y, m, 1 - startOffset);
115
+ const weeks = [];
116
+ for (let w = 0; w < 6; w++) {
117
+ const row = [];
118
+ for (let d = 0; d < 7; d++) {
119
+ const date = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + w * 7 + d);
120
+ const iso = toIso(date);
121
+ row.push({
122
+ day: date.getDate(),
123
+ iso,
124
+ inMonth: date.getMonth() === m,
125
+ today: iso === todayIso,
126
+ selected: iso === selectedIso,
127
+ disabled: (minDate != null && iso < minDate) || (maxDate != null && iso > maxDate),
128
+ });
129
+ }
130
+ weeks.push(row);
131
+ }
132
+ return weeks;
133
+ }
134
+ /** Rok i miesiąc bieżącej wartości — zaznaczenie w siatkach wyższych poziomów. */
135
+ export function selectedYearMonth(value, mode) {
136
+ switch (mode) {
137
+ case 'month': {
138
+ const match = /^(\d{2})$/.exec(value);
139
+ return { y: null, m: match ? +match[1] - 1 : null };
140
+ }
141
+ case 'year':
142
+ return { y: /^\d{4}$/.test(value) ? +value : null, m: null };
143
+ case 'monthYear': {
144
+ const match = /^(\d{4})-(\d{2})$/.exec(value);
145
+ return match ? { y: +match[1], m: +match[2] - 1 } : { y: null, m: null };
146
+ }
147
+ case 'time':
148
+ return { y: null, m: null };
149
+ default: {
150
+ const date = parseDate(value);
151
+ return date ? { y: date.getFullYear(), m: date.getMonth() } : { y: null, m: null };
152
+ }
153
+ }
154
+ }
155
+ export function buildMonthCells({ year, monthNames, selected, today, monthOnly = false, numeric = false, minDate = null, maxDate = null, }) {
156
+ const todayY = today.getFullYear();
157
+ const todayM = today.getMonth();
158
+ return monthNames.map((name, i) => ({
159
+ label: numeric ? pad(i + 1) : name,
160
+ y: year,
161
+ m: i,
162
+ // W trybie 'month' rok nie istnieje, więc zaznaczenie i „dziś" nie mogą go brać pod uwagę.
163
+ selected: selected.m === i && (monthOnly || selected.y === year),
164
+ current: todayM === i && (monthOnly || todayY === year),
165
+ disabled: monthOnly ? false : isMonthYearDisabled(year, i, minDate, maxDate),
166
+ }));
167
+ }
168
+ /** Strona lat to pełna dekada: 10 komórek, 1970–1979. */
169
+ export function buildYearCells({ year, selected, today, minDate = null, maxDate = null, }) {
170
+ const start = decadeStart(year);
171
+ const todayY = today.getFullYear();
172
+ return Array.from({ length: 10 }, (_, i) => {
173
+ const cellYear = start + i;
174
+ return {
175
+ label: String(cellYear),
176
+ y: cellYear,
177
+ m: 0,
178
+ selected: selected.y === cellYear,
179
+ current: todayY === cellYear,
180
+ disabled: isYearDisabled(cellYear, minDate, maxDate),
181
+ };
182
+ });
183
+ }
184
+ /** Strona dekad to 12 dekad, czyli 120 lat. */
185
+ export function buildDecadeCells({ year, selected, today, minDate = null, maxDate = null, }) {
186
+ const start = centuryStart(year);
187
+ const todayY = today.getFullYear();
188
+ return Array.from({ length: 12 }, (_, i) => {
189
+ const from = start + i * 10;
190
+ const to = from + 9;
191
+ return {
192
+ label: `${from} – ${to}`,
193
+ y: from,
194
+ m: 0,
195
+ selected: selected.y != null && selected.y >= from && selected.y <= to,
196
+ current: todayY >= from && todayY <= to,
197
+ // Dekada wypada z zakresu dopiero, gdy poza nim są oba jej krańce.
198
+ disabled: isYearDisabled(from, minDate, maxDate) && isYearDisabled(to, minDate, maxDate),
199
+ };
200
+ });
201
+ }
202
+ /** Strona kalendarza, od której otwiera się panel dla danej wartości i trybu. */
203
+ export function initialPage(value, mode, today) {
204
+ const fallback = { y: today.getFullYear(), m: today.getMonth() };
205
+ switch (mode) {
206
+ case 'monthYear': {
207
+ const match = /^(\d{4})-(\d{2})$/.exec(value);
208
+ return match ? { y: +match[1], m: +match[2] - 1 } : fallback;
209
+ }
210
+ case 'year':
211
+ return { y: /^\d{4}$/.test(value) ? +value : today.getFullYear(), m: 0 };
212
+ case 'month':
213
+ return { y: today.getFullYear(), m: 0 };
214
+ case 'datetime': {
215
+ const date = parseDate(value.slice(0, 10));
216
+ return date ? { y: date.getFullYear(), m: date.getMonth() } : fallback;
217
+ }
218
+ default: {
219
+ const date = parseDate(value) ?? today;
220
+ return { y: date.getFullYear(), m: date.getMonth() };
221
+ }
222
+ }
223
+ }
@@ -0,0 +1,35 @@
1
+ export interface KptClockMark {
2
+ label: string;
3
+ /** Wartość docelowa: godzina 24h dla tarczy godzin, minuta dla tarczy minut. */
4
+ value: number;
5
+ /** Pozycja w procentach tarczy (0–100). */
6
+ x: number;
7
+ y: number;
8
+ inner: boolean;
9
+ selected: boolean;
10
+ }
11
+ export declare const KPT_CLOCK_R_OUTER = 40;
12
+ export declare const KPT_CLOCK_R_INNER = 26;
13
+ /** Pozycja znacznika (% tarczy) na pierścieniu o promieniu `radius`: 12 na górze, zegarowo. */
14
+ export declare function markPosition(index: number, radius: number): {
15
+ x: number;
16
+ y: number;
17
+ };
18
+ /**
19
+ * Znaczniki godzin. W 12h jeden pierścień pokazujący wybraną połowę doby; w 24h zewnętrzny
20
+ * (12, 1–11) i wewnętrzny (00, 13–23), bo 24 liczby na jednym okręgu byłyby nieczytelne.
21
+ */
22
+ export declare function hourMarks(hour: number, hourFormat: 12 | 24): KptClockMark[];
23
+ /** Znaczniki minut co pięć — pozostałe wartości wchodzą przeciąganiem. */
24
+ export declare function minuteMarks(minute: number): KptClockMark[];
25
+ /**
26
+ * Godzina z kąta wskaźnika. W 24h o pierścieniu — a więc o 12–23 kontra 0–11 — decyduje promień
27
+ * kliknięcia, podany jako ułamek promienia tarczy.
28
+ */
29
+ export declare function hourFromAngle(deg: number, radiusRatio: number, hourFormat: 12 | 24, hour: number): number;
30
+ /** Minuta z kąta wskaźnika, dociągnięta do `minuteStep`. */
31
+ export declare function minuteFromAngle(deg: number, minuteStep: number): number;
32
+ /** Kąt wskaźnika w stopniach dla bieżącego etapu tarczy. */
33
+ export declare function handAngle(hour: number, minute: number, phase: 'hours' | 'minutes'): number;
34
+ /** Czy wskaźnik sięga wewnętrznego pierścienia (24h, godziny 0 i 13–23). */
35
+ export declare function handIsInner(hour: number, hourFormat: 12 | 24, phase: 'hours' | 'minutes'): boolean;
package/dist/clock.js ADDED
@@ -0,0 +1,93 @@
1
+ import { from12h, pad } from "./iso.js";
2
+ export const KPT_CLOCK_R_OUTER = 40;
3
+ export const KPT_CLOCK_R_INNER = 26;
4
+ /** Pozycja znacznika (% tarczy) na pierścieniu o promieniu `radius`: 12 na górze, zegarowo. */
5
+ export function markPosition(index, radius) {
6
+ const theta = (index / 12) * 2 * Math.PI;
7
+ return { x: 50 + radius * Math.sin(theta), y: 50 - radius * Math.cos(theta) };
8
+ }
9
+ /**
10
+ * Znaczniki godzin. W 12h jeden pierścień pokazujący wybraną połowę doby; w 24h zewnętrzny
11
+ * (12, 1–11) i wewnętrzny (00, 13–23), bo 24 liczby na jednym okręgu byłyby nieczytelne.
12
+ */
13
+ export function hourMarks(hour, hourFormat) {
14
+ if (hourFormat === 12) {
15
+ const ampm = hour < 12 ? 'AM' : 'PM';
16
+ return Array.from({ length: 12 }, (_, i) => {
17
+ const h12 = i === 0 ? 12 : i;
18
+ // Wartość docelowa bierze bieżące AM/PM — tarcza pokazuje tylko jedną połowę doby.
19
+ const value = from12h(h12, ampm);
20
+ return {
21
+ label: String(h12),
22
+ value,
23
+ ...markPosition(i, KPT_CLOCK_R_OUTER),
24
+ inner: false,
25
+ selected: hour === value,
26
+ };
27
+ });
28
+ }
29
+ const outer = Array.from({ length: 12 }, (_, i) => {
30
+ const value = i === 0 ? 12 : i;
31
+ return {
32
+ label: String(value),
33
+ value,
34
+ ...markPosition(i, KPT_CLOCK_R_OUTER),
35
+ inner: false,
36
+ selected: hour === value,
37
+ };
38
+ });
39
+ const inner = Array.from({ length: 12 }, (_, i) => {
40
+ const value = i === 0 ? 0 : i + 12;
41
+ // `pad` daje '00' dla północy; 13–23 są już dwucyfrowe.
42
+ return {
43
+ label: pad(value),
44
+ value,
45
+ ...markPosition(i, KPT_CLOCK_R_INNER),
46
+ inner: true,
47
+ selected: hour === value,
48
+ };
49
+ });
50
+ return [...outer, ...inner];
51
+ }
52
+ /** Znaczniki minut co pięć — pozostałe wartości wchodzą przeciąganiem. */
53
+ export function minuteMarks(minute) {
54
+ return Array.from({ length: 12 }, (_, i) => {
55
+ const value = i * 5;
56
+ return {
57
+ label: pad(value),
58
+ value,
59
+ ...markPosition(i, KPT_CLOCK_R_OUTER),
60
+ inner: false,
61
+ selected: minute === value,
62
+ };
63
+ });
64
+ }
65
+ /**
66
+ * Godzina z kąta wskaźnika. W 24h o pierścieniu — a więc o 12–23 kontra 0–11 — decyduje promień
67
+ * kliknięcia, podany jako ułamek promienia tarczy.
68
+ */
69
+ export function hourFromAngle(deg, radiusRatio, hourFormat, hour) {
70
+ const index = Math.round(deg / 30) % 12;
71
+ if (hourFormat === 12) {
72
+ const ampm = hour < 12 ? 'AM' : 'PM';
73
+ return from12h(index === 0 ? 12 : index, ampm);
74
+ }
75
+ const inner = radiusRatio < (KPT_CLOCK_R_OUTER + KPT_CLOCK_R_INNER) / 2 / 50;
76
+ if (inner)
77
+ return index === 0 ? 0 : index + 12;
78
+ return index === 0 ? 12 : index;
79
+ }
80
+ /** Minuta z kąta wskaźnika, dociągnięta do `minuteStep`. */
81
+ export function minuteFromAngle(deg, minuteStep) {
82
+ const step = Math.max(1, minuteStep);
83
+ const raw = Math.round(deg / 6) % 60;
84
+ return (Math.round(raw / step) * step) % 60;
85
+ }
86
+ /** Kąt wskaźnika w stopniach dla bieżącego etapu tarczy. */
87
+ export function handAngle(hour, minute, phase) {
88
+ return phase === 'hours' ? (hour % 12) * 30 : minute * 6;
89
+ }
90
+ /** Czy wskaźnik sięga wewnętrznego pierścienia (24h, godziny 0 i 13–23). */
91
+ export function handIsInner(hour, hourFormat, phase) {
92
+ return phase === 'hours' && hourFormat === 24 && (hour === 0 || hour >= 13);
93
+ }
@@ -0,0 +1,7 @@
1
+ export { datePart, formatDate, formatTime, from12h, pad, parseDate, parseDateTime, parseTime, timePart, toIso, } from './iso.ts';
2
+ export { KPT_CALENDAR_LEVELS, buildDayGrid, buildDecadeCells, buildMonthCells, buildYearCells, canPageBack, canPageForward, centuryStart, commitLevelFor, decadeStart, initialPage, isMonthYearDisabled, isYearDisabled, pageBy, resolveStartView, selectedYearMonth, topLevelFor, } from './calendar.ts';
3
+ export type { DayGridOptions, KptCalendarPage, KptCalendarView, KptDateSelectionMode, KptDayCell, KptGridCell, MonthCellsOptions, YearCellsOptions, } from './calendar.ts';
4
+ export { buildRangeGrid, clampVisibleMonths, pickRangeDay } from './range.ts';
5
+ export type { KptDateRangeMode, KptDateRangeValue, KptMonthPanel, KptRangeDayCell, RangeGridOptions, } from './range.ts';
6
+ export { KPT_CLOCK_R_INNER, KPT_CLOCK_R_OUTER, handAngle, handIsInner, hourFromAngle, hourMarks, markPosition, minuteFromAngle, minuteMarks, } from './clock.ts';
7
+ export type { KptClockMark } from './clock.ts';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { datePart, formatDate, formatTime, from12h, pad, parseDate, parseDateTime, parseTime, timePart, toIso, } from "./iso.js";
2
+ export { KPT_CALENDAR_LEVELS, buildDayGrid, buildDecadeCells, buildMonthCells, buildYearCells, canPageBack, canPageForward, centuryStart, commitLevelFor, decadeStart, initialPage, isMonthYearDisabled, isYearDisabled, pageBy, resolveStartView, selectedYearMonth, topLevelFor, } from "./calendar.js";
3
+ export { buildRangeGrid, clampVisibleMonths, pickRangeDay } from "./range.js";
4
+ export { KPT_CLOCK_R_INNER, KPT_CLOCK_R_OUTER, handAngle, handIsInner, hourFromAngle, hourMarks, markPosition, minuteFromAngle, minuteMarks, } from "./clock.js";
package/dist/iso.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Formaty tekstowe kontrolek daty. Wszystkie wartości są stringami ISO-podobnymi — nigdy `Date`
3
+ * — bo to one jadą do formularza i na serwer, a `Date` niesie strefę, której kalendarz nie ma.
4
+ */
5
+ /** Liczba na dwie cyfry: 7 → '07'. */
6
+ export declare function pad(value: number): string;
7
+ /** Data lokalna → 'RRRR-MM-DD'. Celowo bez `toISOString()`, które przesuwa dzień o strefę. */
8
+ export declare function toIso(date: Date): string;
9
+ /** 'RRRR-MM-DD' → `Date` (lokalna północ) albo `null`, gdy format się nie zgadza. */
10
+ export declare function parseDate(iso: string): Date | null;
11
+ /** 'HH:mm' → `{ h, m }` albo `null`. */
12
+ export declare function parseTime(value: string): {
13
+ h: number;
14
+ m: number;
15
+ } | null;
16
+ /** 'RRRR-MM-DDTHH:mm' (albo ze spacją) → części albo `null`. */
17
+ export declare function parseDateTime(value: string): {
18
+ date: string;
19
+ h: number;
20
+ m: number;
21
+ } | null;
22
+ /** Sama data z wartości 'RRRR-MM-DD' lub 'RRRR-MM-DDTHH:mm'. */
23
+ export declare function datePart(value: string): string;
24
+ /** Sama godzina z wartości 'RRRR-MM-DDTHH:mm'; pusty string, gdy jej nie ma. */
25
+ export declare function timePart(value: string): string;
26
+ /** Godzina do prezentacji: 24h ('09:05') albo 12h z AM/PM ('9:05 AM'). */
27
+ export declare function formatTime(h: number, m: number, hourFormat?: 12 | 24): string;
28
+ /** Data do prezentacji: 'iso' zostawia 'RRRR-MM-DD', 'pl' daje 'DD.MM.RRRR'. */
29
+ export declare function formatDate(iso: string, displayFormat?: 'pl' | 'iso'): string;
30
+ /** Godzina 12h + AM/PM → doba 24h (12 AM = 0, 12 PM = 12). */
31
+ export declare function from12h(h12: number, ampm: 'AM' | 'PM'): number;
package/dist/iso.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Formaty tekstowe kontrolek daty. Wszystkie wartości są stringami ISO-podobnymi — nigdy `Date`
3
+ * — bo to one jadą do formularza i na serwer, a `Date` niesie strefę, której kalendarz nie ma.
4
+ */
5
+ /** Liczba na dwie cyfry: 7 → '07'. */
6
+ export function pad(value) {
7
+ return String(value).padStart(2, '0');
8
+ }
9
+ /** Data lokalna → 'RRRR-MM-DD'. Celowo bez `toISOString()`, które przesuwa dzień o strefę. */
10
+ export function toIso(date) {
11
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
12
+ }
13
+ /** 'RRRR-MM-DD' → `Date` (lokalna północ) albo `null`, gdy format się nie zgadza. */
14
+ export function parseDate(iso) {
15
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso ?? '');
16
+ if (!match)
17
+ return null;
18
+ return new Date(+match[1], +match[2] - 1, +match[3]);
19
+ }
20
+ /** 'HH:mm' → `{ h, m }` albo `null`. */
21
+ export function parseTime(value) {
22
+ const match = /^(\d{2}):(\d{2})$/.exec(value ?? '');
23
+ return match ? { h: +match[1], m: +match[2] } : null;
24
+ }
25
+ /** 'RRRR-MM-DDTHH:mm' (albo ze spacją) → części albo `null`. */
26
+ export function parseDateTime(value) {
27
+ const match = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})$/.exec(value ?? '');
28
+ return match ? { date: match[1], h: +match[2], m: +match[3] } : null;
29
+ }
30
+ /** Sama data z wartości 'RRRR-MM-DD' lub 'RRRR-MM-DDTHH:mm'. */
31
+ export function datePart(value) {
32
+ return value ? value.slice(0, 10) : '';
33
+ }
34
+ /** Sama godzina z wartości 'RRRR-MM-DDTHH:mm'; pusty string, gdy jej nie ma. */
35
+ export function timePart(value) {
36
+ const match = /[T ](\d{2}:\d{2})$/.exec(value ?? '');
37
+ return match ? match[1] : '';
38
+ }
39
+ /** Godzina do prezentacji: 24h ('09:05') albo 12h z AM/PM ('9:05 AM'). */
40
+ export function formatTime(h, m, hourFormat = 24) {
41
+ if (hourFormat === 12) {
42
+ const h12 = h % 12 === 0 ? 12 : h % 12;
43
+ return `${h12}:${pad(m)} ${h < 12 ? 'AM' : 'PM'}`;
44
+ }
45
+ return `${pad(h)}:${pad(m)}`;
46
+ }
47
+ /** Data do prezentacji: 'iso' zostawia 'RRRR-MM-DD', 'pl' daje 'DD.MM.RRRR'. */
48
+ export function formatDate(iso, displayFormat = 'pl') {
49
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso ?? '');
50
+ if (!match)
51
+ return '';
52
+ return displayFormat === 'iso' ? iso : `${match[3]}.${match[2]}.${match[1]}`;
53
+ }
54
+ /** Godzina 12h + AM/PM → doba 24h (12 AM = 0, 12 PM = 12). */
55
+ export function from12h(h12, ampm) {
56
+ const base = h12 % 12;
57
+ return ampm === 'PM' ? base + 12 : base;
58
+ }
@@ -0,0 +1,49 @@
1
+ import type { KptCalendarPage } from './calendar.ts';
2
+ export interface KptDateRangeValue {
3
+ start: string;
4
+ end: string;
5
+ }
6
+ /** Granulacja zakresu: same daty, daty z godziną, same godziny. */
7
+ export type KptDateRangeMode = 'date' | 'datetime' | 'time';
8
+ export interface KptRangeDayCell {
9
+ day: number;
10
+ iso: string;
11
+ inMonth: boolean;
12
+ today: boolean;
13
+ isStart: boolean;
14
+ isEnd: boolean;
15
+ inRange: boolean;
16
+ /** Pasmo zakresu wychodzi z tej komórki w prawo / dochodzi z lewej — do zaokrągleń kapsuły. */
17
+ rangeStart: boolean;
18
+ rangeEnd: boolean;
19
+ /** Pusty slot siatki: dni sąsiednich miesięcy przy wielu panelach. */
20
+ blank: boolean;
21
+ disabled: boolean;
22
+ }
23
+ /** Jeden miesiąc panelu kalendarza. */
24
+ export interface KptMonthPanel {
25
+ key: string;
26
+ label: string;
27
+ weeks: KptRangeDayCell[][];
28
+ }
29
+ export interface RangeGridOptions {
30
+ page: KptCalendarPage;
31
+ start: string;
32
+ end: string;
33
+ todayIso: string;
34
+ /** Ukrywa dni sąsiednich miesięcy — konieczne, gdy paneli jest kilka. */
35
+ hideOutside?: boolean;
36
+ }
37
+ /**
38
+ * Siatka dni z pasmem zakresu. Przy kilku panelach dni sąsiednich miesięcy stają się pustymi
39
+ * slotami: inaczej ta sama data byłaby klikalna dwa razy — jako wygaszona w jednym panelu
40
+ * i właściwa w drugim — a pasmo malowałoby się w obu miejscach.
41
+ */
42
+ export declare function buildRangeGrid({ page, start, end, todayIso, hideOutside, }: RangeGridOptions): KptRangeDayCell[][];
43
+ /**
44
+ * Kolejny klik w zakresie: pierwszy ustawia początek, drugi koniec, trzeci zaczyna od nowa.
45
+ * Klik przed początkiem nie tworzy odwróconego zakresu — kolejność jest normalizowana.
46
+ */
47
+ export declare function pickRangeDay(current: KptDateRangeValue, iso: string): KptDateRangeValue;
48
+ /** Liczba paneli miesięcy: 1–3, bo więcej nie mieści się w typowym overlayu. */
49
+ export declare function clampVisibleMonths(value: number): number;
package/dist/range.js ADDED
@@ -0,0 +1,52 @@
1
+ import { toIso } from "./iso.js";
2
+ /**
3
+ * Siatka dni z pasmem zakresu. Przy kilku panelach dni sąsiednich miesięcy stają się pustymi
4
+ * slotami: inaczej ta sama data byłaby klikalna dwa razy — jako wygaszona w jednym panelu
5
+ * i właściwa w drugim — a pasmo malowałoby się w obu miejscach.
6
+ */
7
+ export function buildRangeGrid({ page, start, end, todayIso, hideOutside = false, }) {
8
+ const { y, m } = page;
9
+ const hasBand = !!start && !!end && start < end;
10
+ const first = new Date(y, m, 1);
11
+ const startOffset = (first.getDay() + 6) % 7;
12
+ const gridStart = new Date(y, m, 1 - startOffset);
13
+ const weeks = [];
14
+ for (let w = 0; w < 6; w++) {
15
+ const row = [];
16
+ for (let d = 0; d < 7; d++) {
17
+ const date = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + w * 7 + d);
18
+ const iso = toIso(date);
19
+ const inMonth = date.getMonth() === m;
20
+ row.push({
21
+ day: date.getDate(),
22
+ iso,
23
+ inMonth,
24
+ today: iso === todayIso,
25
+ isStart: iso === start,
26
+ isEnd: iso === end,
27
+ inRange: hasBand && iso > start && iso < end,
28
+ rangeStart: hasBand && iso === start,
29
+ rangeEnd: hasBand && iso === end,
30
+ blank: hideOutside && !inMonth,
31
+ disabled: false,
32
+ });
33
+ }
34
+ weeks.push(row);
35
+ }
36
+ return weeks;
37
+ }
38
+ /**
39
+ * Kolejny klik w zakresie: pierwszy ustawia początek, drugi koniec, trzeci zaczyna od nowa.
40
+ * Klik przed początkiem nie tworzy odwróconego zakresu — kolejność jest normalizowana.
41
+ */
42
+ export function pickRangeDay(current, iso) {
43
+ const { start, end } = current;
44
+ if (!start || (start && end))
45
+ return { start: iso, end: '' };
46
+ return iso < start ? { start: iso, end: start } : { start, end: iso };
47
+ }
48
+ /** Liczba paneli miesięcy: 1–3, bo więcej nie mieści się w typowym overlayu. */
49
+ export function clampVisibleMonths(value) {
50
+ const n = Math.trunc(value);
51
+ return Number.isFinite(n) ? Math.min(3, Math.max(1, n)) : 1;
52
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@konce-pt/datetime",
3
+ "version": "0.8.0",
4
+ "description": "Calendar and clock core for Koncept UI date controls — day/month/year/decade grids, ranges, dial marks. Framework-free TypeScript, zero runtime dependencies.",
5
+ "license": "MIT",
6
+ "author": "konce.pt",
7
+ "homepage": "https://ui.konce.pt/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://gitlab.com/konce-pt/koncept-ui.git",
11
+ "directory": "packages/datetime"
12
+ },
13
+ "bugs": {
14
+ "url": "https://gitlab.com/konce-pt/koncept-ui/-/issues"
15
+ },
16
+ "keywords": [
17
+ "calendar",
18
+ "datepicker",
19
+ "date-range",
20
+ "design-system",
21
+ "koncept-ui",
22
+ "kpt"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ }
36
+ },
37
+ "devDependencies": {
38
+ "typescript": "~6.0.3"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "test": "node --test \"src/**/*.test.ts\"",
43
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
44
+ }
45
+ }