@nomideusz/svelte-calendar 0.3.0 → 0.4.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.
Files changed (36) hide show
  1. package/README.md +568 -517
  2. package/dist/adapters/memory.d.ts +7 -2
  3. package/dist/adapters/memory.js +9 -6
  4. package/dist/adapters/recurring.d.ts +7 -2
  5. package/dist/adapters/recurring.js +9 -6
  6. package/dist/calendar/Calendar.svelte +21 -1
  7. package/dist/calendar/Calendar.svelte.d.ts +1 -0
  8. package/dist/calendar/Toolbar.svelte +5 -2
  9. package/dist/calendar/Toolbar.svelte.d.ts +2 -0
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.js +2 -0
  12. package/dist/core/palette.d.ts +23 -0
  13. package/dist/core/palette.js +109 -0
  14. package/dist/engine/view-state.svelte.d.ts +1 -0
  15. package/dist/engine/view-state.svelte.js +3 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/views/agenda/Agenda.svelte +117 -5
  19. package/dist/views/agenda/Agenda.svelte.d.ts +1 -0
  20. package/dist/views/day/DayGrid.svelte +39 -3
  21. package/dist/views/day/DayGrid.svelte.d.ts +2 -0
  22. package/dist/views/day/DayTimeline.svelte +31 -3
  23. package/dist/views/day/DayTimeline.svelte.d.ts +5 -1
  24. package/dist/views/schedule/WeekSchedule.svelte +4 -0
  25. package/dist/views/schedule/WeekSchedule.svelte.d.ts +2 -0
  26. package/dist/views/settings/Settings.svelte +451 -173
  27. package/dist/views/settings/Settings.svelte.d.ts +22 -6
  28. package/dist/views/week/WeekGrid.svelte +44 -11
  29. package/dist/views/week/WeekGrid.svelte.d.ts +1 -0
  30. package/dist/views/week/WeekHeatmap.svelte +8 -7
  31. package/dist/views/week/WeekHeatmap.svelte.d.ts +1 -0
  32. package/dist/widget/CalendarWidget.svelte +0 -2
  33. package/dist/widget/widget.d.ts +1 -7
  34. package/dist/widget/widget.js +44 -3
  35. package/package.json +65 -67
  36. package/widget/widget.js +260 -260
@@ -14,7 +14,12 @@ import type { CalendarAdapter } from './types.js';
14
14
  export interface MemoryAdapterOptions {
15
15
  /** Map of category/title to color */
16
16
  colorMap?: Record<string, string>;
17
- /** Auto-assign colors to events by category or title */
18
- autoColor?: boolean;
17
+ /**
18
+ * Auto-assign colors to events by category or title.
19
+ * true → use the default vivid palette
20
+ * string → hex accent color (e.g. '#6366f1') to generate a
21
+ * theme-harmonious palette via golden-angle hue rotation
22
+ */
23
+ autoColor?: boolean | string;
19
24
  }
20
25
  export declare function createMemoryAdapter(initial?: TimelineEvent[], options?: MemoryAdapterOptions): CalendarAdapter;
@@ -1,16 +1,19 @@
1
+ import { generatePalette, VIVID_PALETTE } from '../core/palette.js';
1
2
  let counter = 0;
2
3
  function uid() {
3
4
  return `mem-${Date.now()}-${++counter}`;
4
5
  }
5
6
  /** Default palette for auto-coloring */
