@aiaiai-pt/design-system 0.4.3 → 0.5.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/components/Calendar.svelte +971 -0
- package/components/Calendar.svelte.d.ts +50 -0
- package/components/DatePicker.svelte +473 -0
- package/components/DatePicker.svelte.d.ts +59 -0
- package/components/DateRangePicker.svelte +558 -0
- package/components/DateRangePicker.svelte.d.ts +55 -0
- package/components/DateTimePicker.svelte +275 -0
- package/components/DateTimePicker.svelte.d.ts +55 -0
- package/components/FilterPanel.svelte +53 -8
- package/components/MapCluster.svelte +220 -0
- package/components/MapCluster.svelte.d.ts +39 -0
- package/components/MapDisplay.svelte +139 -0
- package/components/MapDisplay.svelte.d.ts +35 -0
- package/components/MapHeatmap.svelte +164 -0
- package/components/MapHeatmap.svelte.d.ts +50 -0
- package/components/MapPicker.svelte +243 -0
- package/components/MapPicker.svelte.d.ts +49 -0
- package/components/MapPopup.svelte +101 -0
- package/components/MapPopup.svelte.d.ts +30 -0
- package/components/StatCard.svelte +195 -0
- package/components/StatCard.svelte.d.ts +42 -0
- package/components/StatGrid.svelte +39 -0
- package/components/StatGrid.svelte.d.ts +29 -0
- package/components/index.d.ts +12 -0
- package/components/index.js +17 -0
- package/components/map-utils.d.ts +100 -0
- package/components/map-utils.js +338 -0
- package/package.json +8 -1
- package/tokens/components.css +215 -0
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Calendar
|
|
3
|
+
|
|
4
|
+
Full-page scheduling calendar with month, week, and day views.
|
|
5
|
+
Events rendered as colored blocks with title, time, and optional status.
|
|
6
|
+
Navigation with prev/next, today button, and view switcher.
|
|
7
|
+
Consumes --calendar-* tokens from components.css.
|
|
8
|
+
|
|
9
|
+
@example Basic month view
|
|
10
|
+
<Calendar events={events} oneventclick={(ev) => console.log(ev)} />
|
|
11
|
+
|
|
12
|
+
@example Week view with locale
|
|
13
|
+
<Calendar view="week" events={events} locale={pt} />
|
|
14
|
+
|
|
15
|
+
@example Custom event rendering
|
|
16
|
+
<Calendar events={events}>
|
|
17
|
+
{#snippet event(ev)}
|
|
18
|
+
<span>{ev.title}</span>
|
|
19
|
+
<Badge>{ev.status}</Badge>
|
|
20
|
+
{/snippet}
|
|
21
|
+
</Calendar>
|
|
22
|
+
-->
|
|
23
|
+
<script>
|
|
24
|
+
import {
|
|
25
|
+
format,
|
|
26
|
+
startOfMonth, endOfMonth,
|
|
27
|
+
startOfWeek, endOfWeek,
|
|
28
|
+
startOfDay, endOfDay,
|
|
29
|
+
addDays, addWeeks, subWeeks, addMonths, subMonths,
|
|
30
|
+
isSameDay, isSameMonth, isBefore
|
|
31
|
+
} from 'date-fns';
|
|
32
|
+
import { enUS } from 'date-fns/locale';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {'month' | 'week' | 'day'} View
|
|
36
|
+
* @typedef {Object} CalendarEvent
|
|
37
|
+
* @property {string | number} id
|
|
38
|
+
* @property {string} title
|
|
39
|
+
* @property {Date} start
|
|
40
|
+
* @property {Date} [end]
|
|
41
|
+
* @property {string} [color] - CSS color or custom property
|
|
42
|
+
* @property {string} [status]
|
|
43
|
+
* @property {boolean} [allDay]
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
let {
|
|
47
|
+
/** @type {View} */
|
|
48
|
+
view = $bindable('month'),
|
|
49
|
+
/** @type {Date} */
|
|
50
|
+
date = $bindable(new Date()),
|
|
51
|
+
/** @type {CalendarEvent[]} */
|
|
52
|
+
events = [],
|
|
53
|
+
/** @type {number} — max visible events per month cell */
|
|
54
|
+
maxVisible = 3,
|
|
55
|
+
/** @type {import('date-fns').Locale} */
|
|
56
|
+
locale = enUS,
|
|
57
|
+
/** @type {((event: CalendarEvent) => void) | undefined} */
|
|
58
|
+
oneventclick = undefined,
|
|
59
|
+
/** @type {((date: Date) => void) | undefined} */
|
|
60
|
+
ondateclick = undefined,
|
|
61
|
+
/** @type {import('svelte').Snippet<[CalendarEvent]> | undefined} */
|
|
62
|
+
event: eventSnippet = undefined,
|
|
63
|
+
/** @type {string} */
|
|
64
|
+
class: className = '',
|
|
65
|
+
...rest
|
|
66
|
+
} = $props();
|
|
67
|
+
|
|
68
|
+
// ─── Shared derived ───
|
|
69
|
+
|
|
70
|
+
const weekdays = $derived(getWeekdays(locale));
|
|
71
|
+
const hours = Array.from({ length: 24 }, (_, i) => i);
|
|
72
|
+
|
|
73
|
+
// ─── Month derived ───
|
|
74
|
+
|
|
75
|
+
const calendarDays = $derived(getCalendarDays(date, locale));
|
|
76
|
+
const weekCount = $derived(Math.ceil(calendarDays.length / 7));
|
|
77
|
+
|
|
78
|
+
// ─── Week derived ───
|
|
79
|
+
|
|
80
|
+
const weekDays = $derived(getWeekDayDates(date, locale));
|
|
81
|
+
const weekTitle = $derived(formatWeekTitle(date, locale));
|
|
82
|
+
|
|
83
|
+
// ─── Day derived ───
|
|
84
|
+
|
|
85
|
+
const dayTitle = $derived(format(date, 'EEEE, MMMM d', { locale }));
|
|
86
|
+
const dayIsToday = $derived(isSameDay(date, new Date()));
|
|
87
|
+
const dayViewEvents = $derived(getTimedEventsForDate(date));
|
|
88
|
+
const dayViewLayout = $derived(computeEventLayout(dayViewEvents));
|
|
89
|
+
|
|
90
|
+
// ─── Helpers: calendar grid generation ───
|
|
91
|
+
|
|
92
|
+
/** @param {import('date-fns').Locale} loc */
|
|
93
|
+
function getWeekdays(loc) {
|
|
94
|
+
const s = startOfWeek(new Date(), { locale: loc });
|
|
95
|
+
return Array.from({ length: 7 }, (_, i) =>
|
|
96
|
+
format(addDays(s, i), 'EEEEEE', { locale: loc })
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @param {Date} d @param {import('date-fns').Locale} loc */
|
|
101
|
+
function getCalendarDays(d, loc) {
|
|
102
|
+
const monthStart = startOfMonth(d);
|
|
103
|
+
const monthEnd = endOfMonth(d);
|
|
104
|
+
const calStart = startOfWeek(monthStart, { locale: loc });
|
|
105
|
+
const calEnd = endOfWeek(monthEnd, { locale: loc });
|
|
106
|
+
|
|
107
|
+
/** @type {Date[]} */
|
|
108
|
+
const days = [];
|
|
109
|
+
let cursor = calStart;
|
|
110
|
+
while (isBefore(cursor, calEnd) || isSameDay(cursor, calEnd)) {
|
|
111
|
+
days.push(cursor);
|
|
112
|
+
cursor = addDays(cursor, 1);
|
|
113
|
+
}
|
|
114
|
+
return days;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @param {Date} d @param {import('date-fns').Locale} loc */
|
|
118
|
+
function getWeekDayDates(d, loc) {
|
|
119
|
+
const ws = startOfWeek(d, { locale: loc });
|
|
120
|
+
return Array.from({ length: 7 }, (_, i) => addDays(ws, i));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @param {Date} d @param {import('date-fns').Locale} loc */
|
|
124
|
+
function formatWeekTitle(d, loc) {
|
|
125
|
+
const ws = startOfWeek(d, { locale: loc });
|
|
126
|
+
const we = endOfWeek(d, { locale: loc });
|
|
127
|
+
if (ws.getMonth() === we.getMonth()) {
|
|
128
|
+
return `${format(ws, 'MMM d', { locale: loc })} – ${format(we, 'd, yyyy', { locale: loc })}`;
|
|
129
|
+
}
|
|
130
|
+
return `${format(ws, 'MMM d', { locale: loc })} – ${format(we, 'MMM d, yyyy', { locale: loc })}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Helpers: event filtering ───
|
|
134
|
+
|
|
135
|
+
/** @param {CalendarEvent} ev @param {Date} day */
|
|
136
|
+
function isEventOnDate(ev, day) {
|
|
137
|
+
if (!ev.end) return isSameDay(ev.start, day);
|
|
138
|
+
const dayStart = startOfDay(day);
|
|
139
|
+
const dayEnd = endOfDay(day);
|
|
140
|
+
return ev.start <= dayEnd && ev.end >= dayStart;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** @param {Date} day */
|
|
144
|
+
function getEventsForDate(day) {
|
|
145
|
+
return events.filter(ev => isEventOnDate(ev, day));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @param {Date} day — timed (non-allDay) events starting on this day */
|
|
149
|
+
function getTimedEventsForDate(day) {
|
|
150
|
+
return events.filter(ev =>
|
|
151
|
+
!ev.allDay && isSameDay(ev.start, day)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── Helpers: week/day event positioning ───
|
|
156
|
+
|
|
157
|
+
/** @param {CalendarEvent} ev */
|
|
158
|
+
function getEventTopHours(ev) {
|
|
159
|
+
return ev.start.getHours() + ev.start.getMinutes() / 60;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @param {CalendarEvent} ev */
|
|
163
|
+
function getEventDurationHours(ev) {
|
|
164
|
+
if (!ev.end) return 1;
|
|
165
|
+
const mins = (ev.end.getTime() - ev.start.getTime()) / 60000;
|
|
166
|
+
return Math.max(mins / 60, 0.5); // min half-hour height
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Compute column layout for overlapping events within a single day.
|
|
171
|
+
* Returns Map<id, { column: number, totalColumns: number }>
|
|
172
|
+
* @param {CalendarEvent[]} dayEvents
|
|
173
|
+
*/
|
|
174
|
+
function computeEventLayout(dayEvents) {
|
|
175
|
+
/** @type {Map<string|number, { column: number, totalColumns: number }>} */
|
|
176
|
+
const result = new Map();
|
|
177
|
+
if (dayEvents.length === 0) return result;
|
|
178
|
+
|
|
179
|
+
const sorted = [...dayEvents].sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
180
|
+
|
|
181
|
+
// Split into non-overlapping clusters
|
|
182
|
+
/** @type {CalendarEvent[][]} */
|
|
183
|
+
const clusters = [];
|
|
184
|
+
let cluster = [sorted[0]];
|
|
185
|
+
let clusterEnd = sorted[0].end || new Date(sorted[0].start.getTime() + 3600000);
|
|
186
|
+
|
|
187
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
188
|
+
const ev = sorted[i];
|
|
189
|
+
if (ev.start < clusterEnd) {
|
|
190
|
+
cluster.push(ev);
|
|
191
|
+
const evEnd = ev.end || new Date(ev.start.getTime() + 3600000);
|
|
192
|
+
if (evEnd > clusterEnd) clusterEnd = evEnd;
|
|
193
|
+
} else {
|
|
194
|
+
clusters.push(cluster);
|
|
195
|
+
cluster = [ev];
|
|
196
|
+
clusterEnd = ev.end || new Date(ev.start.getTime() + 3600000);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
clusters.push(cluster);
|
|
200
|
+
|
|
201
|
+
for (const group of clusters) {
|
|
202
|
+
/** @type {Date[]} */
|
|
203
|
+
const columns = [];
|
|
204
|
+
for (const ev of group) {
|
|
205
|
+
const evEnd = ev.end || new Date(ev.start.getTime() + 3600000);
|
|
206
|
+
let col = columns.findIndex(colEnd => colEnd <= ev.start);
|
|
207
|
+
if (col === -1) {
|
|
208
|
+
col = columns.length;
|
|
209
|
+
columns.push(evEnd);
|
|
210
|
+
} else {
|
|
211
|
+
columns[col] = evEnd;
|
|
212
|
+
}
|
|
213
|
+
result.set(ev.id, { column: col, totalColumns: 0 });
|
|
214
|
+
}
|
|
215
|
+
for (const ev of group) {
|
|
216
|
+
const layout = result.get(ev.id);
|
|
217
|
+
if (layout) layout.totalColumns = columns.length;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Now indicator ───
|
|
225
|
+
|
|
226
|
+
/** @returns {{ hours: number, isToday: boolean }} */
|
|
227
|
+
function getNowPosition() {
|
|
228
|
+
const now = new Date();
|
|
229
|
+
return {
|
|
230
|
+
hours: now.getHours() + now.getMinutes() / 60,
|
|
231
|
+
isToday: true
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const nowPos = $derived(getNowPosition());
|
|
236
|
+
|
|
237
|
+
// ─── Navigation ───
|
|
238
|
+
|
|
239
|
+
function prev() {
|
|
240
|
+
if (view === 'month') date = subMonths(date, 1);
|
|
241
|
+
else if (view === 'week') date = subWeeks(date, 1);
|
|
242
|
+
else date = addDays(date, -1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function next() {
|
|
246
|
+
if (view === 'month') date = addMonths(date, 1);
|
|
247
|
+
else if (view === 'week') date = addWeeks(date, 1);
|
|
248
|
+
else date = addDays(date, 1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function goToday() {
|
|
252
|
+
date = new Date();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** @param {CalendarEvent} ev */
|
|
256
|
+
function handleEventClick(ev) {
|
|
257
|
+
oneventclick?.(ev);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** @param {Date} d */
|
|
261
|
+
function handleDateClick(d) {
|
|
262
|
+
ondateclick?.(d);
|
|
263
|
+
}
|
|
264
|
+
</script>
|
|
265
|
+
|
|
266
|
+
<div class="calendar {className}" {...rest}>
|
|
267
|
+
<!-- ═══ Toolbar ═══ -->
|
|
268
|
+
<!-- Toolbar buttons blur() on click to prevent stale focus rings when user presses keys afterward -->
|
|
269
|
+
<div class="calendar-toolbar">
|
|
270
|
+
<div class="calendar-toolbar-left">
|
|
271
|
+
<button type="button" class="calendar-nav-btn" onclick={(e) => { prev(); /** @type {HTMLElement} */ (e.currentTarget).blur(); }} aria-label="Previous">
|
|
272
|
+
<svg viewBox="0 0 256 256" aria-hidden="true">
|
|
273
|
+
<polyline points="160,208 80,128 160,48" fill="none" stroke="currentColor" stroke-width="16" stroke-linecap="round" stroke-linejoin="round" />
|
|
274
|
+
</svg>
|
|
275
|
+
</button>
|
|
276
|
+
<button type="button" class="calendar-nav-btn" onclick={(e) => { next(); /** @type {HTMLElement} */ (e.currentTarget).blur(); }} aria-label="Next">
|
|
277
|
+
<svg viewBox="0 0 256 256" aria-hidden="true">
|
|
278
|
+
<polyline points="96,48 176,128 96,208" fill="none" stroke="currentColor" stroke-width="16" stroke-linecap="round" stroke-linejoin="round" />
|
|
279
|
+
</svg>
|
|
280
|
+
</button>
|
|
281
|
+
<button type="button" class="calendar-today-btn" onclick={(e) => { goToday(); /** @type {HTMLElement} */ (e.currentTarget).blur(); }}>Today</button>
|
|
282
|
+
<h2 class="calendar-title">
|
|
283
|
+
{#if view === 'month'}
|
|
284
|
+
{format(date, 'LLLL yyyy', { locale })}
|
|
285
|
+
{:else if view === 'week'}
|
|
286
|
+
{weekTitle}
|
|
287
|
+
{:else}
|
|
288
|
+
{dayTitle}
|
|
289
|
+
{/if}
|
|
290
|
+
</h2>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="calendar-toolbar-right">
|
|
293
|
+
<div class="calendar-view-toggle" role="group" aria-label="Calendar view">
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
class="calendar-toggle-btn"
|
|
297
|
+
class:calendar-toggle-active={view === 'month'}
|
|
298
|
+
onclick={(e) => { view = 'month'; /** @type {HTMLElement} */ (e.currentTarget).blur(); }}
|
|
299
|
+
>Month</button>
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
class="calendar-toggle-btn"
|
|
303
|
+
class:calendar-toggle-active={view === 'week'}
|
|
304
|
+
onclick={(e) => { view = 'week'; /** @type {HTMLElement} */ (e.currentTarget).blur(); }}
|
|
305
|
+
>Week</button>
|
|
306
|
+
<button
|
|
307
|
+
type="button"
|
|
308
|
+
class="calendar-toggle-btn"
|
|
309
|
+
class:calendar-toggle-active={view === 'day'}
|
|
310
|
+
onclick={(e) => { view = 'day'; /** @type {HTMLElement} */ (e.currentTarget).blur(); }}
|
|
311
|
+
>Day</button>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<!-- ═══ Month View ═══ -->
|
|
317
|
+
{#if view === 'month'}
|
|
318
|
+
<div class="calendar-month" role="grid" aria-label="Calendar month view">
|
|
319
|
+
<div class="calendar-weekday-row" role="row">
|
|
320
|
+
{#each weekdays as day}
|
|
321
|
+
<span class="calendar-weekday" role="columnheader">{day}</span>
|
|
322
|
+
{/each}
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<div
|
|
326
|
+
class="calendar-month-grid"
|
|
327
|
+
role="rowgroup"
|
|
328
|
+
style="grid-template-rows: repeat({weekCount}, minmax(var(--calendar-cell-min-height), 1fr));"
|
|
329
|
+
>
|
|
330
|
+
{#each calendarDays as day}
|
|
331
|
+
{@const isToday = isSameDay(day, new Date())}
|
|
332
|
+
{@const isOutside = !isSameMonth(day, date)}
|
|
333
|
+
{@const dayEvents = getEventsForDate(day)}
|
|
334
|
+
{@const visibleEvents = dayEvents.slice(0, maxVisible)}
|
|
335
|
+
{@const overflow = dayEvents.length - maxVisible}
|
|
336
|
+
<div
|
|
337
|
+
class="calendar-cell"
|
|
338
|
+
class:calendar-cell-today={isToday}
|
|
339
|
+
class:calendar-cell-outside={isOutside}
|
|
340
|
+
role="gridcell"
|
|
341
|
+
aria-label={format(day, 'EEEE, MMMM d', { locale })}
|
|
342
|
+
onclick={() => handleDateClick(day)}
|
|
343
|
+
>
|
|
344
|
+
<span class="calendar-day-number" class:calendar-day-today={isToday}>
|
|
345
|
+
{day.getDate()}
|
|
346
|
+
</span>
|
|
347
|
+
<div class="calendar-cell-events">
|
|
348
|
+
{#each visibleEvents as ev (ev.id)}
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
class="calendar-event-pill"
|
|
352
|
+
style="background: {ev.color || 'var(--calendar-event-default-bg)'}; color: {ev.color ? 'var(--color-text-on-accent)' : 'var(--calendar-event-default-text)'};"
|
|
353
|
+
onclick={(e) => { e.stopPropagation(); handleEventClick(ev); }}
|
|
354
|
+
title={ev.title}
|
|
355
|
+
>
|
|
356
|
+
{#if eventSnippet}
|
|
357
|
+
{@render eventSnippet(ev)}
|
|
358
|
+
{:else}
|
|
359
|
+
<span class="calendar-event-pill-title">{ev.title}</span>
|
|
360
|
+
{/if}
|
|
361
|
+
</button>
|
|
362
|
+
{/each}
|
|
363
|
+
{#if overflow > 0}
|
|
364
|
+
<span class="calendar-overflow">+{overflow} more</span>
|
|
365
|
+
{/if}
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
{/each}
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<!-- ═══ Week View ═══ -->
|
|
373
|
+
{:else if view === 'week'}
|
|
374
|
+
<div class="calendar-week" role="grid" aria-label="Calendar week view">
|
|
375
|
+
<!-- Weekday header row -->
|
|
376
|
+
<div class="calendar-week-header">
|
|
377
|
+
<div class="calendar-time-spacer"></div>
|
|
378
|
+
{#each weekDays as day}
|
|
379
|
+
{@const isToday = isSameDay(day, new Date())}
|
|
380
|
+
<div class="calendar-week-day-header" class:calendar-week-day-today={isToday}>
|
|
381
|
+
<span class="calendar-week-day-name">{format(day, 'EEE', { locale })}</span>
|
|
382
|
+
<span class="calendar-week-day-num" class:calendar-week-day-num-today={isToday}>{day.getDate()}</span>
|
|
383
|
+
</div>
|
|
384
|
+
{/each}
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<!-- Scrollable time grid -->
|
|
388
|
+
<div class="calendar-week-body">
|
|
389
|
+
<!-- Time gutter -->
|
|
390
|
+
<div class="calendar-time-gutter">
|
|
391
|
+
{#each hours as hour}
|
|
392
|
+
<div class="calendar-time-label">
|
|
393
|
+
{String(hour).padStart(2, '0')}:00
|
|
394
|
+
</div>
|
|
395
|
+
{/each}
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<!-- Day columns -->
|
|
399
|
+
{#each weekDays as day}
|
|
400
|
+
{@const isToday = isSameDay(day, new Date())}
|
|
401
|
+
{@const dayEvents = getTimedEventsForDate(day)}
|
|
402
|
+
{@const layout = computeEventLayout(dayEvents)}
|
|
403
|
+
<div class="calendar-day-column" onclick={() => handleDateClick(day)}>
|
|
404
|
+
{#each hours as hour}
|
|
405
|
+
<div class="calendar-hour-slot"></div>
|
|
406
|
+
{/each}
|
|
407
|
+
|
|
408
|
+
<!-- Now indicator -->
|
|
409
|
+
{#if isToday}
|
|
410
|
+
<div
|
|
411
|
+
class="calendar-now-line"
|
|
412
|
+
style="top: calc({nowPos.hours} * var(--calendar-slot-height));"
|
|
413
|
+
>
|
|
414
|
+
<div class="calendar-now-dot"></div>
|
|
415
|
+
</div>
|
|
416
|
+
{/if}
|
|
417
|
+
|
|
418
|
+
<!-- Events -->
|
|
419
|
+
{#each dayEvents as ev (ev.id)}
|
|
420
|
+
{@const pos = layout.get(ev.id)}
|
|
421
|
+
{@const top = getEventTopHours(ev)}
|
|
422
|
+
{@const height = getEventDurationHours(ev)}
|
|
423
|
+
<button
|
|
424
|
+
type="button"
|
|
425
|
+
class="calendar-event-block"
|
|
426
|
+
style="
|
|
427
|
+
top: calc({top} * var(--calendar-slot-height));
|
|
428
|
+
height: calc({height} * var(--calendar-slot-height));
|
|
429
|
+
left: calc({pos ? pos.column / pos.totalColumns * 100 : 0}%);
|
|
430
|
+
width: calc({pos ? 100 / pos.totalColumns : 100}%);
|
|
431
|
+
background: {ev.color || 'var(--calendar-event-default-bg)'};
|
|
432
|
+
color: {ev.color ? 'var(--color-text-on-accent)' : 'var(--calendar-event-default-text)'};
|
|
433
|
+
"
|
|
434
|
+
onclick={(e) => { e.stopPropagation(); handleEventClick(ev); }}
|
|
435
|
+
title="{ev.title} ({format(ev.start, 'HH:mm', { locale })}{ev.end ? ' – ' + format(ev.end, 'HH:mm', { locale }) : ''})"
|
|
436
|
+
>
|
|
437
|
+
{#if eventSnippet}
|
|
438
|
+
{@render eventSnippet(ev)}
|
|
439
|
+
{:else}
|
|
440
|
+
<span class="calendar-event-time">{format(ev.start, 'HH:mm', { locale })}</span>
|
|
441
|
+
<span class="calendar-event-title">{ev.title}</span>
|
|
442
|
+
{/if}
|
|
443
|
+
</button>
|
|
444
|
+
{/each}
|
|
445
|
+
</div>
|
|
446
|
+
{/each}
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<!-- ═══ Day View ═══ -->
|
|
451
|
+
{:else}
|
|
452
|
+
<div class="calendar-day-view" role="grid" aria-label="Calendar day view">
|
|
453
|
+
<!-- Day header -->
|
|
454
|
+
<div class="calendar-day-header">
|
|
455
|
+
<div class="calendar-time-spacer"></div>
|
|
456
|
+
<div class="calendar-day-header-cell" class:calendar-week-day-today={dayIsToday}>
|
|
457
|
+
<span class="calendar-week-day-name">{format(date, 'EEEE', { locale })}</span>
|
|
458
|
+
<span class="calendar-week-day-num" class:calendar-week-day-num-today={dayIsToday}>{date.getDate()}</span>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<!-- Scrollable time grid -->
|
|
463
|
+
<div class="calendar-day-body">
|
|
464
|
+
<div class="calendar-time-gutter">
|
|
465
|
+
{#each hours as hour}
|
|
466
|
+
<div class="calendar-time-label">
|
|
467
|
+
{String(hour).padStart(2, '0')}:00
|
|
468
|
+
</div>
|
|
469
|
+
{/each}
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<div class="calendar-day-column" onclick={() => handleDateClick(date)}>
|
|
473
|
+
{#each hours as hour}
|
|
474
|
+
<div class="calendar-hour-slot"></div>
|
|
475
|
+
{/each}
|
|
476
|
+
|
|
477
|
+
{#if dayIsToday}
|
|
478
|
+
<div
|
|
479
|
+
class="calendar-now-line"
|
|
480
|
+
style="top: calc({nowPos.hours} * var(--calendar-slot-height));"
|
|
481
|
+
>
|
|
482
|
+
<div class="calendar-now-dot"></div>
|
|
483
|
+
</div>
|
|
484
|
+
{/if}
|
|
485
|
+
|
|
486
|
+
{#each dayViewEvents as ev (ev.id)}
|
|
487
|
+
{@const pos = dayViewLayout.get(ev.id)}
|
|
488
|
+
{@const top = getEventTopHours(ev)}
|
|
489
|
+
{@const height = getEventDurationHours(ev)}
|
|
490
|
+
<button
|
|
491
|
+
type="button"
|
|
492
|
+
class="calendar-event-block"
|
|
493
|
+
style="
|
|
494
|
+
top: calc({top} * var(--calendar-slot-height));
|
|
495
|
+
height: calc({height} * var(--calendar-slot-height));
|
|
496
|
+
left: calc({pos ? pos.column / pos.totalColumns * 100 : 0}%);
|
|
497
|
+
width: calc({pos ? 100 / pos.totalColumns : 100}%);
|
|
498
|
+
background: {ev.color || 'var(--calendar-event-default-bg)'};
|
|
499
|
+
color: {ev.color ? 'var(--color-text-on-accent)' : 'var(--calendar-event-default-text)'};
|
|
500
|
+
"
|
|
501
|
+
onclick={(e) => { e.stopPropagation(); handleEventClick(ev); }}
|
|
502
|
+
title="{ev.title} ({format(ev.start, 'HH:mm', { locale })}{ev.end ? ' – ' + format(ev.end, 'HH:mm', { locale }) : ''})"
|
|
503
|
+
>
|
|
504
|
+
{#if eventSnippet}
|
|
505
|
+
{@render eventSnippet(ev)}
|
|
506
|
+
{:else}
|
|
507
|
+
<span class="calendar-event-time">{format(ev.start, 'HH:mm', { locale })}</span>
|
|
508
|
+
<span class="calendar-event-title">{ev.title}</span>
|
|
509
|
+
{/if}
|
|
510
|
+
</button>
|
|
511
|
+
{/each}
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
{/if}
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
<style>
|
|
519
|
+
/* ═══ Container ═══ */
|
|
520
|
+
.calendar {
|
|
521
|
+
display: flex;
|
|
522
|
+
flex-direction: column;
|
|
523
|
+
background: var(--calendar-bg);
|
|
524
|
+
border: var(--calendar-border);
|
|
525
|
+
border-radius: var(--calendar-radius);
|
|
526
|
+
padding: var(--calendar-padding);
|
|
527
|
+
min-height: 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/* ═══ Toolbar ═══ */
|
|
531
|
+
.calendar-toolbar {
|
|
532
|
+
display: flex;
|
|
533
|
+
align-items: center;
|
|
534
|
+
justify-content: space-between;
|
|
535
|
+
gap: var(--calendar-toolbar-gap);
|
|
536
|
+
margin-bottom: var(--calendar-padding);
|
|
537
|
+
flex-wrap: wrap;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.calendar-toolbar-left {
|
|
541
|
+
display: flex;
|
|
542
|
+
align-items: center;
|
|
543
|
+
gap: var(--calendar-toolbar-gap);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.calendar-toolbar-right {
|
|
547
|
+
display: flex;
|
|
548
|
+
align-items: center;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.calendar-title {
|
|
552
|
+
font-family: var(--calendar-title-font);
|
|
553
|
+
font-size: var(--calendar-title-size);
|
|
554
|
+
font-weight: var(--calendar-title-weight);
|
|
555
|
+
letter-spacing: var(--calendar-title-tracking);
|
|
556
|
+
color: var(--calendar-title-color);
|
|
557
|
+
margin: 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/* ─── Nav buttons ─── */
|
|
561
|
+
.calendar-nav-btn {
|
|
562
|
+
display: flex;
|
|
563
|
+
align-items: center;
|
|
564
|
+
justify-content: center;
|
|
565
|
+
width: var(--calendar-nav-btn-size);
|
|
566
|
+
height: var(--calendar-nav-btn-size);
|
|
567
|
+
border: none;
|
|
568
|
+
border-radius: var(--calendar-nav-btn-radius);
|
|
569
|
+
background: transparent;
|
|
570
|
+
color: var(--color-text-secondary);
|
|
571
|
+
cursor: pointer;
|
|
572
|
+
transition: background var(--duration-instant) var(--easing-default);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.calendar-nav-btn:hover {
|
|
576
|
+
background: var(--calendar-nav-btn-hover-bg);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.calendar-nav-btn svg {
|
|
580
|
+
width: var(--icon-size-xs);
|
|
581
|
+
height: var(--icon-size-xs);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* ─── Today button ─── */
|
|
585
|
+
.calendar-today-btn {
|
|
586
|
+
font-family: var(--calendar-toggle-font);
|
|
587
|
+
font-size: var(--calendar-toggle-size);
|
|
588
|
+
letter-spacing: var(--calendar-toggle-tracking);
|
|
589
|
+
padding: var(--calendar-toggle-padding);
|
|
590
|
+
border: var(--calendar-cell-border);
|
|
591
|
+
border-radius: var(--calendar-toggle-radius);
|
|
592
|
+
background: transparent;
|
|
593
|
+
color: var(--calendar-toggle-color);
|
|
594
|
+
cursor: pointer;
|
|
595
|
+
transition: background var(--duration-instant) var(--easing-default);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.calendar-today-btn:hover {
|
|
599
|
+
background: var(--calendar-toggle-hover-bg);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/* ─── View toggle ─── */
|
|
603
|
+
.calendar-view-toggle {
|
|
604
|
+
display: flex;
|
|
605
|
+
border: var(--calendar-cell-border);
|
|
606
|
+
border-radius: var(--calendar-toggle-radius);
|
|
607
|
+
overflow: hidden;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.calendar-toggle-btn {
|
|
611
|
+
font-family: var(--calendar-toggle-font);
|
|
612
|
+
font-size: var(--calendar-toggle-size);
|
|
613
|
+
letter-spacing: var(--calendar-toggle-tracking);
|
|
614
|
+
padding: var(--calendar-toggle-padding);
|
|
615
|
+
border: none;
|
|
616
|
+
background: transparent;
|
|
617
|
+
color: var(--calendar-toggle-color);
|
|
618
|
+
cursor: pointer;
|
|
619
|
+
transition: background var(--duration-instant) var(--easing-default),
|
|
620
|
+
color var(--duration-instant) var(--easing-default);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.calendar-toggle-btn:hover:not(.calendar-toggle-active) {
|
|
624
|
+
background: var(--calendar-toggle-hover-bg);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.calendar-toggle-active {
|
|
628
|
+
background: var(--calendar-toggle-active-bg);
|
|
629
|
+
color: var(--calendar-toggle-active-text);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/* ═══ Month View ═══ */
|
|
633
|
+
.calendar-month {
|
|
634
|
+
display: flex;
|
|
635
|
+
flex-direction: column;
|
|
636
|
+
flex: 1;
|
|
637
|
+
min-height: 0;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.calendar-weekday-row {
|
|
641
|
+
display: grid;
|
|
642
|
+
grid-template-columns: repeat(7, 1fr);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.calendar-weekday {
|
|
646
|
+
display: flex;
|
|
647
|
+
align-items: center;
|
|
648
|
+
justify-content: center;
|
|
649
|
+
height: var(--calendar-weekday-height);
|
|
650
|
+
font-family: var(--calendar-weekday-font);
|
|
651
|
+
font-size: var(--calendar-weekday-size);
|
|
652
|
+
letter-spacing: var(--calendar-weekday-tracking);
|
|
653
|
+
color: var(--calendar-weekday-color);
|
|
654
|
+
text-transform: uppercase;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.calendar-month-grid {
|
|
658
|
+
display: grid;
|
|
659
|
+
grid-template-columns: repeat(7, 1fr);
|
|
660
|
+
flex: 1;
|
|
661
|
+
min-height: 0;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.calendar-cell {
|
|
665
|
+
display: flex;
|
|
666
|
+
flex-direction: column;
|
|
667
|
+
gap: var(--space-2xs);
|
|
668
|
+
padding: var(--calendar-cell-padding);
|
|
669
|
+
border-right: var(--calendar-cell-border);
|
|
670
|
+
border-bottom: var(--calendar-cell-border);
|
|
671
|
+
cursor: pointer;
|
|
672
|
+
transition: background var(--duration-instant) var(--easing-default);
|
|
673
|
+
overflow: hidden;
|
|
674
|
+
min-height: 0;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.calendar-cell:hover {
|
|
678
|
+
background: var(--calendar-cell-hover-bg);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.calendar-cell:nth-child(7n + 1) {
|
|
682
|
+
border-left: var(--calendar-cell-border);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.calendar-cell:nth-child(-n + 7) {
|
|
686
|
+
border-top: var(--calendar-cell-border);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.calendar-cell-today {
|
|
690
|
+
background: var(--calendar-cell-today-bg);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.calendar-cell-outside {
|
|
694
|
+
opacity: var(--calendar-day-outside-opacity);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/* ─── Day number ─── */
|
|
698
|
+
.calendar-day-number {
|
|
699
|
+
font-family: var(--calendar-day-font);
|
|
700
|
+
font-size: var(--calendar-day-size);
|
|
701
|
+
color: var(--calendar-day-color);
|
|
702
|
+
line-height: 1;
|
|
703
|
+
align-self: flex-end;
|
|
704
|
+
padding: var(--space-2xs);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.calendar-day-today {
|
|
708
|
+
display: inline-flex;
|
|
709
|
+
align-items: center;
|
|
710
|
+
justify-content: center;
|
|
711
|
+
width: var(--calendar-day-today-size);
|
|
712
|
+
height: var(--calendar-day-today-size);
|
|
713
|
+
background: var(--calendar-day-today-bg);
|
|
714
|
+
color: var(--calendar-day-today-text);
|
|
715
|
+
border-radius: var(--radius-circle);
|
|
716
|
+
font-weight: 600;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/* ─── Month event pills ─── */
|
|
720
|
+
.calendar-cell-events {
|
|
721
|
+
display: flex;
|
|
722
|
+
flex-direction: column;
|
|
723
|
+
gap: var(--space-2xs);
|
|
724
|
+
min-height: 0;
|
|
725
|
+
overflow: hidden;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.calendar-event-pill {
|
|
729
|
+
display: flex;
|
|
730
|
+
align-items: center;
|
|
731
|
+
gap: var(--space-2xs);
|
|
732
|
+
padding: var(--calendar-event-padding);
|
|
733
|
+
border: none;
|
|
734
|
+
border-radius: var(--calendar-event-radius);
|
|
735
|
+
cursor: pointer;
|
|
736
|
+
text-align: left;
|
|
737
|
+
width: 100%;
|
|
738
|
+
min-height: 0;
|
|
739
|
+
overflow: hidden;
|
|
740
|
+
transition: opacity var(--duration-instant) var(--easing-default);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.calendar-event-pill:hover {
|
|
744
|
+
opacity: 0.85;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.calendar-event-pill-title {
|
|
748
|
+
font-family: var(--calendar-event-font);
|
|
749
|
+
font-size: var(--calendar-event-size);
|
|
750
|
+
font-weight: var(--calendar-event-weight);
|
|
751
|
+
white-space: nowrap;
|
|
752
|
+
overflow: hidden;
|
|
753
|
+
text-overflow: ellipsis;
|
|
754
|
+
min-width: 0;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.calendar-overflow {
|
|
758
|
+
font-family: var(--calendar-overflow-font);
|
|
759
|
+
font-size: var(--calendar-overflow-size);
|
|
760
|
+
color: var(--calendar-overflow-color);
|
|
761
|
+
padding-left: var(--space-xs);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/* ═══ Week View ═══ */
|
|
765
|
+
.calendar-week {
|
|
766
|
+
display: flex;
|
|
767
|
+
flex-direction: column;
|
|
768
|
+
flex: 1;
|
|
769
|
+
min-height: 0;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.calendar-week-header {
|
|
773
|
+
display: grid;
|
|
774
|
+
grid-template-columns: var(--calendar-time-width) repeat(7, 1fr);
|
|
775
|
+
border-bottom: var(--calendar-cell-border);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.calendar-time-spacer {
|
|
779
|
+
width: var(--calendar-time-width);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.calendar-week-day-header {
|
|
783
|
+
display: flex;
|
|
784
|
+
flex-direction: column;
|
|
785
|
+
align-items: center;
|
|
786
|
+
gap: var(--space-2xs);
|
|
787
|
+
padding: var(--space-xs) 0;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.calendar-week-day-name {
|
|
791
|
+
font-family: var(--calendar-weekday-font);
|
|
792
|
+
font-size: var(--calendar-weekday-size);
|
|
793
|
+
letter-spacing: var(--calendar-weekday-tracking);
|
|
794
|
+
color: var(--calendar-weekday-color);
|
|
795
|
+
text-transform: uppercase;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.calendar-week-day-num {
|
|
799
|
+
font-family: var(--calendar-day-font);
|
|
800
|
+
font-size: var(--type-heading-lg-size);
|
|
801
|
+
color: var(--calendar-day-color);
|
|
802
|
+
line-height: 1;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.calendar-week-day-today .calendar-week-day-name {
|
|
806
|
+
color: var(--color-accent);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.calendar-week-day-num-today {
|
|
810
|
+
color: var(--color-accent);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.calendar-week-body {
|
|
814
|
+
display: grid;
|
|
815
|
+
grid-template-columns: var(--calendar-time-width) repeat(7, 1fr);
|
|
816
|
+
flex: 1;
|
|
817
|
+
overflow-y: auto;
|
|
818
|
+
min-height: 0;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/* ─── Time gutter ─── */
|
|
822
|
+
.calendar-time-gutter {
|
|
823
|
+
position: relative;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.calendar-time-label {
|
|
827
|
+
height: var(--calendar-slot-height);
|
|
828
|
+
display: flex;
|
|
829
|
+
align-items: flex-start;
|
|
830
|
+
justify-content: flex-end;
|
|
831
|
+
padding-right: var(--space-sm);
|
|
832
|
+
font-family: var(--calendar-time-font);
|
|
833
|
+
font-size: var(--calendar-time-size);
|
|
834
|
+
color: var(--calendar-time-color);
|
|
835
|
+
transform: translateY(-50%);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.calendar-time-label:first-child {
|
|
839
|
+
visibility: hidden;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/* ─── Day columns ─── */
|
|
843
|
+
.calendar-day-column {
|
|
844
|
+
position: relative;
|
|
845
|
+
border-left: var(--calendar-cell-border);
|
|
846
|
+
cursor: pointer;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.calendar-hour-slot {
|
|
850
|
+
height: var(--calendar-slot-height);
|
|
851
|
+
border-bottom: var(--calendar-slot-border);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/* ─── Now indicator ─── */
|
|
855
|
+
.calendar-now-line {
|
|
856
|
+
position: absolute;
|
|
857
|
+
left: 0;
|
|
858
|
+
right: 0;
|
|
859
|
+
border-top: var(--calendar-now-width) solid var(--calendar-now-color);
|
|
860
|
+
z-index: 2;
|
|
861
|
+
pointer-events: none;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.calendar-now-dot {
|
|
865
|
+
position: absolute;
|
|
866
|
+
top: 50%;
|
|
867
|
+
left: 0;
|
|
868
|
+
transform: translate(-50%, -50%);
|
|
869
|
+
width: var(--space-sm);
|
|
870
|
+
height: var(--space-sm);
|
|
871
|
+
background: var(--calendar-now-color);
|
|
872
|
+
border-radius: var(--radius-circle);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/* ─── Event blocks (week/day) ─── */
|
|
876
|
+
.calendar-event-block {
|
|
877
|
+
position: absolute;
|
|
878
|
+
display: flex;
|
|
879
|
+
flex-direction: column;
|
|
880
|
+
gap: var(--space-2xs);
|
|
881
|
+
padding: var(--calendar-event-padding);
|
|
882
|
+
border-radius: var(--calendar-event-radius);
|
|
883
|
+
border: none;
|
|
884
|
+
cursor: pointer;
|
|
885
|
+
text-align: left;
|
|
886
|
+
overflow: hidden;
|
|
887
|
+
z-index: 1;
|
|
888
|
+
transition: opacity var(--duration-instant) var(--easing-default);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.calendar-event-block:hover {
|
|
892
|
+
opacity: 0.85;
|
|
893
|
+
z-index: 3;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.calendar-event-time {
|
|
897
|
+
font-family: var(--calendar-event-font);
|
|
898
|
+
font-size: var(--calendar-event-size);
|
|
899
|
+
opacity: 0.8;
|
|
900
|
+
white-space: nowrap;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.calendar-event-title {
|
|
904
|
+
font-family: var(--calendar-event-font);
|
|
905
|
+
font-size: var(--calendar-event-size);
|
|
906
|
+
font-weight: var(--calendar-event-weight);
|
|
907
|
+
white-space: nowrap;
|
|
908
|
+
overflow: hidden;
|
|
909
|
+
text-overflow: ellipsis;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/* ═══ Day View ═══ */
|
|
913
|
+
.calendar-day-view {
|
|
914
|
+
display: flex;
|
|
915
|
+
flex-direction: column;
|
|
916
|
+
flex: 1;
|
|
917
|
+
min-height: 0;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.calendar-day-header {
|
|
921
|
+
display: grid;
|
|
922
|
+
grid-template-columns: var(--calendar-time-width) 1fr;
|
|
923
|
+
border-bottom: var(--calendar-cell-border);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.calendar-day-header-cell {
|
|
927
|
+
display: flex;
|
|
928
|
+
flex-direction: column;
|
|
929
|
+
align-items: center;
|
|
930
|
+
gap: var(--space-2xs);
|
|
931
|
+
padding: var(--space-xs) 0;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.calendar-day-body {
|
|
935
|
+
display: grid;
|
|
936
|
+
grid-template-columns: var(--calendar-time-width) 1fr;
|
|
937
|
+
flex: 1;
|
|
938
|
+
overflow-y: auto;
|
|
939
|
+
min-height: 0;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/* ═══ Focus ═══ */
|
|
943
|
+
.calendar-nav-btn:focus-visible,
|
|
944
|
+
.calendar-today-btn:focus-visible,
|
|
945
|
+
.calendar-toggle-btn:focus-visible,
|
|
946
|
+
.calendar-event-pill:focus-visible,
|
|
947
|
+
.calendar-event-block:focus-visible {
|
|
948
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
949
|
+
outline-offset: var(--focus-ring-offset);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.calendar-nav-btn:focus:not(:focus-visible),
|
|
953
|
+
.calendar-today-btn:focus:not(:focus-visible),
|
|
954
|
+
.calendar-toggle-btn:focus:not(:focus-visible),
|
|
955
|
+
.calendar-event-pill:focus:not(:focus-visible),
|
|
956
|
+
.calendar-event-block:focus:not(:focus-visible) {
|
|
957
|
+
outline: none;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/* ═══ Reduced motion ═══ */
|
|
961
|
+
@media (prefers-reduced-motion: reduce) {
|
|
962
|
+
.calendar-nav-btn,
|
|
963
|
+
.calendar-today-btn,
|
|
964
|
+
.calendar-toggle-btn,
|
|
965
|
+
.calendar-cell,
|
|
966
|
+
.calendar-event-pill,
|
|
967
|
+
.calendar-event-block {
|
|
968
|
+
transition: none;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
</style>
|