@nomideusz/svelte-calendar 0.11.0 → 0.12.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 CHANGED
@@ -46,7 +46,7 @@ Users can also switch via the built-in Day/Week/Month pills.
46
46
  Planner views are designed for direct manipulation:
47
47
 
48
48
  - **Move** — drag an event to another day or time; a ghost previews the target before the move commits.
49
- - **Resize** — drag an event's edge handles (`day-planner`: left/right, `day-mobile`: top/bottom) to change its duration; `minDuration`/`maxDuration` clamp at commit.
49
+ - **Resize** — drag an event's top/bottom edge handles to change its duration; `minDuration`/`maxDuration` clamp at commit.
50
50
  - **Drag-to-create** — press on empty canvas and sweep to draw a new event; a plain click still creates a default-length slot. Both fire `oneventcreate` with the final range, after blocked-slot and disabled-date validation.
51
51
 
52
52
  The month grid lists events as chips per day with a "+N more" overflow; clicking a day fires `ondayclick(date)` — the natural month → day drill-down.
@@ -743,21 +743,6 @@ const now = nowInZone('Asia/Tokyo');
743
743
  formatInTimeZone(date, 'America/New_York', { hour: '2-digit', minute: '2-digit' });
744
744
  ```
745
745
 
746
- ## Text Fitting (optional)
747
-
748
- Event cards fit their labels using character-width heuristics. For pixel-precise fitting (long titles, narrow columns, custom fonts), initialize the optional [Pretext](https://www.npmjs.com/package/pretext) measurement engine once at app startup — everything falls back gracefully if you don't:
749
-
750
- ```svelte
751
- <script>
752
- import { onMount } from 'svelte';
753
- import { initTextMeasure } from '@nomideusz/svelte-calendar';
754
-
755
- onMount(() => initTextMeasure()); // resolves true if Pretext loaded
756
- </script>
757
- ```
758
-
759
- Custom views can measure text themselves via `createTextMeasure(options)` → `fitContent({ title, subtitle, maxWidth, maxHeight, … })`.
760
-
761
746
  ## Utilities
762
747
 
763
748
  Small helpers used by the built-in views, exported for custom rendering:
@@ -838,6 +823,7 @@ The widget renders inside shadow DOM: host-page CSS (resets, theme stylesheets)
838
823
  | `minDuration` | `number` | — | Minimum event duration in minutes (enforced on create & resize) |
839
824
  | `maxDuration` | `number` | — | Maximum event duration in minutes (enforced on create & resize) |
840
825
  | `compact` | `boolean` | `false` | Minimal text-row rendering in Agenda views (dot + time + title) |
826
+ | `columns` | `boolean` | `false` | Timetable layout: week-agenda days as side-by-side columns on desktop (mobile keeps the stacked list). Pairs well with `equalDays`; overrides `compact` while active |
841
827
  | `dayHeader` | `Snippet<[{ date, isToday, dayName }]>` | — | Custom day header snippet for planner/agenda views |
842
828
  | `header` | `Snippet<[HeaderContext]>` | — | Replace entire header chrome (date label + mode pills + nav) |
843
829
  | `navigation` | `Snippet<[NavigationContext]>` | — | Replace just the prev/next/today controls |
@@ -69,6 +69,7 @@ let {
69
69
  maxDuration,
70
70
  disabledDates,
71
71
  compact = false,
72
+ columns = false,
72
73
  mobile: mobileProp = "auto",
73
74
  event: eventSnippet,
74
75
  empty: emptySnippet,
@@ -294,6 +295,9 @@ setContext("calendar", {
294
295
  get compact() {
295
296
  return compact;
296
297
  },
298
+ get columns() {
299
+ return columns;
300
+ },
297
301
  get labels() {
298
302
  return mergedLabels;
299
303
  },
@@ -81,6 +81,14 @@ interface Props {
81
81
  disabledDates?: Date[];
82
82
  /** Compact mode: use minimal text-row rendering in Agenda views (dot + time + title). */
83
83
  compact?: boolean;
84
+ /**
85
+ * Timetable layout: week-agenda days render as side-by-side columns
86
+ * (classic class-schedule grid) instead of a vertical list. Desktop
87
+ * only — mobile keeps the stacked layout. Pairs well with `equalDays`
88
+ * for recurring/template schedules. Overrides `compact` while active
89
+ * (single-line rows truncate at column width).
90
+ */
91
+ columns?: boolean;
84
92
  /**
85
93
  * Mobile mode.
86
94
  * - `'auto'` (default): detect via viewport width (< 768 px)
@@ -7,5 +7,3 @@ export type { CalendarLabels } from './locale.js';
7
7
  export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
8
8
  export type { TimelineEvent, BlockedSlot, EventStatus, } from './types.js';
9
9
  export { generatePalette, extractAccent, VIVID_PALETTE } from './palette.js';
10
- export { createTextMeasure, initTextMeasure } from './measure.js';
11
- export type { TextMeasure, TextMeasureOptions, ContentFit } from './measure.js';
@@ -9,5 +9,3 @@ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, fmtTime, fmtD
9
9
  export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
10
10
  // Palette
11
11
  export { generatePalette, extractAccent, VIVID_PALETTE } from './palette.js';
12
- // Text measurement (optional Pretext integration)
13
- export { createTextMeasure, initTextMeasure } from './measure.js';
@@ -1,15 +1,16 @@
1
1
  import type { DateRange } from '../adapters/types.js';
2
2
  export type { DateRange };
3
3
  /**
4
- * Built-in view IDs. Custom view IDs are also supported — CalendarViewId
5
- * is typed as `string` so consumers can register any ID.
4
+ * Built-in view IDs. Custom view IDs are also supported — see CalendarViewId.
6
5
  */
7
- export type BuiltInViewId = 'day-planner' | 'day-agenda' | 'week-planner' | 'week-agenda' | 'month-grid';
6
+ export type BuiltInViewId = 'day-planner' | 'day-agenda' | 'day-mobile' | 'week-planner' | 'week-agenda' | 'week-mobile' | 'month-grid';
8
7
  /**
9
8
  * Any view identifier. Use built-in strings like 'day-planner' or your own
10
9
  * custom IDs like 'day-kanban', 'week-resource', etc.
10
+ * (`string & {}` keeps the type open while preserving IDE autocomplete
11
+ * for the built-in IDs.)
11
12
  */
12
- export type CalendarViewId = string;
13
+ export type CalendarViewId = BuiltInViewId | (string & {});
13
14
  export type ViewMode = 'day' | 'week' | 'month';
14
15
  export interface ViewStateOptions {
15
16
  view?: CalendarViewId;
package/dist/index.d.ts CHANGED
@@ -4,11 +4,11 @@ export { Planner, Agenda, Mobile } from './views/index.js';
4
4
  export { default as MonthGrid } from './views/month/MonthGrid.svelte';
5
5
  export type { CalendarView } from './calendar/index.js';
6
6
  export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
7
- export type { EventStore, ViewState, ViewStateOptions, CalendarViewId, BuiltInViewId, ViewMode, Selection, DragState, DragMode, DragPayload, } from './engine/index.js';
7
+ export type { EventStore, ViewState, ViewStateOptions, CalendarViewId, BuiltInViewId, ViewMode, Selection as CalendarSelection, DragState, DragMode, DragPayload, } from './engine/index.js';
8
8
  export { createMemoryAdapter, createRestAdapter, createRecurringAdapter, createMappedAdapter, createCompositeAdapter, createJmapAdapter } from './adapters/index.js';
9
- export type { CalendarAdapter, WritableCalendarAdapter, DateRange, RestAdapterOptions, RecurringEvent, RecurringAdapterOptions, FieldMapping, MappedAdapterOptions, MutationHandler, CompositeAdapterOptions, JmapClient, JmapCalendarAdapterOptions, } from './adapters/index.js';
10
- export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, createTextMeasure, initTextMeasure, } from './core/index.js';
11
- export type { Clock, TimelineEvent, BlockedSlot, DaySegment, CalendarLabels, EventStatus, TextMeasure, TextMeasureOptions, ContentFit, } from './core/index.js';
9
+ export type { CalendarAdapter, WritableCalendarAdapter, DateRange, MemoryAdapterOptions, RestAdapterOptions, RecurringEvent, RecurringAdapterOptions, FieldMapping, MappedAdapterOptions, MutationHandler, CompositeAdapterOptions, JmapClient, JmapCalendarAdapterOptions, } from './adapters/index.js';
10
+ export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
11
+ export type { Clock, TimelineEvent, BlockedSlot, DaySegment, CalendarLabels, EventStatus, } from './core/index.js';
12
12
  export { auto, neutral, midnight, presets } from './theme/index.js';
13
13
  export { probeHostTheme, observeHostTheme } from './theme/index.js';
14
14
  export { wrapAdapterWithTimezone } from './core/timezone.js';
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ export { createEventStore, createViewState, createSelection, createDragState, }
10
10
  // ─── Adapters ───────────────────────────────────────────
11
11
  export { createMemoryAdapter, createRestAdapter, createRecurringAdapter, createMappedAdapter, createCompositeAdapter, createJmapAdapter } from './adapters/index.js';
12
12
  // ─── Core: clock, time, locale, types ───────────────────
13
- export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, createTextMeasure, initTextMeasure, } from './core/index.js';
13
+ export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
14
14
  // ─── Themes ─────────────────────────────────────────────
15
15
  export { auto, neutral, midnight, presets } from './theme/index.js';
16
16
  export { probeHostTheme, observeHostTheme } from './theme/index.js';
@@ -302,28 +302,59 @@ function probeAccent(root) {
302
302
  return null;
303
303
  }
304
304
  // ── Font detection ──────────────────────────────────────
305
- function probeFonts(el) {
306
- let bodyFont = 'system-ui, sans-serif';
305
+ /** Common CSS variable names for the host's monospace font stack. */
306
+ const MONO_VAR_CANDIDATES = [
307
+ '--font-mono', // Tailwind v4 theme tokens, common convention
308
+ '--font-family-mono',
309
+ '--font-monospace',
310
+ '--mono-font',
311
+ '--code-font',
312
+ ];
313
+ const MONO_FALLBACK = "ui-monospace, 'SFMono-Regular', monospace";
314
+ /**
315
+ * Adopt the host page's fonts.
316
+ *
317
+ * Sans: the host element's *computed* font-family — the resolved authored
318
+ * stack, so webfont names come through verbatim. (Declaring `--dt-sans:
319
+ * inherit` does NOT work: a custom property with no ancestor value computes
320
+ * to guaranteed-invalid, so `var(--dt-sans, fallback)` used the fallback and
321
+ * the host font never applied.)
322
+ *
323
+ * Mono: common CSS variables on :root, then any code-ish element's computed
324
+ * font, then a generic stack.
325
+ */
326
+ function probeFonts(host) {
327
+ let sans = 'system-ui, sans-serif';
307
328
  try {
308
- const cs = getComputedStyle(el);
309
- if (cs.fontFamily)
310
- bodyFont = cs.fontFamily;
329
+ const f = getComputedStyle(host).fontFamily;
330
+ if (f)
331
+ sans = f;
311
332
  }
312
333
  catch {
313
334
  // getComputedStyle may fail in test environments
314
335
  }
315
- // For monospace, check if the page defines one
316
- const pre = el.querySelector('pre, code, .mono, [class*="mono"]');
317
- let mono = "ui-monospace, 'SFMono-Regular', monospace";
318
- if (pre) {
319
- try {
320
- const pf = getComputedStyle(pre).fontFamily;
321
- if (pf)
322
- mono = pf;
336
+ let mono = '';
337
+ try {
338
+ const rootCs = getComputedStyle(document.documentElement);
339
+ for (const name of MONO_VAR_CANDIDATES) {
340
+ const val = rootCs.getPropertyValue(name).trim();
341
+ if (val) {
342
+ mono = val;
343
+ break;
344
+ }
323
345
  }
324
- catch { /* ignore */ }
325
346
  }
326
- return { sans: bodyFont, mono };
347
+ catch { /* ignore */ }
348
+ if (!mono) {
349
+ const code = document.querySelector('pre, code, kbd, samp');
350
+ if (code) {
351
+ try {
352
+ mono = getComputedStyle(code).fontFamily || '';
353
+ }
354
+ catch { /* ignore */ }
355
+ }
356
+ }
357
+ return { sans, mono: mono || MONO_FALLBACK };
327
358
  }
328
359
  // ── Background walking ──────────────────────────────────
329
360
  /**
@@ -436,13 +467,9 @@ export function probeHostTheme(el, options = {}) {
436
467
  }
437
468
  const [aH, aS, aL] = rgbToHsl(...accent);
438
469
  // ── Fonts ──
439
- // When auto-probing, use `inherit` so the calendar naturally inherits the
440
- // host page's font via CSS inheritance. Probing getComputedStyle().fontFamily
441
- // and re-declaring it can break the cascade (resolved names differ from the
442
- // authored font stack). Only use an explicit value if the user overrides.
443
470
  const fonts = options.font
444
- ? { sans: options.font, mono: "ui-monospace, 'SFMono-Regular', monospace" }
445
- : { sans: 'inherit', mono: "ui-monospace, 'SFMono-Regular', monospace" };
471
+ ? { sans: options.font, mono: MONO_FALLBACK }
472
+ : probeFonts(host);
446
473
  // ── Text colors (probe host's actual text, validate contrast) ──
447
474
  const probedText = probeTextColor(host, bg);
448
475
  const textBase = probedText ?? (isDark ? [226, 232, 240] : [30, 30, 46]);
@@ -545,11 +572,19 @@ export function observeHostTheme(el, callback, options = {}) {
545
572
  attributes: true,
546
573
  attributeFilter: ['class', 'style', 'data-theme', 'data-mode', 'color-scheme'],
547
574
  });
575
+ // 3. Re-probe once the document settles. A probe during initial load can
576
+ // read pre-stylesheet values (wrong background → wrong light/dark call);
577
+ // late webfonts change computed font stacks. Both are one-shot events.
578
+ if (document.readyState !== 'complete') {
579
+ window.addEventListener('load', scheduleUpdate, { once: true });
580
+ }
581
+ document.fonts?.ready?.then(scheduleUpdate).catch(() => { });
548
582
  // Initial probe
549
583
  update();
550
584
  return () => {
551
585
  cancelAnimationFrame(rafId);
552
586
  mql?.removeEventListener('change', onScheme);
587
+ window.removeEventListener('load', scheduleUpdate);
553
588
  observer.disconnect();
554
589
  };
555
590
  }
@@ -23,16 +23,17 @@
23
23
  */
24
24
  export declare const auto = "";
25
25
  /**
26
- * Neutral — explicit light theme. White bg, blue accent, inherits host fonts.
26
+ * Neutral — explicit light theme. White bg, blue accent, system font stack
27
+ * (set --dt-sans on an ancestor or via CSS to use a custom font).
27
28
  * Use when embedding standalone without ancestor --dt-* vars.
28
29
  */
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: var(--asini-accent, #2563eb);\n\t--dt-accent-dim: color-mix(in srgb, var(--dt-accent) 12%, transparent);\n\t--dt-glow: color-mix(in srgb, var(--dt-accent) 25%, transparent);\n\t--dt-today-bg: color-mix(in srgb, var(--dt-accent) 7%, transparent);\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-weekend-bg: rgba(0, 0, 0, 0.02);\n\t--dt-hover: rgba(0, 0, 0, 0.04);\n\t--dt-sans: inherit;\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
30
+ 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: var(--asini-accent, #2563eb);\n\t--dt-accent-dim: color-mix(in srgb, var(--dt-accent) 12%, transparent);\n\t--dt-glow: color-mix(in srgb, var(--dt-accent) 25%, transparent);\n\t--dt-today-bg: color-mix(in srgb, var(--dt-accent) 7%, transparent);\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-weekend-bg: rgba(0, 0, 0, 0.02);\n\t--dt-hover: rgba(0, 0, 0, 0.04);\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
30
31
  /** Midnight Industrial — dark charcoal + red accent, tech monitoring */
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.07);\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-weekend-bg: rgba(148, 163, 184, 0.03);\n\t--dt-hover: rgba(148, 163, 184, 0.06);\n\t--dt-sans: inherit;\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
32
+ 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.07);\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-weekend-bg: rgba(148, 163, 184, 0.03);\n\t--dt-hover: rgba(148, 163, 184, 0.06);\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
32
33
  /** All available presets keyed by name */
33
34
  export declare const presets: {
34
35
  readonly auto: "";
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: var(--asini-accent, #2563eb);\n\t--dt-accent-dim: color-mix(in srgb, var(--dt-accent) 12%, transparent);\n\t--dt-glow: color-mix(in srgb, var(--dt-accent) 25%, transparent);\n\t--dt-today-bg: color-mix(in srgb, var(--dt-accent) 7%, transparent);\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-weekend-bg: rgba(0, 0, 0, 0.02);\n\t--dt-hover: rgba(0, 0, 0, 0.04);\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.07);\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-weekend-bg: rgba(148, 163, 184, 0.03);\n\t--dt-hover: rgba(148, 163, 184, 0.06);\n\t--dt-sans: inherit;\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
36
+ 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: var(--asini-accent, #2563eb);\n\t--dt-accent-dim: color-mix(in srgb, var(--dt-accent) 12%, transparent);\n\t--dt-glow: color-mix(in srgb, var(--dt-accent) 25%, transparent);\n\t--dt-today-bg: color-mix(in srgb, var(--dt-accent) 7%, transparent);\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-weekend-bg: rgba(0, 0, 0, 0.02);\n\t--dt-hover: rgba(0, 0, 0, 0.04);\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
37
+ 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.07);\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-weekend-bg: rgba(148, 163, 184, 0.03);\n\t--dt-hover: rgba(148, 163, 184, 0.06);\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
37
38
  };
38
39
  export type PresetName = keyof typeof presets;
@@ -23,7 +23,8 @@
23
23
  */
24
24
  export const auto = ``;
25
25
  /**
26
- * Neutral — explicit light theme. White bg, blue accent, inherits host fonts.
26
+ * Neutral — explicit light theme. White bg, blue accent, system font stack
27
+ * (set --dt-sans on an ancestor or via CSS to use a custom font).
27
28
  * Use when embedding standalone without ancestor --dt-* vars.
28
29
  */
29
30
  export const neutral = `