6
- const AUTO_COLORS = [
7
- '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6',
8
- '#3b82f6', '#6366f1', '#a855f7', '#ec4899', '#f43f5e',
9
- '#06b6d4', '#84cc16', '#d946ef', '#0ea5e9', '#10b981',
10
- ];
7
+ const AUTO_COLORS = VIVID_PALETTE;
11
8
  export function createMemoryAdapter(initial = [], options = {}) {
12
9
  const { colorMap, autoColor } = options;
13
10
  const events = [...initial];
11
+ // Resolve palette: vivid default or theme-aware
12
+ const palette = autoColor
13
+ ? typeof autoColor === 'string'
14
+ ? generatePalette(autoColor)
15
+ : AUTO_COLORS
16
+ : AUTO_COLORS;
14
17
  // Build auto-color assignments
15
18
  const colorAssignments = new Map();
16
19
  let colorIndex = 0;
@@ -24,7 +27,7 @@ export function createMemoryAdapter(initial = [], options = {}) {
24
27
  return colorMap[key];
25
28
  if (autoColor) {
26
29
  if (!colorAssignments.has(key)) {
27
- colorAssignments.set(key, AUTO_COLORS[colorIndex % AUTO_COLORS.length]);
30
+ colorAssignments.set(key, palette[colorIndex % palette.length]);
28
31
  colorIndex++;
29
32
  }
30
33
  return colorAssignments.get(key);
@@ -27,8 +27,13 @@ export interface RecurringAdapterOptions {
27
27
  mondayStart?: boolean;
28
28
  /** Map of category/title to color */
29
29
  colorMap?: Record<string, string>;
30
- /** Auto-assign colors to events by category or title */
31
- autoColor?: boolean;
30
+ /**
31
+ * Auto-assign colors to events by category or title.
32
+ * true → use the default vivid palette
33
+ * string → hex accent color (e.g. '#6366f1') to generate a
34
+ * theme-harmonious palette via golden-angle hue rotation
35
+ */
36
+ autoColor?: boolean | string;
32
37
  }
33
38
  /**
34
39
  * Create a CalendarAdapter that projects recurring weekly events
@@ -1,5 +1,6 @@
1
1
  import { startOfWeek } from '../core/time.js';
2
2
  import { DAY_MS } from '../core/time.js';
3
+ import { generatePalette, VIVID_PALETTE } from '../core/palette.js';
3
4
  /** Parse "HH:MM" into [hours, minutes] */
4
5
  function parseTime(time) {
5
6
  const [h, m] = time.split(':').map(Number);
@@ -54,11 +55,7 @@ function getOverlappingWeeks(range, mondayStart) {
54
55
  return weeks;
55
56
  }
56
57
  /** Default palette for auto-coloring */
57
- const AUTO_COLORS = [
58
- '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6',
59
- '#3b82f6', '#6366f1', '#a855f7', '#ec4899', '#f43f5e',
60
- '#06b6d4', '#84cc16', '#d946ef', '#0ea5e9', '#10b981',
61
- ];
58
+ const AUTO_COLORS = VIVID_PALETTE;
62
59
  /**
63
60
  * Create a CalendarAdapter that projects recurring weekly events
64
61
  * onto concrete dates for whatever range the calendar requests.
@@ -67,6 +64,12 @@ const AUTO_COLORS = [
67
64
  */
68
65
  export function createRecurringAdapter(schedule, options = {}) {
69
66
  const { mondayStart = true, colorMap, autoColor } = options;
67
+ // Resolve palette: vivid default or theme-aware
68
+ const palette = autoColor
69
+ ? typeof autoColor === 'string'
70
+ ? generatePalette(autoColor)
71
+ : AUTO_COLORS
72
+ : AUTO_COLORS;
70
73
  // Build auto-color assignments
71
74
  const colorAssignments = new Map();
72
75
  if (autoColor || colorMap) {
@@ -77,7 +80,7 @@ export function createRecurringAdapter(schedule, options = {}) {
77
80
  colorAssignments.set(key, colorMap[key]);
78
81
  }
79
82
  else if (autoColor && !colorAssignments.has(key)) {
80
- colorAssignments.set(key, AUTO_COLORS[colorIndex % AUTO_COLORS.length]);
83
+ colorAssignments.set(key, palette[colorIndex % palette.length]);
81
84
  colorIndex++;
82
85
  }
83
86
  }
@@ -72,6 +72,7 @@
72
72
  oneventclick?: (event: TimelineEvent) => void;
73
73
  oneventcreate?: (range: { start: Date; end: Date }) => void;
74
74
  oneventmove?: (event: TimelineEvent, newStart: Date, newEnd: Date) => void;
75
+ onviewchange?: (viewId: CalendarViewId) => void;
75
76
  }
76
77
 
77
78
  let {
@@ -90,6 +91,7 @@
90
91
  oneventclick,
91
92
  oneventcreate,
92
93
  oneventmove,
94
+ onviewchange,
93
95
  }: Props = $props();
94
96
 
95
97
  // In readOnly mode, suppress mutation callbacks
@@ -153,6 +155,23 @@
153
155
  store.load({ start, end });
154
156
  });
155
157
 
158
+ // Keep active view in sync when external defaultView changes after mount.
159
+ $effect(() => {
160
+ viewState.setView(defaultView);
161
+ });
162
+
163
+ // Keep view state's week-start rule in sync with incoming prop changes.
164
+ $effect(() => {
165
+ if (viewState.mondayStart !== mondayStart) {
166
+ viewState.setMondayStart(mondayStart);
167
+ }
168
+ });
169
+
170
+ // Notify host when active view changes (e.g. via toolbar concept/granularity toggles).
171
+ $effect(() => {
172
+ onviewchange?.(viewState.view);
173
+ });
174
+
156
175
  // ── Resolve active view ──
157
176
  const activeView = $derived(views.find((v) => v.id === viewState.view) ?? views[0]);
158
177
 
@@ -170,7 +189,7 @@
170
189
  lang={locale}
171
190
  >
172
191
  {#if showToolbar}
173
- <Toolbar {viewState} views={toolbarViews} {links} />
192
+ <Toolbar {viewState} views={toolbarViews} {links} {locale} />
174
193
  {/if}
175
194
 
176
195
  <div class="cal-body">
@@ -181,6 +200,7 @@
181
200
  style={theme}
182
201
  height={height - (showToolbar ? 48 : 0)}
183
202
  mondayStart={viewState.mondayStart}
203
+ {locale}
184
204
  focusDate={viewState.focusDate}
185
205
  oneventclick={oneventclick}
186
206
  oneventcreate={effectiveCreate}
@@ -47,6 +47,7 @@ interface Props {
47
47
  end: Date;
48
48
  }) => void;
49
49
  oneventmove?: (event: TimelineEvent, newStart: Date, newEnd: Date) => void;
50
+ onviewchange?: (viewId: CalendarViewId) => void;
50
51
  }
51
52
  declare const Calendar: Component<Props, {}, "">;
52
53
  type Calendar = ReturnType<typeof Calendar>;
@@ -19,20 +19,23 @@
19
19
  views?: ViewOption[];
20
20
  /** Links to show in the toolbar */
21
21
  links?: { href: string; label: string }[];
22
+ /** Locale used for labels */
23
+ locale?: string;
22
24
  }
