@nomideusz/svelte-calendar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/memory.d.ts +14 -0
- package/dist/adapters/memory.js +33 -0
- package/dist/adapters/rest.d.ts +20 -0
- package/dist/adapters/rest.js +48 -0
- package/dist/adapters/types.d.ts +23 -0
- package/dist/adapters/types.js +1 -0
- package/dist/assets/favicon.svg +1 -0
- package/dist/calendar/Calendar.svelte +213 -0
- package/dist/calendar/Calendar.svelte.d.ts +45 -0
- package/dist/calendar/Toolbar.svelte +241 -0
- package/dist/calendar/Toolbar.svelte.d.ts +19 -0
- package/dist/calendar/index.d.ts +3 -0
- package/dist/calendar/index.js +3 -0
- package/dist/core/clock.svelte.d.ts +21 -0
- package/dist/core/clock.svelte.js +53 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +8 -0
- package/dist/core/locale.d.ts +38 -0
- package/dist/core/locale.js +96 -0
- package/dist/core/time.d.ts +37 -0
- package/dist/core/time.js +68 -0
- package/dist/core/types.d.ts +91 -0
- package/dist/core/types.js +11 -0
- package/dist/engine/drag.svelte.d.ts +31 -0
- package/dist/engine/drag.svelte.js +58 -0
- package/dist/engine/event-store.svelte.d.ts +47 -0
- package/dist/engine/event-store.svelte.js +120 -0
- package/dist/engine/index.d.ts +8 -0
- package/dist/engine/index.js +5 -0
- package/dist/engine/selection.svelte.d.ts +24 -0
- package/dist/engine/selection.svelte.js +58 -0
- package/dist/engine/view-state.svelte.d.ts +24 -0
- package/dist/engine/view-state.svelte.js +74 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/primitives/DayHeader.svelte +103 -0
- package/dist/primitives/DayHeader.svelte.d.ts +15 -0
- package/dist/primitives/EmptySlot.svelte +101 -0
- package/dist/primitives/EmptySlot.svelte.d.ts +16 -0
- package/dist/primitives/EventBlock.svelte +241 -0
- package/dist/primitives/EventBlock.svelte.d.ts +24 -0
- package/dist/primitives/NowIndicator.svelte +175 -0
- package/dist/primitives/NowIndicator.svelte.d.ts +22 -0
- package/dist/primitives/TimeGutter.svelte +105 -0
- package/dist/primitives/TimeGutter.svelte.d.ts +14 -0
- package/dist/primitives/index.d.ts +5 -0
- package/dist/primitives/index.js +6 -0
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/presets.d.ts +24 -0
- package/dist/theme/presets.js +89 -0
- package/dist/views/agenda/Agenda.svelte +1404 -0
- package/dist/views/agenda/Agenda.svelte.d.ts +20 -0
- package/dist/views/agenda/index.d.ts +1 -0
- package/dist/views/agenda/index.js +1 -0
- package/dist/views/day/DayGrid.svelte +1063 -0
- package/dist/views/day/DayGrid.svelte.d.ts +33 -0
- package/dist/views/day/DayTimeline.svelte +505 -0
- package/dist/views/day/DayTimeline.svelte.d.ts +4 -0
- package/dist/views/day/index.d.ts +2 -0
- package/dist/views/day/index.js +2 -0
- package/dist/views/index.d.ts +5 -0
- package/dist/views/index.js +5 -0
- package/dist/views/settings/Settings.svelte +303 -0
- package/dist/views/settings/Settings.svelte.d.ts +23 -0
- package/dist/views/settings/index.d.ts +2 -0
- package/dist/views/settings/index.js +1 -0
- package/dist/views/week/WeekGrid.svelte +594 -0
- package/dist/views/week/WeekGrid.svelte.d.ts +21 -0
- package/dist/views/week/WeekHeatmap.svelte +453 -0
- package/dist/views/week/WeekHeatmap.svelte.d.ts +4 -0
- package/dist/views/week/index.d.ts +2 -0
- package/dist/views/week/index.js +2 -0
- package/package.json +62 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
WeekGrid — Multi-week vertical scroll.
|
|
3
|
+
|
|
4
|
+
Modelled after Hey Calendar's week view:
|
|
5
|
+
• Weeks stack vertically. Scroll up = past, down = future.
|
|
6
|
+
• Day headers in each week row: "MON 23", "TUE 24", with accent pill for today.
|
|
7
|
+
• Month label in left gutter, vertical bottom-to-top.
|
|
8
|
+
• Events are clean horizontal bars with colour fill, "9AM- 10AM Title" inline.
|
|
9
|
+
• Generous whitespace, thin dividers, minimal chrome.
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { getContext, onMount, tick } from 'svelte';
|
|
13
|
+
import { createClock } from '../../core/clock.svelte.js';
|
|
14
|
+
import type { TimelineEvent } from '../../core/types.js';
|
|
15
|
+
import type { DragState } from '../../engine/drag.svelte.js';
|
|
16
|
+
import { DAY_MS, sod } from '../../core/time.js';
|
|
17
|
+
import { startOfWeek as sowFn, fractionalHour } from '../../core/time.js';
|
|
18
|
+
import { weekdayShort, monthLong } from '../../core/locale.js';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
weekOffset?: number;
|
|
22
|
+
mondayStart?: boolean;
|
|
23
|
+
hourHeight?: number;
|
|
24
|
+
dayWidth?: number;
|
|
25
|
+
nowPosition?: number;
|
|
26
|
+
height?: number;
|
|
27
|
+
events?: TimelineEvent[];
|
|
28
|
+
style?: string;
|
|
29
|
+
focusDate?: Date;
|
|
30
|
+
oneventclick?: (event: TimelineEvent) => void;
|
|
31
|
+
oneventcreate?: (range: { start: Date; end: Date }) => void;
|
|
32
|
+
selectedEventId?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
mondayStart = true,
|
|
37
|
+
height = 520,
|
|
38
|
+
events = [],
|
|
39
|
+
style = '',
|
|
40
|
+
focusDate,
|
|
41
|
+
oneventclick,
|
|
42
|
+
oneventcreate,
|
|
43
|
+
selectedEventId = null,
|
|
44
|
+
}: Props = $props();
|
|
45
|
+
|
|
46
|
+
// ── Drag support (available when inside Calendar) ──
|
|
47
|
+
const drag = getContext<DragState>('calendar:drag') as DragState | undefined;
|
|
48
|
+
const commitDragCtx = getContext<() => void>('calendar:commitDrag') as (() => void) | undefined;
|
|
49
|
+
|
|
50
|
+
const clock = createClock();
|
|
51
|
+
|
|
52
|
+
// ─── Config ─────────────────────────────────────────
|
|
53
|
+
const PAST_WEEKS = 4;
|
|
54
|
+
const FUTURE_WEEKS = 12;
|
|
55
|
+
const MAX_EVENTS_SHOWN = 3;
|
|
56
|
+
|
|
57
|
+
let el: HTMLDivElement;
|
|
58
|
+
let scrolled = $state(false);
|
|
59
|
+
|
|
60
|
+
// ─── Derived ────────────────────────────────────────
|
|
61
|
+
const todayMs = $derived(clock.today);
|
|
62
|
+
const focusMs = $derived(focusDate ? sod(focusDate.getTime()) : todayMs);
|
|
63
|
+
const anchorWeekStart = $derived(sowFn(focusMs, mondayStart));
|
|
64
|
+
|
|
65
|
+
// ─── Week data ──────────────────────────────────────
|
|
66
|
+
interface WeekRow {
|
|
67
|
+
weekStart: number;
|
|
68
|
+
isCurrent: boolean;
|
|
69
|
+
monthLabel: string | null;
|
|
70
|
+
days: DayCell[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface DayCell {
|
|
74
|
+
ms: number;
|
|
75
|
+
dayNum: number;
|
|
76
|
+
isToday: boolean;
|
|
77
|
+
isPast: boolean;
|
|
78
|
+
isWeekend: boolean;
|
|
79
|
+
events: TimelineEvent[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const weeks = $derived.by(() => {
|
|
83
|
+
const result: WeekRow[] = [];
|
|
84
|
+
const currentWeekStart = sowFn(todayMs, mondayStart);
|
|
85
|
+
|
|
86
|
+
for (let w = -PAST_WEEKS; w <= FUTURE_WEEKS; w++) {
|
|
87
|
+
const weekStart = anchorWeekStart + w * 7 * DAY_MS;
|
|
88
|
+
const isCurrent = weekStart === currentWeekStart;
|
|
89
|
+
const days: DayCell[] = [];
|
|
90
|
+
|
|
91
|
+
const firstMonth = new Date(weekStart).getMonth();
|
|
92
|
+
const lastMonth = new Date(weekStart + 6 * DAY_MS).getMonth();
|
|
93
|
+
|
|
94
|
+
for (let d = 0; d < 7; d++) {
|
|
95
|
+
const ms = weekStart + d * DAY_MS;
|
|
96
|
+
const date = new Date(ms);
|
|
97
|
+
const dayNum = date.getDate();
|
|
98
|
+
const dow = date.getDay();
|
|
99
|
+
const isWeekend = dow === 0 || dow === 6;
|
|
100
|
+
const isToday = ms === todayMs;
|
|
101
|
+
const isPast = ms < todayMs;
|
|
102
|
+
|
|
103
|
+
const dayEnd = ms + DAY_MS;
|
|
104
|
+
const dayEvents = events
|
|
105
|
+
.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms)
|
|
106
|
+
.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
107
|
+
|
|
108
|
+
days.push({ ms, dayNum, isToday, isPast, isWeekend, events: dayEvents });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Month label: show when first day of week is day 1-7
|
|
112
|
+
const startDate = new Date(weekStart);
|
|
113
|
+
const showMonth = startDate.getDate() <= 7;
|
|
114
|
+
const monthLabel = showMonth ? monthLong(weekStart).toUpperCase() : null;
|
|
115
|
+
|
|
116
|
+
result.push({ weekStart, isCurrent, monthLabel, days });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ─── Format helpers ─────────────────────────────────
|
|
123
|
+
function fmtAmPm(d: Date): string {
|
|
124
|
+
let h = d.getHours();
|
|
125
|
+
const m = d.getMinutes();
|
|
126
|
+
const ampm = h >= 12 ? 'PM' : 'AM';
|
|
127
|
+
h = h % 12 || 12;
|
|
128
|
+
return m > 0 ? `${h}:${m.toString().padStart(2, '0')}${ampm}` : `${h}${ampm}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function fmtNowTime(tick: number): string {
|
|
132
|
+
const d = new Date(tick);
|
|
133
|
+
let h = d.getHours();
|
|
134
|
+
const m = d.getMinutes();
|
|
135
|
+
const ampm = h >= 12 ? 'pm' : 'am';
|
|
136
|
+
h = h % 12 || 12;
|
|
137
|
+
return `${h}:${m.toString().padStart(2, '0')}${ampm}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Now indicator fraction ─────────────────────────
|
|
141
|
+
const nowFrac = $derived(fractionalHour(clock.tick) / 24);
|
|
142
|
+
|
|
143
|
+
// ─── Scroll to current week on mount ────────────────
|
|
144
|
+
onMount(async () => {
|
|
145
|
+
await tick();
|
|
146
|
+
if (el) {
|
|
147
|
+
const current = el.querySelector('.wg-week--current');
|
|
148
|
+
if (current) current.scrollIntoView({ block: 'center' });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
function handleUserScroll() {
|
|
153
|
+
scrolled = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function jumpToday() {
|
|
157
|
+
const current = el?.querySelector('.wg-week--current');
|
|
158
|
+
if (current) current.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
159
|
+
scrolled = false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleDayCellClick(ms: number, e: Event) {
|
|
163
|
+
const target = e.target as HTMLElement;
|
|
164
|
+
if (target.closest('.wg-ev')) return;
|
|
165
|
+
if (!oneventcreate) return;
|
|
166
|
+
const start = new Date(ms + 9 * 3_600_000);
|
|
167
|
+
const end = new Date(ms + 10 * 3_600_000);
|
|
168
|
+
oneventcreate({ start, end });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Event drag-to-move ───────────────────────────────────────
|
|
172
|
+
const DRAG_THRESHOLD = 8;
|
|
173
|
+
let evDragStartX = 0;
|
|
174
|
+
let evDragStarted = false;
|
|
175
|
+
let evDragging = $state(false);
|
|
176
|
+
let evDragId = $state<string | null>(null);
|
|
177
|
+
let evDragEvent: TimelineEvent | null = null;
|
|
178
|
+
|
|
179
|
+
function getCellWidth(): number {
|
|
180
|
+
const cell = el?.querySelector('.wg-cell');
|
|
181
|
+
return cell ? cell.getBoundingClientRect().width : 100;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function onEventPointerDown(e: PointerEvent, ev: TimelineEvent) {
|
|
185
|
+
if (e.button !== 0 || !drag) return;
|
|
186
|
+
e.stopPropagation();
|
|
187
|
+
evDragStartX = e.clientX;
|
|
188
|
+
evDragStarted = false;
|
|
189
|
+
evDragId = ev.id;
|
|
190
|
+
evDragEvent = ev;
|
|
191
|
+
|
|
192
|
+
window.addEventListener('pointermove', onEvWindowPointerMove);
|
|
193
|
+
window.addEventListener('pointerup', onEvWindowPointerUp, { once: true });
|
|
194
|
+
window.addEventListener('pointercancel', onEvWindowPointerCancel, { once: true });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function onEvWindowPointerMove(e: PointerEvent) {
|
|
198
|
+
const ev = evDragEvent;
|
|
199
|
+
if (!drag || !ev || evDragId !== ev.id) return;
|
|
200
|
+
const dx = e.clientX - evDragStartX;
|
|
201
|
+
if (!evDragStarted && Math.abs(dx) < DRAG_THRESHOLD) return;
|
|
202
|
+
|
|
203
|
+
if (!evDragStarted) {
|
|
204
|
+
evDragStarted = true;
|
|
205
|
+
evDragging = true;
|
|
206
|
+
drag.beginMove(ev.id, ev.start, ev.end);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const cellW = getCellWidth();
|
|
210
|
+
const dayOffset = Math.round(dx / cellW);
|
|
211
|
+
const deltaMs = dayOffset * DAY_MS;
|
|
212
|
+
drag.updatePointer(
|
|
213
|
+
new Date(ev.start.getTime() + deltaMs),
|
|
214
|
+
new Date(ev.end.getTime() + deltaMs),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function cleanupEvDrag() {
|
|
219
|
+
window.removeEventListener('pointermove', onEvWindowPointerMove);
|
|
220
|
+
window.removeEventListener('pointerup', onEvWindowPointerUp);
|
|
221
|
+
window.removeEventListener('pointercancel', onEvWindowPointerCancel);
|
|
222
|
+
evDragStarted = false;
|
|
223
|
+
evDragging = false;
|
|
224
|
+
evDragId = null;
|
|
225
|
+
evDragEvent = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function onEvWindowPointerUp() {
|
|
229
|
+
if (!drag) { cleanupEvDrag(); return; }
|
|
230
|
+
if (!evDragStarted) {
|
|
231
|
+
if (evDragEvent) oneventclick?.(evDragEvent);
|
|
232
|
+
} else {
|
|
233
|
+
commitDragCtx?.();
|
|
234
|
+
}
|
|
235
|
+
cleanupEvDrag();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function onEvWindowPointerCancel() {
|
|
239
|
+
if (drag && evDragStarted) drag.cancel();
|
|
240
|
+
cleanupEvDrag();
|
|
241
|
+
}
|
|
242
|
+
</script>
|
|
243
|
+
|
|
244
|
+
<div class="wg" style={style || undefined} style:height="{height}px">
|
|
245
|
+
<div
|
|
246
|
+
class="wg-body"
|
|
247
|
+
bind:this={el}
|
|
248
|
+
onscroll={handleUserScroll}
|
|
249
|
+
role="grid"
|
|
250
|
+
aria-label="Multi-week calendar grid"
|
|
251
|
+
>
|
|
252
|
+
{#each weeks as week (week.weekStart)}
|
|
253
|
+
<div class="wg-week" class:wg-week--current={week.isCurrent}>
|
|
254
|
+
<!-- Month gutter (left) -->
|
|
255
|
+
<div class="wg-gutter">
|
|
256
|
+
{#if week.monthLabel}
|
|
257
|
+
<span class="wg-gutter-month">{week.monthLabel}</span>
|
|
258
|
+
{/if}
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div class="wg-week-body">
|
|
262
|
+
<!-- Day columns (header inside each cell) -->
|
|
263
|
+
<div class="wg-days">
|
|
264
|
+
{#each week.days as day (day.ms)}
|
|
265
|
+
<div
|
|
266
|
+
class="wg-cell"
|
|
267
|
+
class:wg-cell--today={day.isToday}
|
|
268
|
+
class:wg-cell--past={day.isPast}
|
|
269
|
+
class:wg-cell--weekend={day.isWeekend}
|
|
270
|
+
role="gridcell"
|
|
271
|
+
tabindex="0"
|
|
272
|
+
aria-label="{new Date(day.ms).toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}{day.isToday ? ' (today)' : ''}, {day.events.length} events"
|
|
273
|
+
onclick={(e) => handleDayCellClick(day.ms, e)}
|
|
274
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleDayCellClick(day.ms, e); } }}
|
|
275
|
+
>
|
|
276
|
+
<!-- Day label inside the cell -->
|
|
277
|
+
<div class="wg-cell-hd" class:wg-cell-hd--today={day.isToday}>
|
|
278
|
+
<span class="wg-day-wd">{weekdayShort(day.ms)}</span>
|
|
279
|
+
<span class="wg-day-num" class:wg-day-num--today={day.isToday}>
|
|
280
|
+
{day.dayNum}
|
|
281
|
+
</span>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<!-- Events -->
|
|
285
|
+
<div class="wg-cell-events">
|
|
286
|
+
{#each day.events.slice(0, MAX_EVENTS_SHOWN) as ev (ev.id)}
|
|
287
|
+
<div
|
|
288
|
+
class="wg-ev"
|
|
289
|
+
class:wg-ev--selected={selectedEventId === ev.id}
|
|
290
|
+
class:wg-ev--current={ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
|
|
291
|
+
class:wg-ev--dragging={evDragging && evDragId === ev.id}
|
|
292
|
+
style:--ev-color={ev.color ?? 'var(--dt-accent)'}
|
|
293
|
+
role="button"
|
|
294
|
+
tabindex="0"
|
|
295
|
+
aria-label="{ev.title}{ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick ? ' (in progress)' : ''}"
|
|
296
|
+
onpointerdown={(e) => onEventPointerDown(e, ev)}
|
|
297
|
+
|
|
298
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev); } }}
|
|
299
|
+
>
|
|
300
|
+
<span class="wg-ev-time">{fmtAmPm(ev.start)}</span>
|
|
301
|
+
<span class="wg-ev-title">{ev.title}</span>
|
|
302
|
+
</div>
|
|
303
|
+
{/each}
|
|
304
|
+
{#if day.events.length > MAX_EVENTS_SHOWN}
|
|
305
|
+
<div class="wg-ev-more">+{day.events.length - MAX_EVENTS_SHOWN} more</div>
|
|
306
|
+
{/if}
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<!-- Now indicator with time -->
|
|
310
|
+
{#if day.isToday}
|
|
311
|
+
<div class="wg-now" style:top="calc({nowFrac * 100}% + 28px)">
|
|
312
|
+
<span class="wg-now-time">{fmtNowTime(clock.tick)}</span>
|
|
313
|
+
<div class="wg-now-line"></div>
|
|
314
|
+
</div>
|
|
315
|
+
{/if}
|
|
316
|
+
</div>
|
|
317
|
+
{/each}
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
{/each}
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
{#if scrolled}
|
|
325
|
+
<button class="wg-btn" onclick={jumpToday}>
|
|
326
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
|
327
|
+
<circle cx="12" cy="12" r="10" />
|
|
328
|
+
<circle cx="12" cy="12" r="3" fill="currentColor"/>
|
|
329
|
+
</svg>
|
|
330
|
+
Today
|
|
331
|
+
</button>
|
|
332
|
+
{/if}
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<style>
|
|
336
|
+
/* ─── Container ──────────────────────────────────── */
|
|
337
|
+
.wg {
|
|
338
|
+
position: relative;
|
|
339
|
+
background: var(--dt-bg, #fff);
|
|
340
|
+
border: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
341
|
+
border-radius: 10px;
|
|
342
|
+
overflow: hidden;
|
|
343
|
+
display: flex;
|
|
344
|
+
flex-direction: column;
|
|
345
|
+
user-select: none;
|
|
346
|
+
font-variant-numeric: tabular-nums;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* ─── Scrollable body ────────────────────────────── */
|
|
350
|
+
.wg-body {
|
|
351
|
+
flex: 1;
|
|
352
|
+
overflow-y: auto;
|
|
353
|
+
overflow-x: hidden;
|
|
354
|
+
scrollbar-width: thin;
|
|
355
|
+
scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.08)) transparent;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.wg-body::-webkit-scrollbar { width: 4px; }
|
|
359
|
+
.wg-body::-webkit-scrollbar-thumb {
|
|
360
|
+
background: var(--dt-scrollbar, rgba(0, 0, 0, 0.1));
|
|
361
|
+
border-radius: 4px;
|
|
362
|
+
}
|
|
363
|
+
.wg-body::-webkit-scrollbar-track { background: transparent; }
|
|
364
|
+
|
|
365
|
+
/* ─── Week row ───────────────────────────────────── */
|
|
366
|
+
.wg-week {
|
|
367
|
+
display: flex;
|
|
368
|
+
border-bottom: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.wg-week--current {
|
|
372
|
+
background: var(--dt-today-bg, rgba(239, 68, 68, 0.02));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* ─── Left gutter (month label) ──────────────────── */
|
|
376
|
+
.wg-gutter {
|
|
377
|
+
flex-shrink: 0;
|
|
378
|
+
width: 48px;
|
|
379
|
+
display: flex;
|
|
380
|
+
align-items: center;
|
|
381
|
+
justify-content: center;
|
|
382
|
+
border-right: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.wg-gutter-month {
|
|
386
|
+
font: 800 11px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
387
|
+
letter-spacing: 0.06em;
|
|
388
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.45));
|
|
389
|
+
writing-mode: vertical-rl;
|
|
390
|
+
transform: rotate(180deg);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* ─── Week body ──────────────────────────────────── */
|
|
394
|
+
.wg-week-body {
|
|
395
|
+
flex: 1;
|
|
396
|
+
min-width: 0;
|
|
397
|
+
display: flex;
|
|
398
|
+
flex-direction: column;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* ─── Day columns ────────────────────────────────── */
|
|
402
|
+
.wg-days {
|
|
403
|
+
display: flex;
|
|
404
|
+
flex: 1;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.wg-cell {
|
|
408
|
+
flex: 1;
|
|
409
|
+
position: relative;
|
|
410
|
+
min-height: 130px;
|
|
411
|
+
padding: 0 4px 8px;
|
|
412
|
+
border-right: 1px solid var(--dt-border, rgba(0, 0, 0, 0.06));
|
|
413
|
+
cursor: pointer;
|
|
414
|
+
transition: background 0.15s;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.wg-cell:last-child { border-right: none; }
|
|
418
|
+
.wg-cell:hover { background: var(--dt-hover, rgba(0, 0, 0, 0.015)); }
|
|
419
|
+
|
|
420
|
+
.wg-cell--today { background: var(--dt-today-bg, rgba(239, 68, 68, 0.03)); }
|
|
421
|
+
.wg-cell--today:hover { background: rgba(239, 68, 68, 0.05); }
|
|
422
|
+
|
|
423
|
+
.wg-cell--past { opacity: 0.55; }
|
|
424
|
+
.wg-cell--past:hover { opacity: 0.75; }
|
|
425
|
+
|
|
426
|
+
.wg-cell--weekend { background: var(--dt-weekend-bg, rgba(0, 0, 0, 0.012)); }
|
|
427
|
+
|
|
428
|
+
/* ─── Cell header (day label inside cell) ────────── */
|
|
429
|
+
.wg-cell-hd {
|
|
430
|
+
display: flex;
|
|
431
|
+
align-items: center;
|
|
432
|
+
justify-content: center;
|
|
433
|
+
gap: 5px;
|
|
434
|
+
padding: 6px 0 6px;
|
|
435
|
+
border-bottom: 1px solid var(--dt-border, rgba(0, 0, 0, 0.04));
|
|
436
|
+
margin-bottom: 4px;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.wg-day-wd {
|
|
440
|
+
font: 400 11px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
441
|
+
letter-spacing: 0.04em;
|
|
442
|
+
text-transform: uppercase;
|
|
443
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.35));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.wg-cell-hd--today .wg-day-wd {
|
|
447
|
+
color: var(--dt-accent, #ef4444);
|
|
448
|
+
font-weight: 600;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.wg-day-num {
|
|
452
|
+
font: 700 14px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
453
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.85));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.wg-day-num--today {
|
|
457
|
+
background: var(--dt-accent, #ef4444);
|
|
458
|
+
color: #fff;
|
|
459
|
+
width: 26px;
|
|
460
|
+
height: 26px;
|
|
461
|
+
display: inline-flex;
|
|
462
|
+
align-items: center;
|
|
463
|
+
justify-content: center;
|
|
464
|
+
border-radius: 50%;
|
|
465
|
+
font-size: 13px;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* ─── Events ─────────────────────────────────────── */
|
|
469
|
+
.wg-cell-events {
|
|
470
|
+
display: flex;
|
|
471
|
+
flex-direction: column;
|
|
472
|
+
gap: 3px;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.wg-ev {
|
|
476
|
+
display: flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
gap: 5px;
|
|
479
|
+
padding: 3px 6px;
|
|
480
|
+
border-radius: 4px;
|
|
481
|
+
background: color-mix(in srgb, var(--ev-color) 12%, transparent);
|
|
482
|
+
cursor: pointer;
|
|
483
|
+
overflow: hidden;
|
|
484
|
+
transition: background 0.12s;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.wg-ev:hover {
|
|
488
|
+
background: color-mix(in srgb, var(--ev-color) 22%, transparent);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.wg-ev--selected {
|
|
492
|
+
box-shadow: 0 0 0 1.5px var(--ev-color);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.wg-ev--current {
|
|
496
|
+
background: color-mix(in srgb, var(--ev-color) 18%, transparent);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.wg-ev-time {
|
|
500
|
+
font: 400 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
501
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.4));
|
|
502
|
+
flex-shrink: 0;
|
|
503
|
+
white-space: nowrap;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.wg-ev-title {
|
|
507
|
+
font: 500 12px / 1.1 var(--dt-sans, system-ui, sans-serif);
|
|
508
|
+
color: var(--dt-text, rgba(0, 0, 0, 0.85));
|
|
509
|
+
white-space: nowrap;
|
|
510
|
+
overflow: hidden;
|
|
511
|
+
text-overflow: ellipsis;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.wg-ev-more {
|
|
515
|
+
font: 500 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
516
|
+
color: var(--dt-text-3, rgba(0, 0, 0, 0.35));
|
|
517
|
+
padding: 2px 8px;
|
|
518
|
+
cursor: pointer;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.wg-ev-more:hover {
|
|
522
|
+
color: var(--dt-text-2, rgba(0, 0, 0, 0.55));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* ─── Now indicator with time label ──────────────── */
|
|
526
|
+
.wg-now {
|
|
527
|
+
position: absolute;
|
|
528
|
+
left: 0;
|
|
529
|
+
right: 0;
|
|
530
|
+
z-index: 4;
|
|
531
|
+
pointer-events: none;
|
|
532
|
+
display: flex;
|
|
533
|
+
align-items: center;
|
|
534
|
+
gap: 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.wg-now-time {
|
|
538
|
+
flex-shrink: 0;
|
|
539
|
+
font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
540
|
+
color: var(--dt-accent, #ef4444);
|
|
541
|
+
padding: 1px 4px;
|
|
542
|
+
background: color-mix(in srgb, var(--dt-accent, #ef4444) 12%, var(--dt-bg, #fff));
|
|
543
|
+
border-radius: 3px;
|
|
544
|
+
margin-left: -2px;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.wg-now-line {
|
|
548
|
+
flex: 1;
|
|
549
|
+
height: 1px;
|
|
550
|
+
background: var(--dt-accent, #ef4444);
|
|
551
|
+
opacity: 0.35;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* ─── Jump button ────────────────────────────────── */
|
|
555
|
+
.wg-btn {
|
|
556
|
+
position: absolute;
|
|
557
|
+
bottom: 14px;
|
|
558
|
+
right: 14px;
|
|
559
|
+
z-index: 20;
|
|
560
|
+
display: inline-flex;
|
|
561
|
+
align-items: center;
|
|
562
|
+
gap: 5px;
|
|
563
|
+
padding: 5px 13px 5px 9px;
|
|
564
|
+
font: 600 11px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
565
|
+
color: #fff;
|
|
566
|
+
background: var(--dt-accent, #ef4444);
|
|
567
|
+
border: none;
|
|
568
|
+
border-radius: 100px;
|
|
569
|
+
cursor: pointer;
|
|
570
|
+
box-shadow: 0 2px 16px color-mix(in srgb, var(--dt-accent, #ef4444) 35%, transparent);
|
|
571
|
+
transition: transform 120ms ease, box-shadow 120ms ease;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.wg-btn svg { width: 13px; height: 13px; }
|
|
575
|
+
.wg-btn:hover { transform: scale(1.06); }
|
|
576
|
+
.wg-btn:active { transform: scale(0.96); }
|
|
577
|
+
|
|
578
|
+
/* ─── Focus-visible ──────────────────────────────── */
|
|
579
|
+
.wg-cell:focus-visible {
|
|
580
|
+
outline: 2px solid var(--dt-accent, #ef4444);
|
|
581
|
+
outline-offset: -2px;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.wg-ev:focus-visible {
|
|
585
|
+
outline: 2px solid var(--ev-color, var(--dt-accent, #ef4444));
|
|
586
|
+
outline-offset: 1px;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.wg-ev--dragging {
|
|
590
|
+
opacity: 0.6;
|
|
591
|
+
cursor: grabbing;
|
|
592
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
593
|
+
}
|
|
594
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TimelineEvent } from '../../core/types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
weekOffset?: number;
|
|
4
|
+
mondayStart?: boolean;
|
|
5
|
+
hourHeight?: number;
|
|
6
|
+
dayWidth?: number;
|
|
7
|
+
nowPosition?: number;
|
|
8
|
+
height?: number;
|
|
9
|
+
events?: TimelineEvent[];
|
|
10
|
+
style?: string;
|
|
11
|
+
focusDate?: Date;
|
|
12
|
+
oneventclick?: (event: TimelineEvent) => void;
|
|
13
|
+
oneventcreate?: (range: {
|
|
14
|
+
start: Date;
|
|
15
|
+
end: Date;
|
|
16
|
+
}) => void;
|
|
17
|
+
selectedEventId?: string | null;
|
|
18
|
+
}
|
|
19
|
+
declare const WeekGrid: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type WeekGrid = ReturnType<typeof WeekGrid>;
|
|
21
|
+
export default WeekGrid;
|