@nomideusz/svelte-calendar 0.1.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/README.md +168 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/memory.d.ts +14 -0
- package/dist/adapters/memory.js +33 -0
- package/dist/adapters/rest.d.ts +20 -0
- package/dist/adapters/rest.js +48 -0
- package/dist/adapters/types.d.ts +23 -0
- package/dist/adapters/types.js +1 -0
- package/dist/assets/favicon.svg +1 -0
- package/dist/calendar/Calendar.svelte +213 -0
- package/dist/calendar/Calendar.svelte.d.ts +45 -0
- package/dist/calendar/Toolbar.svelte +241 -0
- package/dist/calendar/Toolbar.svelte.d.ts +19 -0
- package/dist/calendar/index.d.ts +3 -0
- package/dist/calendar/index.js +3 -0
- package/dist/core/clock.svelte.d.ts +21 -0
- package/dist/core/clock.svelte.js +53 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +8 -0
- package/dist/core/locale.d.ts +38 -0
- package/dist/core/locale.js +96 -0
- package/dist/core/time.d.ts +37 -0
- package/dist/core/time.js +68 -0
- package/dist/core/types.d.ts +91 -0
- package/dist/core/types.js +11 -0
- package/dist/engine/drag.svelte.d.ts +31 -0
- package/dist/engine/drag.svelte.js +58 -0
- package/dist/engine/event-store.svelte.d.ts +47 -0
- package/dist/engine/event-store.svelte.js +120 -0
- package/dist/engine/index.d.ts +8 -0
- package/dist/engine/index.js +5 -0
- package/dist/engine/selection.svelte.d.ts +24 -0
- package/dist/engine/selection.svelte.js +58 -0
- package/dist/engine/view-state.svelte.d.ts +24 -0
- package/dist/engine/view-state.svelte.js +74 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/primitives/DayHeader.svelte +103 -0
- package/dist/primitives/DayHeader.svelte.d.ts +15 -0
- package/dist/primitives/EmptySlot.svelte +101 -0
- package/dist/primitives/EmptySlot.svelte.d.ts +16 -0
- package/dist/primitives/EventBlock.svelte +241 -0
- package/dist/primitives/EventBlock.svelte.d.ts +24 -0
- package/dist/primitives/NowIndicator.svelte +175 -0
- package/dist/primitives/NowIndicator.svelte.d.ts +22 -0
- package/dist/primitives/TimeGutter.svelte +105 -0
- package/dist/primitives/TimeGutter.svelte.d.ts +14 -0
- package/dist/primitives/index.d.ts +5 -0
- package/dist/primitives/index.js +6 -0
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/presets.d.ts +24 -0
- package/dist/theme/presets.js +89 -0
- package/dist/views/agenda/Agenda.svelte +1404 -0
- package/dist/views/agenda/Agenda.svelte.d.ts +20 -0
- package/dist/views/agenda/index.d.ts +1 -0
- package/dist/views/agenda/index.js +1 -0
- package/dist/views/day/DayGrid.svelte +1063 -0
- package/dist/views/day/DayGrid.svelte.d.ts +33 -0
- package/dist/views/day/DayTimeline.svelte +505 -0
- package/dist/views/day/DayTimeline.svelte.d.ts +4 -0
- package/dist/views/day/index.d.ts +2 -0
- package/dist/views/day/index.js +2 -0
- package/dist/views/index.d.ts +5 -0
- package/dist/views/index.js +5 -0
- package/dist/views/settings/Settings.svelte +303 -0
- package/dist/views/settings/Settings.svelte.d.ts +23 -0
- package/dist/views/settings/index.d.ts +2 -0
- package/dist/views/settings/index.js +1 -0
- package/dist/views/week/WeekGrid.svelte +594 -0
- package/dist/views/week/WeekGrid.svelte.d.ts +21 -0
- package/dist/views/week/WeekHeatmap.svelte +453 -0
- package/dist/views/week/WeekHeatmap.svelte.d.ts +4 -0
- package/dist/views/week/index.d.ts +2 -0
- package/dist/views/week/index.js +2 -0
- package/package.json +62 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Toolbar — navigation + granularity toggle + concept switcher.
|
|
3
|
+
|
|
4
|
+
Layout: [← Today →] date-label [links] [Day | Week] [concept pills]
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import type { CalendarViewId, ViewState } from '../engine/view-state.svelte.js';
|
|
8
|
+
import { fmtDay, fmtWeekRange, weekdayLong } from '../core/locale.js';
|
|
9
|
+
|
|
10
|
+
interface ViewOption {
|
|
11
|
+
id: CalendarViewId;
|
|
12
|
+
label: string;
|
|
13
|
+
granularity: 'day' | 'week';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
viewState: ViewState;
|
|
18
|
+
/** All registered views */
|
|
19
|
+
views?: ViewOption[];
|
|
20
|
+
/** Links to show in the toolbar */
|
|
21
|
+
links?: { href: string; label: string }[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
viewState,
|
|
26
|
+
views = [],
|
|
27
|
+
links = [],
|
|
28
|
+
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
const dateLabel = $derived(() => {
|
|
31
|
+
if (viewState.granularity === 'day') {
|
|
32
|
+
return `${weekdayLong(viewState.focusDate.getTime())}, ${fmtDay(viewState.focusDate.getTime(), Date.now())}`;
|
|
33
|
+
}
|
|
34
|
+
const ws = viewState.range.start.getTime();
|
|
35
|
+
return fmtWeekRange(ws);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Which granularities are available?
|
|
39
|
+
const granularities = $derived(() => {
|
|
40
|
+
const g = new Set(views.map((v) => v.granularity));
|
|
41
|
+
return [...g] as ('day' | 'week')[];
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Concepts available for current granularity
|
|
45
|
+
const concepts = $derived(
|
|
46
|
+
views.filter((v) => v.granularity === viewState.granularity),
|
|
47
|
+
);
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<nav class="tb">
|
|
51
|
+
<!-- Nav: ← Today → -->
|
|
52
|
+
<div class="tb-nav">
|
|
53
|
+
<button class="tb-btn" onclick={() => viewState.prev()} aria-label="Previous">
|
|
54
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
55
|
+
<path d="M10 3L5 8L10 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
56
|
+
</svg>
|
|
57
|
+
</button>
|
|
58
|
+
<button class="tb-btn tb-today" onclick={() => viewState.goToday()}>Today</button>
|
|
59
|
+
<button class="tb-btn" onclick={() => viewState.next()} aria-label="Next">
|
|
60
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
61
|
+
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
62
|
+
</svg>
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Date label -->
|
|
67
|
+
<span class="tb-label">{dateLabel()}</span>
|
|
68
|
+
|
|
69
|
+
<!-- Links -->
|
|
70
|
+
{#if links.length > 0}
|
|
71
|
+
<div class="tb-links">
|
|
72
|
+
{#each links as link}
|
|
73
|
+
<a class="tb-link" href={link.href}>{link.label}</a>
|
|
74
|
+
{/each}
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
77
|
+
|
|
78
|
+
<!-- Granularity toggle: Day / Week -->
|
|
79
|
+
{#if granularities().length > 1}
|
|
80
|
+
<div class="tb-group">
|
|
81
|
+
{#each granularities() as g}
|
|
82
|
+
<button
|
|
83
|
+
class="tb-seg"
|
|
84
|
+
class:tb-seg-active={viewState.granularity === g}
|
|
85
|
+
onclick={() => {
|
|
86
|
+
// Preserve concept: find the view in target granularity with same label
|
|
87
|
+
const currentLabel = views.find((v) => v.id === viewState.view)?.label;
|
|
88
|
+
const match = views.find((v) => v.granularity === g && v.label === currentLabel);
|
|
89
|
+
const fallback = views.find((v) => v.granularity === g);
|
|
90
|
+
const target = match ?? fallback;
|
|
91
|
+
if (target) viewState.setView(target.id);
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{g === 'day' ? 'Day' : 'Week'}
|
|
95
|
+
</button>
|
|
96
|
+
{/each}
|
|
97
|
+
</div>
|
|
98
|
+
{/if}
|
|
99
|
+
|
|
100
|
+
<!-- Concept pills -->
|
|
101
|
+
{#if concepts.length > 1}
|
|
102
|
+
<div class="tb-concepts">
|
|
103
|
+
{#each concepts as c}
|
|
104
|
+
<button
|
|
105
|
+
class="tb-pill"
|
|
106
|
+
class:tb-pill-active={viewState.view === c.id}
|
|
107
|
+
onclick={() => viewState.setView(c.id)}
|
|
108
|
+
>
|
|
109
|
+
{c.label}
|
|
110
|
+
</button>
|
|
111
|
+
{/each}
|
|
112
|
+
</div>
|
|
113
|
+
{/if}
|
|
114
|
+
</nav>
|
|
115
|
+
|
|
116
|
+
<style>
|
|
117
|
+
.tb {
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
gap: 10px;
|
|
121
|
+
padding: 8px 12px;
|
|
122
|
+
background: var(--dt-surface, #10141c);
|
|
123
|
+
border-bottom: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
124
|
+
border-radius: 10px 10px 0 0;
|
|
125
|
+
flex-wrap: wrap;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.tb-nav {
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 4px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.tb-btn {
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
justify-content: center;
|
|
138
|
+
border: none;
|
|
139
|
+
background: transparent;
|
|
140
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
padding: 4px 6px;
|
|
143
|
+
border-radius: 6px;
|
|
144
|
+
font: 500 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
145
|
+
transition: background 100ms, color 100ms;
|
|
146
|
+
}
|
|
147
|
+
.tb-btn:hover {
|
|
148
|
+
background: var(--dt-accent-dim, rgba(239, 68, 68, 0.18));
|
|
149
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.tb-today {
|
|
153
|
+
padding: 4px 10px;
|
|
154
|
+
border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.tb-label {
|
|
158
|
+
font: 500 13px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
159
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
160
|
+
letter-spacing: 0.01em;
|
|
161
|
+
flex: 1;
|
|
162
|
+
min-width: 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* ── Links ── */
|
|
166
|
+
.tb-links {
|
|
167
|
+
display: flex;
|
|
168
|
+
gap: 6px;
|
|
169
|
+
}
|
|
170
|
+
.tb-link {
|
|
171
|
+
font: 400 10px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
172
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
173
|
+
border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
174
|
+
padding: 4px 8px;
|
|
175
|
+
border-radius: 5px;
|
|
176
|
+
text-decoration: none;
|
|
177
|
+
transition: color 100ms, border-color 100ms;
|
|
178
|
+
}
|
|
179
|
+
.tb-link:hover {
|
|
180
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
181
|
+
border-color: var(--dt-text-2, rgba(148, 163, 184, 0.25));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ── Granularity toggle ── */
|
|
185
|
+
.tb-group {
|
|
186
|
+
display: flex;
|
|
187
|
+
gap: 2px;
|
|
188
|
+
background: var(--dt-bg, #0b0e14);
|
|
189
|
+
border-radius: 6px;
|
|
190
|
+
padding: 2px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.tb-seg {
|
|
194
|
+
border: none;
|
|
195
|
+
background: transparent;
|
|
196
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
font: 600 10px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
199
|
+
padding: 5px 12px;
|
|
200
|
+
border-radius: 4px;
|
|
201
|
+
letter-spacing: 0.04em;
|
|
202
|
+
text-transform: uppercase;
|
|
203
|
+
transition: background 100ms, color 100ms;
|
|
204
|
+
}
|
|
205
|
+
.tb-seg:hover {
|
|
206
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
207
|
+
}
|
|
208
|
+
.tb-seg-active {
|
|
209
|
+
background: var(--dt-accent, #ef4444);
|
|
210
|
+
color: var(--dt-btn-text, #fff);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ── Concept pills ── */
|
|
214
|
+
.tb-concepts {
|
|
215
|
+
display: flex;
|
|
216
|
+
gap: 3px;
|
|
217
|
+
background: var(--dt-bg, #0b0e14);
|
|
218
|
+
border-radius: 6px;
|
|
219
|
+
padding: 2px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.tb-pill {
|
|
223
|
+
border: none;
|
|
224
|
+
background: transparent;
|
|
225
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
226
|
+
cursor: pointer;
|
|
227
|
+
font: 400 10px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
228
|
+
padding: 4px 9px;
|
|
229
|
+
border-radius: 4px;
|
|
230
|
+
letter-spacing: 0.02em;
|
|
231
|
+
transition: background 100ms, color 100ms;
|
|
232
|
+
white-space: nowrap;
|
|
233
|
+
}
|
|
234
|
+
.tb-pill:hover {
|
|
235
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
236
|
+
}
|
|
237
|
+
.tb-pill-active {
|
|
238
|
+
background: var(--dt-surface, #10141c);
|
|
239
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
240
|
+
}
|
|
241
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CalendarViewId, ViewState } from '../engine/view-state.svelte.js';
|
|
2
|
+
interface ViewOption {
|
|
3
|
+
id: CalendarViewId;
|
|
4
|
+
label: string;
|
|
5
|
+
granularity: 'day' | 'week';
|
|
6
|
+
}
|
|
7
|
+
interface Props {
|
|
8
|
+
viewState: ViewState;
|
|
9
|
+
/** All registered views */
|
|
10
|
+
views?: ViewOption[];
|
|
11
|
+
/** Links to show in the toolbar */
|
|
12
|
+
links?: {
|
|
13
|
+
href: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
declare const Toolbar: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type Toolbar = ReturnType<typeof Toolbar>;
|
|
19
|
+
export default Toolbar;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface Clock {
|
|
2
|
+
/** Current epoch ms, updated every second */
|
|
3
|
+
readonly tick: number;
|
|
4
|
+
/** Start-of-day epoch ms for today */
|
|
5
|
+
readonly today: number;
|
|
6
|
+
/** Formatted hours:minutes — "14:30" */
|
|
7
|
+
readonly hm: string;
|
|
8
|
+
/** Formatted seconds — ":05" */
|
|
9
|
+
readonly s: string;
|
|
10
|
+
/** Fractional hours since midnight — 14.5 */
|
|
11
|
+
readonly fractionalHour: number;
|
|
12
|
+
/** Stop the clock (called automatically on component unmount) */
|
|
13
|
+
destroy: () => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a shared reactive clock.
|
|
17
|
+
*
|
|
18
|
+
* Must be called during component initialisation (before first await).
|
|
19
|
+
* Automatically cleans up on unmount via onMount return.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createClock(): Clock;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive clock state factory for Svelte 5 rune-mode.
|
|
3
|
+
*
|
|
4
|
+
* Usage inside a component:
|
|
5
|
+
* const clock = createClock();
|
|
6
|
+
* // clock.tick — current timestamp, updates every second
|
|
7
|
+
* // clock.today — start-of-day timestamp, rolls over at midnight
|
|
8
|
+
* // clock.hm — "14:30"
|
|
9
|
+
* // clock.s — ":05"
|
|
10
|
+
* // clock.fractionalHour — 14.5
|
|
11
|
+
*
|
|
12
|
+
* Call clock.destroy() in onMount's cleanup (or it auto-cleans via $effect).
|
|
13
|
+
*/
|
|
14
|
+
import { onMount } from 'svelte';
|
|
15
|
+
import { sod, fmtHM, fmtS, fractionalHour } from './time.js';
|
|
16
|
+
/**
|
|
17
|
+
* Create a shared reactive clock.
|
|
18
|
+
*
|
|
19
|
+
* Must be called during component initialisation (before first await).
|
|
20
|
+
* Automatically cleans up on unmount via onMount return.
|
|
21
|
+
*/
|
|
22
|
+
export function createClock() {
|
|
23
|
+
let tick = $state(Date.now());
|
|
24
|
+
let today = $state(sod(Date.now()));
|
|
25
|
+
let intervalId = null;
|
|
26
|
+
function start() {
|
|
27
|
+
intervalId = setInterval(() => {
|
|
28
|
+
tick = Date.now();
|
|
29
|
+
const sd = sod(tick);
|
|
30
|
+
if (sd !== today)
|
|
31
|
+
today = sd;
|
|
32
|
+
}, 1000);
|
|
33
|
+
}
|
|
34
|
+
function destroy() {
|
|
35
|
+
if (intervalId !== null) {
|
|
36
|
+
clearInterval(intervalId);
|
|
37
|
+
intervalId = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Auto-start and auto-cleanup
|
|
41
|
+
onMount(() => {
|
|
42
|
+
start();
|
|
43
|
+
return destroy;
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
get tick() { return tick; },
|
|
47
|
+
get today() { return today; },
|
|
48
|
+
get hm() { return fmtHM(tick); },
|
|
49
|
+
get s() { return fmtS(tick); },
|
|
50
|
+
get fractionalHour() { return fractionalHour(tick); },
|
|
51
|
+
destroy,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createClock } from './clock.svelte.js';
|
|
2
|
+
export type { Clock } from './clock.svelte.js';
|
|
3
|
+
export { DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, } from './time.js';
|
|
4
|
+
export { setDefaultLocale, getDefaultLocale, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, } from './locale.js';
|
|
5
|
+
export type { TimelineEvent, WeekTimelineProps, DayTimelineProps, } from './types.js';
|
|
6
|
+
export { timeToX } from './types.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// ─── Core barrel export ─────────────────────────────────
|
|
2
|
+
// Clock
|
|
3
|
+
export { createClock } from './clock.svelte.js';
|
|
4
|
+
// Time constants & math
|
|
5
|
+
export { DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, } from './time.js';
|
|
6
|
+
// Locale-aware formatting
|
|
7
|
+
export { setDefaultLocale, getDefaultLocale, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, } from './locale.js';
|
|
8
|
+
export { timeToX } from './types.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale-aware formatting utilities.
|
|
3
|
+
*
|
|
4
|
+
* All functions accept an optional `locale` string (BCP 47 tag, e.g. 'en-US',
|
|
5
|
+
* 'pl-PL'). When omitted they fall back to `defaultLocale`.
|
|
6
|
+
*
|
|
7
|
+
* For date-fns locale integration later, this module can import from
|
|
8
|
+
* `date-fns/locale/*` and pass to `format()`.
|
|
9
|
+
*/
|
|
10
|
+
/** Change the default locale for all formatting functions */
|
|
11
|
+
export declare function setDefaultLocale(tag: string): void;
|
|
12
|
+
/** Get the current default locale */
|
|
13
|
+
export declare function getDefaultLocale(): string;
|
|
14
|
+
/** Format hour index (0-23) as compact 12h label: 12a, 1a … 12p, 1p … */
|
|
15
|
+
export declare function fmtH(h: number): string;
|
|
16
|
+
/** Short weekday name for a timestamp: "Mon", "Tue", etc. */
|
|
17
|
+
export declare function weekdayShort(ms: number, locale?: string): string;
|
|
18
|
+
/** Long weekday name for a timestamp: "Monday", "Tuesday", etc. */
|
|
19
|
+
export declare function weekdayLong(ms: number, locale?: string): string;
|
|
20
|
+
/** Short month name for a timestamp: "Jan", "Feb", etc. */
|
|
21
|
+
export declare function monthShort(ms: number, locale?: string): string;
|
|
22
|
+
/** Long month name: "January", "February", etc. */
|
|
23
|
+
export declare function monthLong(ms: number, locale?: string): string;
|
|
24
|
+
/** Short date: "Feb 21" */
|
|
25
|
+
export declare function dateShort(ms: number, locale?: string): string;
|
|
26
|
+
/** Weekday + short date: "Mon, Feb 17" */
|
|
27
|
+
export declare function dateWithWeekday(ms: number, locale?: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Format a timestamp as a smart day label:
|
|
30
|
+
* "Today · Feb 21", "Yesterday · Feb 20", "Mon, Feb 17", etc.
|
|
31
|
+
*/
|
|
32
|
+
export declare function fmtDay(ms: number, todayMs: number, opts?: {
|
|
33
|
+
short?: boolean;
|
|
34
|
+
}, locale?: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Format a week range label: "Feb 17 – 23, 2026" or "Jan 27 – Feb 2, 2026"
|
|
37
|
+
*/
|
|
38
|
+
export declare function fmtWeekRange(weekStartMs: number, locale?: string): string;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale-aware formatting utilities.
|
|
3
|
+
*
|
|
4
|
+
* All functions accept an optional `locale` string (BCP 47 tag, e.g. 'en-US',
|
|
5
|
+
* 'pl-PL'). When omitted they fall back to `defaultLocale`.
|
|
6
|
+
*
|
|
7
|
+
* For date-fns locale integration later, this module can import from
|
|
8
|
+
* `date-fns/locale/*` and pass to `format()`.
|
|
9
|
+
*/
|
|
10
|
+
/** Module-level default locale — consumers can override via setDefaultLocale() */
|
|
11
|
+
let defaultLocale = 'en-US';
|
|
12
|
+
/** Change the default locale for all formatting functions */
|
|
13
|
+
export function setDefaultLocale(tag) {
|
|
14
|
+
defaultLocale = tag;
|
|
15
|
+
}
|
|
16
|
+
/** Get the current default locale */
|
|
17
|
+
export function getDefaultLocale() {
|
|
18
|
+
return defaultLocale;
|
|
19
|
+
}
|
|
20
|
+
/** Format hour index (0-23) as compact 12h label: 12a, 1a … 12p, 1p … */
|
|
21
|
+
export function fmtH(h) {
|
|
22
|
+
if (h === 0)
|
|
23
|
+
return '12a';
|
|
24
|
+
if (h === 12)
|
|
25
|
+
return '12p';
|
|
26
|
+
return h < 12 ? h + 'a' : h - 12 + 'p';
|
|
27
|
+
}
|
|
28
|
+
/** Short weekday name for a timestamp: "Mon", "Tue", etc. */
|
|
29
|
+
export function weekdayShort(ms, locale) {
|
|
30
|
+
return new Date(ms).toLocaleDateString(locale ?? defaultLocale, { weekday: 'short' });
|
|
31
|
+
}
|
|
32
|
+
/** Long weekday name for a timestamp: "Monday", "Tuesday", etc. */
|
|
33
|
+
export function weekdayLong(ms, locale) {
|
|
34
|
+
return new Date(ms).toLocaleDateString(locale ?? defaultLocale, { weekday: 'long' });
|
|
35
|
+
}
|
|
36
|
+
/** Short month name for a timestamp: "Jan", "Feb", etc. */
|
|
37
|
+
export function monthShort(ms, locale) {
|
|
38
|
+
return new Date(ms).toLocaleDateString(locale ?? defaultLocale, { month: 'short' });
|
|
39
|
+
}
|
|
40
|
+
/** Long month name: "January", "February", etc. */
|
|
41
|
+
export function monthLong(ms, locale) {
|
|
42
|
+
return new Date(ms).toLocaleDateString(locale ?? defaultLocale, { month: 'long' });
|
|
43
|
+
}
|
|
44
|
+
/** Short date: "Feb 21" */
|
|
45
|
+
export function dateShort(ms, locale) {
|
|
46
|
+
return new Date(ms).toLocaleDateString(locale ?? defaultLocale, {
|
|
47
|
+
month: 'short',
|
|
48
|
+
day: 'numeric',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Weekday + short date: "Mon, Feb 17" */
|
|
52
|
+
export function dateWithWeekday(ms, locale) {
|
|
53
|
+
return new Date(ms).toLocaleDateString(locale ?? defaultLocale, {
|
|
54
|
+
weekday: 'short',
|
|
55
|
+
month: 'short',
|
|
56
|
+
day: 'numeric',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
import { DAY_MS } from './time.js';
|
|
60
|
+
/**
|
|
61
|
+
* Format a timestamp as a smart day label:
|
|
62
|
+
* "Today · Feb 21", "Yesterday · Feb 20", "Mon, Feb 17", etc.
|
|
63
|
+
*/
|
|
64
|
+
export function fmtDay(ms, todayMs, opts, locale) {
|
|
65
|
+
const loc = locale ?? defaultLocale;
|
|
66
|
+
const short = new Date(ms).toLocaleDateString(loc, { month: 'short', day: 'numeric' });
|
|
67
|
+
if (ms === todayMs)
|
|
68
|
+
return opts?.short ? 'Today' : `Today · ${short}`;
|
|
69
|
+
if (ms === todayMs - DAY_MS)
|
|
70
|
+
return opts?.short ? 'Yesterday' : `Yesterday · ${short}`;
|
|
71
|
+
if (ms === todayMs + DAY_MS)
|
|
72
|
+
return opts?.short ? 'Tomorrow' : `Tomorrow · ${short}`;
|
|
73
|
+
if (opts?.short) {
|
|
74
|
+
return new Date(ms).toLocaleDateString(loc, { weekday: 'short', day: 'numeric' });
|
|
75
|
+
}
|
|
76
|
+
return new Date(ms).toLocaleDateString(loc, { weekday: 'short', month: 'short', day: 'numeric' });
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Format a week range label: "Feb 17 – 23, 2026" or "Jan 27 – Feb 2, 2026"
|
|
80
|
+
*/
|
|
81
|
+
export function fmtWeekRange(weekStartMs, locale) {
|
|
82
|
+
const loc = locale ?? defaultLocale;
|
|
83
|
+
const s = new Date(weekStartMs);
|
|
84
|
+
const e = new Date(weekStartMs + 6 * DAY_MS);
|
|
85
|
+
const sm = s.toLocaleDateString(loc, { month: 'short' });
|
|
86
|
+
const em = e.toLocaleDateString(loc, { month: 'short' });
|
|
87
|
+
const sy = s.getFullYear();
|
|
88
|
+
const ey = e.getFullYear();
|
|
89
|
+
if (sy !== ey) {
|
|
90
|
+
return `${sm} ${s.getDate()}, ${sy} – ${em} ${e.getDate()}, ${ey}`;
|
|
91
|
+
}
|
|
92
|
+
if (sm !== em) {
|
|
93
|
+
return `${sm} ${s.getDate()} – ${em} ${e.getDate()}, ${ey}`;
|
|
94
|
+
}
|
|
95
|
+
return `${sm} ${s.getDate()} – ${e.getDate()}, ${ey}`;
|
|
96
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Milliseconds in one day */
|
|
2
|
+
export declare const DAY_MS = 86400000;
|
|
3
|
+
/** Milliseconds in one hour */
|
|
4
|
+
export declare const HOUR_MS = 3600000;
|
|
5
|
+
/** Array [0..23] for iterating over hour slots */
|
|
6
|
+
export declare const HOURS: readonly number[];
|
|
7
|
+
/** Start-of-day timestamp (midnight, local time) */
|
|
8
|
+
export declare function sod(ms: number): number;
|
|
9
|
+
/**
|
|
10
|
+
* Start-of-week timestamp.
|
|
11
|
+
* @param ms Any timestamp within the target week
|
|
12
|
+
* @param mondayStart true → weeks begin Monday; false → Sunday
|
|
13
|
+
*/
|
|
14
|
+
export declare function startOfWeek(ms: number, mondayStart?: boolean): number;
|
|
15
|
+
/** Add `n` days to a timestamp and return the new timestamp */
|
|
16
|
+
export declare function addDaysMs(ms: number, n: number): number;
|
|
17
|
+
/** Calendar-day difference between two timestamps */
|
|
18
|
+
export declare function diffDays(a: number, b: number): number;
|
|
19
|
+
/** Zero-pad a number to 2 digits */
|
|
20
|
+
export declare function pad(n: number): string;
|
|
21
|
+
/**
|
|
22
|
+
* Fractional hours elapsed since midnight for a given timestamp.
|
|
23
|
+
* e.g. 14:30:00 → 14.5
|
|
24
|
+
*/
|
|
25
|
+
export declare function fractionalHour(ms: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* Format hours + minutes from a timestamp: "14:30"
|
|
28
|
+
*/
|
|
29
|
+
export declare function fmtHM(ms: number): string;
|
|
30
|
+
/**
|
|
31
|
+
* Format seconds from a timestamp: ":05"
|
|
32
|
+
*/
|
|
33
|
+
export declare function fmtS(ms: number): string;
|
|
34
|
+
/** Day-of-month number for a timestamp */
|
|
35
|
+
export declare function dayNum(ms: number): number;
|
|
36
|
+
/** Day-of-week index (0 = Sun … 6 = Sat) */
|
|
37
|
+
export declare function dayOfWeek(ms: number): number;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core time constants and pure date-math utilities.
|
|
3
|
+
*
|
|
4
|
+
* Uses date-fns for reliable calendar operations (DST-safe, tree-shakeable).
|
|
5
|
+
* Re-exports commonly used date-fns functions so consumers have a single import.
|
|
6
|
+
*/
|
|
7
|
+
import { startOfDay, startOfWeek as dfnsStartOfWeek, addDays, differenceInCalendarDays, getDay, getDate, getHours, getMinutes, getSeconds, } from 'date-fns';
|
|
8
|
+
// ─── Constants ──────────────────────────────────────────
|
|
9
|
+
/** Milliseconds in one day */
|
|
10
|
+
export const DAY_MS = 86_400_000;
|
|
11
|
+
/** Milliseconds in one hour */
|
|
12
|
+
export const HOUR_MS = 3_600_000;
|
|
13
|
+
/** Array [0..23] for iterating over hour slots */
|
|
14
|
+
export const HOURS = Array.from({ length: 24 }, (_, i) => i);
|
|
15
|
+
// ─── Pure date helpers ──────────────────────────────────
|
|
16
|
+
/** Start-of-day timestamp (midnight, local time) */
|
|
17
|
+
export function sod(ms) {
|
|
18
|
+
return startOfDay(ms).getTime();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Start-of-week timestamp.
|
|
22
|
+
* @param ms Any timestamp within the target week
|
|
23
|
+
* @param mondayStart true → weeks begin Monday; false → Sunday
|
|
24
|
+
*/
|
|
25
|
+
export function startOfWeek(ms, mondayStart = true) {
|
|
26
|
+
return dfnsStartOfWeek(ms, { weekStartsOn: mondayStart ? 1 : 0 }).getTime();
|
|
27
|
+
}
|
|
28
|
+
/** Add `n` days to a timestamp and return the new timestamp */
|
|
29
|
+
export function addDaysMs(ms, n) {
|
|
30
|
+
return addDays(ms, n).getTime();
|
|
31
|
+
}
|
|
32
|
+
/** Calendar-day difference between two timestamps */
|
|
33
|
+
export function diffDays(a, b) {
|
|
34
|
+
return differenceInCalendarDays(a, b);
|
|
35
|
+
}
|
|
36
|
+
/** Zero-pad a number to 2 digits */
|
|
37
|
+
export function pad(n) {
|
|
38
|
+
return n < 10 ? '0' + n : '' + n;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Fractional hours elapsed since midnight for a given timestamp.
|
|
42
|
+
* e.g. 14:30:00 → 14.5
|
|
43
|
+
*/
|
|
44
|
+
export function fractionalHour(ms) {
|
|
45
|
+
const d = new Date(ms);
|
|
46
|
+
return getHours(d) + getMinutes(d) / 60 + getSeconds(d) / 3600;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Format hours + minutes from a timestamp: "14:30"
|
|
50
|
+
*/
|
|
51
|
+
export function fmtHM(ms) {
|
|
52
|
+
const d = new Date(ms);
|
|
53
|
+
return pad(getHours(d)) + ':' + pad(getMinutes(d));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Format seconds from a timestamp: ":05"
|
|
57
|
+
*/
|
|
58
|
+
export function fmtS(ms) {
|
|
59
|
+
return ':' + pad(getSeconds(new Date(ms)));
|
|
60
|
+
}
|
|
61
|
+
/** Day-of-month number for a timestamp */
|
|
62
|
+
export function dayNum(ms) {
|
|
63
|
+
return getDate(new Date(ms));
|
|
64
|
+
}
|
|
65
|
+
/** Day-of-week index (0 = Sun … 6 = Sat) */
|
|
66
|
+
export function dayOfWeek(ms) {
|
|
67
|
+
return getDay(new Date(ms));
|
|
68
|
+
}
|