23
25
 
24
26
  let {
25
27
  viewState,
26
28
  views = [],
27
29
  links = [],
30
+ locale,
28
31
  }: Props = $props();
29
32
 
30
33
  const dateLabel = $derived(() => {
31
34
  if (viewState.granularity === 'day') {
32
- return `${weekdayLong(viewState.focusDate.getTime())}, ${fmtDay(viewState.focusDate.getTime(), Date.now())}`;
35
+ return `${weekdayLong(viewState.focusDate.getTime(), locale)}, ${fmtDay(viewState.focusDate.getTime(), Date.now(), undefined, locale)}`;
33
36
  }
34
37
  const ws = viewState.range.start.getTime();
35
- return fmtWeekRange(ws);
38
+ return fmtWeekRange(ws, locale);
36
39
  });
37
40
 
38
41
  // Which granularities are available?
@@ -13,6 +13,8 @@ interface Props {
13
13
  href: string;
14
14
  label: string;
15
15
  }[];
16
+ /** Locale used for labels */
17
+ locale?: string;
16
18
  }
17
19
  declare const Toolbar: import("svelte").Component<Props, {}, "">;
18
20
  type Toolbar = ReturnType<typeof Toolbar>;
@@ -5,3 +5,4 @@ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, weekdayShort,
5
5
  export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
