@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,33 @@
|
|
|
1
|
+
import type { TimelineEvent } from '../../core/types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Number of past days visible */
|
|
4
|
+
pastDays?: number;
|
|
5
|
+
/** Number of future days visible */
|
|
6
|
+
futureDays?: number;
|
|
7
|
+
/** Pixel width of one hour */
|
|
8
|
+
hourWidth?: number;
|
|
9
|
+
/** Where "now" sits in the viewport (0 = left, 1 = right) */
|
|
10
|
+
nowPosition?: number;
|
|
11
|
+
/** Total height */
|
|
12
|
+
height?: number;
|
|
13
|
+
/** Events to render */
|
|
14
|
+
events?: TimelineEvent[];
|
|
15
|
+
/** Inline style for CSS variable overrides (theme) */
|
|
16
|
+
style?: string;
|
|
17
|
+
/** The date to centre this view on */
|
|
18
|
+
focusDate?: Date;
|
|
19
|
+
/** Called when the user clicks an event */
|
|
20
|
+
oneventclick?: (event: TimelineEvent) => void;
|
|
21
|
+
/** Called when the user clicks an empty time slot */
|
|
22
|
+
oneventcreate?: (range: {
|
|
23
|
+
start: Date;
|
|
24
|
+
end: Date;
|
|
25
|
+
}) => void;
|
|
26
|
+
/** Currently selected event ID (for highlight) */
|
|
27
|
+
selectedEventId?: string | null;
|
|
28
|
+
/** Start weeks on Monday */
|
|
29
|
+
mondayStart?: boolean;
|
|
30
|
+
}
|
|
31
|
+
declare const DayGrid: import("svelte").Component<Props, {}, "">;
|
|
32
|
+
type DayGrid = ReturnType<typeof DayGrid>;
|
|
33
|
+
export default DayGrid;
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext, onMount } from 'svelte';
|
|
3
|
+
import { fly } from 'svelte/transition';
|
|
4
|
+
import { createClock } from '../../core/clock.svelte.js';
|
|
5
|
+
import type { DayTimelineProps } from '../../core/types.js';
|
|
6
|
+
import type { DragState } from '../../engine/drag.svelte.js';
|
|
7
|
+
import { DAY_MS, HOUR_MS, HOURS, sod } from '../../core/time.js';
|
|
8
|
+
import { fmtH, fmtDay } from '../../core/locale.js';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
pastDays = 1,
|
|
12
|
+
futureDays = 30,
|
|
13
|
+
hourWidth = 120,
|
|
14
|
+
nowPosition = 0.25,
|
|
15
|
+
events = [],
|
|
16
|
+
style = '',
|
|
17
|
+
focusDate,
|
|
18
|
+
oneventclick,
|
|
19
|
+
selectedEventId = null,
|
|
20
|
+
}: DayTimelineProps = $props();
|
|
21
|
+
|
|
22
|
+
// ── Drag support (available when inside Calendar) ──
|
|
23
|
+
const drag = getContext<DragState>('calendar:drag') as DragState | undefined;
|
|
24
|
+
const commitDragCtx = getContext<() => void>('calendar:commitDrag') as (() => void) | undefined;
|
|
25
|
+
|
|
26
|
+
const clock = createClock();
|
|
27
|
+
|
|
28
|
+
let following = $state(true);
|
|
29
|
+
let dragging = $state(false);
|
|
30
|
+
let rafId = 0;
|
|
31
|
+
let dragStartX = 0;
|
|
32
|
+
let dragScrollStart = 0;
|
|
33
|
+
let el: HTMLDivElement;
|
|
34
|
+
|
|
35
|
+
// ─── Derived ────────────────────────────────────────────
|
|
36
|
+
const dayW = $derived(hourWidth * 24);
|
|
37
|
+
const count = $derived(pastDays + 1 + futureDays);
|
|
38
|
+
const totalW = $derived(count * dayW);
|
|
39
|
+
/** When focusDate is set, centre the timeline on that date */
|
|
40
|
+
const centerMs = $derived(focusDate ? sod(focusDate.getTime()) : clock.today);
|
|
41
|
+
const origin = $derived(centerMs - pastDays * DAY_MS);
|
|
42
|
+
const nowPx = $derived(((clock.tick - origin) / HOUR_MS) * hourWidth);
|
|
43
|
+
|
|
44
|
+
const days = $derived.by(() => {
|
|
45
|
+
const out: { ms: number; x: number; name: string; today: boolean }[] = [];
|
|
46
|
+
for (let i = 0; i < count; i++) {
|
|
47
|
+
const ms = origin + i * DAY_MS;
|
|
48
|
+
out.push({
|
|
49
|
+
ms,
|
|
50
|
+
x: i * dayW,
|
|
51
|
+
name: fmtDay(ms, clock.today),
|
|
52
|
+
today: ms === clock.today
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ─── Lifecycle ──────────────────────────────────────────
|
|
59
|
+
onMount(() => {
|
|
60
|
+
function frame() {
|
|
61
|
+
if (following && el && !dragging) {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const px = ((now - origin) / HOUR_MS) * hourWidth;
|
|
64
|
+
el.scrollLeft = px - el.clientWidth * nowPosition;
|
|
65
|
+
}
|
|
66
|
+
rafId = requestAnimationFrame(frame);
|
|
67
|
+
}
|
|
68
|
+
rafId = requestAnimationFrame(frame);
|
|
69
|
+
return () => cancelAnimationFrame(rafId);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ─── Drag-to-scroll ────────────────────────────────────
|
|
73
|
+
const SCROLL_THRESHOLD = 3;
|
|
74
|
+
|
|
75
|
+
function onPointerDown(e: PointerEvent) {
|
|
76
|
+
if (e.button !== 0) return;
|
|
77
|
+
const target = e.target as HTMLElement;
|
|
78
|
+
if (target.closest('.dt-event')) return;
|
|
79
|
+
dragStartX = e.clientX;
|
|
80
|
+
dragScrollStart = el.scrollLeft;
|
|
81
|
+
|
|
82
|
+
window.addEventListener('pointermove', onScrollPointerMove);
|
|
83
|
+
window.addEventListener('pointerup', onScrollPointerUp, { once: true });
|
|
84
|
+
window.addEventListener('pointercancel', onScrollPointerUp, { once: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function onScrollPointerMove(e: PointerEvent) {
|
|
88
|
+
const dx = e.clientX - dragStartX;
|
|
89
|
+
if (!dragging && Math.abs(dx) >= SCROLL_THRESHOLD) {
|
|
90
|
+
dragging = true;
|
|
91
|
+
following = false;
|
|
92
|
+
}
|
|
93
|
+
if (dragging) {
|
|
94
|
+
el.scrollLeft = dragScrollStart - dx;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function onScrollPointerUp() {
|
|
99
|
+
window.removeEventListener('pointermove', onScrollPointerMove);
|
|
100
|
+
dragging = false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function userInput() {
|
|
104
|
+
if (!dragging) following = false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function jumpNow() {
|
|
108
|
+
following = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Event positioning (horizontal) ─────────────────────
|
|
112
|
+
import type { TimelineEvent } from '../../core/types.js';
|
|
113
|
+
|
|
114
|
+
interface PositionedEvent {
|
|
115
|
+
ev: TimelineEvent;
|
|
116
|
+
x: number;
|
|
117
|
+
width: number;
|
|
118
|
+
row: number;
|
|
119
|
+
isDragged: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const positionedEvents = $derived.by(() => {
|
|
123
|
+
const result: PositionedEvent[] = [];
|
|
124
|
+
const dragP = drag?.active && drag.mode === 'move' ? drag.payload : null;
|
|
125
|
+
|
|
126
|
+
const raw = events.map((ev) => {
|
|
127
|
+
const isDragged = dragP?.eventId === ev.id;
|
|
128
|
+
const startMs = isDragged ? dragP!.start.getTime() : ev.start.getTime();
|
|
129
|
+
const endMs = isDragged ? dragP!.end.getTime() : ev.end.getTime();
|
|
130
|
+
return { ev, startMs, endMs, isDragged };
|
|
131
|
+
});
|
|
132
|
+
raw.sort((a, b) => a.startMs - b.startMs);
|
|
133
|
+
|
|
134
|
+
// Simple row-packing for overlap
|
|
135
|
+
const rows: number[] = [];
|
|
136
|
+
|
|
137
|
+
for (const { ev, startMs, endMs, isDragged } of raw) {
|
|
138
|
+
const x = ((startMs - origin) / HOUR_MS) * hourWidth;
|
|
139
|
+
const w = Math.max(((endMs - startMs) / HOUR_MS) * hourWidth, 20);
|
|
140
|
+
|
|
141
|
+
let row = 0;
|
|
142
|
+
for (let r = 0; r < rows.length; r++) {
|
|
143
|
+
if (rows[r] <= startMs) {
|
|
144
|
+
row = r;
|
|
145
|
+
rows[r] = endMs;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
row = r + 1;
|
|
149
|
+
}
|
|
150
|
+
if (row >= rows.length) rows.push(endMs);
|
|
151
|
+
|
|
152
|
+
result.push({ ev, x, width: w, row, isDragged });
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ─── Event drag-to-move ───────────────────────────────────────
|
|
158
|
+
const DRAG_THRESHOLD = 5;
|
|
159
|
+
const SNAP_MS = 15 * 60_000;
|
|
160
|
+
let evDragStartX = 0;
|
|
161
|
+
let evDragStarted = false;
|
|
162
|
+
let evDragging = $state(false);
|
|
163
|
+
let evDragId = $state<string | null>(null);
|
|
164
|
+
let evDragEvent: TimelineEvent | null = null;
|
|
165
|
+
|
|
166
|
+
function onEventPointerDown(e: PointerEvent, ev: TimelineEvent) {
|
|
167
|
+
if (e.button !== 0 || !drag) return;
|
|
168
|
+
e.stopPropagation();
|
|
169
|
+
evDragStartX = e.clientX;
|
|
170
|
+
evDragStarted = false;
|
|
171
|
+
evDragId = ev.id;
|
|
172
|
+
evDragEvent = ev;
|
|
173
|
+
|
|
174
|
+
window.addEventListener('pointermove', onEvWindowPointerMove);
|
|
175
|
+
window.addEventListener('pointerup', onEvWindowPointerUp, { once: true });
|
|
176
|
+
window.addEventListener('pointercancel', onEvWindowPointerCancel, { once: true });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function onEvWindowPointerMove(e: PointerEvent) {
|
|
180
|
+
const ev = evDragEvent;
|
|
181
|
+
if (!drag || !ev || evDragId !== ev.id) return;
|
|
182
|
+
const dx = e.clientX - evDragStartX;
|
|
183
|
+
if (!evDragStarted && Math.abs(dx) < DRAG_THRESHOLD) return;
|
|
184
|
+
|
|
185
|
+
if (!evDragStarted) {
|
|
186
|
+
evDragStarted = true;
|
|
187
|
+
evDragging = true;
|
|
188
|
+
drag.beginMove(ev.id, ev.start, ev.end);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const rawDeltaMs = (dx / hourWidth) * HOUR_MS;
|
|
192
|
+
const snappedDelta = Math.round(rawDeltaMs / SNAP_MS) * SNAP_MS;
|
|
193
|
+
drag.updatePointer(
|
|
194
|
+
new Date(ev.start.getTime() + snappedDelta),
|
|
195
|
+
new Date(ev.end.getTime() + snappedDelta),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function cleanupEvDrag() {
|
|
200
|
+
window.removeEventListener('pointermove', onEvWindowPointerMove);
|
|
201
|
+
window.removeEventListener('pointerup', onEvWindowPointerUp);
|
|
202
|
+
window.removeEventListener('pointercancel', onEvWindowPointerCancel);
|
|
203
|
+
evDragStarted = false;
|
|
204
|
+
evDragging = false;
|
|
205
|
+
evDragId = null;
|
|
206
|
+
evDragEvent = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function onEvWindowPointerUp() {
|
|
210
|
+
if (!drag) { cleanupEvDrag(); return; }
|
|
211
|
+
if (!evDragStarted) {
|
|
212
|
+
if (evDragEvent) oneventclick?.(evDragEvent);
|
|
213
|
+
} else {
|
|
214
|
+
commitDragCtx?.();
|
|
215
|
+
}
|
|
216
|
+
cleanupEvDrag();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function onEvWindowPointerCancel() {
|
|
220
|
+
if (drag && evDragStarted) drag.cancel();
|
|
221
|
+
cleanupEvDrag();
|
|
222
|
+
}
|
|
223
|
+
</script>
|
|
224
|
+
|
|
225
|
+
<div class="dt" style={style || undefined}>
|
|
226
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
227
|
+
<div
|
|
228
|
+
class="dt-scroll"
|
|
229
|
+
class:dt-grabbing={dragging}
|
|
230
|
+
bind:this={el}
|
|
231
|
+
onwheel={userInput}
|
|
232
|
+
onpointerdown={onPointerDown}
|
|
233
|
+
>
|
|
234
|
+
<div class="dt-track" style:width="{totalW}px">
|
|
235
|
+
{#each days as d (d.ms)}
|
|
236
|
+
<div class="dt-day" class:dt-today={d.today} style:left="{d.x}px" style:width="{dayW}px">
|
|
237
|
+
<div class="dt-day-hd">
|
|
238
|
+
<span class="dt-day-nm">{d.name}</span>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{#each HOURS as h}
|
|
243
|
+
{@const x = d.x + h * hourWidth}
|
|
244
|
+
<div class="dt-tick" style:left="{x}px">
|
|
245
|
+
<span class="dt-tick-lb">{fmtH(h)}</span>
|
|
246
|
+
</div>
|
|
247
|
+
<div class="dt-tick dt-tick--h" style:left="{x + hourWidth * 0.5}px"></div>
|
|
248
|
+
{/each}
|
|
249
|
+
{/each}
|
|
250
|
+
|
|
251
|
+
<div class="dt-now" style:left="{nowPx}px">
|
|
252
|
+
<span class="dt-now-tag">{clock.hm}<span class="dt-now-sec">{clock.s}</span></span>
|
|
253
|
+
<div class="dt-now-line"></div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<!-- Events -->
|
|
257
|
+
{#each positionedEvents as p (p.ev.id)}
|
|
258
|
+
<div
|
|
259
|
+
class="dt-event"
|
|
260
|
+
class:dt-event--selected={selectedEventId === p.ev.id}
|
|
261
|
+
class:dt-event--dragging={p.isDragged}
|
|
262
|
+
style:left="{p.x}px"
|
|
263
|
+
style:width="{p.width}px"
|
|
264
|
+
style:top="calc(38px + {p.row * 34}px)"
|
|
265
|
+
style:--ev-color={p.ev.color ?? 'var(--dt-accent)'}
|
|
266
|
+
role="button"
|
|
267
|
+
tabindex="0"
|
|
268
|
+
onpointerdown={(e) => onEventPointerDown(e, p.ev)}
|
|
269
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); oneventclick?.(p.ev); } }}
|
|
270
|
+
>
|
|
271
|
+
<span class="dt-ev-title">{p.ev.title}</span>
|
|
272
|
+
</div>
|
|
273
|
+
{/each}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<div class="dt-fog dt-fog--l"></div>
|
|
278
|
+
<div class="dt-fog dt-fog--r"></div>
|
|
279
|
+
|
|
280
|
+
{#if !following}
|
|
281
|
+
<button class="dt-btn" onclick={jumpNow} transition:fly={{ y: 6, duration: 180 }}>
|
|
282
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
|
283
|
+
<circle cx="12" cy="12" r="10" />
|
|
284
|
+
<circle cx="12" cy="12" r="3" fill="currentColor" />
|
|
285
|
+
</svg>
|
|
286
|
+
Now
|
|
287
|
+
</button>
|
|
288
|
+
{/if}
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<style>
|
|
292
|
+
.dt {
|
|
293
|
+
--dt-bg: #0b0e14;
|
|
294
|
+
--dt-surface: #10141c;
|
|
295
|
+
--dt-border: rgba(148, 163, 184, 0.07);
|
|
296
|
+
--dt-border-day: rgba(148, 163, 184, 0.14);
|
|
297
|
+
--dt-text: rgba(226, 232, 240, 0.85);
|
|
298
|
+
--dt-text-2: rgba(148, 163, 184, 0.55);
|
|
299
|
+
--dt-text-3: rgba(100, 116, 139, 0.55);
|
|
300
|
+
--dt-accent: #ef4444;
|
|
301
|
+
--dt-accent-dim: rgba(239, 68, 68, 0.18);
|
|
302
|
+
--dt-glow: rgba(239, 68, 68, 0.35);
|
|
303
|
+
--dt-today-bg: rgba(239, 68, 68, 0.02);
|
|
304
|
+
--dt-btn-text: #fff;
|
|
305
|
+
--dt-scrollbar: rgba(148, 163, 184, 0.12);
|
|
306
|
+
--dt-header: 34px;
|
|
307
|
+
--dt-ruler: 16px;
|
|
308
|
+
--dt-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
|
|
309
|
+
--dt-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
310
|
+
|
|
311
|
+
position: relative;
|
|
312
|
+
height: 160px;
|
|
313
|
+
background: var(--dt-bg);
|
|
314
|
+
border: 1px solid var(--dt-border);
|
|
315
|
+
border-radius: 10px;
|
|
316
|
+
overflow: hidden;
|
|
317
|
+
user-select: none;
|
|
318
|
+
font-variant-numeric: tabular-nums;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.dt-scroll {
|
|
322
|
+
width: 100%;
|
|
323
|
+
height: 100%;
|
|
324
|
+
overflow-x: auto;
|
|
325
|
+
overflow-y: hidden;
|
|
326
|
+
touch-action: pan-x;
|
|
327
|
+
cursor: grab;
|
|
328
|
+
scrollbar-width: thin;
|
|
329
|
+
scrollbar-color: var(--dt-scrollbar) transparent;
|
|
330
|
+
}
|
|
331
|
+
.dt-scroll::-webkit-scrollbar { height: 5px; }
|
|
332
|
+
.dt-scroll::-webkit-scrollbar-thumb { background: var(--dt-scrollbar); border-radius: 4px; }
|
|
333
|
+
.dt-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
334
|
+
.dt-grabbing { cursor: grabbing; }
|
|
335
|
+
|
|
336
|
+
.dt-track { position: relative; height: 100%; }
|
|
337
|
+
|
|
338
|
+
.dt-day {
|
|
339
|
+
position: absolute;
|
|
340
|
+
top: 0;
|
|
341
|
+
height: 100%;
|
|
342
|
+
border-left: 1px solid var(--dt-border-day);
|
|
343
|
+
box-sizing: border-box;
|
|
344
|
+
}
|
|
345
|
+
.dt-day:first-of-type { border-left: none; }
|
|
346
|
+
.dt-today { background: var(--dt-today-bg); }
|
|
347
|
+
|
|
348
|
+
.dt-day-hd {
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: baseline;
|
|
351
|
+
gap: 8px;
|
|
352
|
+
padding: 9px 14px;
|
|
353
|
+
height: var(--dt-header);
|
|
354
|
+
box-sizing: border-box;
|
|
355
|
+
border-bottom: 1px solid var(--dt-border);
|
|
356
|
+
}
|
|
357
|
+
.dt-day-nm {
|
|
358
|
+
font: 600 11px / 1 var(--dt-sans);
|
|
359
|
+
letter-spacing: 0.04em;
|
|
360
|
+
text-transform: uppercase;
|
|
361
|
+
color: var(--dt-text-2);
|
|
362
|
+
white-space: nowrap;
|
|
363
|
+
}
|
|
364
|
+
.dt-today .dt-day-nm { color: var(--dt-accent); }
|
|
365
|
+
|
|
366
|
+
.dt-tick {
|
|
367
|
+
position: absolute;
|
|
368
|
+
top: var(--dt-header);
|
|
369
|
+
bottom: 0;
|
|
370
|
+
width: 0;
|
|
371
|
+
}
|
|
372
|
+
.dt-tick::before {
|
|
373
|
+
content: '';
|
|
374
|
+
position: absolute;
|
|
375
|
+
top: var(--dt-ruler);
|
|
376
|
+
bottom: 0;
|
|
377
|
+
width: 1px;
|
|
378
|
+
background: var(--dt-border);
|
|
379
|
+
}
|
|
380
|
+
.dt-tick-lb {
|
|
381
|
+
position: absolute;
|
|
382
|
+
top: 1px;
|
|
383
|
+
left: 5px;
|
|
384
|
+
font: 500 9px / 1 var(--dt-mono);
|
|
385
|
+
color: var(--dt-text-3);
|
|
386
|
+
white-space: nowrap;
|
|
387
|
+
pointer-events: none;
|
|
388
|
+
}
|
|
389
|
+
.dt-tick--h {
|
|
390
|
+
bottom: auto;
|
|
391
|
+
height: calc(var(--dt-ruler) + 8px);
|
|
392
|
+
}
|
|
393
|
+
.dt-tick--h::before {
|
|
394
|
+
top: var(--dt-ruler);
|
|
395
|
+
height: 6px;
|
|
396
|
+
bottom: auto;
|
|
397
|
+
opacity: 0.45;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.dt-now {
|
|
401
|
+
position: absolute;
|
|
402
|
+
top: 0;
|
|
403
|
+
bottom: 0;
|
|
404
|
+
z-index: 10;
|
|
405
|
+
pointer-events: none;
|
|
406
|
+
transform: translateX(-1px);
|
|
407
|
+
}
|
|
408
|
+
.dt-now-line {
|
|
409
|
+
position: absolute;
|
|
410
|
+
top: 0;
|
|
411
|
+
bottom: 0;
|
|
412
|
+
left: 0;
|
|
413
|
+
width: 2px;
|
|
414
|
+
background: var(--dt-accent);
|
|
415
|
+
box-shadow: 0 0 10px var(--dt-glow), 0 0 24px var(--dt-accent-dim);
|
|
416
|
+
animation: now-pulse 3s ease-in-out infinite;
|
|
417
|
+
}
|
|
418
|
+
@keyframes now-pulse {
|
|
419
|
+
0%, 100% { box-shadow: 0 0 10px var(--dt-glow), 0 0 24px var(--dt-accent-dim); }
|
|
420
|
+
50% { box-shadow: 0 0 16px var(--dt-glow), 0 0 36px var(--dt-glow); }
|
|
421
|
+
}
|
|
422
|
+
.dt-now-tag {
|
|
423
|
+
position: absolute;
|
|
424
|
+
top: 7px;
|
|
425
|
+
left: 8px;
|
|
426
|
+
font: 700 10px / 1 var(--dt-mono);
|
|
427
|
+
color: var(--dt-accent);
|
|
428
|
+
background: var(--dt-bg);
|
|
429
|
+
border: 1px solid var(--dt-accent-dim);
|
|
430
|
+
padding: 3px 6px;
|
|
431
|
+
border-radius: 4px;
|
|
432
|
+
white-space: nowrap;
|
|
433
|
+
z-index: 1;
|
|
434
|
+
}
|
|
435
|
+
.dt-now-sec { font-weight: 400; opacity: 0.5; font-size: 9px; }
|
|
436
|
+
|
|
437
|
+
.dt-fog {
|
|
438
|
+
position: absolute;
|
|
439
|
+
top: 0;
|
|
440
|
+
bottom: 0;
|
|
441
|
+
width: 48px;
|
|
442
|
+
pointer-events: none;
|
|
443
|
+
z-index: 5;
|
|
444
|
+
}
|
|
445
|
+
.dt-fog--l { left: 0; background: linear-gradient(to right, var(--dt-bg), transparent); }
|
|
446
|
+
.dt-fog--r { right: 0; background: linear-gradient(to left, var(--dt-bg), transparent); }
|
|
447
|
+
|
|
448
|
+
.dt-btn {
|
|
449
|
+
position: absolute;
|
|
450
|
+
bottom: 14px;
|
|
451
|
+
right: 14px;
|
|
452
|
+
z-index: 20;
|
|
453
|
+
display: inline-flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
gap: 5px;
|
|
456
|
+
padding: 5px 13px 5px 9px;
|
|
457
|
+
font: 600 11px / 1 var(--dt-sans);
|
|
458
|
+
color: var(--dt-btn-text);
|
|
459
|
+
background: var(--dt-accent);
|
|
460
|
+
border: none;
|
|
461
|
+
border-radius: 100px;
|
|
462
|
+
cursor: pointer;
|
|
463
|
+
box-shadow: 0 2px 16px var(--dt-glow);
|
|
464
|
+
transition: transform 120ms ease, box-shadow 120ms ease;
|
|
465
|
+
}
|
|
466
|
+
.dt-btn svg { width: 13px; height: 13px; }
|
|
467
|
+
.dt-btn:hover { transform: scale(1.06); box-shadow: 0 4px 24px var(--dt-glow); }
|
|
468
|
+
.dt-btn:active { transform: scale(0.96); }
|
|
469
|
+
|
|
470
|
+
/* ─── Events ─────────────────────────────────────── */
|
|
471
|
+
.dt-event {
|
|
472
|
+
position: absolute;
|
|
473
|
+
height: 28px;
|
|
474
|
+
z-index: 6;
|
|
475
|
+
border-radius: 5px;
|
|
476
|
+
padding: 0 8px;
|
|
477
|
+
display: flex;
|
|
478
|
+
align-items: center;
|
|
479
|
+
cursor: pointer;
|
|
480
|
+
background: color-mix(in srgb, var(--ev-color) 22%, transparent);
|
|
481
|
+
border-left: 3px solid var(--ev-color);
|
|
482
|
+
transition: box-shadow 120ms, transform 80ms;
|
|
483
|
+
}
|
|
484
|
+
.dt-event:hover {
|
|
485
|
+
box-shadow: 0 2px 10px color-mix(in srgb, var(--ev-color) 30%, transparent);
|
|
486
|
+
transform: translateY(-1px);
|
|
487
|
+
}
|
|
488
|
+
.dt-event--selected {
|
|
489
|
+
box-shadow: 0 0 0 2px var(--ev-color), 0 2px 12px color-mix(in srgb, var(--ev-color) 40%, transparent);
|
|
490
|
+
}
|
|
491
|
+
.dt-event--dragging {
|
|
492
|
+
opacity: 0.85;
|
|
493
|
+
z-index: 50;
|
|
494
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.35);
|
|
495
|
+
cursor: grabbing;
|
|
496
|
+
transition: none;
|
|
497
|
+
}
|
|
498
|
+
.dt-ev-title {
|
|
499
|
+
font: 600 10px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
500
|
+
color: var(--dt-text, #e2e8f0);
|
|
501
|
+
white-space: nowrap;
|
|
502
|
+
overflow: hidden;
|
|
503
|
+
text-overflow: ellipsis;
|
|
504
|
+
}
|
|
505
|
+
</style>
|