@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.
- package/README.md +408 -178
- package/dist/adapters/memory.d.ts +7 -9
- package/dist/adapters/memory.js +8 -23
- package/dist/adapters/recurring.d.ts +8 -6
- package/dist/adapters/recurring.js +28 -30
- package/dist/calendar/Calendar.svelte +776 -356
- package/dist/calendar/Calendar.svelte.d.ts +57 -6
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/core/locale.js +2 -2
- package/dist/core/palette.d.ts +5 -0
- package/dist/core/palette.js +8 -0
- package/dist/core/types.d.ts +14 -0
- package/dist/engine/event-store.svelte.d.ts +0 -17
- package/dist/engine/event-store.svelte.js +30 -17
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/view-state.svelte.d.ts +10 -5
- package/dist/engine/view-state.svelte.js +32 -16
- package/dist/index.d.ts +7 -7
- package/dist/index.js +3 -4
- package/dist/primitives/DayHeader.svelte +103 -103
- package/dist/primitives/EmptySlot.svelte +105 -105
- package/dist/primitives/EventBlock.svelte +312 -312
- package/dist/primitives/NowIndicator.svelte +178 -178
- package/dist/primitives/TimeGutter.svelte +104 -104
- package/dist/theme/auto.d.ts +53 -0
- package/dist/theme/auto.js +534 -0
- package/dist/theme/index.d.ts +3 -1
- package/dist/theme/index.js +3 -1
- package/dist/theme/presets.d.ts +20 -6
- package/dist/theme/presets.js +55 -42
- package/dist/views/agenda/AgendaDay.svelte +1073 -976
- package/dist/views/agenda/AgendaWeek.svelte +934 -775
- package/dist/views/agenda/index.d.ts +0 -2
- package/dist/views/agenda/index.js +0 -2
- package/dist/views/index.d.ts +3 -2
- package/dist/views/index.js +3 -2
- package/dist/views/mobile/Mobile.svelte +17 -0
- package/dist/views/mobile/Mobile.svelte.d.ts +7 -0
- package/dist/views/mobile/MobileDay.svelte +702 -0
- package/dist/views/mobile/MobileDay.svelte.d.ts +20 -0
- package/dist/views/mobile/MobileWeek.svelte +461 -0
- package/dist/views/mobile/MobileWeek.svelte.d.ts +20 -0
- package/dist/views/mobile/index.d.ts +1 -0
- package/dist/views/mobile/index.js +1 -0
- package/dist/views/planner/PlannerDay.svelte +1129 -957
- package/dist/views/planner/PlannerWeek.svelte +949 -840
- package/dist/widget/CalendarWidget.svelte +142 -157
- 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
|
+
}
|
package/dist/theme/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export {
|
|
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';
|
package/dist/theme/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
// ─── Theme barrel export ────────────────────────────────
|
|
2
|
-
export {
|
|
2
|
+
export { auto, neutral, midnight, presets, } from './presets.js';
|
|
3
|
+
// ─── Smart auto-theme ───────────────────────────────────
|
|
4
|
+
export { probeHostTheme, observeHostTheme } from './auto.js';
|
package/dist/theme/presets.d.ts
CHANGED
|
@@ -5,20 +5,34 @@
|
|
|
5
5
|
* Pass to the `theme` prop of any timeline component.
|
|
6
6
|
*
|
|
7
7
|
* Presets:
|
|
8
|
-
*
|
|
9
|
-
* neutral —
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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;
|