6
6
  export type { TimelineEvent, WeekTimelineProps, DayTimelineProps, } from './types.js';
7
7
  export { timeToX } from './types.js';
8
+ export { generatePalette, VIVID_PALETTE } from './palette.js';
@@ -8,3 +8,5 @@ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, weekdayShort,
8
8
  // Timezone utilities
9
9
  export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
10
10
  export { timeToX } from './types.js';
11
+ // Palette
12
+ export { generatePalette, VIVID_PALETTE } from './palette.js';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Smart auto-color palette generator.
3
+ *
4
+ * Given a base accent hex (e.g. from `--dt-accent`), generates a
5
+ * palette of perceptually distinct colors that harmonize with the theme.
6
+ *
7
+ * Usage:
8
+ * generatePalette('#ef4444', 8) // 8 theme-harmonious colors
9
+ * generatePalette(undefined, 8) // falls back to the vivid default
10
+ */
11
+ export declare const VIVID_PALETTE: string[];
12
+ /**
13
+ * Generate `count` visually distinct and theme-harmonious colors
14
+ * by rotating hue evenly from the base accent, keeping saturation
15
+ * and lightness within a pleasant range.
16
+ *
17
+ * Dark themes (l < 0.5): bump lightness to 0.55–0.65 so colors pop on dark bg.
18
+ * Light themes (l ≥ 0.5): pull lightness to 0.38–0.48 so colors read on light bg.
19
+ *
20
+ * @param accent Hex color string (e.g. '#ef4444'). If undefined, returns VIVID_PALETTE.
21
+ * @param count Number of colors to generate (default: 15).
22
+ */
23
+ export declare function generatePalette(accent?: string, count?: number): string[];
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Smart auto-color palette generator.
3
+ *
4
+ * Given a base accent hex (e.g. from `--dt-accent`), generates a
5
+ * palette of perceptually distinct colors that harmonize with the theme.
6
+ *
7
+ * Usage:
8
+ * generatePalette('#ef4444', 8) // 8 theme-harmonious colors
9
+ * generatePalette(undefined, 8) // falls back to the vivid default
10
+ */
11
+ // ── Hardcoded vivid fallback (original behavior) ────────
12
+ export const VIVID_PALETTE = [
13
+ '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6',
14
+ '#3b82f6', '#6366f1', '#a855f7', '#ec4899', '#f43f5e',
15
+ '#06b6d4', '#84cc16', '#d946ef', '#0ea5e9', '#10b981',
16
+ ];
17
+ // ── Color math (hex ↔ HSL) ──────────────────────────────
18
+ function hexToRgb(hex) {
19
+ const h = hex.replace('#', '');
20
+ const n = h.length === 3
21
+ ? parseInt(h[0] + h[0] + h[1] + h[1] + h[2] + h[2], 16)
22
+ : parseInt(h, 16);
23
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
24
+ }
25
+ function rgbToHsl(r, g, b) {
26
+ r /= 255;
27
+ g /= 255;
28
+ b /= 255;
29
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
30
+ const l = (max + min) / 2;
31
+ if (max === min)
32
+ return [0, 0, l];
33
+ const d = max - min;
34
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
35
+ let h = 0;
36
+ if (max === r)
37
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
38
+ else if (max === g)
39
+ h = ((b - r) / d + 2) / 6;
40
+ else
41
+ h = ((r - g) / d + 4) / 6;
42
+ return [h, s, l];
43
+ }
44
+ function hslToHex(h, s, l) {
45
+ h = ((h % 1) + 1) % 1; // normalize to [0, 1)
46
+ const hue2rgb = (p, q, t) => {
47
+ if (t < 0)
48
+ t += 1;
49
+ if (t > 1)
50
+ t -= 1;
51
+ if (t < 1 / 6)
52
+ return p + (q - p) * 6 * t;
53
+ if (t < 1 / 2)
54
+ return q;
55
+ if (t < 2 / 3)
56
+ return p + (q - p) * (2 / 3 - t) * 6;
57
+ return p;
58
+ };
59
+ let r, g, b;
60
+ if (s === 0) {
61
+ r = g = b = l;
62
+ }
63
+ else {
64
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
65
+ const p = 2 * l - q;
66
+ r = hue2rgb(p, q, h + 1 / 3);
67
+ g = hue2rgb(p, q, h);
68
+ b = hue2rgb(p, q, h - 1 / 3);
69
+ }
70
+ const toHex = (v) => Math.round(v * 255).toString(16).padStart(2, '0');
71
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
72
+ }
73
+ // ── Palette generation ──────────────────────────────────
74
+ /**
75
+ * Generate `count` visually distinct and theme-harmonious colors
76
+ * by rotating hue evenly from the base accent, keeping saturation
77
+ * and lightness within a pleasant range.
78
+ *
79
+ * Dark themes (l < 0.5): bump lightness to 0.55–0.65 so colors pop on dark bg.
80
+ * Light themes (l ≥ 0.5): pull lightness to 0.38–0.48 so colors read on light bg.
81
+ *
82
+ * @param accent Hex color string (e.g. '#ef4444'). If undefined, returns VIVID_PALETTE.
83
+ * @param count Number of colors to generate (default: 15).
84
+ */
85
+ export function generatePalette(accent, count = 15) {
86
+ if (!accent)
87
+ return VIVID_PALETTE.slice(0, count);
88
+ const [r, g, b] = hexToRgb(accent);
89
+ const [baseH, baseS, baseL] = rgbToHsl(r, g, b);
90
+ // Determine a good saturation range
91
+ const sat = Math.max(0.45, Math.min(0.8, baseS));
92
+ // Light vs dark theme: adjust lightness for contrast
93
+ const isDark = baseL < 0.5;
94
+ const lCenter = isDark ? 0.6 : 0.43;
95
+ const lRange = 0.05;
96
+ const colors = [];
97
+ for (let i = 0; i < count; i++) {
98
+ // Golden-angle hue rotation for maximum perceptual spread
99
+ const hue = baseH + (i * 0.618033988749895);
100
+ // Slight lightness oscillation for differentiation
101
+ const lOff = ((i % 3) - 1) * lRange;
102
+ // Slight saturation variation
103
+ const sOff = ((i % 2) === 0 ? 0.04 : -0.04);
104
+ const s = Math.max(0.35, Math.min(0.85, sat + sOff));
105
+ const l = Math.max(0.3, Math.min(0.7, lCenter + lOff));
106
+ colors.push(hslToHex(hue, s, l));
107
+ }
108
+ return colors;
109
+ }
@@ -28,6 +28,7 @@ export interface ViewState {
28
28
  /** IANA timezone, or undefined for local */
29
29
  readonly timezone: string | undefined;
30
30
  setView(id: CalendarViewId): void;
31
+ setMondayStart(value: boolean): void;
31
32
  setFocusDate(date: Date): void;
32
33
  next(): void;
33
34
  prev(): void;
@@ -60,6 +60,9 @@ export function createViewState(options = {}) {
60
60
  setView(id) {
61
61
  view = id;
62
62
  },
63
+ setMondayStart(value) {
64
+ mondayStart = value;
65
+ },
63
66
  setFocusDate(date) {
64
67
  focusDate = date;
65
68
  },
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { createEventStore, createViewState, createSelection, createDragState, }
7
7
  export type { EventStore, ViewState, ViewStateOptions, CalendarViewId, BuiltInViewId, ViewGranularity, ViewDateRange, Selection, DragState, DragMode, DragPayload, } from './engine/index.js';
8
8
  export { createMemoryAdapter, createRestAdapter, createRecurringAdapter } from './adapters/index.js';
9
9
  export type { CalendarAdapter, DateRange, RestAdapterOptions, RecurringEvent, RecurringAdapterOptions, MemoryAdapterOptions, } from './adapters/index.js';
10
- export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, timeToX, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './core/index.js';
10
+ export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, timeToX, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, VIVID_PALETTE, } from './core/index.js';
11
11
  export type { Clock, TimelineEvent, WeekTimelineProps, DayTimelineProps, } from './core/index.js';
12
12
  export { midnight, parchment, indigo, neutral, bare, presets } from './theme/index.js';
13
13
  export type { PresetName } from './theme/index.js';
package/dist/index.js CHANGED
@@ -9,6 +9,6 @@ export { createEventStore, createViewState, createSelection, createDragState, }
9
9
  // ─── Adapters ───────────────────────────────────────────
10
10
  export { createMemoryAdapter, createRestAdapter, createRecurringAdapter } from './adapters/index.js';
11
11
  // ─── Core: clock, time, locale, types ───────────────────
12
- export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, timeToX, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './core/index.js';
12
+ export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, timeToX, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, VIVID_PALETTE, } from './core/index.js';
13
13
  // ─── Themes ─────────────────────────────────────────────
