@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,103 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
DayHeader — shared day label primitive.
|
|
3
|
+
|
|
4
|
+
Handles both:
|
|
5
|
+
- Relative labels ("Today", "Tomorrow") for day views via fmtDay
|
|
6
|
+
- Short/long weekday + date number for week views
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { weekdayShort, weekdayLong, monthShort, fmtDay } from '../core/locale.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Timestamp (ms) of the day to label */
|
|
13
|
+
dayMs: number;
|
|
14
|
+
/** Timestamp (ms) of today (for relative labels) */
|
|
15
|
+
todayMs?: number;
|
|
16
|
+
/** Display format */
|
|
17
|
+
format?: 'relative' | 'short' | 'long';
|
|
18
|
+
/** Whether this day is "today" */
|
|
19
|
+
isToday?: boolean;
|
|
20
|
+
/** Whether this day is in the past */
|
|
21
|
+
isPast?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
dayMs,
|
|
26
|
+
todayMs = Date.now(),
|
|
27
|
+
format = 'short',
|
|
28
|
+
isToday = false,
|
|
29
|
+
isPast = false,
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
const dayNum = $derived(new Date(dayMs).getDate());
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<div
|
|
36
|
+
class="dh"
|
|
37
|
+
class:dh-today={isToday}
|
|
38
|
+
class:dh-past={isPast}
|
|
39
|
+
>
|
|
40
|
+
{#if format === 'relative'}
|
|
41
|
+
<span class="dh-rel">{fmtDay(dayMs, todayMs)}</span>
|
|
42
|
+
{:else if format === 'long'}
|
|
43
|
+
<span class="dh-name dh-name-long">{weekdayLong(dayMs)}</span>
|
|
44
|
+
<span class="dh-date">{monthShort(dayMs)} {dayNum}</span>
|
|
45
|
+
{:else}
|
|
46
|
+
<span class="dh-name">{weekdayShort(dayMs)}</span>
|
|
47
|
+
<span class="dh-num" class:dh-num-today={isToday}>{dayNum}</span>
|
|
48
|
+
{/if}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<style>
|
|
52
|
+
.dh {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-direction: column;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: 2px;
|
|
57
|
+
user-select: none;
|
|
58
|
+
}
|
|
59
|
+
.dh-past {
|
|
60
|
+
opacity: 0.45;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.dh-name {
|
|
64
|
+
font: 500 10px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
65
|
+
letter-spacing: 0.04em;
|
|
66
|
+
text-transform: uppercase;
|
|
67
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
68
|
+
}
|
|
69
|
+
.dh-name-long {
|
|
70
|
+
font-size: 12px;
|
|
71
|
+
text-transform: none;
|
|
72
|
+
letter-spacing: 0.01em;
|
|
73
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.dh-num {
|
|
77
|
+
font: 600 14px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
78
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
79
|
+
width: 28px;
|
|
80
|
+
height: 28px;
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
border-radius: 50%;
|
|
85
|
+
}
|
|
86
|
+
.dh-num-today {
|
|
87
|
+
background: var(--dt-accent, #ef4444);
|
|
88
|
+
color: var(--dt-btn-text, #fff);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.dh-date {
|
|
92
|
+
font: 400 10px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
93
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.dh-rel {
|
|
97
|
+
font: 500 12px / 1.3 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
98
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
99
|
+
}
|
|
100
|
+
.dh-today .dh-rel {
|
|
101
|
+
color: var(--dt-accent, #ef4444);
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Timestamp (ms) of the day to label */
|
|
3
|
+
dayMs: number;
|
|
4
|
+
/** Timestamp (ms) of today (for relative labels) */
|
|
5
|
+
todayMs?: number;
|
|
6
|
+
/** Display format */
|
|
7
|
+
format?: 'relative' | 'short' | 'long';
|
|
8
|
+
/** Whether this day is "today" */
|
|
9
|
+
isToday?: boolean;
|
|
10
|
+
/** Whether this day is in the past */
|
|
11
|
+
isPast?: boolean;
|
|
12
|
+
}
|
|
13
|
+
declare const DayHeader: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type DayHeader = ReturnType<typeof DayHeader>;
|
|
15
|
+
export default DayHeader;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
EmptySlot — clickable empty time slot for "click to create" interactions.
|
|
3
|
+
|
|
4
|
+
Renders a transparent hit-target that highlights on hover and emits
|
|
5
|
+
the time range when clicked. View components place these in gaps
|
|
6
|
+
between events.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
interface Props {
|
|
10
|
+
/** Start time of the empty slot */
|
|
11
|
+
start: Date;
|
|
12
|
+
/** End time of the empty slot */
|
|
13
|
+
end: Date;
|
|
14
|
+
/** Click handler — create event in this range */
|
|
15
|
+
onclick?: (range: { start: Date; end: Date }) => void;
|
|
16
|
+
/** Orientation for cursor hint */
|
|
17
|
+
orientation?: 'horizontal' | 'vertical';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
start,
|
|
22
|
+
end,
|
|
23
|
+
onclick,
|
|
24
|
+
orientation = 'vertical',
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
function fmtTime(d: Date): string {
|
|
28
|
+
const h = d.getHours();
|
|
29
|
+
const m = d.getMinutes();
|
|
30
|
+
const suffix = h >= 12 ? 'p' : 'a';
|
|
31
|
+
const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
|
32
|
+
return m === 0 ? `${h12}${suffix}` : `${h12}:${String(m).padStart(2, '0')}${suffix}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dur = $derived(() => {
|
|
36
|
+
const mins = Math.round((end.getTime() - start.getTime()) / 60_000);
|
|
37
|
+
if (mins < 60) return `${mins}m free`;
|
|
38
|
+
const h = Math.floor(mins / 60);
|
|
39
|
+
const m = mins % 60;
|
|
40
|
+
return m > 0 ? `${h}h ${m}m free` : `${h}h free`;
|
|
41
|
+
});
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
45
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
46
|
+
<div
|
|
47
|
+
class="es"
|
|
48
|
+
class:es-v={orientation === 'vertical'}
|
|
49
|
+
class:es-h={orientation === 'horizontal'}
|
|
50
|
+
onclick={() => onclick?.({ start, end })}
|
|
51
|
+
>
|
|
52
|
+
<div class="es-hint">
|
|
53
|
+
<span class="es-plus">+</span>
|
|
54
|
+
<span class="es-range">{fmtTime(start)} – {fmtTime(end)}</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<style>
|
|
59
|
+
.es {
|
|
60
|
+
position: relative;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
border: 1px dashed transparent;
|
|
63
|
+
border-radius: 6px;
|
|
64
|
+
transition: border-color 150ms, background 150ms;
|
|
65
|
+
min-height: 24px;
|
|
66
|
+
}
|
|
67
|
+
.es:hover {
|
|
68
|
+
border-color: var(--dt-accent-dim, rgba(239, 68, 68, 0.18));
|
|
69
|
+
background: var(--dt-accent-dim, rgba(239, 68, 68, 0.05));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.es-hint {
|
|
73
|
+
position: absolute;
|
|
74
|
+
inset: 0;
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
gap: 6px;
|
|
79
|
+
opacity: 0;
|
|
80
|
+
transition: opacity 150ms;
|
|
81
|
+
}
|
|
82
|
+
.es:hover .es-hint {
|
|
83
|
+
opacity: 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.es-plus {
|
|
87
|
+
font: 500 14px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
88
|
+
color: var(--dt-accent, #ef4444);
|
|
89
|
+
}
|
|
90
|
+
.es-range {
|
|
91
|
+
font: 400 10px / 1 var(--dt-mono, 'SF Mono', monospace);
|
|
92
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.es-v {
|
|
96
|
+
width: 100%;
|
|
97
|
+
}
|
|
98
|
+
.es-h {
|
|
99
|
+
height: 100%;
|
|
100
|
+
}
|
|
101
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Start time of the empty slot */
|
|
3
|
+
start: Date;
|
|
4
|
+
/** End time of the empty slot */
|
|
5
|
+
end: Date;
|
|
6
|
+
/** Click handler — create event in this range */
|
|
7
|
+
onclick?: (range: {
|
|
8
|
+
start: Date;
|
|
9
|
+
end: Date;
|
|
10
|
+
}) => void;
|
|
11
|
+
/** Orientation for cursor hint */
|
|
12
|
+
orientation?: 'horizontal' | 'vertical';
|
|
13
|
+
}
|
|
14
|
+
declare const EmptySlot: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type EmptySlot = ReturnType<typeof EmptySlot>;
|
|
16
|
+
export default EmptySlot;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
EventBlock — shared event rendering primitive.
|
|
3
|
+
|
|
4
|
+
Three display variants:
|
|
5
|
+
"chip" — compact colored chip (dot + title)
|
|
6
|
+
"card" — full card (title, time, duration, color stripe)
|
|
7
|
+
"row" — full-width row (time, title, duration — agenda style)
|
|
8
|
+
|
|
9
|
+
Emits: onclick, ondragstart, onresize (for future interaction wiring)
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import type { TimelineEvent } from '../core/types.js';
|
|
13
|
+
import type { Snippet } from 'svelte';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
event: TimelineEvent;
|
|
17
|
+
/** Display variant */
|
|
18
|
+
variant?: 'chip' | 'card' | 'row';
|
|
19
|
+
/** Is this event currently in-progress? */
|
|
20
|
+
active?: boolean;
|
|
21
|
+
/** Is this event in the past? */
|
|
22
|
+
past?: boolean;
|
|
23
|
+
/** Show the time range */
|
|
24
|
+
showTime?: boolean;
|
|
25
|
+
/** Show the duration */
|
|
26
|
+
showDuration?: boolean;
|
|
27
|
+
/** Editable (shows resize handles in future) */
|
|
28
|
+
editable?: boolean;
|
|
29
|
+
/** Click handler */
|
|
30
|
+
onclick?: (event: TimelineEvent) => void;
|
|
31
|
+
/** Custom content slot */
|
|
32
|
+
children?: Snippet<[TimelineEvent]>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
event,
|
|
37
|
+
variant = 'chip',
|
|
38
|
+
active = false,
|
|
39
|
+
past = false,
|
|
40
|
+
showTime = false,
|
|
41
|
+
showDuration = false,
|
|
42
|
+
editable = false,
|
|
43
|
+
onclick,
|
|
44
|
+
children,
|
|
45
|
+
}: Props = $props();
|
|
46
|
+
|
|
47
|
+
function fmtTime(d: Date): string {
|
|
48
|
+
const h = d.getHours();
|
|
49
|
+
const m = d.getMinutes();
|
|
50
|
+
const suffix = h >= 12 ? 'p' : 'a';
|
|
51
|
+
const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
|
52
|
+
return m === 0 ? `${h12}${suffix}` : `${h12}:${String(m).padStart(2, '0')}${suffix}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fmtDuration(start: Date, end: Date): string {
|
|
56
|
+
const mins = Math.round((end.getTime() - start.getTime()) / 60_000);
|
|
57
|
+
if (mins < 60) return `${mins}m`;
|
|
58
|
+
const h = Math.floor(mins / 60);
|
|
59
|
+
const m = mins % 60;
|
|
60
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const accentColor = $derived(event.color || 'var(--dt-accent, #ef4444)');
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
67
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
68
|
+
<div
|
|
69
|
+
class="eb eb-{variant}"
|
|
70
|
+
class:eb-active={active}
|
|
71
|
+
class:eb-past={past}
|
|
72
|
+
class:eb-editable={editable}
|
|
73
|
+
style="--eb-color: {accentColor}"
|
|
74
|
+
onclick={() => onclick?.(event)}
|
|
75
|
+
>
|
|
76
|
+
{#if children}
|
|
77
|
+
{@render children(event)}
|
|
78
|
+
{:else if variant === 'chip'}
|
|
79
|
+
<span class="eb-dot"></span>
|
|
80
|
+
<span class="eb-title">{event.title}</span>
|
|
81
|
+
{#if active}<span class="eb-live">●</span>{/if}
|
|
82
|
+
{:else if variant === 'card'}
|
|
83
|
+
<div class="eb-stripe"></div>
|
|
84
|
+
<div class="eb-body">
|
|
85
|
+
<span class="eb-title">{event.title}</span>
|
|
86
|
+
{#if showTime}
|
|
87
|
+
<span class="eb-time">{fmtTime(event.start)} – {fmtTime(event.end)}</span>
|
|
88
|
+
{/if}
|
|
89
|
+
{#if showDuration}
|
|
90
|
+
<span class="eb-dur">{fmtDuration(event.start, event.end)}</span>
|
|
91
|
+
{/if}
|
|
92
|
+
{#if active}<span class="eb-live-badge">now</span>{/if}
|
|
93
|
+
</div>
|
|
94
|
+
{:else}
|
|
95
|
+
<!-- row -->
|
|
96
|
+
<span class="eb-stripe"></span>
|
|
97
|
+
<span class="eb-time">{fmtTime(event.start)} – {fmtTime(event.end)}</span>
|
|
98
|
+
<span class="eb-title">{event.title}</span>
|
|
99
|
+
{#if showDuration}
|
|
100
|
+
<span class="eb-dur">{fmtDuration(event.start, event.end)}</span>
|
|
101
|
+
{/if}
|
|
102
|
+
{#if active}<span class="eb-live-badge">now</span>{/if}
|
|
103
|
+
{/if}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<style>
|
|
107
|
+
.eb {
|
|
108
|
+
--_color: var(--eb-color, var(--dt-accent, #ef4444));
|
|
109
|
+
cursor: default;
|
|
110
|
+
transition: opacity 120ms, transform 80ms;
|
|
111
|
+
}
|
|
112
|
+
.eb-editable {
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
}
|
|
115
|
+
.eb-editable:hover {
|
|
116
|
+
transform: translateY(-1px);
|
|
117
|
+
}
|
|
118
|
+
.eb-past {
|
|
119
|
+
opacity: 0.5;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* ── Chip ── */
|
|
123
|
+
.eb-chip {
|
|
124
|
+
display: inline-flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
gap: 5px;
|
|
127
|
+
font: 400 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
128
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
text-overflow: ellipsis;
|
|
132
|
+
}
|
|
133
|
+
.eb-chip .eb-dot {
|
|
134
|
+
width: 6px;
|
|
135
|
+
height: 6px;
|
|
136
|
+
border-radius: 50%;
|
|
137
|
+
background: var(--_color);
|
|
138
|
+
flex-shrink: 0;
|
|
139
|
+
}
|
|
140
|
+
.eb-chip .eb-title {
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
text-overflow: ellipsis;
|
|
143
|
+
}
|
|
144
|
+
.eb-live {
|
|
145
|
+
color: var(--_color);
|
|
146
|
+
font-size: 8px;
|
|
147
|
+
animation: eb-blink 1.5s ease-in-out infinite;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* ── Card ── */
|
|
151
|
+
.eb-card {
|
|
152
|
+
display: flex;
|
|
153
|
+
border-radius: 8px;
|
|
154
|
+
background: var(--dt-surface, #10141c);
|
|
155
|
+
border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
}
|
|
158
|
+
.eb-card .eb-stripe {
|
|
159
|
+
width: 4px;
|
|
160
|
+
flex-shrink: 0;
|
|
161
|
+
background: var(--_color);
|
|
162
|
+
}
|
|
163
|
+
.eb-card .eb-body {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-direction: column;
|
|
166
|
+
gap: 3px;
|
|
167
|
+
padding: 8px 10px;
|
|
168
|
+
min-width: 0;
|
|
169
|
+
}
|
|
170
|
+
.eb-card .eb-title {
|
|
171
|
+
font: 500 12px / 1.3 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
172
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
text-overflow: ellipsis;
|
|
175
|
+
white-space: nowrap;
|
|
176
|
+
}
|
|
177
|
+
.eb-card .eb-time {
|
|
178
|
+
font: 400 10px / 1 var(--dt-mono, 'SF Mono', monospace);
|
|
179
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
180
|
+
}
|
|
181
|
+
.eb-card .eb-dur {
|
|
182
|
+
font: 300 10px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
183
|
+
color: var(--dt-text-3, rgba(100, 116, 139, 0.55));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ── Row ── */
|
|
187
|
+
.eb-row {
|
|
188
|
+
display: flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
gap: 10px;
|
|
191
|
+
padding: 6px 12px;
|
|
192
|
+
border-bottom: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
193
|
+
}
|
|
194
|
+
.eb-row .eb-stripe {
|
|
195
|
+
width: 3px;
|
|
196
|
+
height: 100%;
|
|
197
|
+
min-height: 20px;
|
|
198
|
+
border-radius: 2px;
|
|
199
|
+
background: var(--_color);
|
|
200
|
+
flex-shrink: 0;
|
|
201
|
+
}
|
|
202
|
+
.eb-row .eb-time {
|
|
203
|
+
font: 400 10px / 1 var(--dt-mono, 'SF Mono', monospace);
|
|
204
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
205
|
+
flex-shrink: 0;
|
|
206
|
+
width: 90px;
|
|
207
|
+
}
|
|
208
|
+
.eb-row .eb-title {
|
|
209
|
+
font: 400 12px / 1.3 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
210
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
211
|
+
flex: 1;
|
|
212
|
+
overflow: hidden;
|
|
213
|
+
text-overflow: ellipsis;
|
|
214
|
+
white-space: nowrap;
|
|
215
|
+
}
|
|
216
|
+
.eb-row .eb-dur {
|
|
217
|
+
font: 300 10px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
218
|
+
color: var(--dt-text-3, rgba(100, 116, 139, 0.55));
|
|
219
|
+
flex-shrink: 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* ── Shared ── */
|
|
223
|
+
.eb-live-badge {
|
|
224
|
+
display: inline-flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
font: 700 8px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
227
|
+
letter-spacing: 0.08em;
|
|
228
|
+
text-transform: uppercase;
|
|
229
|
+
color: var(--dt-btn-text, #fff);
|
|
230
|
+
background: var(--_color);
|
|
231
|
+
padding: 2px 6px;
|
|
232
|
+
border-radius: 3px;
|
|
233
|
+
width: fit-content;
|
|
234
|
+
animation: eb-blink 2s ease-in-out infinite;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@keyframes eb-blink {
|
|
238
|
+
0%, 100% { opacity: 1; }
|
|
239
|
+
50% { opacity: 0.6; }
|
|
240
|
+
}
|
|
241
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TimelineEvent } from '../core/types.js';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
interface Props {
|
|
4
|
+
event: TimelineEvent;
|
|
5
|
+
/** Display variant */
|
|
6
|
+
variant?: 'chip' | 'card' | 'row';
|
|
7
|
+
/** Is this event currently in-progress? */
|
|
8
|
+
active?: boolean;
|
|
9
|
+
/** Is this event in the past? */
|
|
10
|
+
past?: boolean;
|
|
11
|
+
/** Show the time range */
|
|
12
|
+
showTime?: boolean;
|
|
13
|
+
/** Show the duration */
|
|
14
|
+
showDuration?: boolean;
|
|
15
|
+
/** Editable (shows resize handles in future) */
|
|
16
|
+
editable?: boolean;
|
|
17
|
+
/** Click handler */
|
|
18
|
+
onclick?: (event: TimelineEvent) => void;
|
|
19
|
+
/** Custom content slot */
|
|
20
|
+
children?: Snippet<[TimelineEvent]>;
|
|
21
|
+
}
|
|
22
|
+
declare const EventBlock: import("svelte").Component<Props, {}, "">;
|
|
23
|
+
type EventBlock = ReturnType<typeof EventBlock>;
|
|
24
|
+
export default EventBlock;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
NowIndicator — shared "now" marker component.
|
|
3
|
+
|
|
4
|
+
Three visual modes:
|
|
5
|
+
"line" — vertical/horizontal line with time label (default)
|
|
6
|
+
"dot" — circular dot with time label
|
|
7
|
+
"badge" — inline text badge ("now")
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
<NowIndicator position={nowPx} time={clock.hm} seconds={clock.s} />
|
|
11
|
+
<NowIndicator mode="dot" position={nowPx} time={clock.hm} />
|
|
12
|
+
<NowIndicator mode="badge" />
|
|
13
|
+
-->
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import type { Snippet } from 'svelte';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** Visual mode */
|
|
19
|
+
mode?: 'line' | 'dot' | 'badge';
|
|
20
|
+
/** Pixel offset for positioning (line/dot modes) */
|
|
21
|
+
position?: number;
|
|
22
|
+
/** Orientation of the line/dot */
|
|
23
|
+
orientation?: 'vertical' | 'horizontal';
|
|
24
|
+
/** Formatted time string (e.g. "14:30") */
|
|
25
|
+
time?: string;
|
|
26
|
+
/** Formatted seconds string (e.g. ":05") */
|
|
27
|
+
seconds?: string;
|
|
28
|
+
/** Show the time label */
|
|
29
|
+
showLabel?: boolean;
|
|
30
|
+
/** Accent color override */
|
|
31
|
+
color?: string;
|
|
32
|
+
/** Custom content slot */
|
|
33
|
+
children?: Snippet;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
mode = 'line',
|
|
38
|
+
position = 0,
|
|
39
|
+
orientation = 'vertical',
|
|
40
|
+
time = '',
|
|
41
|
+
seconds = '',
|
|
42
|
+
showLabel = true,
|
|
43
|
+
color,
|
|
44
|
+
children,
|
|
45
|
+
}: Props = $props();
|
|
46
|
+
|
|
47
|
+
const posStyle = $derived(
|
|
48
|
+
orientation === 'vertical'
|
|
49
|
+
? `left: ${position}px`
|
|
50
|
+
: `top: ${position}px`,
|
|
51
|
+
);
|
|
52
|
+
const colorVar = $derived(color ? `--ni-color: ${color}` : '');
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
{#if mode === 'badge'}
|
|
56
|
+
<span class="ni-badge" style={colorVar}>
|
|
57
|
+
{#if children}
|
|
58
|
+
{@render children()}
|
|
59
|
+
{:else}
|
|
60
|
+
now
|
|
61
|
+
{/if}
|
|
62
|
+
</span>
|
|
63
|
+
{:else if mode === 'dot'}
|
|
64
|
+
<div class="ni ni-dot {orientation}" style="{posStyle}; {colorVar}">
|
|
65
|
+
<div class="ni-dot-circle"></div>
|
|
66
|
+
{#if showLabel && time}
|
|
67
|
+
<div class="ni-label">
|
|
68
|
+
<span class="ni-time">{time}</span>
|
|
69
|
+
{#if seconds}<span class="ni-sec">{seconds}</span>{/if}
|
|
70
|
+
</div>
|
|
71
|
+
{/if}
|
|
72
|
+
</div>
|
|
73
|
+
{:else}
|
|
74
|
+
<div class="ni ni-line {orientation}" style="{posStyle}; {colorVar}">
|
|
75
|
+
<div class="ni-line-bar"></div>
|
|
76
|
+
{#if showLabel && time}
|
|
77
|
+
<div class="ni-label">
|
|
78
|
+
<span class="ni-time">{time}</span>
|
|
79
|
+
{#if seconds}<span class="ni-sec">{seconds}</span>{/if}
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
{/if}
|
|
84
|
+
|
|
85
|
+
<style>
|
|
86
|
+
.ni {
|
|
87
|
+
position: absolute;
|
|
88
|
+
z-index: 10;
|
|
89
|
+
pointer-events: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ── Line mode ── */
|
|
93
|
+
.ni-line.vertical {
|
|
94
|
+
top: 0;
|
|
95
|
+
bottom: 0;
|
|
96
|
+
width: 0;
|
|
97
|
+
}
|
|
98
|
+
.ni-line.horizontal {
|
|
99
|
+
left: 0;
|
|
100
|
+
right: 0;
|
|
101
|
+
height: 0;
|
|
102
|
+
}
|
|
103
|
+
.ni-line-bar {
|
|
104
|
+
background: var(--ni-color, var(--dt-accent, #ef4444));
|
|
105
|
+
}
|
|
106
|
+
.ni-line.vertical .ni-line-bar {
|
|
107
|
+
width: 2px;
|
|
108
|
+
height: 100%;
|
|
109
|
+
margin-left: -1px;
|
|
110
|
+
}
|
|
111
|
+
.ni-line.horizontal .ni-line-bar {
|
|
112
|
+
height: 2px;
|
|
113
|
+
width: 100%;
|
|
114
|
+
margin-top: -1px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ── Dot mode ── */
|
|
118
|
+
.ni-dot-circle {
|
|
119
|
+
width: 10px;
|
|
120
|
+
height: 10px;
|
|
121
|
+
border-radius: 50%;
|
|
122
|
+
background: var(--ni-color, var(--dt-accent, #ef4444));
|
|
123
|
+
box-shadow: 0 0 6px var(--ni-color, var(--dt-glow, rgba(239, 68, 68, 0.35)));
|
|
124
|
+
}
|
|
125
|
+
.ni-dot.vertical .ni-dot-circle {
|
|
126
|
+
margin-left: -5px;
|
|
127
|
+
}
|
|
128
|
+
.ni-dot.horizontal .ni-dot-circle {
|
|
129
|
+
margin-top: -5px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ── Label ── */
|
|
133
|
+
.ni-label {
|
|
134
|
+
position: absolute;
|
|
135
|
+
white-space: nowrap;
|
|
136
|
+
font: 600 10px / 1 var(--dt-mono, 'SF Mono', 'Fira Code', monospace);
|
|
137
|
+
color: var(--dt-btn-text, #fff);
|
|
138
|
+
background: var(--ni-color, var(--dt-accent, #ef4444));
|
|
139
|
+
padding: 3px 6px;
|
|
140
|
+
border-radius: 4px;
|
|
141
|
+
}
|
|
142
|
+
.ni-line.vertical .ni-label,
|
|
143
|
+
.ni-dot.vertical .ni-label {
|
|
144
|
+
top: 0;
|
|
145
|
+
transform: translateX(-50%);
|
|
146
|
+
}
|
|
147
|
+
.ni-line.horizontal .ni-label,
|
|
148
|
+
.ni-dot.horizontal .ni-label {
|
|
149
|
+
left: 0;
|
|
150
|
+
transform: translateY(-100%) translateY(-4px);
|
|
151
|
+
}
|
|
152
|
+
.ni-sec {
|
|
153
|
+
opacity: 0.6;
|
|
154
|
+
font-weight: 400;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ── Badge mode ── */
|
|
158
|
+
.ni-badge {
|
|
159
|
+
display: inline-flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
font: 700 9px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
162
|
+
letter-spacing: 0.08em;
|
|
163
|
+
text-transform: uppercase;
|
|
164
|
+
color: var(--dt-btn-text, #fff);
|
|
165
|
+
background: var(--ni-color, var(--dt-accent, #ef4444));
|
|
166
|
+
padding: 2px 7px;
|
|
167
|
+
border-radius: 3px;
|
|
168
|
+
animation: ni-pulse 2s ease-in-out infinite;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@keyframes ni-pulse {
|
|
172
|
+
0%, 100% { opacity: 1; }
|
|
173
|
+
50% { opacity: 0.7; }
|
|
174
|
+
}
|
|
175
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Visual mode */
|
|
4
|
+
mode?: 'line' | 'dot' | 'badge';
|
|
5
|
+
/** Pixel offset for positioning (line/dot modes) */
|
|
6
|
+
position?: number;
|
|
7
|
+
/** Orientation of the line/dot */
|
|
8
|
+
orientation?: 'vertical' | 'horizontal';
|
|
9
|
+
/** Formatted time string (e.g. "14:30") */
|
|
10
|
+
time?: string;
|
|
11
|
+
/** Formatted seconds string (e.g. ":05") */
|
|
12
|
+
seconds?: string;
|
|
13
|
+
/** Show the time label */
|
|
14
|
+
showLabel?: boolean;
|
|
15
|
+
/** Accent color override */
|
|
16
|
+
color?: string;
|
|
17
|
+
/** Custom content slot */
|
|
18
|
+
children?: Snippet;
|
|
19
|
+
}
|
|
20
|
+
declare const NowIndicator: import("svelte").Component<Props, {}, "">;
|
|
21
|
+
type NowIndicator = ReturnType<typeof NowIndicator>;
|
|
22
|
+
export default NowIndicator;
|