@joewinke/jatui 0.41.0 → 0.42.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/CLAUDE.md +9 -3
- package/dist/components/AnimatedNumber.svelte +22 -4
- package/dist/components/BookingScheduler.svelte +412 -0
- package/dist/components/BookingScheduler.svelte.d.ts +66 -0
- package/dist/components/CalendarPicker.svelte +190 -205
- package/dist/components/CalendarPicker.svelte.d.ts +19 -1
- package/dist/components/DatePicker.svelte +419 -0
- package/dist/components/DatePicker.svelte.d.ts +44 -0
- package/dist/components/DateRangePicker.svelte +471 -5
- package/dist/components/DateRangePicker.svelte.d.ts +32 -3
- package/dist/components/EventCalendar.svelte +1324 -0
- package/dist/components/EventCalendar.svelte.d.ts +101 -0
- package/dist/components/TimeSlotPicker.svelte +174 -108
- package/dist/components/TimeSlotPicker.svelte.d.ts +27 -0
- package/dist/components/calendar/CalendarHeader.svelte +98 -0
- package/dist/components/calendar/CalendarHeader.svelte.d.ts +24 -0
- package/dist/components/calendar/MonthGrid.svelte +361 -0
- package/dist/components/calendar/MonthGrid.svelte.d.ts +55 -0
- package/dist/components/calendar/calendarGrid.d.ts +219 -0
- package/dist/components/calendar/calendarGrid.js +494 -0
- package/dist/components/calendar/createCalendarGrid.d.ts +25 -0
- package/dist/components/calendar/createCalendarGrid.js +57 -0
- package/dist/components/calendar/dayOverlap.d.ts +92 -0
- package/dist/components/calendar/dayOverlap.js +147 -0
- package/dist/components/calendar/eventCalendar.d.ts +75 -0
- package/dist/components/calendar/eventCalendar.js +187 -0
- package/dist/components/calendar/index.d.ts +32 -0
- package/dist/components/calendar/index.js +25 -0
- package/dist/components/calendar/monthPacking.d.ts +97 -0
- package/dist/components/calendar/monthPacking.js +254 -0
- package/dist/components/calendar/timeSlots.d.ts +57 -0
- package/dist/components/calendar/timeSlots.js +163 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +11 -0
- package/dist/showcase/Globe.svelte +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,1324 @@
|
|
|
1
|
+
<script lang="ts" generics="E extends CalendarEventItem = CalendarEventItem">
|
|
2
|
+
/**
|
|
3
|
+
* EventCalendar — month / week / day / agenda event calendar.
|
|
4
|
+
*
|
|
5
|
+
* The engine is upstreamed from MOS `CalendarGrid` (jatui-lfxyy): one lane
|
|
6
|
+
* allocator (`monthPacking`) places every bar and chip in a month row so they
|
|
7
|
+
* can't collide, lane capacity is measured from the row height, and timed
|
|
8
|
+
* events in week/day view split concurrent blocks side by side (`dayOverlap`).
|
|
9
|
+
* The skin takes clean-room ideas from BoardUI Pro's public calendar page:
|
|
10
|
+
* per-hue event token triplets (`--jatui-event-N{,-bg,-fg}`), a "+N more" that
|
|
11
|
+
* expands the week in place instead of navigating, a month-switcher pill that
|
|
12
|
+
* reuses the date-picker panel, and an agenda list.
|
|
13
|
+
*
|
|
14
|
+
* The host owns the events. Callbacks are all optional; each one switches a
|
|
15
|
+
* behaviour on (click to create, drag to move, drag/Shift-select a range).
|
|
16
|
+
* `onRangeChange` reports the visible range so a host can load just that.
|
|
17
|
+
*
|
|
18
|
+
* Keyboard (month): one tab stop on the grid; arrows / Home / End move the day,
|
|
19
|
+
* PageUp/PageDown change month (Shift = year), Enter creates (or opens the
|
|
20
|
+
* day), Shift+arrows extend a range when `onRangeSelect` is set. Event chips
|
|
21
|
+
* are buttons; Escape closes the details popover.
|
|
22
|
+
*/
|
|
23
|
+
import type { Snippet } from 'svelte';
|
|
24
|
+
import { tick } from 'svelte';
|
|
25
|
+
import { floating } from '../actions/floating';
|
|
26
|
+
import MonthGrid from './calendar/MonthGrid.svelte';
|
|
27
|
+
import CalendarHeader from './calendar/CalendarHeader.svelte';
|
|
28
|
+
import IconButton from './IconButton.svelte';
|
|
29
|
+
import {
|
|
30
|
+
addMonths,
|
|
31
|
+
formatLongDate,
|
|
32
|
+
formatMonthYear,
|
|
33
|
+
isSameDay,
|
|
34
|
+
isSameMonth,
|
|
35
|
+
nextFocusDate,
|
|
36
|
+
resolveWeekStart,
|
|
37
|
+
startOfDay,
|
|
38
|
+
startOfMonth,
|
|
39
|
+
toIsoDate,
|
|
40
|
+
weekdayLabels,
|
|
41
|
+
type Weekday
|
|
42
|
+
} from './calendar/calendarGrid';
|
|
43
|
+
import { computeVisibility, packWeek, parseLocalDate, type WeekSegment } from './calendar/monthPacking';
|
|
44
|
+
import { layoutDay } from './calendar/dayOverlap';
|
|
45
|
+
import {
|
|
46
|
+
agendaDays,
|
|
47
|
+
buildMonthWeeks,
|
|
48
|
+
buildTimeGridDays,
|
|
49
|
+
chipTime,
|
|
50
|
+
eventColorStyle,
|
|
51
|
+
eventWhen,
|
|
52
|
+
hourLabel,
|
|
53
|
+
stepDate,
|
|
54
|
+
viewTitle,
|
|
55
|
+
visibleRange,
|
|
56
|
+
weekDates,
|
|
57
|
+
type CalendarEventItem,
|
|
58
|
+
type EventCalendarView
|
|
59
|
+
} from './calendar/eventCalendar';
|
|
60
|
+
import { localeHourCycle, type HourCycle } from './calendar/timeSlots';
|
|
61
|
+
|
|
62
|
+
interface Props {
|
|
63
|
+
events: E[];
|
|
64
|
+
/** Current view (bindable). */
|
|
65
|
+
view?: EventCalendarView;
|
|
66
|
+
/** The focus date — which month / week / day is shown (bindable). */
|
|
67
|
+
date?: Date;
|
|
68
|
+
/** Views offered in the toolbar switcher. */
|
|
69
|
+
views?: EventCalendarView[];
|
|
70
|
+
weekStart?: Weekday | 'locale';
|
|
71
|
+
locale?: string;
|
|
72
|
+
hourCycle?: HourCycle;
|
|
73
|
+
/** First and last (exclusive) hour of the week/day time grid. */
|
|
74
|
+
dayStartHour?: number;
|
|
75
|
+
dayEndHour?: number;
|
|
76
|
+
/** Pixel height of one hour in week/day view. */
|
|
77
|
+
hourHeight?: number;
|
|
78
|
+
/** Hour the time grid scrolls to on first show. */
|
|
79
|
+
scrollToHour?: number;
|
|
80
|
+
/** CSS height of the calendar body. */
|
|
81
|
+
height?: string;
|
|
82
|
+
toolbar?: boolean;
|
|
83
|
+
/** Extra toolbar content (filters, a "New event" button). */
|
|
84
|
+
toolbarEnd?: Snippet;
|
|
85
|
+
/** Custom chip content. */
|
|
86
|
+
eventContent?: Snippet<[E, { view: EventCalendarView; time: string }]>;
|
|
87
|
+
/** Details popover content, opened by clicking a chip. Gets the event and a close fn. */
|
|
88
|
+
eventDetails?: Snippet<[E, () => void]>;
|
|
89
|
+
/** `expand` (default): "+N more" grows the week in place. `day`: open day view. */
|
|
90
|
+
moreAction?: 'expand' | 'day';
|
|
91
|
+
onEventClick?: (ev: E, e: MouseEvent) => void;
|
|
92
|
+
/** Day number clicked. Defaults to opening day view when it is offered. */
|
|
93
|
+
onDayClick?: (date: Date) => void;
|
|
94
|
+
/** Empty cell / hour slot activated — the host opens its create flow. */
|
|
95
|
+
onCreate?: (date: Date, hour: number | undefined, e: MouseEvent | KeyboardEvent) => void;
|
|
96
|
+
/** Chip dropped on a day (and hour, in week/day view). Turns drag-to-move on. */
|
|
97
|
+
onEventMove?: (ev: E, target: { date: Date; hour?: number; allDay: boolean }) => void;
|
|
98
|
+
/** Month cells dragged (or Shift+arrowed) across. */
|
|
99
|
+
onRangeSelect?: (start: Date, end: Date) => void;
|
|
100
|
+
/** The visible range changed — load events for it. */
|
|
101
|
+
onRangeChange?: (range: { start: Date; end: Date }, view: EventCalendarView) => void;
|
|
102
|
+
ariaLabel?: string;
|
|
103
|
+
class?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let {
|
|
107
|
+
events,
|
|
108
|
+
view = $bindable('month'),
|
|
109
|
+
date = $bindable(startOfDay(new Date())),
|
|
110
|
+
views = ['month', 'week', 'day', 'agenda'],
|
|
111
|
+
weekStart = 'locale',
|
|
112
|
+
locale,
|
|
113
|
+
hourCycle,
|
|
114
|
+
dayStartHour = 6,
|
|
115
|
+
dayEndHour = 23,
|
|
116
|
+
hourHeight = 48,
|
|
117
|
+
scrollToHour = 8,
|
|
118
|
+
height = '40rem',
|
|
119
|
+
toolbar = true,
|
|
120
|
+
toolbarEnd,
|
|
121
|
+
eventContent,
|
|
122
|
+
eventDetails,
|
|
123
|
+
moreAction = 'expand',
|
|
124
|
+
onEventClick,
|
|
125
|
+
onDayClick,
|
|
126
|
+
onCreate,
|
|
127
|
+
onEventMove,
|
|
128
|
+
onRangeSelect,
|
|
129
|
+
onRangeChange,
|
|
130
|
+
ariaLabel = 'Calendar',
|
|
131
|
+
class: className = ''
|
|
132
|
+
}: Props = $props();
|
|
133
|
+
|
|
134
|
+
const VIEW_LABEL: Record<EventCalendarView, string> = { month: 'Month', week: 'Week', day: 'Day', agenda: 'Agenda' };
|
|
135
|
+
const uid = `jec-${Math.random().toString(36).slice(2, 8)}`;
|
|
136
|
+
|
|
137
|
+
const firstDay = $derived(resolveWeekStart(weekStart, locale));
|
|
138
|
+
const cycle = $derived<HourCycle>(hourCycle ?? localeHourCycle(locale));
|
|
139
|
+
const title = $derived(viewTitle(view, date, firstDay, locale));
|
|
140
|
+
const dayNames = $derived(weekdayLabels(firstDay, locale, 'short'));
|
|
141
|
+
|
|
142
|
+
// Host data loading follows the visible range.
|
|
143
|
+
$effect(() => {
|
|
144
|
+
onRangeChange?.(visibleRange(view, date, firstDay), view);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Clock for the now-line and "past" dimming.
|
|
148
|
+
let now = $state(new Date());
|
|
149
|
+
$effect(() => {
|
|
150
|
+
const id = setInterval(() => (now = new Date()), 60_000);
|
|
151
|
+
return () => clearInterval(id);
|
|
152
|
+
});
|
|
153
|
+
const today = $derived(startOfDay(now));
|
|
154
|
+
|
|
155
|
+
function go(dir: -1 | 1) {
|
|
156
|
+
date = stepDate(view, date, dir);
|
|
157
|
+
focusedDay = date;
|
|
158
|
+
}
|
|
159
|
+
function goToday() {
|
|
160
|
+
date = startOfDay(new Date());
|
|
161
|
+
focusedDay = date;
|
|
162
|
+
}
|
|
163
|
+
function openDay(d: Date) {
|
|
164
|
+
if (onDayClick) return onDayClick(d);
|
|
165
|
+
if (views.includes('day')) {
|
|
166
|
+
date = startOfDay(d);
|
|
167
|
+
view = 'day';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Details popover ────────────────────────────────────────────────────
|
|
172
|
+
let details = $state<{ ev: E; anchor: HTMLElement } | null>(null);
|
|
173
|
+
function closeDetails() {
|
|
174
|
+
details = null;
|
|
175
|
+
}
|
|
176
|
+
function handleEventClick(ev: E, e: MouseEvent) {
|
|
177
|
+
e.stopPropagation();
|
|
178
|
+
onEventClick?.(ev, e);
|
|
179
|
+
if (eventDetails) details = { ev, anchor: e.currentTarget as HTMLElement };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Drag to move ───────────────────────────────────────────────────────
|
|
183
|
+
let draggingId = $state<string | null>(null);
|
|
184
|
+
let dragOverKey = $state<string | null>(null);
|
|
185
|
+
const canDrag = (ev: E) => !!onEventMove && ev.draggable !== false;
|
|
186
|
+
|
|
187
|
+
function onChipDragStart(ev: E, e: DragEvent) {
|
|
188
|
+
if (!canDrag(ev) || !e.dataTransfer) return;
|
|
189
|
+
draggingId = ev.id;
|
|
190
|
+
closeDetails();
|
|
191
|
+
e.dataTransfer.setData('text/plain', ev.id);
|
|
192
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
193
|
+
}
|
|
194
|
+
function endDrag() {
|
|
195
|
+
draggingId = null;
|
|
196
|
+
dragOverKey = null;
|
|
197
|
+
}
|
|
198
|
+
function onCellDragOver(key: string, e: DragEvent) {
|
|
199
|
+
if (!draggingId) return;
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
202
|
+
dragOverKey = key;
|
|
203
|
+
}
|
|
204
|
+
function onCellDrop(target: { date: Date; hour?: number; allDay: boolean }, e: DragEvent) {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
const ev = events.find((x) => x.id === draggingId);
|
|
207
|
+
endDrag();
|
|
208
|
+
if (ev) onEventMove?.(ev, target);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Month view ─────────────────────────────────────────────────────────
|
|
212
|
+
const DAY_NUM_H = 26;
|
|
213
|
+
const LANE_H = 22;
|
|
214
|
+
const ROW_PAD_B = 4;
|
|
215
|
+
|
|
216
|
+
const weeks = $derived(buildMonthWeeks(date, firstDay, events));
|
|
217
|
+
const packed = $derived(weeks.map((w) => packWeek(w, now)));
|
|
218
|
+
let weeksBoxH = $state(0);
|
|
219
|
+
const laneCapacity = $derived.by(() => {
|
|
220
|
+
const rowH = weeks.length > 0 ? weeksBoxH / weeks.length : 0;
|
|
221
|
+
return rowH > 0 ? Math.max(1, Math.floor((rowH - DAY_NUM_H - ROW_PAD_B) / LANE_H)) : 3;
|
|
222
|
+
});
|
|
223
|
+
/** Weeks (keyed by their first day) whose "+N more" is expanded in place. */
|
|
224
|
+
let expanded = $state(new Set<string>());
|
|
225
|
+
|
|
226
|
+
function weekVis(wi: number) {
|
|
227
|
+
const p = packed[wi];
|
|
228
|
+
const vis = computeVisibility(p.segments, Math.min(laneCapacity, Math.max(1, p.laneCount)));
|
|
229
|
+
const key = toIsoDate(weeks[wi][0].date);
|
|
230
|
+
if (!expanded.has(key)) return { vis, open: false, foldedMore: vis.moreCounts };
|
|
231
|
+
const full = computeVisibility(p.segments, Math.max(1, p.laneCount));
|
|
232
|
+
return { vis: full, open: true, foldedMore: vis.moreCounts };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function toggleWeek(wi: number, d: Date) {
|
|
236
|
+
if (moreAction === 'day') return openDay(d);
|
|
237
|
+
const key = toIsoDate(weeks[wi][0].date);
|
|
238
|
+
const next = new Set(expanded);
|
|
239
|
+
if (next.has(key)) next.delete(key);
|
|
240
|
+
else next.add(key);
|
|
241
|
+
expanded = next;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// svelte-ignore state_referenced_locally
|
|
245
|
+
let focusedDay = $state(startOfDay(date));
|
|
246
|
+
let monthEl = $state<HTMLDivElement | null>(null);
|
|
247
|
+
const focusIso = $derived.by(() => {
|
|
248
|
+
const f = toIsoDate(focusedDay);
|
|
249
|
+
const all = weeks.flat();
|
|
250
|
+
if (all.some((d) => d.inMonth && toIsoDate(d.date) === f)) return f;
|
|
251
|
+
return toIsoDate(startOfMonth(date));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Range selection (pointer drag or Shift+arrows).
|
|
255
|
+
let selAnchor = $state<Date | null>(null);
|
|
256
|
+
let selEnd = $state<Date | null>(null);
|
|
257
|
+
let selPointer = false;
|
|
258
|
+
let selMoved = false;
|
|
259
|
+
const selRange = $derived.by(() => {
|
|
260
|
+
if (!selAnchor || !selEnd) return null;
|
|
261
|
+
return selAnchor <= selEnd ? [selAnchor, selEnd] : [selEnd, selAnchor];
|
|
262
|
+
});
|
|
263
|
+
const inSel = (d: Date) => !!selRange && d >= selRange[0] && d <= selRange[1];
|
|
264
|
+
function clearSel() {
|
|
265
|
+
selAnchor = null;
|
|
266
|
+
selEnd = null;
|
|
267
|
+
selPointer = false;
|
|
268
|
+
selMoved = false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function cellPointerDown(d: Date, e: PointerEvent) {
|
|
272
|
+
if (e.button !== 0 || !(onRangeSelect || onCreate) || draggingId) return;
|
|
273
|
+
selPointer = true;
|
|
274
|
+
selMoved = false;
|
|
275
|
+
selAnchor = d;
|
|
276
|
+
selEnd = d;
|
|
277
|
+
}
|
|
278
|
+
function cellPointerEnter(d: Date) {
|
|
279
|
+
if (!selPointer || !selEnd) return;
|
|
280
|
+
if (!isSameDay(selEnd, d)) {
|
|
281
|
+
selEnd = d;
|
|
282
|
+
selMoved = true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function cellPointerUp(d: Date, e: PointerEvent) {
|
|
286
|
+
if (!selPointer) return;
|
|
287
|
+
const range = selRange;
|
|
288
|
+
const moved = selMoved;
|
|
289
|
+
clearSel();
|
|
290
|
+
if (moved && range && onRangeSelect) onRangeSelect(range[0], range[1]);
|
|
291
|
+
else if (onCreate) onCreate(d, undefined, e);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function focusMonthCell(d: Date) {
|
|
295
|
+
await tick();
|
|
296
|
+
monthEl?.querySelector<HTMLElement>(`[data-day="${toIsoDate(d)}"]`)?.focus();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function cellKeydown(d: Date, e: KeyboardEvent) {
|
|
300
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
301
|
+
e.preventDefault();
|
|
302
|
+
if (selRange && onRangeSelect && !isSameDay(selRange[0], selRange[1])) {
|
|
303
|
+
const [s, end] = selRange;
|
|
304
|
+
clearSel();
|
|
305
|
+
onRangeSelect(s, end);
|
|
306
|
+
} else if (onCreate) onCreate(d, undefined, e);
|
|
307
|
+
else openDay(d);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (e.key === 'Escape' && selAnchor) {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
clearSel();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
|
316
|
+
const isPage = e.key === 'PageUp' || e.key === 'PageDown';
|
|
317
|
+
const next = nextFocusDate(d, e.key, { weekStart: firstDay, shiftKey: isPage && e.shiftKey });
|
|
318
|
+
if (!next) return;
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
if (e.shiftKey && !isPage && onRangeSelect) {
|
|
321
|
+
if (!selAnchor) selAnchor = d;
|
|
322
|
+
selEnd = next;
|
|
323
|
+
} else if (!e.shiftKey) clearSel();
|
|
324
|
+
focusedDay = next;
|
|
325
|
+
if (!isSameMonth(next, date)) date = startOfMonth(next);
|
|
326
|
+
void focusMonthCell(next);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function dayLabel(d: Date, count: number): string {
|
|
330
|
+
return `${formatLongDate(d, locale)}, ${count ? `${count} event${count === 1 ? '' : 's'}` : 'no events'}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Grid placement + shape for a bar. `col`/`row` offset it (the week view has a time gutter). */
|
|
334
|
+
function segStyle(seg: WeekSegment<E>, lane: number, opts: { col?: number; row?: number; lastCol?: number } = {}): string {
|
|
335
|
+
const { col = 1, row = 2, lastCol = 6 } = opts;
|
|
336
|
+
const r = 'var(--radius-selector, 0.375rem)';
|
|
337
|
+
const endCol = Math.min(seg.endCol, lastCol);
|
|
338
|
+
return [
|
|
339
|
+
`grid-column: ${seg.startCol + col} / span ${endCol - seg.startCol + 1}`,
|
|
340
|
+
`grid-row: ${lane + row}`,
|
|
341
|
+
`margin-inline-start: ${seg.continuesLeft ? 0 : 3}px`,
|
|
342
|
+
`margin-inline-end: ${seg.continuesRight ? 0 : 3}px`,
|
|
343
|
+
`border-start-start-radius: ${seg.continuesLeft ? 0 : r}`,
|
|
344
|
+
`border-end-start-radius: ${seg.continuesLeft ? 0 : r}`,
|
|
345
|
+
`border-start-end-radius: ${seg.continuesRight ? 0 : r}`,
|
|
346
|
+
`border-end-end-radius: ${seg.continuesRight ? 0 : r}`,
|
|
347
|
+
eventColorStyle(seg.ev.color)
|
|
348
|
+
].join('; ');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Week / day view ────────────────────────────────────────────────────
|
|
352
|
+
const gridDays = $derived(view === 'day' ? [startOfDay(date)] : weekDates(date, firstDay));
|
|
353
|
+
const timeDays = $derived(buildTimeGridDays(gridDays, events));
|
|
354
|
+
const allDayPacked = $derived.by(() => {
|
|
355
|
+
if (view !== 'week' && view !== 'day') return null;
|
|
356
|
+
// The 7-column packer works for a single day too: pad to a week and use column 0.
|
|
357
|
+
const week = Array.from({ length: 7 }, (_, i) => timeDays[i] ?? { date: new Date(0), inMonth: false, events: [] });
|
|
358
|
+
return packWeek(week as typeof timeDays, now);
|
|
359
|
+
});
|
|
360
|
+
const hours = $derived(Array.from({ length: Math.max(1, dayEndHour - dayStartHour) }, (_, i) => dayStartHour + i));
|
|
361
|
+
const gridMinutes = $derived((dayEndHour - dayStartHour) * 60);
|
|
362
|
+
|
|
363
|
+
function timedBlocks(day: (typeof timeDays)[number]) {
|
|
364
|
+
const startMin = dayStartHour * 60;
|
|
365
|
+
const endMin = dayEndHour * 60;
|
|
366
|
+
return layoutDay(day.timed)
|
|
367
|
+
.filter((p) => p.endMin > startMin && p.startMin < endMin)
|
|
368
|
+
.map((p) => {
|
|
369
|
+
const top = Math.max(p.startMin, startMin) - startMin;
|
|
370
|
+
const bottom = Math.min(p.endMin, endMin) - startMin;
|
|
371
|
+
return {
|
|
372
|
+
p,
|
|
373
|
+
style: [
|
|
374
|
+
`top: ${(top / 60) * hourHeight}px`,
|
|
375
|
+
`height: ${Math.max(((bottom - top) / 60) * hourHeight - 2, 18)}px`,
|
|
376
|
+
`inset-inline-start: calc(${p.leftPct}% + 2px)`,
|
|
377
|
+
`width: calc(${p.widthPct}% - 4px)`,
|
|
378
|
+
eventColorStyle(p.ev.color)
|
|
379
|
+
].join('; ')
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const nowTop = $derived.by(() => {
|
|
385
|
+
const m = now.getHours() * 60 + now.getMinutes() - dayStartHour * 60;
|
|
386
|
+
return m >= 0 && m <= gridMinutes ? (m / 60) * hourHeight : null;
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
let scrollEl = $state<HTMLDivElement | null>(null);
|
|
390
|
+
let scrolledFor = '';
|
|
391
|
+
$effect(() => {
|
|
392
|
+
const key = view;
|
|
393
|
+
if (!scrollEl || (view !== 'week' && view !== 'day') || scrolledFor === key) return;
|
|
394
|
+
scrolledFor = key;
|
|
395
|
+
scrollEl.scrollTop = Math.max(0, (scrollToHour - dayStartHour) * hourHeight);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
function slotKey(d: Date, hour?: number) {
|
|
399
|
+
return hour == null ? toIsoDate(d) : `${toIsoDate(d)}:${hour}`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── Agenda ─────────────────────────────────────────────────────────────
|
|
403
|
+
const agenda = $derived.by(() => {
|
|
404
|
+
if (view !== 'agenda') return [];
|
|
405
|
+
const r = visibleRange('agenda', date, firstDay);
|
|
406
|
+
return agendaDays(events, r.start, r.end);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ── Month switcher ─────────────────────────────────────────────────────
|
|
410
|
+
let switcherOpen = $state(false);
|
|
411
|
+
let switcherEl = $state<HTMLButtonElement | null>(null);
|
|
412
|
+
let switcherPop = $state<HTMLDivElement | null>(null);
|
|
413
|
+
let switcherMonth = $state(startOfMonth(new Date()));
|
|
414
|
+
let switcherFocus = $state(startOfDay(new Date()));
|
|
415
|
+
let switcherDismissedAt = 0;
|
|
416
|
+
async function openSwitcher() {
|
|
417
|
+
if (switcherOpen) return closeSwitcher();
|
|
418
|
+
if (performance.now() - switcherDismissedAt < 250) return;
|
|
419
|
+
switcherMonth = startOfMonth(date);
|
|
420
|
+
switcherFocus = startOfDay(date);
|
|
421
|
+
switcherOpen = true;
|
|
422
|
+
await tick();
|
|
423
|
+
switcherPop?.querySelector<HTMLElement>('.jatui-month-grid__day[tabindex="0"]')?.focus();
|
|
424
|
+
}
|
|
425
|
+
function closeSwitcher() {
|
|
426
|
+
switcherOpen = false;
|
|
427
|
+
switcherDismissedAt = performance.now();
|
|
428
|
+
}
|
|
429
|
+
function pickFromSwitcher(d: Date) {
|
|
430
|
+
date = startOfDay(d);
|
|
431
|
+
focusedDay = date;
|
|
432
|
+
closeSwitcher();
|
|
433
|
+
}
|
|
434
|
+
</script>
|
|
435
|
+
|
|
436
|
+
<svelte:document ondragend={endDrag} onpointerup={() => selPointer && clearSel()} />
|
|
437
|
+
|
|
438
|
+
<div class="jatui-ec {className}" role="region" aria-label={ariaLabel}>
|
|
439
|
+
{#if toolbar}
|
|
440
|
+
<div class="jatui-ec__toolbar">
|
|
441
|
+
<div class="jatui-ec__nav">
|
|
442
|
+
<button type="button" class="btn btn-sm btn-ghost" onclick={goToday}>Today</button>
|
|
443
|
+
<IconButton label="Previous {VIEW_LABEL[view].toLowerCase()}" size="sm" shape="circle" onclick={() => go(-1)}>
|
|
444
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" class="jatui-ec__chev"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" /></svg>
|
|
445
|
+
</IconButton>
|
|
446
|
+
<IconButton label="Next {VIEW_LABEL[view].toLowerCase()}" size="sm" shape="circle" onclick={() => go(1)}>
|
|
447
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" class="jatui-ec__chev"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /></svg>
|
|
448
|
+
</IconButton>
|
|
449
|
+
<button
|
|
450
|
+
bind:this={switcherEl}
|
|
451
|
+
type="button"
|
|
452
|
+
class="jatui-ec__title"
|
|
453
|
+
aria-haspopup="dialog"
|
|
454
|
+
aria-expanded={switcherOpen}
|
|
455
|
+
onclick={openSwitcher}
|
|
456
|
+
>
|
|
457
|
+
<span aria-live="polite">{title}</span>
|
|
458
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
|
|
459
|
+
</button>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="jatui-ec__tools">
|
|
462
|
+
{#if toolbarEnd}{@render toolbarEnd()}{/if}
|
|
463
|
+
{#if views.length > 1}
|
|
464
|
+
<div class="join" role="group" aria-label="View">
|
|
465
|
+
{#each views as v (v)}
|
|
466
|
+
<button
|
|
467
|
+
type="button"
|
|
468
|
+
class="join-item btn btn-sm {view === v ? 'btn-active' : 'btn-ghost'}"
|
|
469
|
+
aria-pressed={view === v}
|
|
470
|
+
onclick={() => (view = v)}>{VIEW_LABEL[v]}</button
|
|
471
|
+
>
|
|
472
|
+
{/each}
|
|
473
|
+
</div>
|
|
474
|
+
{/if}
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
{/if}
|
|
478
|
+
|
|
479
|
+
{#snippet chipBody(ev: E, time: string)}
|
|
480
|
+
{#if eventContent}
|
|
481
|
+
{@render eventContent(ev, { view, time })}
|
|
482
|
+
{:else}
|
|
483
|
+
{#if time}<span class="jatui-ec__time">{time}</span>{/if}
|
|
484
|
+
<span class="jatui-ec__label">{ev.title}</span>
|
|
485
|
+
{/if}
|
|
486
|
+
{/snippet}
|
|
487
|
+
|
|
488
|
+
{#snippet monthSegments(segments: WeekSegment<E>[], hidden: Set<string>, overrides: Map<string, number>, wi: number)}
|
|
489
|
+
{#each segments as seg (seg.ev.id + ':' + wi)}
|
|
490
|
+
{#if !hidden.has(seg.ev.id)}
|
|
491
|
+
{@const time = seg.filled ? '' : chipTime(seg.ev, cycle)}
|
|
492
|
+
<button
|
|
493
|
+
type="button"
|
|
494
|
+
class="jatui-ec__seg"
|
|
495
|
+
class:is-filled={seg.filled}
|
|
496
|
+
class:is-past={seg.past}
|
|
497
|
+
class:is-dragging={draggingId === seg.ev.id}
|
|
498
|
+
class:is-inert={!!draggingId && draggingId !== seg.ev.id}
|
|
499
|
+
style={segStyle(seg, overrides.get(seg.ev.id) ?? seg.lane)}
|
|
500
|
+
aria-label="{seg.ev.title}, {eventWhen(seg.ev, { hourCycle: cycle, locale })}"
|
|
501
|
+
aria-haspopup={eventDetails ? 'dialog' : undefined}
|
|
502
|
+
draggable={canDrag(seg.ev)}
|
|
503
|
+
data-event-id={seg.ev.id}
|
|
504
|
+
onclick={(e) => handleEventClick(seg.ev, e)}
|
|
505
|
+
ondragstart={(e) => onChipDragStart(seg.ev, e)}
|
|
506
|
+
ondragend={endDrag}
|
|
507
|
+
>
|
|
508
|
+
{#if seg.continuesLeft}<span class="jatui-ec__cont" aria-hidden="true">‹</span>{/if}
|
|
509
|
+
{#if !seg.filled}<span class="jatui-ec__dot" aria-hidden="true"></span>{/if}
|
|
510
|
+
{@render chipBody(seg.ev, time)}
|
|
511
|
+
{#if seg.continuesRight}<span class="jatui-ec__cont jatui-ec__cont--end" aria-hidden="true">›</span>{/if}
|
|
512
|
+
</button>
|
|
513
|
+
{/if}
|
|
514
|
+
{/each}
|
|
515
|
+
{/snippet}
|
|
516
|
+
|
|
517
|
+
{#if view === 'month'}
|
|
518
|
+
<div class="jatui-ec__month" role="grid" aria-label={formatMonthYear(date, locale)} style="height: {height}" bind:this={monthEl}>
|
|
519
|
+
<div class="jatui-ec__weekdays" role="row">
|
|
520
|
+
{#each dayNames as d (d.long)}
|
|
521
|
+
<div role="columnheader" class="jatui-ec__weekday" aria-label={d.long}>{d.short}</div>
|
|
522
|
+
{/each}
|
|
523
|
+
</div>
|
|
524
|
+
<div class="jatui-ec__weeks" bind:clientHeight={weeksBoxH}>
|
|
525
|
+
{#each weeks as week, wi (toIsoDate(week[0].date))}
|
|
526
|
+
{@const w = weekVis(wi)}
|
|
527
|
+
<div
|
|
528
|
+
class="jatui-ec__week"
|
|
529
|
+
class:is-open={w.open}
|
|
530
|
+
role="row"
|
|
531
|
+
style="grid-template-rows: {DAY_NUM_H}px repeat({w.vis.visibleLanes}, {LANE_H}px) {w.open ? 'auto' : ''} minmax(0, 1fr);"
|
|
532
|
+
>
|
|
533
|
+
{#each week as day, ci (toIsoDate(day.date))}
|
|
534
|
+
{@const iso = toIsoDate(day.date)}
|
|
535
|
+
{@const isToday = isSameDay(day.date, today)}
|
|
536
|
+
<div role="gridcell" class="jatui-ec__cell" aria-selected={inSel(day.date) ? 'true' : undefined}>
|
|
537
|
+
<div
|
|
538
|
+
class="jatui-ec__day"
|
|
539
|
+
role="button"
|
|
540
|
+
class:is-outside={!day.inMonth}
|
|
541
|
+
class:is-selecting={inSel(day.date)}
|
|
542
|
+
class:is-drop={dragOverKey === iso}
|
|
543
|
+
class:is-actionable={!!(onCreate || onRangeSelect)}
|
|
544
|
+
style="grid-column: {ci + 1}; grid-row: 1 / -1;"
|
|
545
|
+
data-day={iso}
|
|
546
|
+
tabindex={iso === focusIso ? 0 : -1}
|
|
547
|
+
aria-label={dayLabel(day.date, day.events.length)}
|
|
548
|
+
onpointerdown={(e) => e.target === e.currentTarget && cellPointerDown(day.date, e)}
|
|
549
|
+
onpointerenter={() => cellPointerEnter(day.date)}
|
|
550
|
+
onpointerup={(e) => cellPointerUp(day.date, e)}
|
|
551
|
+
onkeydown={(e) => cellKeydown(day.date, e)}
|
|
552
|
+
onfocus={() => (focusedDay = day.date)}
|
|
553
|
+
ondragover={(e) => onCellDragOver(iso, e)}
|
|
554
|
+
ondragleave={() => (dragOverKey = null)}
|
|
555
|
+
ondrop={(e) => onCellDrop({ date: day.date, allDay: true }, e)}
|
|
556
|
+
></div>
|
|
557
|
+
<div class="jatui-ec__daynum" style="grid-column: {ci + 1}; grid-row: 1;">
|
|
558
|
+
<button
|
|
559
|
+
type="button"
|
|
560
|
+
class="jatui-ec__num"
|
|
561
|
+
class:is-today={isToday}
|
|
562
|
+
class:is-past={day.date < today}
|
|
563
|
+
tabindex="-1"
|
|
564
|
+
aria-label="Open {formatLongDate(day.date, locale)}"
|
|
565
|
+
onclick={(e) => {
|
|
566
|
+
e.stopPropagation();
|
|
567
|
+
openDay(day.date);
|
|
568
|
+
}}
|
|
569
|
+
>
|
|
570
|
+
{day.date.getDate() === 1
|
|
571
|
+
? new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric' }).format(day.date)
|
|
572
|
+
: day.date.getDate()}
|
|
573
|
+
</button>
|
|
574
|
+
</div>
|
|
575
|
+
{@render monthSegments(
|
|
576
|
+
packed[wi].segments.filter((s) => s.startCol === ci),
|
|
577
|
+
w.vis.hiddenIds,
|
|
578
|
+
w.vis.laneOverrides,
|
|
579
|
+
wi
|
|
580
|
+
)}
|
|
581
|
+
{#if !w.open && w.vis.moreCounts[ci] > 0}
|
|
582
|
+
<button
|
|
583
|
+
type="button"
|
|
584
|
+
class="jatui-ec__more"
|
|
585
|
+
class:is-inert={!!draggingId}
|
|
586
|
+
style="grid-column: {ci + 1}; grid-row: {w.vis.visibleLanes + 1};"
|
|
587
|
+
aria-expanded={moreAction === 'expand' ? false : undefined}
|
|
588
|
+
aria-label="Show {w.vis.moreCounts[ci]} more event{w.vis.moreCounts[ci] === 1 ? '' : 's'} on {formatLongDate(day.date, locale)}"
|
|
589
|
+
onclick={(e) => {
|
|
590
|
+
e.stopPropagation();
|
|
591
|
+
toggleWeek(wi, day.date);
|
|
592
|
+
}}>+{w.vis.moreCounts[ci]} more</button
|
|
593
|
+
>
|
|
594
|
+
{:else if w.open && w.foldedMore[ci] > 0}
|
|
595
|
+
<button
|
|
596
|
+
type="button"
|
|
597
|
+
class="jatui-ec__more"
|
|
598
|
+
style="grid-column: {ci + 1}; grid-row: {w.vis.visibleLanes + 2};"
|
|
599
|
+
aria-expanded="true"
|
|
600
|
+
aria-label="Show fewer events this week"
|
|
601
|
+
onclick={(e) => {
|
|
602
|
+
e.stopPropagation();
|
|
603
|
+
toggleWeek(wi, day.date);
|
|
604
|
+
}}>Show less</button
|
|
605
|
+
>
|
|
606
|
+
{/if}
|
|
607
|
+
</div>
|
|
608
|
+
{/each}
|
|
609
|
+
</div>
|
|
610
|
+
{/each}
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
{:else if view === 'week' || view === 'day'}
|
|
614
|
+
{@const cols = gridDays.length}
|
|
615
|
+
<div class="jatui-ec__tg" style="height: {height}; --cols: {cols}; --hour-h: {hourHeight}px">
|
|
616
|
+
<div class="jatui-ec__tg-head">
|
|
617
|
+
<div class="jatui-ec__gutter" aria-hidden="true"></div>
|
|
618
|
+
{#each gridDays as d (toIsoDate(d))}
|
|
619
|
+
<div class="jatui-ec__tg-day">
|
|
620
|
+
<button type="button" class="jatui-ec__tg-daybtn" onclick={() => openDay(d)} aria-label="Open {formatLongDate(d, locale)}" disabled={view === 'day'}>
|
|
621
|
+
<span class="jatui-ec__tg-wd">{new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(d)}</span>
|
|
622
|
+
<span class="jatui-ec__tg-dn" class:is-today={isSameDay(d, today)}>{d.getDate()}</span>
|
|
623
|
+
</button>
|
|
624
|
+
</div>
|
|
625
|
+
{/each}
|
|
626
|
+
</div>
|
|
627
|
+
{#if allDayPacked && allDayPacked.laneCount > 0}
|
|
628
|
+
<div class="jatui-ec__allday" style="grid-template-rows: repeat({allDayPacked.laneCount}, {LANE_H}px);">
|
|
629
|
+
<div class="jatui-ec__gutter jatui-ec__allday-label" style="grid-row: 1 / -1;">All day</div>
|
|
630
|
+
{#each gridDays as d, ci (toIsoDate(d))}
|
|
631
|
+
<div
|
|
632
|
+
class="jatui-ec__allday-cell"
|
|
633
|
+
class:is-drop={dragOverKey === toIsoDate(d)}
|
|
634
|
+
style="grid-column: {ci + 2}; grid-row: 1 / -1;"
|
|
635
|
+
role="presentation"
|
|
636
|
+
ondragover={(e) => onCellDragOver(toIsoDate(d), e)}
|
|
637
|
+
ondragleave={() => (dragOverKey = null)}
|
|
638
|
+
ondrop={(e) => onCellDrop({ date: d, allDay: true }, e)}
|
|
639
|
+
></div>
|
|
640
|
+
{/each}
|
|
641
|
+
{#each allDayPacked.segments.filter((s) => s.startCol < cols) as seg (seg.ev.id)}
|
|
642
|
+
<button
|
|
643
|
+
type="button"
|
|
644
|
+
class="jatui-ec__seg is-filled"
|
|
645
|
+
class:is-inert={!!draggingId && draggingId !== seg.ev.id}
|
|
646
|
+
style={segStyle(seg, seg.lane, { col: 2, row: 1, lastCol: cols - 1 })}
|
|
647
|
+
aria-label="{seg.ev.title}, {eventWhen(seg.ev, { hourCycle: cycle, locale })}"
|
|
648
|
+
aria-haspopup={eventDetails ? 'dialog' : undefined}
|
|
649
|
+
draggable={canDrag(seg.ev)}
|
|
650
|
+
onclick={(e) => handleEventClick(seg.ev, e)}
|
|
651
|
+
ondragstart={(e) => onChipDragStart(seg.ev, e)}
|
|
652
|
+
ondragend={endDrag}
|
|
653
|
+
>
|
|
654
|
+
{@render chipBody(seg.ev, '')}
|
|
655
|
+
</button>
|
|
656
|
+
{/each}
|
|
657
|
+
</div>
|
|
658
|
+
{/if}
|
|
659
|
+
<div class="jatui-ec__tg-scroll" bind:this={scrollEl}>
|
|
660
|
+
<div class="jatui-ec__tg-body" style="height: {(gridMinutes / 60) * hourHeight}px">
|
|
661
|
+
<div class="jatui-ec__gutter jatui-ec__hours" aria-hidden="true">
|
|
662
|
+
{#each hours as h (h)}
|
|
663
|
+
<div class="jatui-ec__hour-label" style="top: {(h - dayStartHour) * hourHeight}px">{h === dayStartHour ? '' : hourLabel(h, cycle)}</div>
|
|
664
|
+
{/each}
|
|
665
|
+
</div>
|
|
666
|
+
{#each timeDays as day (toIsoDate(day.date))}
|
|
667
|
+
<div class="jatui-ec__tg-col" class:is-today={isSameDay(day.date, today)} role="group" aria-label={formatLongDate(day.date, locale)}>
|
|
668
|
+
{#each hours as h (h)}
|
|
669
|
+
{@const key = slotKey(day.date, h)}
|
|
670
|
+
{#if onCreate}
|
|
671
|
+
<button
|
|
672
|
+
type="button"
|
|
673
|
+
class="jatui-ec__slot"
|
|
674
|
+
class:is-drop={dragOverKey === key}
|
|
675
|
+
aria-label="Create at {hourLabel(h, cycle)}, {formatLongDate(day.date, locale)}"
|
|
676
|
+
tabindex="-1"
|
|
677
|
+
onclick={(e) => onCreate?.(day.date, h, e)}
|
|
678
|
+
ondragover={(e) => onCellDragOver(key, e)}
|
|
679
|
+
ondragleave={() => (dragOverKey = null)}
|
|
680
|
+
ondrop={(e) => onCellDrop({ date: day.date, hour: h, allDay: false }, e)}
|
|
681
|
+
></button>
|
|
682
|
+
{:else}
|
|
683
|
+
<div
|
|
684
|
+
class="jatui-ec__slot"
|
|
685
|
+
class:is-drop={dragOverKey === key}
|
|
686
|
+
role="presentation"
|
|
687
|
+
ondragover={(e) => onCellDragOver(key, e)}
|
|
688
|
+
ondragleave={() => (dragOverKey = null)}
|
|
689
|
+
ondrop={(e) => onCellDrop({ date: day.date, hour: h, allDay: false }, e)}
|
|
690
|
+
></div>
|
|
691
|
+
{/if}
|
|
692
|
+
{/each}
|
|
693
|
+
{#each timedBlocks(day) as b (b.p.ev.id)}
|
|
694
|
+
<button
|
|
695
|
+
type="button"
|
|
696
|
+
class="jatui-ec__block"
|
|
697
|
+
class:is-past={parseLocalDate(b.p.ev.end ?? b.p.ev.start) < now}
|
|
698
|
+
class:is-dragging={draggingId === b.p.ev.id}
|
|
699
|
+
class:is-inert={!!draggingId && draggingId !== b.p.ev.id}
|
|
700
|
+
style={b.style}
|
|
701
|
+
aria-label="{b.p.ev.title}, {eventWhen(b.p.ev, { hourCycle: cycle, locale })}"
|
|
702
|
+
aria-haspopup={eventDetails ? 'dialog' : undefined}
|
|
703
|
+
draggable={canDrag(b.p.ev)}
|
|
704
|
+
onclick={(e) => handleEventClick(b.p.ev, e)}
|
|
705
|
+
ondragstart={(e) => onChipDragStart(b.p.ev, e)}
|
|
706
|
+
ondragend={endDrag}
|
|
707
|
+
>
|
|
708
|
+
{#if eventContent}
|
|
709
|
+
{@render eventContent(b.p.ev, { view, time: chipTime(b.p.ev, cycle) })}
|
|
710
|
+
{:else}
|
|
711
|
+
<span class="jatui-ec__block-title">{b.p.ev.title}</span>
|
|
712
|
+
<span class="jatui-ec__time">{chipTime(b.p.ev, cycle)}</span>
|
|
713
|
+
{/if}
|
|
714
|
+
</button>
|
|
715
|
+
{/each}
|
|
716
|
+
{#if nowTop !== null && isSameDay(day.date, today)}
|
|
717
|
+
<div class="jatui-ec__now" style="top: {nowTop}px" aria-hidden="true"></div>
|
|
718
|
+
{/if}
|
|
719
|
+
</div>
|
|
720
|
+
{/each}
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
{:else}
|
|
725
|
+
<div class="jatui-ec__agenda" style="max-height: {height}">
|
|
726
|
+
{#if agenda.length === 0}
|
|
727
|
+
<p class="jatui-ec__empty">No events in {formatMonthYear(date, locale)}.</p>
|
|
728
|
+
{:else}
|
|
729
|
+
{#each agenda as group (toIsoDate(group.date))}
|
|
730
|
+
<section class="jatui-ec__agenda-day" aria-labelledby="{uid}-{toIsoDate(group.date)}">
|
|
731
|
+
<h3 id="{uid}-{toIsoDate(group.date)}" class="jatui-ec__agenda-date" class:is-today={isSameDay(group.date, today)}>
|
|
732
|
+
{new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric' }).format(group.date)}
|
|
733
|
+
</h3>
|
|
734
|
+
<ul class="jatui-ec__agenda-list">
|
|
735
|
+
{#each group.events as ev (ev.id)}
|
|
736
|
+
<li>
|
|
737
|
+
<button
|
|
738
|
+
type="button"
|
|
739
|
+
class="jatui-ec__agenda-item"
|
|
740
|
+
style={eventColorStyle(ev.color)}
|
|
741
|
+
aria-haspopup={eventDetails ? 'dialog' : undefined}
|
|
742
|
+
onclick={(e) => handleEventClick(ev, e)}
|
|
743
|
+
>
|
|
744
|
+
<span class="jatui-ec__dot" aria-hidden="true"></span>
|
|
745
|
+
<span class="jatui-ec__agenda-time">{ev.allDay ? 'All day' : chipTime(ev, cycle)}</span>
|
|
746
|
+
<span class="jatui-ec__label">{ev.title}</span>
|
|
747
|
+
{#if ev.location}<span class="jatui-ec__agenda-loc">{ev.location}</span>{/if}
|
|
748
|
+
</button>
|
|
749
|
+
</li>
|
|
750
|
+
{/each}
|
|
751
|
+
</ul>
|
|
752
|
+
</section>
|
|
753
|
+
{/each}
|
|
754
|
+
{/if}
|
|
755
|
+
</div>
|
|
756
|
+
{/if}
|
|
757
|
+
</div>
|
|
758
|
+
|
|
759
|
+
{#if details && eventDetails}
|
|
760
|
+
<div
|
|
761
|
+
class="jatui-ec__details bg-base-100 rounded-box border border-base-300"
|
|
762
|
+
role="dialog"
|
|
763
|
+
aria-label={details.ev.title}
|
|
764
|
+
style={eventColorStyle(details.ev.color)}
|
|
765
|
+
use:floating={{ anchor: details.anchor, placement: 'bottom-start', offset: 6, onDismiss: closeDetails }}
|
|
766
|
+
>
|
|
767
|
+
{@render eventDetails(details.ev, closeDetails)}
|
|
768
|
+
</div>
|
|
769
|
+
{/if}
|
|
770
|
+
|
|
771
|
+
{#if switcherOpen}
|
|
772
|
+
<div
|
|
773
|
+
bind:this={switcherPop}
|
|
774
|
+
class="jatui-ec__switcher bg-base-100 rounded-box border border-base-300"
|
|
775
|
+
role="dialog"
|
|
776
|
+
aria-label="Jump to date"
|
|
777
|
+
data-calendar-root
|
|
778
|
+
use:floating={{ anchor: switcherEl, placement: 'bottom-start', offset: 6, onDismiss: closeSwitcher }}
|
|
779
|
+
>
|
|
780
|
+
<CalendarHeader
|
|
781
|
+
label={formatMonthYear(switcherMonth, locale)}
|
|
782
|
+
onPrev={() => (switcherMonth = addMonths(switcherMonth, -1))}
|
|
783
|
+
onNext={() => (switcherMonth = addMonths(switcherMonth, 1))}
|
|
784
|
+
size="sm"
|
|
785
|
+
/>
|
|
786
|
+
<MonthGrid
|
|
787
|
+
month={switcherMonth}
|
|
788
|
+
focusedDate={switcherFocus}
|
|
789
|
+
weekStart={firstDay}
|
|
790
|
+
{locale}
|
|
791
|
+
selected={date}
|
|
792
|
+
size="sm"
|
|
793
|
+
fixedWeeks
|
|
794
|
+
onSelect={pickFromSwitcher}
|
|
795
|
+
onFocusDate={(d) => {
|
|
796
|
+
switcherFocus = d;
|
|
797
|
+
if (!isSameMonth(d, switcherMonth)) switcherMonth = startOfMonth(d);
|
|
798
|
+
}}
|
|
799
|
+
/>
|
|
800
|
+
</div>
|
|
801
|
+
{/if}
|
|
802
|
+
|
|
803
|
+
<style>
|
|
804
|
+
.jatui-ec {
|
|
805
|
+
display: flex;
|
|
806
|
+
flex-direction: column;
|
|
807
|
+
gap: 0.75rem;
|
|
808
|
+
min-width: 0;
|
|
809
|
+
}
|
|
810
|
+
.jatui-ec__toolbar {
|
|
811
|
+
display: flex;
|
|
812
|
+
flex-wrap: wrap;
|
|
813
|
+
align-items: center;
|
|
814
|
+
justify-content: space-between;
|
|
815
|
+
gap: 0.5rem;
|
|
816
|
+
}
|
|
817
|
+
.jatui-ec__nav,
|
|
818
|
+
.jatui-ec__tools {
|
|
819
|
+
display: flex;
|
|
820
|
+
align-items: center;
|
|
821
|
+
gap: 0.25rem;
|
|
822
|
+
}
|
|
823
|
+
.jatui-ec__tools {
|
|
824
|
+
gap: 0.5rem;
|
|
825
|
+
}
|
|
826
|
+
:global([dir='rtl']) .jatui-ec__chev {
|
|
827
|
+
transform: scaleX(-1);
|
|
828
|
+
}
|
|
829
|
+
/* Month-switcher pill. */
|
|
830
|
+
.jatui-ec__title {
|
|
831
|
+
display: inline-flex;
|
|
832
|
+
align-items: center;
|
|
833
|
+
gap: 0.375rem;
|
|
834
|
+
margin-inline-start: 0.25rem;
|
|
835
|
+
padding: 0.25rem 0.75rem;
|
|
836
|
+
border-radius: 9999px;
|
|
837
|
+
font-size: 1rem;
|
|
838
|
+
font-weight: 600;
|
|
839
|
+
cursor: pointer;
|
|
840
|
+
}
|
|
841
|
+
.jatui-ec__title:hover {
|
|
842
|
+
background: var(--jatui-hover-bg, color-mix(in oklab, var(--color-base-content) 7%, transparent));
|
|
843
|
+
}
|
|
844
|
+
.jatui-ec__title:focus-visible,
|
|
845
|
+
.jatui-ec__day:focus-visible,
|
|
846
|
+
.jatui-ec__seg:focus-visible,
|
|
847
|
+
.jatui-ec__block:focus-visible,
|
|
848
|
+
.jatui-ec__more:focus-visible,
|
|
849
|
+
.jatui-ec__agenda-item:focus-visible,
|
|
850
|
+
.jatui-ec__tg-daybtn:focus-visible,
|
|
851
|
+
.jatui-ec__num:focus-visible {
|
|
852
|
+
outline: var(--jatui-focus-ring-width, 2px) solid var(--jatui-focus-ring, var(--color-primary));
|
|
853
|
+
outline-offset: -2px;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/* ── Month ── */
|
|
857
|
+
.jatui-ec__month {
|
|
858
|
+
display: flex;
|
|
859
|
+
flex-direction: column;
|
|
860
|
+
min-height: 0;
|
|
861
|
+
border: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
862
|
+
border-radius: var(--radius-box, 0.75rem);
|
|
863
|
+
overflow: hidden;
|
|
864
|
+
}
|
|
865
|
+
.jatui-ec__weekdays {
|
|
866
|
+
display: grid;
|
|
867
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
868
|
+
border-block-end: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
869
|
+
background: color-mix(in oklab, var(--color-base-200) 40%, transparent);
|
|
870
|
+
}
|
|
871
|
+
.jatui-ec__weekday {
|
|
872
|
+
padding-block: 0.375rem;
|
|
873
|
+
text-align: center;
|
|
874
|
+
font-size: 0.6875rem;
|
|
875
|
+
font-weight: 600;
|
|
876
|
+
letter-spacing: 0.08em;
|
|
877
|
+
text-transform: uppercase;
|
|
878
|
+
color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
|
|
879
|
+
}
|
|
880
|
+
.jatui-ec__weeks {
|
|
881
|
+
display: flex;
|
|
882
|
+
flex: 1;
|
|
883
|
+
flex-direction: column;
|
|
884
|
+
min-height: 0;
|
|
885
|
+
overflow-y: auto;
|
|
886
|
+
}
|
|
887
|
+
.jatui-ec__week {
|
|
888
|
+
display: grid;
|
|
889
|
+
flex: 1;
|
|
890
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
891
|
+
/* No min-height: 0 — a row never shrinks below its lanes; the weeks box scrolls. */
|
|
892
|
+
border-block-end: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
893
|
+
}
|
|
894
|
+
.jatui-ec__week:last-child {
|
|
895
|
+
border-block-end: 0;
|
|
896
|
+
}
|
|
897
|
+
/* An expanded week sizes to its lanes; the weeks box scrolls instead of clipping. */
|
|
898
|
+
.jatui-ec__week.is-open {
|
|
899
|
+
flex: none;
|
|
900
|
+
min-height: fit-content;
|
|
901
|
+
}
|
|
902
|
+
/* Each gridcell's parts are items of the week's grid, so a bar can span columns. */
|
|
903
|
+
.jatui-ec__cell {
|
|
904
|
+
display: contents;
|
|
905
|
+
}
|
|
906
|
+
.jatui-ec__day {
|
|
907
|
+
z-index: 0;
|
|
908
|
+
border-inline-end: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
909
|
+
transition: background-color var(--motion-fast, 120ms) var(--motion-standard, ease);
|
|
910
|
+
}
|
|
911
|
+
.jatui-ec__cell:last-child .jatui-ec__day {
|
|
912
|
+
border-inline-end: 0;
|
|
913
|
+
}
|
|
914
|
+
.jatui-ec__day.is-outside {
|
|
915
|
+
background: color-mix(in oklab, var(--color-base-content) 4%, transparent);
|
|
916
|
+
}
|
|
917
|
+
.jatui-ec__day.is-actionable {
|
|
918
|
+
cursor: pointer;
|
|
919
|
+
}
|
|
920
|
+
.jatui-ec__day.is-actionable:hover {
|
|
921
|
+
background: var(--jatui-hover-bg, color-mix(in oklab, var(--color-base-content) 5%, transparent));
|
|
922
|
+
}
|
|
923
|
+
.jatui-ec__day.is-selecting {
|
|
924
|
+
background: var(--jatui-selection-bg, color-mix(in oklab, var(--color-primary) 14%, transparent));
|
|
925
|
+
box-shadow: inset 0 0 0 1px var(--jatui-selection-edge, color-mix(in oklab, var(--color-primary) 55%, transparent));
|
|
926
|
+
}
|
|
927
|
+
.jatui-ec__day.is-drop,
|
|
928
|
+
.jatui-ec__allday-cell.is-drop,
|
|
929
|
+
.jatui-ec__slot.is-drop {
|
|
930
|
+
background: var(--jatui-selection-bg, color-mix(in oklab, var(--color-primary) 14%, transparent));
|
|
931
|
+
box-shadow: inset 0 0 0 1px var(--jatui-selection-edge, color-mix(in oklab, var(--color-primary) 55%, transparent));
|
|
932
|
+
}
|
|
933
|
+
.jatui-ec__daynum {
|
|
934
|
+
z-index: 1;
|
|
935
|
+
display: flex;
|
|
936
|
+
align-items: center;
|
|
937
|
+
justify-content: flex-end;
|
|
938
|
+
padding-inline: 0.25rem;
|
|
939
|
+
pointer-events: none;
|
|
940
|
+
}
|
|
941
|
+
.jatui-ec__num {
|
|
942
|
+
pointer-events: auto;
|
|
943
|
+
min-width: 1.375rem;
|
|
944
|
+
height: 1.375rem;
|
|
945
|
+
padding-inline: 0.25rem;
|
|
946
|
+
border-radius: 9999px;
|
|
947
|
+
font-size: 0.75rem;
|
|
948
|
+
font-weight: 600;
|
|
949
|
+
font-variant-numeric: tabular-nums;
|
|
950
|
+
color: color-mix(in oklab, var(--color-base-content) 85%, transparent);
|
|
951
|
+
cursor: pointer;
|
|
952
|
+
}
|
|
953
|
+
.jatui-ec__num.is-past {
|
|
954
|
+
color: color-mix(in oklab, var(--color-base-content) 55%, transparent);
|
|
955
|
+
}
|
|
956
|
+
.jatui-ec__num:hover {
|
|
957
|
+
background: var(--color-base-300);
|
|
958
|
+
}
|
|
959
|
+
.jatui-ec__num.is-today {
|
|
960
|
+
background: var(--jatui-selection-solid, var(--color-primary));
|
|
961
|
+
color: var(--jatui-selection-solid-fg, var(--color-primary-content));
|
|
962
|
+
}
|
|
963
|
+
.jatui-ec__day.is-outside ~ .jatui-ec__daynum .jatui-ec__num:not(.is-today) {
|
|
964
|
+
color: color-mix(in oklab, var(--color-base-content) 45%, transparent);
|
|
965
|
+
}
|
|
966
|
+
.jatui-ec__seg {
|
|
967
|
+
z-index: 1;
|
|
968
|
+
display: flex;
|
|
969
|
+
align-items: center;
|
|
970
|
+
gap: 0.25rem;
|
|
971
|
+
min-width: 0;
|
|
972
|
+
height: 20px;
|
|
973
|
+
padding-inline: 0.25rem;
|
|
974
|
+
font-size: 0.75rem;
|
|
975
|
+
font-weight: 500;
|
|
976
|
+
line-height: 1;
|
|
977
|
+
text-align: start;
|
|
978
|
+
color: var(--color-base-content);
|
|
979
|
+
cursor: pointer;
|
|
980
|
+
transition:
|
|
981
|
+
background-color var(--motion-fast, 120ms) var(--motion-standard, ease),
|
|
982
|
+
opacity var(--motion-fast, 120ms) var(--motion-standard, ease);
|
|
983
|
+
}
|
|
984
|
+
.jatui-ec__seg:hover {
|
|
985
|
+
background: var(--jatui-hover-bg, color-mix(in oklab, var(--color-base-content) 7%, transparent));
|
|
986
|
+
}
|
|
987
|
+
.jatui-ec__seg.is-filled {
|
|
988
|
+
background: var(--ev-bg);
|
|
989
|
+
box-shadow: inset 3px 0 0 var(--ev);
|
|
990
|
+
}
|
|
991
|
+
:global([dir='rtl']) .jatui-ec__seg.is-filled {
|
|
992
|
+
box-shadow: inset -3px 0 0 var(--ev);
|
|
993
|
+
}
|
|
994
|
+
.jatui-ec__seg.is-filled:hover {
|
|
995
|
+
background: color-mix(in oklab, var(--ev) 26%, var(--color-base-100));
|
|
996
|
+
}
|
|
997
|
+
.jatui-ec__seg.is-past,
|
|
998
|
+
.jatui-ec__block.is-past {
|
|
999
|
+
opacity: 0.7;
|
|
1000
|
+
}
|
|
1001
|
+
.jatui-ec__seg.is-dragging,
|
|
1002
|
+
.jatui-ec__block.is-dragging {
|
|
1003
|
+
opacity: 0.35;
|
|
1004
|
+
}
|
|
1005
|
+
.is-inert {
|
|
1006
|
+
pointer-events: none;
|
|
1007
|
+
}
|
|
1008
|
+
.jatui-ec__seg[draggable='true'],
|
|
1009
|
+
.jatui-ec__block[draggable='true'] {
|
|
1010
|
+
cursor: grab;
|
|
1011
|
+
}
|
|
1012
|
+
.jatui-ec__dot {
|
|
1013
|
+
flex-shrink: 0;
|
|
1014
|
+
width: 0.375rem;
|
|
1015
|
+
height: 0.375rem;
|
|
1016
|
+
border-radius: 9999px;
|
|
1017
|
+
background: var(--ev);
|
|
1018
|
+
}
|
|
1019
|
+
.jatui-ec__time {
|
|
1020
|
+
flex-shrink: 0;
|
|
1021
|
+
font-variant-numeric: tabular-nums;
|
|
1022
|
+
color: var(--ev-fg, color-mix(in oklab, var(--color-base-content) 60%, transparent));
|
|
1023
|
+
}
|
|
1024
|
+
.jatui-ec__label {
|
|
1025
|
+
min-width: 0;
|
|
1026
|
+
overflow: hidden;
|
|
1027
|
+
text-overflow: ellipsis;
|
|
1028
|
+
white-space: nowrap;
|
|
1029
|
+
}
|
|
1030
|
+
.jatui-ec__cont {
|
|
1031
|
+
flex-shrink: 0;
|
|
1032
|
+
opacity: 0.7;
|
|
1033
|
+
}
|
|
1034
|
+
.jatui-ec__cont--end {
|
|
1035
|
+
margin-inline-start: auto;
|
|
1036
|
+
}
|
|
1037
|
+
.jatui-ec__more {
|
|
1038
|
+
z-index: 1;
|
|
1039
|
+
min-width: 0;
|
|
1040
|
+
height: 20px;
|
|
1041
|
+
margin-inline: 3px;
|
|
1042
|
+
padding-inline: 0.25rem;
|
|
1043
|
+
border-radius: var(--radius-selector, 0.375rem);
|
|
1044
|
+
font-size: 0.75rem;
|
|
1045
|
+
font-weight: 500;
|
|
1046
|
+
text-align: start;
|
|
1047
|
+
white-space: nowrap;
|
|
1048
|
+
overflow: hidden;
|
|
1049
|
+
text-overflow: ellipsis;
|
|
1050
|
+
color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
|
|
1051
|
+
cursor: pointer;
|
|
1052
|
+
}
|
|
1053
|
+
.jatui-ec__more:hover {
|
|
1054
|
+
color: var(--color-base-content);
|
|
1055
|
+
background: var(--jatui-hover-bg, color-mix(in oklab, var(--color-base-content) 7%, transparent));
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/* ── Week / day ── */
|
|
1059
|
+
.jatui-ec__tg {
|
|
1060
|
+
display: flex;
|
|
1061
|
+
flex-direction: column;
|
|
1062
|
+
min-height: 0;
|
|
1063
|
+
border: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
1064
|
+
border-radius: var(--radius-box, 0.75rem);
|
|
1065
|
+
overflow: hidden;
|
|
1066
|
+
}
|
|
1067
|
+
.jatui-ec__tg-head,
|
|
1068
|
+
.jatui-ec__allday,
|
|
1069
|
+
.jatui-ec__tg-body {
|
|
1070
|
+
display: grid;
|
|
1071
|
+
grid-template-columns: 3.5rem repeat(var(--cols), minmax(0, 1fr));
|
|
1072
|
+
}
|
|
1073
|
+
.jatui-ec__tg-head {
|
|
1074
|
+
border-block-end: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
1075
|
+
}
|
|
1076
|
+
.jatui-ec__tg-day {
|
|
1077
|
+
border-inline-start: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
1078
|
+
text-align: center;
|
|
1079
|
+
}
|
|
1080
|
+
.jatui-ec__tg-daybtn {
|
|
1081
|
+
display: inline-flex;
|
|
1082
|
+
flex-direction: column;
|
|
1083
|
+
align-items: center;
|
|
1084
|
+
gap: 0.125rem;
|
|
1085
|
+
padding-block: 0.375rem;
|
|
1086
|
+
width: 100%;
|
|
1087
|
+
cursor: pointer;
|
|
1088
|
+
}
|
|
1089
|
+
.jatui-ec__tg-daybtn:disabled {
|
|
1090
|
+
cursor: default;
|
|
1091
|
+
}
|
|
1092
|
+
.jatui-ec__tg-wd {
|
|
1093
|
+
font-size: 0.6875rem;
|
|
1094
|
+
font-weight: 600;
|
|
1095
|
+
letter-spacing: 0.08em;
|
|
1096
|
+
text-transform: uppercase;
|
|
1097
|
+
color: color-mix(in oklab, var(--color-base-content) 55%, transparent);
|
|
1098
|
+
}
|
|
1099
|
+
.jatui-ec__tg-dn {
|
|
1100
|
+
display: inline-flex;
|
|
1101
|
+
align-items: center;
|
|
1102
|
+
justify-content: center;
|
|
1103
|
+
width: 1.75rem;
|
|
1104
|
+
height: 1.75rem;
|
|
1105
|
+
border-radius: 9999px;
|
|
1106
|
+
font-size: 0.875rem;
|
|
1107
|
+
font-weight: 600;
|
|
1108
|
+
}
|
|
1109
|
+
.jatui-ec__tg-dn.is-today {
|
|
1110
|
+
background: var(--jatui-selection-solid, var(--color-primary));
|
|
1111
|
+
color: var(--jatui-selection-solid-fg, var(--color-primary-content));
|
|
1112
|
+
}
|
|
1113
|
+
.jatui-ec__allday {
|
|
1114
|
+
padding-block: 2px;
|
|
1115
|
+
border-block-end: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
1116
|
+
}
|
|
1117
|
+
.jatui-ec__allday .jatui-ec__seg {
|
|
1118
|
+
align-self: center;
|
|
1119
|
+
}
|
|
1120
|
+
.jatui-ec__allday-label {
|
|
1121
|
+
grid-column: 1;
|
|
1122
|
+
align-self: center;
|
|
1123
|
+
padding-inline-end: 0.375rem;
|
|
1124
|
+
font-size: 0.6875rem;
|
|
1125
|
+
text-align: end;
|
|
1126
|
+
color: color-mix(in oklab, var(--color-base-content) 50%, transparent);
|
|
1127
|
+
}
|
|
1128
|
+
.jatui-ec__allday-cell {
|
|
1129
|
+
border-inline-start: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
1130
|
+
}
|
|
1131
|
+
.jatui-ec__tg-scroll {
|
|
1132
|
+
position: relative;
|
|
1133
|
+
flex: 1;
|
|
1134
|
+
min-height: 0;
|
|
1135
|
+
overflow-y: auto;
|
|
1136
|
+
}
|
|
1137
|
+
.jatui-ec__tg-body {
|
|
1138
|
+
position: relative;
|
|
1139
|
+
}
|
|
1140
|
+
.jatui-ec__hours {
|
|
1141
|
+
position: relative;
|
|
1142
|
+
}
|
|
1143
|
+
.jatui-ec__hour-label {
|
|
1144
|
+
position: absolute;
|
|
1145
|
+
inset-inline-end: 0.375rem;
|
|
1146
|
+
transform: translateY(-50%);
|
|
1147
|
+
font-size: 0.6875rem;
|
|
1148
|
+
font-variant-numeric: tabular-nums;
|
|
1149
|
+
color: color-mix(in oklab, var(--color-base-content) 45%, transparent);
|
|
1150
|
+
}
|
|
1151
|
+
.jatui-ec__tg-col {
|
|
1152
|
+
position: relative;
|
|
1153
|
+
display: flex;
|
|
1154
|
+
flex-direction: column;
|
|
1155
|
+
border-inline-start: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
1156
|
+
}
|
|
1157
|
+
.jatui-ec__tg-col.is-today {
|
|
1158
|
+
background: color-mix(in oklab, var(--color-primary) 3%, transparent);
|
|
1159
|
+
}
|
|
1160
|
+
.jatui-ec__slot {
|
|
1161
|
+
flex: none;
|
|
1162
|
+
height: var(--hour-h);
|
|
1163
|
+
border-block-end: 1px solid color-mix(in oklab, var(--jatui-separator, var(--color-base-300)) 70%, transparent);
|
|
1164
|
+
}
|
|
1165
|
+
button.jatui-ec__slot {
|
|
1166
|
+
cursor: pointer;
|
|
1167
|
+
}
|
|
1168
|
+
button.jatui-ec__slot:hover {
|
|
1169
|
+
background: var(--jatui-hover-bg, color-mix(in oklab, var(--color-base-content) 5%, transparent));
|
|
1170
|
+
}
|
|
1171
|
+
.jatui-ec__block {
|
|
1172
|
+
position: absolute;
|
|
1173
|
+
z-index: 1;
|
|
1174
|
+
display: flex;
|
|
1175
|
+
flex-direction: column;
|
|
1176
|
+
align-items: flex-start;
|
|
1177
|
+
gap: 1px;
|
|
1178
|
+
overflow: hidden;
|
|
1179
|
+
padding: 0.125rem 0.375rem;
|
|
1180
|
+
border-radius: var(--radius-selector, 0.375rem);
|
|
1181
|
+
background: var(--ev-bg);
|
|
1182
|
+
box-shadow:
|
|
1183
|
+
inset 3px 0 0 var(--ev),
|
|
1184
|
+
0 0 0 1px var(--color-base-100);
|
|
1185
|
+
font-size: 0.75rem;
|
|
1186
|
+
line-height: 1.2;
|
|
1187
|
+
text-align: start;
|
|
1188
|
+
color: var(--color-base-content);
|
|
1189
|
+
cursor: pointer;
|
|
1190
|
+
}
|
|
1191
|
+
:global([dir='rtl']) .jatui-ec__block {
|
|
1192
|
+
box-shadow:
|
|
1193
|
+
inset -3px 0 0 var(--ev),
|
|
1194
|
+
0 0 0 1px var(--color-base-100);
|
|
1195
|
+
}
|
|
1196
|
+
.jatui-ec__block:hover {
|
|
1197
|
+
background: color-mix(in oklab, var(--ev) 26%, var(--color-base-100));
|
|
1198
|
+
}
|
|
1199
|
+
.jatui-ec__block-title {
|
|
1200
|
+
max-width: 100%;
|
|
1201
|
+
overflow: hidden;
|
|
1202
|
+
text-overflow: ellipsis;
|
|
1203
|
+
white-space: nowrap;
|
|
1204
|
+
font-weight: 600;
|
|
1205
|
+
}
|
|
1206
|
+
.jatui-ec__now {
|
|
1207
|
+
position: absolute;
|
|
1208
|
+
inset-inline: 0;
|
|
1209
|
+
z-index: 2;
|
|
1210
|
+
height: 2px;
|
|
1211
|
+
background: var(--color-error);
|
|
1212
|
+
pointer-events: none;
|
|
1213
|
+
}
|
|
1214
|
+
.jatui-ec__now::before {
|
|
1215
|
+
content: '';
|
|
1216
|
+
position: absolute;
|
|
1217
|
+
inset-inline-start: -4px;
|
|
1218
|
+
top: -3px;
|
|
1219
|
+
width: 8px;
|
|
1220
|
+
height: 8px;
|
|
1221
|
+
border-radius: 9999px;
|
|
1222
|
+
background: var(--color-error);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/* ── Agenda ── */
|
|
1226
|
+
.jatui-ec__agenda {
|
|
1227
|
+
overflow-y: auto;
|
|
1228
|
+
border: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
1229
|
+
border-radius: var(--radius-box, 0.75rem);
|
|
1230
|
+
}
|
|
1231
|
+
.jatui-ec__agenda-day {
|
|
1232
|
+
display: grid;
|
|
1233
|
+
grid-template-columns: 7rem minmax(0, 1fr);
|
|
1234
|
+
gap: 0.75rem;
|
|
1235
|
+
padding: 0.625rem 0.75rem;
|
|
1236
|
+
border-block-end: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
1237
|
+
}
|
|
1238
|
+
.jatui-ec__agenda-day:last-child {
|
|
1239
|
+
border-block-end: 0;
|
|
1240
|
+
}
|
|
1241
|
+
.jatui-ec__agenda-date {
|
|
1242
|
+
margin: 0;
|
|
1243
|
+
padding-block-start: 0.25rem;
|
|
1244
|
+
font-size: 0.8125rem;
|
|
1245
|
+
font-weight: 600;
|
|
1246
|
+
color: color-mix(in oklab, var(--color-base-content) 70%, transparent);
|
|
1247
|
+
}
|
|
1248
|
+
.jatui-ec__agenda-date.is-today {
|
|
1249
|
+
color: var(--jatui-selection-solid, var(--color-primary));
|
|
1250
|
+
}
|
|
1251
|
+
.jatui-ec__agenda-list {
|
|
1252
|
+
display: flex;
|
|
1253
|
+
flex-direction: column;
|
|
1254
|
+
gap: 2px;
|
|
1255
|
+
margin: 0;
|
|
1256
|
+
padding: 0;
|
|
1257
|
+
list-style: none;
|
|
1258
|
+
}
|
|
1259
|
+
.jatui-ec__agenda-item {
|
|
1260
|
+
display: flex;
|
|
1261
|
+
align-items: center;
|
|
1262
|
+
gap: 0.5rem;
|
|
1263
|
+
width: 100%;
|
|
1264
|
+
padding: 0.25rem 0.375rem;
|
|
1265
|
+
border-radius: var(--radius-selector, 0.375rem);
|
|
1266
|
+
font-size: 0.875rem;
|
|
1267
|
+
text-align: start;
|
|
1268
|
+
cursor: pointer;
|
|
1269
|
+
}
|
|
1270
|
+
.jatui-ec__agenda-item:hover {
|
|
1271
|
+
background: var(--jatui-hover-bg, color-mix(in oklab, var(--color-base-content) 7%, transparent));
|
|
1272
|
+
}
|
|
1273
|
+
.jatui-ec__agenda-time {
|
|
1274
|
+
flex-shrink: 0;
|
|
1275
|
+
width: 4rem;
|
|
1276
|
+
font-size: 0.8125rem;
|
|
1277
|
+
font-variant-numeric: tabular-nums;
|
|
1278
|
+
color: var(--ev-fg);
|
|
1279
|
+
}
|
|
1280
|
+
.jatui-ec__agenda-loc {
|
|
1281
|
+
margin-inline-start: auto;
|
|
1282
|
+
font-size: 0.75rem;
|
|
1283
|
+
color: color-mix(in oklab, var(--color-base-content) 50%, transparent);
|
|
1284
|
+
}
|
|
1285
|
+
.jatui-ec__empty {
|
|
1286
|
+
margin: 0;
|
|
1287
|
+
padding: 2rem;
|
|
1288
|
+
text-align: center;
|
|
1289
|
+
font-size: 0.875rem;
|
|
1290
|
+
color: color-mix(in oklab, var(--color-base-content) 50%, transparent);
|
|
1291
|
+
}
|
|
1292
|
+
@media (max-width: 40rem) {
|
|
1293
|
+
.jatui-ec__agenda-day {
|
|
1294
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1295
|
+
gap: 0.25rem;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/* ── Popovers ── */
|
|
1300
|
+
.jatui-ec__details {
|
|
1301
|
+
z-index: 60;
|
|
1302
|
+
width: 18rem;
|
|
1303
|
+
max-width: calc(100vw - 1rem);
|
|
1304
|
+
padding: 0.75rem;
|
|
1305
|
+
box-shadow: inset 3px 0 0 var(--ev);
|
|
1306
|
+
}
|
|
1307
|
+
.jatui-ec__switcher {
|
|
1308
|
+
z-index: 60;
|
|
1309
|
+
width: max-content;
|
|
1310
|
+
min-width: 17rem;
|
|
1311
|
+
padding: 0.75rem;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1315
|
+
.jatui-ec__day,
|
|
1316
|
+
.jatui-ec__seg {
|
|
1317
|
+
transition: none;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
:global([data-motion='calm']) .jatui-ec__day,
|
|
1321
|
+
:global([data-motion='calm']) .jatui-ec__seg {
|
|
1322
|
+
transition-duration: 0ms;
|
|
1323
|
+
}
|
|
1324
|
+
</style>
|