14
14
  export { midnight, parchment, indigo, neutral, bare, presets } from './theme/index.js';
@@ -25,6 +25,7 @@
25
25
  /** 'day' = single-day timeline rail, 'week' = rolling 7-day view */
26
26
  mode?: 'day' | 'week';
27
27
  mondayStart?: boolean;
28
+ locale?: string;
28
29
  height?: number;
29
30
  events?: TimelineEvent[];
30
31
  style?: string;
@@ -38,6 +39,7 @@
38
39
  let {
39
40
  mode = 'day',
40
41
  mondayStart = true,
42
+ locale,
41
43
  height = 520,
42
44
  events = [],
43
45
  style = '',
@@ -51,7 +53,7 @@
51
53
  // ── Format helpers ──────────────────────────────────
52
54
  function fmtTime(d: Date): string {
53
55
  return d
54
- .toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })
56
+ .toLocaleTimeString(locale ?? 'en-US', { hour: 'numeric', minute: '2-digit', hour12: true })
55
57
  .toLowerCase();
56
58
  }
57
59
 
@@ -220,7 +222,7 @@
220
222
  ? startOfWeek(sod(focusDate.getTime()), mondayStart)
221
223
  : startOfWeek(clock.today, mondayStart),
222
224
  );
223
- const weekLabel = $derived(fmtWeekRange(weekStartMs));
225
+ const weekLabel = $derived(fmtWeekRange(weekStartMs, locale));
224
226
 
