@nomideusz/svelte-calendar 0.5.2 → 0.6.3

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 (49) hide show
  1. package/README.md +408 -178
  2. package/dist/adapters/memory.d.ts +7 -9
  3. package/dist/adapters/memory.js +8 -23
  4. package/dist/adapters/recurring.d.ts +8 -6
  5. package/dist/adapters/recurring.js +28 -30
  6. package/dist/calendar/Calendar.svelte +776 -356
  7. package/dist/calendar/Calendar.svelte.d.ts +57 -6
  8. package/dist/core/index.d.ts +2 -2
  9. package/dist/core/index.js +1 -1
  10. package/dist/core/locale.js +2 -2
  11. package/dist/core/palette.d.ts +5 -0
  12. package/dist/core/palette.js +8 -0
  13. package/dist/core/types.d.ts +14 -0
  14. package/dist/engine/event-store.svelte.d.ts +0 -17
  15. package/dist/engine/event-store.svelte.js +30 -17
  16. package/dist/engine/index.d.ts +1 -1
  17. package/dist/engine/view-state.svelte.d.ts +10 -5
  18. package/dist/engine/view-state.svelte.js +32 -16
  19. package/dist/index.d.ts +7 -7
  20. package/dist/index.js +3 -4
  21. package/dist/primitives/DayHeader.svelte +103 -103
  22. package/dist/primitives/EmptySlot.svelte +105 -105
  23. package/dist/primitives/EventBlock.svelte +312 -312
  24. package/dist/primitives/NowIndicator.svelte +178 -178
  25. package/dist/primitives/TimeGutter.svelte +104 -104
  26. package/dist/theme/auto.d.ts +53 -0
  27. package/dist/theme/auto.js +534 -0
  28. package/dist/theme/index.d.ts +3 -1
  29. package/dist/theme/index.js +3 -1
  30. package/dist/theme/presets.d.ts +20 -6
  31. package/dist/theme/presets.js +55 -42
  32. package/dist/views/agenda/AgendaDay.svelte +1073 -976
  33. package/dist/views/agenda/AgendaWeek.svelte +934 -775
  34. package/dist/views/agenda/index.d.ts +0 -2
  35. package/dist/views/agenda/index.js +0 -2
  36. package/dist/views/index.d.ts +3 -2
  37. package/dist/views/index.js +3 -2
  38. package/dist/views/mobile/Mobile.svelte +17 -0
  39. package/dist/views/mobile/Mobile.svelte.d.ts +7 -0
  40. package/dist/views/mobile/MobileDay.svelte +702 -0
  41. package/dist/views/mobile/MobileDay.svelte.d.ts +20 -0
  42. package/dist/views/mobile/MobileWeek.svelte +461 -0
  43. package/dist/views/mobile/MobileWeek.svelte.d.ts +20 -0
  44. package/dist/views/mobile/index.d.ts +1 -0
  45. package/dist/views/mobile/index.js +1 -0
  46. package/dist/views/planner/PlannerDay.svelte +1129 -957
  47. package/dist/views/planner/PlannerWeek.svelte +949 -840
  48. package/dist/widget/CalendarWidget.svelte +142 -157
  49. package/package.json +2 -1