@@ -44,7 +45,6 @@ export const neutral = `
44
45
  --dt-success: rgba(22, 163, 74, 0.7);
45
46
  --dt-weekend-bg: rgba(0, 0, 0, 0.02);
46
47
  --dt-hover: rgba(0, 0, 0, 0.04);
47
- --dt-sans: inherit;
48
48
  --dt-mono: ui-monospace, 'SFMono-Regular', monospace;
49
49
  `;
50
50
  /** Midnight Industrial — dark charcoal + red accent, tech monitoring */
@@ -66,7 +66,6 @@ export const midnight = `
66
66
  --dt-success: rgba(74, 222, 128, 0.7);
67
67
  --dt-weekend-bg: rgba(148, 163, 184, 0.03);
68
68
  --dt-hover: rgba(148, 163, 184, 0.06);
69
- --dt-sans: inherit;
70
69
  --dt-mono: ui-monospace, 'SFMono-Regular', monospace;
71
70
  `;
72
71
  /** All available presets keyed by name */
@@ -19,6 +19,8 @@ let {
19
19
  const clock = createClock(ctx.timezone);
20
20
  const viewState = $derived(ctx.viewState);
21
21
  const equalDays = $derived(ctx.equalDays);
22
+ const showDates = $derived(ctx.showDates);
23
+ const hideDayHead = $derived(showDates && !!viewState);
22
24
  const isMobile = $derived(ctx.isMobile);
23
25
  const autoHeight = $derived(ctx.autoHeight);
24
26
  const compact = $derived(ctx.compact);
@@ -106,7 +108,8 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
106
108
  onpointercancel={onPointerCancel}
107
109
  >
108
110
  <div class="ag-body" role="group" aria-label={L.todaysLineup}>
109
- <!-- ─── In-view date header (swipe nav is otherwise unlabelled) ─── -->
111
+ <!-- ─── In-view date header (only when the chrome doesn't label the day) ─── -->
112
+ {#if !hideDayHead}
110
113
  <div class="ag-day-head">
111
114
  {#if !equalDays && isToday}
112
115
  <span class="ag-day-head-badge">{L.today}</span>
@@ -116,6 +119,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
116
119
  <span class="ag-day-head-name">{weekdayLong(dayMs, locale)}</span>
117
120
  <span class="ag-day-head-date">{monthLong(dayMs, locale)} {dayNum(dayMs)}</span>
118
121
  </div>
122
+ {/if}
119
123
  {#if allDayBanner.length > 0}
120
124
  <!-- ─── All-day / multi-day events ─── -->
121
125
  <div class="ag-allday">
@@ -160,7 +164,6 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
160
164
  onpointerenter={() => oneventhover?.(ev)}
161
165
  >
162
166
  <EventContent event={ev}>
163
- <span class="ag-compact-row-dot"></span>
164
167
  <span class="ag-compact-row-time">{fmt(ev.start)}</span>
165
168
  <div class="ag-compact-row-main">
166
169
  <span class="ag-compact-row-title">{ev.title}</span>
@@ -256,7 +259,6 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
256
259
  onpointerenter={() => oneventhover?.(ev)}
257
260
  >
258
261
  <EventContent event={ev}>
259
- <span class="ag-compact-row-dot"></span>
260
262
  <span class="ag-compact-row-time">{fmt(ev.start)}</span>
261
263
  <div class="ag-compact-row-main">
262
264
  <span class="ag-compact-row-title">{ev.title}</span>
@@ -1071,11 +1073,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1071
1073
  .ag-log-row--selected {
1072
1074
  background: color-mix(in srgb, var(--ev-color) 6%, transparent);
1073
1075
  border-radius: 6px;
1074
- padding-left: 8px;
1075
- padding-right: 8px;
1076
- margin-left: -8px;
1077
- margin-right: -8px;
1078
- width: calc(100% + 16px);
1076
+ box-shadow: 0 0 0 8px color-mix(in srgb, var(--ev-color) 6%, transparent);
1079
1077
  }
1080
1078
  .ag-log-check {
1081
1079
  font-size: 10px;
@@ -1137,13 +1135,10 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1137
1135
  .ag-compact-row--selected {
1138
1136
  background: color-mix(in srgb, var(--ev-color) 10%, transparent);
1139
1137
  border-radius: 4px;
1140
- /* Highlight gutter comes from negative margins so the row's content
1141
- stays aligned with its unselected siblings (no tap-shift). */
1142
- padding-left: 6px;
1143
- padding-right: 6px;
1144
- margin-left: -6px;
1145
- margin-right: -6px;
1146
- width: calc(100% + 12px);
1138
+ /* The highlight bleeds into the gutter via a spread shadow — zero
1139
+ layout impact, so nothing shifts or clips even when the host
1140
+ reduces the gutters below the bleed width. */
1141
+ box-shadow: 0 0 0 6px color-mix(in srgb, var(--ev-color) 10%, transparent);
1147
1142
  }
1148
1143
  .ag-compact-row:hover .ag-compact-row-title,
1149
1144
  .ag-compact-row:active .ag-compact-row-title { color: var(--dt-text); }
@@ -1156,18 +1151,14 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1156
1151
  box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
1157
1152
  border-radius: 4px;
1158
1153
  }
1159
- .ag-compact-row-dot {
1160
- width: 5px;
1161
- height: 5px;
1162
- border-radius: 50%;
1163
- background: var(--ev-color, var(--dt-accent));
1164
- flex-shrink: 0;
1165
- align-self: center;
1166
- }
1154
+ /* The time label doubles as the class-color signal (replaces the old
1155
+ dot): the event color mixed toward the text color, so it stays
1156
+ legible on any palette and costs zero horizontal space. */
1167
1157
  .ag-compact-row-time {
1168
1158
  font-size: 11px;
1169
1159
  font-family: var(--dt-mono, monospace);
1170
- color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
1160
+ font-weight: 500;
1161
+ color: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 60%, var(--dt-text, rgba(0, 0, 0, 0.87)));
1171
1162
  min-width: 64px;
1172
1163
  flex-shrink: 0;
1173
1164
  line-height: 1.4;
@@ -1195,12 +1186,6 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1195
1186
  .ag--mobile .ag-compact-row-sub {
1196
1187
  max-width: 100%;
1197
1188
  }
1198
- /* Wrapped rows are two lines tall — center-aligning the dot floats it
1199
- between lines; pin it optically to the first (title) line instead. */
1200
- .ag--mobile .ag-compact-row-dot {
1201
- align-self: flex-start;
1202
- margin-top: 8px;
1203
- }
1204
1189
  .ag-compact-row-title {
1205
1190
  font-size: 12px;
1206
1191
  font-weight: 500;
@@ -24,6 +24,7 @@ const hideDays = $derived(ctx.hideDays);
24
24
  const isMobile = $derived(ctx.isMobile);
25
25
  const autoHeight = $derived(ctx.autoHeight);
26
26
  const compact = $derived(ctx.compact);
27
+ const cols = $derived(ctx.columns && !isMobile);
27
28
  const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
28
29
  const oneventhover = $derived(ctx.oneventhover);
29
30
  const disabledSet = $derived(ctx.disabledSet);
@@ -199,7 +200,6 @@ const weekDays = $derived.by(() => {
199
200
  onpointerenter={() => oneventhover?.(ev)}
200
201
  >
201
202
  <EventContent event={ev}>
202
- <span class="ag-compact-dot"></span>
203
203
  <span class="ag-compact-time">{fmt(ev.start)}</span>
204
204
  <div class="ag-compact-main">
205
205
  <span class="ag-compact-title">{ev.title}</span>
@@ -240,13 +240,14 @@ const weekDays = $derived.by(() => {
240
240
  class="ag ag--week"
241
241
  class:ag--mobile={isMobile}
242
242
  class:ag--auto={autoHeight}
243
+ class:ag--cols={cols}
243
244
  style={style || undefined}
244
245
  style:height={height ? `${height}px` : undefined}
245
246
  onpointerdown={onPointerDown}
246
247
  onpointerup={onPointerUp}
247
248
  onpointercancel={onPointerCancel}
248
249
  >
249
- <div class="ag-body" role="list" aria-label={L.weekAhead}>
250
+ <div class="ag-body" role="list" aria-label={L.weekAhead} style:--ag-cols={weekDays.length}>
250
251
  {#each weekDays as day (day.ms)}
251
252
  {@const expanded = day.tier === 'today' || day.tier === 'tomorrow'}
252
253
  {#if day.tier === 'past'}
@@ -329,15 +330,19 @@ const weekDays = $derived.by(() => {
329
330
 
330
331
  {#if day.events.length === 0}
331
332
  <div class="ag-wday-empty">{L.noEvents}</div>
332
- {:else if compact}
333
- <!-- Compact: minimal dot + time + title rows for all days -->
333
+ {:else if compact && !cols}
334
+ <!-- Compact: minimal dot + time + title rows for all days.
335
+ Ignored in columns mode — single-line rows truncate to
336
+ nothing at column width; cards wrap instead. -->
334
337
  <div class="ag-wday-compact">
335
338
  {#each day.timedEvents as ev (ev.id)}
336
339
  {@render compactRow(ev, false, false)}
337
340
  {/each}
338
341
  </div>
339
- {:else if equalDays}
340
- <!-- Equal days: card layout for all days, no time-relative badges -->
342
+ {:else if equalDays || (cols && !expanded)}
343
+ <!-- Equal days (or timetable columns): card layout, no time-relative
344
+ badges. In columns, today/tomorrow still fall through to the
345
+ expanded branch below for now/ETA treatment. -->
341
346
  <div class="ag-wday-expanded">
342
347
  {#each groupIntoSlots(day.timedEvents) as slot (slot.startMs)}
343
348
  <div class="ag-wslot">
@@ -814,8 +819,9 @@ const weekDays = $derived.by(() => {
814
819
  text-decoration: line-through;
815
820
  text-decoration-color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
816
821
  }
817
- .ag-compact--done .ag-compact-dot {
818
- opacity: 0.5;
822
+ .ag-compact--done .ag-compact-time {
823
+ color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
824
+ font-weight: 400;
819
825
  }
820
826
 
821
827
  /* Compact day events */
@@ -834,13 +840,10 @@ const weekDays = $derived.by(() => {
834
840
  .ag-compact--selected {
835
841
  background: color-mix(in srgb, var(--ev-color) 10%, transparent);
836
842
  border-radius: 4px;
837
- /* Highlight gutter comes from negative margins so the row's content
838
- stays aligned with its unselected siblings (no tap-shift). */
839
- padding-left: 6px;
840
- padding-right: 6px;
841
- margin-left: -6px;
842
- margin-right: -6px;
843
- width: calc(100% + 12px);
843
+ /* The highlight bleeds into the gutter via a spread shadow — zero
844
+ layout impact, so nothing shifts or clips even when the host
845
+ reduces the gutters below the bleed width. */
846
+ box-shadow: 0 0 0 6px color-mix(in srgb, var(--ev-color) 10%, transparent);
844
847
  }
845
848
  .ag-compact:hover .ag-compact-title,
846
849
  .ag-compact:active .ag-compact-title {
@@ -855,18 +858,14 @@ const weekDays = $derived.by(() => {
855
858
  box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
856
859
  border-radius: 4px;
857
860
  }
858
- .ag-compact-dot {
859
- width: 5px;
860
- height: 5px;
861
- border-radius: 50%;
862
- background: var(--ev-color, var(--dt-accent));
863
- flex-shrink: 0;
864
- align-self: center;
865
- }
861
+ /* The time label doubles as the class-color signal (replaces the old
862
+ dot): the event color mixed toward the text color, so it stays
863
+ legible on any palette and costs zero horizontal space. */
866
864
  .ag-compact-time {
867
865
  font-size: 11px;
868
866
  font-family: var(--dt-mono, monospace);
869
- color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
867
+ font-weight: 500;
868
+ color: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 60%, var(--dt-text, rgba(0, 0, 0, 0.87)));
870
869
  min-width: 40px;
871
870
  flex-shrink: 0;
872
871
  white-space: nowrap;
@@ -897,12 +896,6 @@ const weekDays = $derived.by(() => {
897
896
  .ag--mobile .ag-compact-sub {
898
897
  max-width: 100%;
899
898
  }
900
- /* Wrapped rows are two/three lines tall — center-aligning the dot floats
901
- it between lines; pin it optically to the first (title) line instead. */
902
- .ag--mobile .ag-compact-dot {
903
- align-self: flex-start;
904
- margin-top: 8px;
905
- }
906
899
  .ag-compact-title {
907
900
  font-size: 12px;
908
901
  font-weight: 500;
@@ -991,6 +984,51 @@ const weekDays = $derived.by(() => {
991
984
  border-radius: 4px;
992
985
  }
993
986
 
987
+ /* ═══ Timetable columns (desktop) ═══ */
988
+ .ag--cols .ag-body {
989
+ display: grid;
990
+ grid-template-columns: repeat(var(--ag-cols, 7), minmax(0, 1fr));
991
+ }
992
+ /* Columns stretch to the tallest day, so the separator runs full height */
993
+ .ag--cols .ag-wday {
994
+ border-bottom: none;
995
+ border-inline-start: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
996
+ min-width: 0;
997
+ }
998
+ .ag--cols .ag-wday:first-child {
999
+ border-inline-start: none;
1000
+ }
1001
+ /* Uniform head padding — the :first-child top bump would misalign columns.
1002
+ Also overrides the past-day head variant (extra class = higher specificity). */
1003
+ .ag--cols .ag-wday .ag-wday-head {
1004
+ padding: 12px 10px 8px;
1005
+ }
1006
+ /* Badge + name + date won't fit one line in a ~160px column */
1007
+ .ag--cols .ag-wday-head-left {
1008
+ flex-wrap: wrap;
1009
+ row-gap: 2px;
1010
+ }
1011
+ .ag--cols .ag-wday-expanded,
1012
+ .ag--cols .ag-wday-compact,
1013
+ .ag--cols .ag-wday-empty,
1014
+ .ag--cols .ag-wday-past-line--summary {
1015
+ padding-left: 10px;
1016
+ padding-right: 10px;
1017
+ }
1018
+ .ag--cols .ag-allday {
1019
+ padding-left: 10px;
1020
+ padding-right: 10px;
1021
+ }
1022
+ /* Narrow cards: long titles get a third line, meta wraps instead of clipping */
1023
+ .ag--cols .ag-card-title {
1024
+ -webkit-line-clamp: 3;
1025
+ line-clamp: 3;
1026
+ }
1027
+ .ag--cols .ag-card-meta {
1028
+ flex-wrap: wrap;
1029
+ row-gap: 3px;
1030
+ }
1031
+
994
1032
  /* ═══ Mobile adaptations ═══ */
995
1033
  .ag--mobile .ag-wday-head {
996
1034
  padding: 12px 16px;
@@ -1,10 +1,6 @@
1
- <script lang="ts">import PlannerDay from "./PlannerDay.svelte";
2
- import PlannerWeek from "./PlannerWeek.svelte";
1
+ <script lang="ts">import PlannerWeek from "./PlannerWeek.svelte";
3
2
  let { mode = "week", ...rest } = $props();
4
3
  </script>
5
4
 
6
- {#if mode === 'day'}
7
- <PlannerDay {...rest} />
8
- {:else}
9
- <PlannerWeek {...rest} />
10
- {/if}
5
+ <!-- Both planner modes are the same vertical time grid; day is one column. -->
6
+ <PlannerWeek {mode} {...rest} />