225
227
  const weekDays = $derived.by((): DayGroup[] => {
226
228
  if (mode !== 'week') return [];
@@ -255,8 +257,8 @@
255
257
 
256
258
  out.push({
257
259
  ms,
258
- dayName: tier === 'today' || tier === 'tomorrow' ? weekdayLong(ms) : weekdayShort(ms),
259
- dateLabel: `${monthShort(ms)} ${dayNum(ms)}`,
260
+ dayName: tier === 'today' || tier === 'tomorrow' ? weekdayLong(ms, locale) : weekdayShort(ms, locale),
261
+ dateLabel: `${monthShort(ms, locale)} ${dayNum(ms)}`,
260
262
  tier,
261
263
  events: dayEvts,
262
264
  pastEvents,
@@ -287,6 +289,9 @@
287
289
  <div class="ag-card-stripe"></div>
288
290
  <div class="ag-card-body">
289
291
  <span class="ag-card-title">{ev.title}</span>
292
+ {#if ev.subtitle}
293
+ <span class="ag-card-sub">{ev.subtitle}</span>
294
+ {/if}
290
295
  <span class="ag-card-meta">
291
296
  {#if isNow}
292
297
  until {fmtTime(ev.end)}
@@ -295,6 +300,13 @@
295
300
  {/if}
296
301
  <span class="ag-card-dur">{duration(ev)}</span>
297
302
  </span>
303
+ {#if ev.tags?.length}
304
+ <div class="ag-card-tags">
305
+ {#each ev.tags as tag}
306
+ <span class="ag-card-tag">{tag}</span>
307
+ {/each}
308
+ </div>
309
+ {/if}
298
310
  {#if isNow}
299
311
  <div class="ag-card-progress">
300
312
  <div class="ag-card-progress-fill" style:width="{progress(ev) * 100}%"></div>
@@ -315,7 +327,7 @@
315
327
  <header class="ag-header">
316
328
  {#if mode === 'day'}
317
329
  <div class="ag-header-left">
318
- <span class="ag-title">{fmtDay(dayMs, clock.today, { short: false })}</span>
330
+ <span class="ag-title">{fmtDay(dayMs, clock.today, { short: false }, locale)}</span>
319
331
  {#if isToday}
320
332
  <span class="ag-clock">{clock.hm}</span>
321
333
  {:else if isPastDay}
@@ -411,10 +423,20 @@
411
423
  <span class="ag-q-card-title">{ev.title}</span>
412
424
  <span class="ag-q-card-eta">{timeUntilEv(ev)}</span>
413
425
  </div>
426
+ {#if ev.subtitle}
427
+ <span class="ag-q-card-sub">{ev.subtitle}</span>
428
+ {/if}
414
429
  <div class="ag-q-card-meta">
415
430
  {fmtTime(ev.start)} – {fmtTime(ev.end)}
416
431
  <span class="ag-q-card-dur">{duration(ev)}</span>
417
432
  </div>
433
+ {#if ev.tags?.length}
434
+ <div class="ag-q-card-tags">
435
+ {#each ev.tags as tag}
436
+ <span class="ag-q-card-tag">{tag}</span>
437
+ {/each}
438
+ </div>
439
+ {/if}
418
440
  </div>
419
441
  </div>
420
442
  {/each}
@@ -494,10 +516,20 @@
494
516
  <span class="ag-plan-order">{i + 1}</span>
495
517
  <span class="ag-plan-title">{ev.title}</span>
496
518
  </div>
519
+ {#if ev.subtitle}
520
+ <span class="ag-plan-sub">{ev.subtitle}</span>
521
+ {/if}
497
522
  <div class="ag-plan-meta">
498
523
  {fmtTime(ev.start)} – {fmtTime(ev.end)}
499
524
  <span class="ag-plan-dur">{duration(ev)}</span>
500
525
  </div>
526
+ {#if ev.tags?.length}
527
+ <div class="ag-plan-tags">
528
+ {#each ev.tags as tag}
529
+ <span class="ag-plan-tag">{tag}</span>
530
+ {/each}
531
+ </div>
532
+ {/if}
501
533
  </div>
502
534
  </div>
503
535
  {/each}
@@ -584,6 +616,14 @@
584
616
  <span class="ag-compact-dot"></span>
585
617
  <span class="ag-compact-time">{fmtTime(ev.start)}</span>
586
618
  <span class="ag-compact-title">{ev.title}</span>
619
+ {#if ev.subtitle}
620
+ <span class="ag-compact-sub">{ev.subtitle}</span>
621
+ {/if}
622
+ {#if ev.tags?.length}
623
+ {#each ev.tags as tag}
624
+ <span class="ag-compact-tag">{tag}</span>
625
+ {/each}
626
+ {/if}
587
627
  <span class="ag-compact-dur">{duration(ev)}</span>
588
628
  </div>
589
629
  {/each}
@@ -740,6 +780,24 @@
740
780
  margin-left: 6px;
741
781
  color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
742
782
  }
783
+ .ag-card-sub {
784
+ font-size: 11px;
785
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.45));
786
+ line-height: 1;
787
+ }
788
+ .ag-card-tags {
789
+ display: flex;
790
+ gap: 4px;
791
+ flex-wrap: wrap;
792
+ }
793
+ .ag-card-tag {
794
+ font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
795
+ color: var(--ev-color, var(--dt-accent));
796
+ background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
797
+ padding: 2px 5px;
798
+ border-radius: 3px;
799
+ white-space: nowrap;
800
+ }
743
801
  .ag-card-progress {
744
802
  height: 3px;
745
803
  background: var(--dt-border, rgba(255, 255, 255, 0.06));
@@ -998,6 +1056,25 @@
998
1056
  margin-left: 6px;
999
1057
  color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
1000
1058
  }
1059
+ .ag-q-card-sub {
1060
+ font-size: 11px;
1061
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.45));
1062
+ line-height: 1;
1063
+ }
1064
+ .ag-q-card-tags {
1065
+ display: flex;
1066
+ gap: 4px;
1067
+ flex-wrap: wrap;
1068
+ margin-top: 2px;
1069
+ }
1070
+ .ag-q-card-tag {
1071
+ font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
1072
+ color: var(--ev-color, var(--dt-accent));
1073
+ background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
1074
+ padding: 2px 5px;
1075
+ border-radius: 3px;
1076
+ white-space: nowrap;
1077
+ }
1001
1078
 
1002
1079
  /* ── PAST: minimal right gutter ── */
1003
1080
  .ag-q-done {
@@ -1214,6 +1291,27 @@
1214
1291
  margin-left: 6px;
1215
1292
  color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
1216
1293
  }
1294
+ .ag-plan-sub {
1295
+ font-size: 11px;
1296
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.45));
1297
+ line-height: 1;
1298
+ padding-left: 22px;
1299
+ }
1300
+ .ag-plan-tags {
1301
+ display: flex;
1302
+ gap: 4px;
1303
+ flex-wrap: wrap;
1304
+ padding-left: 22px;
1305
+ margin-top: 2px;
1306
+ }
1307
+ .ag-plan-tag {
1308
+ font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
1309
+ color: var(--ev-color, var(--dt-accent));
1310
+ background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
1311
+ padding: 2px 5px;
1312
+ border-radius: 3px;
1313
+ white-space: nowrap;
1314
+ }
1217
1315
 
1218
1316
  /* Header badges for past/future days */
1219
1317
  .ag-badge {
@@ -1396,6 +1494,20 @@
1396
1494
  color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
1397
1495
  flex-shrink: 0;
1398
1496
  }
1497
+ .ag-compact-sub {
1498
+ font-size: 10px;
1499
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
1500
+ flex-shrink: 0;
1501
+ }
1502
+ .ag-compact-tag {
1503
+ font: 500 8px / 1 var(--dt-sans, system-ui, sans-serif);
1504
+ color: var(--ev-color, var(--dt-accent));
1505
+ background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 12%, transparent);
1506
+ padding: 1px 4px;
1507
+ border-radius: 3px;
1508
+ white-space: nowrap;
1509
+ flex-shrink: 0;
1510
+ }
1399
1511
  .ag-compact-more {
1400
1512
  font-size: 11px;
1401
1513
  color: var(--dt-text-3);
@@ -3,6 +3,7 @@ interface Props {
3
3
  /** 'day' = single-day timeline rail, 'week' = rolling 7-day view */
4
4
  mode?: 'day' | 'week';
5
5
  mondayStart?: boolean;
6
+ locale?: string;
6
7
  height?: number;
7
8
  events?: TimelineEvent[];
8
9
  style?: string;