@@ -0,0 +1,534 @@
1
+ /**
2
+ * Smart Auto Theme — probes the host page and generates --dt-* CSS tokens
3
+ * that blend the calendar into any design system.
4
+ *
5
+ * How it works:
6
+ * 1. Reads computed styles from the element's ancestors (body, parent)
7
+ * 2. Detects light/dark mode from background luminance
8
+ * 3. Extracts fonts, text colors, accent/brand colors
9
+ * 4. Generates a full --dt-* CSS variable string
10
+ *
11
+ * Usage:
12
+ * const vars = probeHostTheme(calendarElement);
13
+ * // → "--dt-bg: #fff; --dt-text: rgba(0,0,0,0.87); ..."
14
+ *
15
+ * The Calendar component calls this automatically when theme is `auto` (empty string).
16
+ */
17
+ function parseColor(raw) {
18
+ if (!raw || raw === 'transparent' || raw === 'rgba(0, 0, 0, 0)')
19
+ return null;
20
+ // rgb(r, g, b) or rgba(r, g, b, a)
21
+ const rgba = raw.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
22
+ if (rgba)
23
+ return [+rgba[1], +rgba[2], +rgba[3]];
24
+ // #hex
25
+ if (raw.startsWith('#')) {
26
+ const h = raw.replace('#', '');
27
+ const n = h.length === 3
28
+ ? parseInt(h[0] + h[0] + h[1] + h[1] + h[2] + h[2], 16)
29
+ : parseInt(h, 16);
30
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
31
+ }
32
+ return null;
33
+ }
34
+ function luminance([r, g, b]) {
35
+ // Relative luminance (sRGB → linear)
36
+ const lin = (c) => {
37
+ const s = c / 255;
38
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
39
+ };
40
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
41
+ }
42
+ function rgbToHsl(r, g, b) {
43
+ r /= 255;
44
+ g /= 255;
45
+ b /= 255;
46
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
47
+ const l = (max + min) / 2;
48
+ if (max === min)
49
+ return [0, 0, l];
50
+ const d = max - min;
51
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
52
+ let h = 0;
53
+ if (max === r)
54
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
55
+ else if (max === g)
56
+ h = ((b - r) / d + 2) / 6;
57
+ else
58
+ h = ((r - g) / d + 4) / 6;
59
+ return [h, s, l];
60
+ }
61
+ function hslToRgb(h, s, l) {
62
+ h = ((h % 1) + 1) % 1;
63
+ const hue2rgb = (p, q, t) => {
64
+ if (t < 0)
65
+ t += 1;
66
+ if (t > 1)
67
+ t -= 1;
68
+ if (t < 1 / 6)
69
+ return p + (q - p) * 6 * t;
70
+ if (t < 1 / 2)
71
+ return q;
72
+ if (t < 2 / 3)
73
+ return p + (q - p) * (2 / 3 - t) * 6;
74
+ return p;
75
+ };
76
+ if (s === 0) {
77
+ const v = Math.round(l * 255);
78
+ return [v, v, v];
79
+ }
80
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
81
+ const p = 2 * l - q;
82
+ return [
83
+ Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
84
+ Math.round(hue2rgb(p, q, h) * 255),
85
+ Math.round(hue2rgb(p, q, h - 1 / 3) * 255),
86
+ ];
87
+ }
88
+ function rgbStr(r, g, b) {
89
+ return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;
90
+ }
91
+ function rgba(r, g, b, a) {
92
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
93
+ }
94
+ /** Mix two colors. t=0 → c1, t=1 → c2. */
95
+ function mix(c1, c2, t) {
96
+ return [
97
+ Math.round(c1[0] + (c2[0] - c1[0]) * t),
98
+ Math.round(c1[1] + (c2[1] - c1[1]) * t),
99
+ Math.round(c1[2] + (c2[2] - c1[2]) * t),
100
+ ];
101
+ }
102
+ // ── Text color detection ────────────────────────────────
103
+ /**
104
+ * Common CSS variable names for text / foreground color used by popular frameworks.
105
+ */
106
+ const TEXT_VAR_CANDIDATES = [
107
+ // Generic
108
+ '--text',
109
+ '--text-color',
110
+ '--color-text',
111
+ '--foreground', // Shadcn/ui
112
+ '--color-foreground',
113
+ // Bootstrap
114
+ '--bs-body-color',
115
+ // Chakra
116
+ '--chakra-colors-text',
117
+ '--chakra-colors-gray-800',
118
+ // Material
119
+ '--md-sys-color-on-background',
120
+ '--mdc-theme-on-surface',
121
+ // DaisyUI
122
+ '--bc', // DaisyUI base-content
123
+ // Radix
124
+ '--gray-12',
125
+ // Open Props
126
+ '--text-1',
127
+ ];
128
+ /**
129
+ * Probe the host page for a usable text (foreground) color.
130
+ * Uses the same three-pass strategy as `probeBackground()`:
131
+ * 1. CSS custom-property probe on :root (discrete, not animated)
132
+ * 2. Inline-style walk (`element.style.color` — immune to CSS transitions)
133
+ * 3. Computed-style walk (`getComputedStyle().color`)
134
+ *
135
+ * After probing, validates that the text color has adequate contrast against
136
+ * the given background. If contrast is poor (WCAG ratio < 3:1), returns null
137
+ * so the caller can derive text from the background luminance.
138
+ */
139
+ function probeTextColor(el, bg) {
140
+ const candidates = [];
141
+ // 1. Try common CSS variables on :root (discrete, never animated)
142
+ try {
143
+ const rootCs = getComputedStyle(document.documentElement);
144
+ for (const name of TEXT_VAR_CANDIDATES) {
145
+ const val = rootCs.getPropertyValue(name).trim();
146
+ if (val) {
147
+ const rgb = parseColor(val);
148
+ if (rgb) {
149
+ candidates.push(rgb);
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ }
155
+ catch { /* ignore */ }
156
+ // 2. Walk up the DOM reading *inline* style.color
157
+ let node = el;
158
+ while (node) {
159
+ const raw = node.style.color;
160
+ if (raw) {
161
+ const rgb = parseColor(raw);
162
+ if (rgb) {
163
+ candidates.push(rgb);
164
+ break;
165
+ }
166
+ }
167
+ node = node.parentElement;
168
+ }
169
+ // 3. Walk up the DOM reading *computed* color
170
+ node = el;
171
+ while (node) {
172
+ try {
173
+ const raw = getComputedStyle(node).color;
174
+ const rgb = parseColor(raw);
175
+ if (rgb) {
176
+ candidates.push(rgb);
177
+ break;
178
+ }
179
+ }
180
+ catch { /* ignore */ }
181
+ node = node.parentElement;
182
+ }
183
+ // Pick the first candidate with adequate contrast against the background.
184
+ // WCAG AA large-text minimum is 3:1.
185
+ const bgLum = luminance(bg);
186
+ for (const c of candidates) {
187
+ const cLum = luminance(c);
188
+ const ratio = (Math.max(bgLum, cLum) + 0.05) / (Math.min(bgLum, cLum) + 0.05);
189
+ if (ratio >= 3)
190
+ return c;
191
+ }
192
+ // All probed colors have poor contrast — let the caller derive from BG.
193
+ return null;
194
+ }
195
+ // ── Accent detection ────────────────────────────────────
196
+ /**
197
+ * Common CSS variable names used by popular frameworks/design systems
198
+ * for their primary/brand accent color.
199
+ */
200
+ const ACCENT_VAR_CANDIDATES = [
201
+ // Generic
202
+ '--accent',
203
+ '--accent-color',
204
+ '--primary',
205
+ '--primary-color',
206
+ '--brand',
207
+ '--brand-color',
208
+ '--theme-color',
209
+ '--color-primary',
210
+ '--color-accent',
211
+ // Tailwind / DaisyUI
212
+ '--p', // DaisyUI primary
213
+ '--color-primary',
214
+ // Shadcn/ui
215
+ '--primary',
216
+ // MUI / Material
217
+ '--md-sys-color-primary',
218
+ '--mdc-theme-primary',
219
+ // Bootstrap
220
+ '--bs-primary',
221
+ '--bs-primary-rgb',
222
+ // Chakra
223
+ '--chakra-colors-brand-500',
224
+ '--chakra-colors-primary',
225
+ // Open Props
226
+ '--blue-6',
227
+ // Radix
228
+ '--accent-9',
229
+ // Generic numbered
230
+ '--color-primary-500',
231
+ '--primary-500',
232
+ ];
233
+ /**
234
+ * Try to extract a usable accent color from the host page.
235
+ * Priority: CSS variables → link color → selection color → null.
236
+ */
237
+ function probeAccent(root) {
238
+ let cs;
239
+ try {
240
+ cs = getComputedStyle(root);
241
+ }
242
+ catch {
243
+ return null;
244
+ }
245
+ // 1. Try known CSS custom properties
246
+ for (const name of ACCENT_VAR_CANDIDATES) {
247
+ const val = cs.getPropertyValue(name).trim();
248
+ if (val) {
249
+ const rgb = parseColor(val);
250
+ if (rgb) {
251
+ const [, s] = rgbToHsl(...rgb);
252
+ // Only accept if it has some saturation (not grey)
253
+ if (s > 0.15)
254
+ return rgb;
255
+ }
256
+ }
257
+ }
258
+ // 2. Try <a> link color (often the brand color)
259
+ const link = root.querySelector('a[href]');
260
+ if (link) {
261
+ const lc = parseColor(getComputedStyle(link).color);
262
+ if (lc) {
263
+ const [, s] = rgbToHsl(...lc);
264
+ if (s > 0.2)
265
+ return lc;
266
+ }
267
+ }
268
+ // 3. Try the OS accent color (CSS `AccentColor` keyword)
269
+ const accent = cs.getPropertyValue('accent-color').trim();
270
+ if (accent && accent !== 'auto') {
271
+ const rgb = parseColor(accent);
272
+ if (rgb)
273
+ return rgb;
274
+ }
275
+ // 4. Try button/input accent
276
+ const btn = root.querySelector('button:not([class*="cal-"])');
277
+ if (btn) {
278
+ const bg = parseColor(getComputedStyle(btn).backgroundColor);
279
+ if (bg) {
280
+ const [, s] = rgbToHsl(...bg);
281
+ if (s > 0.25)
282
+ return bg;
283
+ }
284
+ }
285
+ return null;
286
+ }
287
+ // ── Font detection ──────────────────────────────────────
288
+ function probeFonts(el) {
289
+ let bodyFont = 'system-ui, sans-serif';
290
+ try {
291
+ const cs = getComputedStyle(el);
292
+ if (cs.fontFamily)
293
+ bodyFont = cs.fontFamily;
294
+ }
295
+ catch {
296
+ // getComputedStyle may fail in test environments
297
+ }
298
+ // For monospace, check if the page defines one
299
+ const pre = el.querySelector('pre, code, .mono, [class*="mono"]');
300
+ let mono = "ui-monospace, 'SFMono-Regular', monospace";
301
+ if (pre) {
302
+ try {
303
+ const pf = getComputedStyle(pre).fontFamily;
304
+ if (pf)
305
+ mono = pf;
306
+ }
307
+ catch { /* ignore */ }
308
+ }
309
+ return { sans: bodyFont, mono };
310
+ }
311
+ // ── Background walking ──────────────────────────────────
312
+ /**
313
+ * Common CSS variable names for background color used by popular frameworks.
314
+ */
315
+ const BG_VAR_CANDIDATES = [
316
+ '--bg',
317
+ '--background',
318
+ '--color-bg',
319
+ '--color-background',
320
+ '--body-bg',
321
+ '--bs-body-bg', // Bootstrap
322
+ '--chakra-colors-bg', // Chakra
323
+ '--md-sys-color-background', // Material
324
+ '--b1', // DaisyUI base
325
+ '--background', // Shadcn/ui
326
+ '--color-background', // Radix / generic
327
+ ];
328
+ /**
329
+ * Walk up the DOM tree to find the first non-transparent background.
330
+ * Also probes common CSS variables for background color.
331
+ * Returns the parsed RGB and whether this is a dark background.
332
+ *
333
+ * Uses a three-pass strategy:
334
+ * 1. CSS custom-property probe on :root (instant, not animated)
335
+ * 2. Inline-style walk (reads `element.style.background` — the *target*
336
+ * value, immune to CSS `transition` interpolation)
337
+ * 3. Computed-style walk (reads `getComputedStyle().backgroundColor` —
338
+ * may return a mid-transition intermediate value)
339
+ *
340
+ * Passes 1-2 are preferred because CSS transitions animate the resolved
341
+ * `background-color` property, making `getComputedStyle` unreliable
342
+ * during the transition window.
343
+ */
344
+ function probeBackground(el) {
345
+ const result = (rgb) => ({ bg: rgb, isDark: luminance(rgb) < 0.4 });
346
+ // 1. Try common CSS variables on the root element (discrete, never animated)
347
+ try {
348
+ const rootCs = getComputedStyle(document.documentElement);
349
+ for (const name of BG_VAR_CANDIDATES) {
350
+ const val = rootCs.getPropertyValue(name).trim();
351
+ if (val) {
352
+ const rgb = parseColor(val);
353
+ if (rgb)
354
+ return result(rgb);
355
+ }
356
+ }
357
+ }
358
+ catch { /* ignore */ }
359
+ // 2. Walk up the DOM reading *inline* style.background / style.backgroundColor.
360
+ // Inline styles reflect the declared target value, not the transition
361
+ // midpoint, so they're reliable even while a CSS transition is running.
362
+ let node = el;
363
+ while (node) {
364
+ const raw = node.style.backgroundColor || node.style.background;
365
+ if (raw) {
366
+ const rgb = parseColor(raw);
367
+ if (rgb)
368
+ return result(rgb);
369
+ }
370
+ node = node.parentElement;
371
+ }
372
+ // 3. Fall back to computed backgroundColor (may be mid-transition, but
373
+ // still correct when no transition is active).
374
+ node = el;
375
+ while (node) {
376
+ try {
377
+ const raw = getComputedStyle(node).backgroundColor;
378
+ const rgb = parseColor(raw);
379
+ if (rgb)
380
+ return result(rgb);
381
+ }
382
+ catch {
383
+ // getComputedStyle may fail in test environments
384
+ }
385
+ node = node.parentElement;
386
+ }
387
+ // 4. Ultimate fallback: check color-scheme preference
388
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
389
+ return { bg: [18, 18, 18], isDark: true };
390
+ }
391
+ return { bg: [255, 255, 255], isDark: false };
392
+ }
393
+ /**
394
+ * Probe the host page surrounding `el` and generate a complete --dt-* CSS string.
395
+ *
396
+ * @param el The calendar's root element (or any element in the host page).
397
+ * @param options Optional overrides for mode, accent, font.
398
+ * @returns A CSS inline-style string of --dt-* custom properties.
399
+ */
400
+ export function probeHostTheme(el, options = {}) {
401
+ // Start probing from the parent — `el` itself is the calendar root, which
402
+ // has its own --dt-bg fallback in CSS. We need the *host page's* context.
403
+ const host = el.parentElement ?? el;
404
+ const htmlRoot = (host.closest('body') ?? host) instanceof HTMLElement
405
+ ? (host.closest('body') ?? host)
406
+ : document.body;
407
+ // ── Background & mode ──
408
+ const { bg, isDark: autoDark } = probeBackground(host);
409
+ const isDark = options.mode === 'auto' || !options.mode ? autoDark : options.mode === 'dark';
410
+ // ── Accent color ──
411
+ let accent;
412
+ if (options.accent) {
413
+ accent = parseColor(options.accent) ?? [37, 99, 235];
414
+ }
415
+ else {
416
+ accent = probeAccent(htmlRoot) ?? (isDark ? [239, 68, 68] : [37, 99, 235]);
417
+ }
418
+ const [aH, aS, aL] = rgbToHsl(...accent);
419
+ // ── Fonts ──
420
+ // When auto-probing, use `inherit` so the calendar naturally inherits the
421
+ // host page's font via CSS inheritance. Probing getComputedStyle().fontFamily
422
+ // and re-declaring it can break the cascade (resolved names differ from the
423
+ // authored font stack). Only use an explicit value if the user overrides.
424
+ const fonts = options.font
425
+ ? { sans: options.font, mono: "ui-monospace, 'SFMono-Regular', monospace" }
426
+ : { sans: 'inherit', mono: "ui-monospace, 'SFMono-Regular', monospace" };
427
+ // ── Text colors (probe host's actual text, validate contrast) ──
428
+ const probedText = probeTextColor(host, bg);
429
+ const textBase = probedText ?? (isDark ? [226, 232, 240] : [30, 30, 46]);
430
+ // ── Generate surfaces ──
431
+ // The probed color is the host page background. The calendar should feel
432
+ // like a card sitting *on* that page, so we lift it slightly toward white
433
+ // (dark mode) or darken it a hair (light mode) for subtle depth.
434
+ const calBg = isDark
435
+ ? mix(bg, [255, 255, 255], 0.02) // barely perceptible lift
436
+ : mix(bg, [0, 0, 0], 0.005); // near-invisible darken
437
+ const stageBg = bg; // stage = the actual page bg
438
+ const surface = isDark
439
+ ? mix(calBg, [255, 255, 255], 0.04) // surface: lift from card
440
+ : mix(calBg, [0, 0, 0], 0.02); // slightly darker stripe
441
+ // ── Border colors ──
442
+ const borderAlpha = isDark ? 0.07 : 0.08;
443
+ const borderDayAlpha = isDark ? 0.14 : 0.14;
444
+ const borderRgb = isDark ? [148, 163, 184] : [0, 0, 0];
445
+ // ── Accent derivatives ──
446
+ const accentDim = isDark ? 0.15 : 0.12;
447
+ const glow = isDark ? 0.30 : 0.25;
448
+ const todayBg = isDark ? 0.03 : 0.04;
449
+ // Ensure accent is readable on the background — adjust lightness if needed
450
+ const accentL = isDark
451
+ ? Math.max(aL, 0.45) // bright enough on dark
452
+ : Math.min(aL, 0.48); // dark enough on light
453
+ const accentAdj = hslToRgb(aH, Math.max(aS, 0.5), accentL);
454
+ // btn-text: white for dark accents, dark for light accents
455
+ const accentLum = luminance(accentAdj);
456
+ const btnText = accentLum < 0.4 ? '#ffffff' : '#1a1a2e';
457
+ // ── Scrollbar ──
458
+ const scrollAlpha = isDark ? 0.12 : 0.10;
459
+ // ── Success (green, theme-adapted) ──
460
+ const successRgb = isDark ? [74, 222, 128] : [22, 163, 74];
461
+ // ── Assemble CSS string ──
462
+ const vars = [
463
+ `--dt-stage-bg: ${rgbStr(...stageBg)}`,
464
+ `--dt-bg: ${rgbStr(...calBg)}`,
465
+ `--dt-surface: ${rgbStr(...surface)}`,
466
+ `--dt-border: ${rgba(...borderRgb, borderAlpha)}`,
467
+ `--dt-border-day: ${rgba(...borderRgb, borderDayAlpha)}`,
468
+ `--dt-text: ${rgba(...textBase, isDark ? 0.87 : 0.87)}`,
469
+ `--dt-text-2: ${rgba(...textBase, isDark ? 0.55 : 0.54)}`,
470
+ `--dt-text-3: ${rgba(...textBase, isDark ? 0.38 : 0.38)}`,
471
+ `--dt-accent: ${rgbStr(...accentAdj)}`,
472
+ `--dt-accent-dim: ${rgba(...accentAdj, accentDim)}`,
473
+ `--dt-glow: ${rgba(...accentAdj, glow)}`,
474
+ `--dt-today-bg: ${rgba(...accentAdj, todayBg)}`,
475
+ `--dt-btn-text: ${btnText}`,
476
+ `--dt-scrollbar: ${rgba(...borderRgb, scrollAlpha)}`,
477
+ `--dt-success: ${rgba(...successRgb, 0.7)}`,
478
+ `--dt-sans: ${fonts.sans}`,
479
+ `--dt-mono: ${fonts.mono}`,
480
+ ];
481
+ return vars.map((v) => `\t${v}`).join(';\n') + ';';
482
+ }
483
+ /**
484
+ * Observe changes to the host page that might affect theming
485
+ * (color-scheme toggle, class changes on <html>/<body>, style attribute changes).
486
+ *
487
+ * Returns a cleanup function to stop observing.
488
+ *
489
+ * @param el The calendar's root element.
490
+ * @param callback Called with the new CSS string whenever the host theme changes.
491
+ * @param options Passthrough to probeHostTheme.
492
+ */
493
+ export function observeHostTheme(el, callback, options = {}) {
494
+ let last = '';
495
+ const update = () => {
496
+ const next = probeHostTheme(el, options);
497
+ if (next !== last) {
498
+ last = next;
499
+ callback(next);
500
+ }
501
+ };
502
+ // 1. Respond to system color-scheme changes
503
+ const hasMQL = typeof window.matchMedia === 'function';
504
+ const mql = hasMQL ? window.matchMedia('(prefers-color-scheme: dark)') : null;
505
+ const onScheme = () => update();
506
+ mql?.addEventListener('change', onScheme);
507
+ // 2. Observe class/style changes on <html> and <body> (common theme-toggle pattern)
508
+ let rafId = 0;
509
+ const scheduleUpdate = () => {
510
+ // Cancel any pending probe — multiple mutations (html + body) often fire
511
+ // in quick succession. Use double-rAF so all style changes have been
512
+ // applied and composited before we read computed styles.
513
+ cancelAnimationFrame(rafId);
514
+ rafId = requestAnimationFrame(() => {
515
+ rafId = requestAnimationFrame(update);
516
+ });
517
+ };
518
+ const observer = new MutationObserver(scheduleUpdate);
519
+ observer.observe(document.documentElement, {
520
+ attributes: true,
521
+ attributeFilter: ['class', 'style', 'data-theme', 'data-mode', 'color-scheme'],
522
+ });
523
+ observer.observe(document.body, {
524
+ attributes: true,
525
+ attributeFilter: ['class', 'style', 'data-theme', 'data-mode', 'color-scheme'],
526
+ });
527
+ // Initial probe
528
+ update();
529
+ return () => {
530
+ cancelAnimationFrame(rafId);
531
+ mql?.removeEventListener('change', onScheme);
532
+ observer.disconnect();
533
+ };
534
+ }
@@ -1,2 +1,4 @@
1
- export { midnight, neutral, presets, } from './presets.js';
1
+ export { auto, neutral, midnight, presets, } from './presets.js';
2
2
  export type { PresetName } from './presets.js';
3
+ export { probeHostTheme, observeHostTheme } from './auto.js';
4
+ export type { AutoThemeOptions } from './auto.js';
@@ -1,2 +1,4 @@
1
1
  // ─── Theme barrel export ────────────────────────────────
2
- export { midnight, neutral, presets, } from './presets.js';
2
+ export { auto, neutral, midnight, presets, } from './presets.js';
3
+ // ─── Smart auto-theme ───────────────────────────────────
4
+ export { probeHostTheme, observeHostTheme } from './auto.js';
@@ -5,20 +5,34 @@
5
5
  * Pass to the `theme` prop of any timeline component.
6
6
  *
7
7
  * Presets:
8
- * midnight — Dark mode with red accent
9
- * neutral — Light mode, inherits host fonts
8
+ * auto — Transparent: inherit --dt-* from the host page (recommended default)
9
+ * neutral — Explicit light theme: white bg, blue accent, works standalone
10
+ * midnight — Explicit dark theme: charcoal bg, red accent
10
11
  */
11
12
  /**
12
- * Neutral — inherits fonts from host page, uses standard grays + safe blue accent.
13
- * Drop-in preset that blends into any website without fighting its design.
14
- * This is the recommended default.
13
+ * Auto — triggers the smart auto-theme engine.
14
+ *
15
+ * When passed to Calendar's `theme` prop, the component will probe the host
16
+ * page at mount time (background, fonts, accent color, light/dark mode)
17
+ * and generate matching --dt-* CSS tokens automatically.
18
+ *
19
+ * Reactively watches for host theme changes (e.g. dark-mode toggle).
20
+ *
21
+ * If you want passive inheritance only (no probing), pass `autoTheme={false}`
22
+ * alongside `theme={auto}`.
23
+ */
24
+ export declare const auto = "";
25
+ /**
26
+ * Neutral — explicit light theme. White bg, blue accent, inherits host fonts.
27
+ * Use when embedding standalone without ancestor --dt-* vars.
15
28
  */
16
29
  export declare const neutral = "\n\t--dt-stage-bg: #ffffff;\n\t--dt-bg: #ffffff;\n\t--dt-surface: #f9fafb;\n\t--dt-border: rgba(0, 0, 0, 0.08);\n\t--dt-border-day: rgba(0, 0, 0, 0.14);\n\t--dt-text: rgba(0, 0, 0, 0.87);\n\t--dt-text-2: rgba(0, 0, 0, 0.54);\n\t--dt-text-3: rgba(0, 0, 0, 0.38);\n\t--dt-accent: #2563eb;\n\t--dt-accent-dim: rgba(37, 99, 235, 0.12);\n\t--dt-glow: rgba(37, 99, 235, 0.25);\n\t--dt-today-bg: rgba(37, 99, 235, 0.04);\n\t--dt-btn-text: #fff;\n\t--dt-scrollbar: rgba(0, 0, 0, 0.1);\n\t--dt-success: rgba(22, 163, 74, 0.7);\n\t--dt-serif: inherit;\n\t--dt-sans: inherit;\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
17
30
  /** Midnight Industrial — dark charcoal + red accent, tech monitoring */
18
31
  export declare const midnight = "\n\t--dt-stage-bg: #080a0f;\n\t--dt-bg: #0b0e14;\n\t--dt-surface: #10141c;\n\t--dt-border: rgba(148, 163, 184, 0.07);\n\t--dt-border-day: rgba(148, 163, 184, 0.14);\n\t--dt-text: rgba(226, 232, 240, 0.85);\n\t--dt-text-2: rgba(148, 163, 184, 0.55);\n\t--dt-text-3: rgba(100, 116, 139, 0.55);\n\t--dt-accent: #ef4444;\n\t--dt-accent-dim: rgba(239, 68, 68, 0.18);\n\t--dt-glow: rgba(239, 68, 68, 0.35);\n\t--dt-today-bg: rgba(239, 68, 68, 0.02);\n\t--dt-btn-text: #fff;\n\t--dt-scrollbar: rgba(148, 163, 184, 0.12);\n\t--dt-success: rgba(74, 222, 128, 0.7);\n\t--dt-serif: Georgia, 'Times New Roman', serif;\n";
19
32
  /** All available presets keyed by name */
20
33
  export declare const presets: {
21
- readonly midnight: "\n\t--dt-stage-bg: #080a0f;\n\t--dt-bg: #0b0e14;\n\t--dt-surface: #10141c;\n\t--dt-border: rgba(148, 163, 184, 0.07);\n\t--dt-border-day: rgba(148, 163, 184, 0.14);\n\t--dt-text: rgba(226, 232, 240, 0.85);\n\t--dt-text-2: rgba(148, 163, 184, 0.55);\n\t--dt-text-3: rgba(100, 116, 139, 0.55);\n\t--dt-accent: #ef4444;\n\t--dt-accent-dim: rgba(239, 68, 68, 0.18);\n\t--dt-glow: rgba(239, 68, 68, 0.35);\n\t--dt-today-bg: rgba(239, 68, 68, 0.02);\n\t--dt-btn-text: #fff;\n\t--dt-scrollbar: rgba(148, 163, 184, 0.12);\n\t--dt-success: rgba(74, 222, 128, 0.7);\n\t--dt-serif: Georgia, 'Times New Roman', serif;\n";
34
+ readonly auto: "";
22
35
  readonly neutral: "\n\t--dt-stage-bg: #ffffff;\n\t--dt-bg: #ffffff;\n\t--dt-surface: #f9fafb;\n\t--dt-border: rgba(0, 0, 0, 0.08);\n\t--dt-border-day: rgba(0, 0, 0, 0.14);\n\t--dt-text: rgba(0, 0, 0, 0.87);\n\t--dt-text-2: rgba(0, 0, 0, 0.54);\n\t--dt-text-3: rgba(0, 0, 0, 0.38);\n\t--dt-accent: #2563eb;\n\t--dt-accent-dim: rgba(37, 99, 235, 0.12);\n\t--dt-glow: rgba(37, 99, 235, 0.25);\n\t--dt-today-bg: rgba(37, 99, 235, 0.04);\n\t--dt-btn-text: #fff;\n\t--dt-scrollbar: rgba(0, 0, 0, 0.1);\n\t--dt-success: rgba(22, 163, 74, 0.7);\n\t--dt-serif: inherit;\n\t--dt-sans: inherit;\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
36
+ readonly midnight: "\n\t--dt-stage-bg: #080a0f;\n\t--dt-bg: #0b0e14;\n\t--dt-surface: #10141c;\n\t--dt-border: rgba(148, 163, 184, 0.07);\n\t--dt-border-day: rgba(148, 163, 184, 0.14);\n\t--dt-text: rgba(226, 232, 240, 0.85);\n\t--dt-text-2: rgba(148, 163, 184, 0.55);\n\t--dt-text-3: rgba(100, 116, 139, 0.55);\n\t--dt-accent: #ef4444;\n\t--dt-accent-dim: rgba(239, 68, 68, 0.18);\n\t--dt-glow: rgba(239, 68, 68, 0.35);\n\t--dt-today-bg: rgba(239, 68, 68, 0.02);\n\t--dt-btn-text: #fff;\n\t--dt-scrollbar: rgba(148, 163, 184, 0.12);\n\t--dt-success: rgba(74, 222, 128, 0.7);\n\t--dt-serif: Georgia, 'Times New Roman', serif;\n";
23
37
  };
24
38
  export type PresetName = keyof typeof presets;