@nomideusz/svelte-calendar 0.1.1 → 0.2.1

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
@@ -90,26 +90,46 @@ The `Settings` component provides a theme picker and dynamic fields for controll
90
90
 
91
91
  ## Themes
92
92
 
93
- Three built-in presets each view reads from the same `--dt-*` CSS custom property contract:
93
+ Five built-in presetseach view reads from the same `--dt-*` CSS custom property contract:
94
94
 
95
95
  | Preset | Tone |
96
96
  |--------|------|
97
- | `midnight` | Dark (deep navy/slate) |
98
- | `parchment` | Warm light (cream, amber accents) |
97
+ | `midnight` | Dark (deep navy/slate, red accent) |
98
+ | `parchment` | Warm light (cream, burnt sienna accents) |
99
99
  | `indigo` | Cool light (white surface, indigo accents) |
100
+ | `neutral` | **Site-friendly** — white/gray, blue accent, `inherit` fonts so it matches your site |
101
+ | `bare` | **Unstyled skeleton** — all `transparent`/`inherit`/`currentColor`, absorbs host styles entirely |
100
102
 
101
103
  ```svelte
102
104
  <script>
103
- import { midnight, parchment, indigo, presets } from '@nomideusz/svelte-calendar';
105
+ import { midnight, parchment, indigo, neutral, bare, presets } from '@nomideusz/svelte-calendar';
104
106
  </script>
105
107
 
106
- <!-- Apply directly -->
107
- <WeekGrid style={midnight} events={events} />
108
+ <!-- Blends into your site with no extra work -->
109
+ <Calendar {views} {adapter} theme={neutral} />
108
110
 
109
111
  <!-- Or use the presets map -->
110
112
  <WeekGrid style={presets['parchment']} events={events} />
111
113
  ```
112
114
 
115
+ ### Matching Your Site
116
+
117
+ The `neutral` preset inherits font families from your page (`--dt-sans: inherit; --dt-serif: inherit`) and uses a standard blue accent. For most sites this is all you need.
118
+
119
+ If you need full control, start from `bare` — it sets everything to `transparent`/`inherit`/`currentColor` — then override only the tokens you care about:
120
+
121
+ ```ts
122
+ import { bare } from '@nomideusz/svelte-calendar';
123
+
124
+ const myTheme = `
125
+ ${bare}
126
+ --dt-accent: #e11d48;
127
+ --dt-bg: var(--my-app-surface);
128
+ --dt-text: var(--my-app-text);
129
+ --dt-border: var(--my-app-border);
130
+ `;
131
+ ```
132
+
113
133
  ### Custom Themes
114
134
 
115
135
  Pass any string of `--dt-*` custom property declarations:
@@ -125,6 +145,36 @@ const custom = `
125
145
  `;
126
146
  ```
127
147
 
148
+ <details>
149
+ <summary><strong>Full token reference</strong></summary>
150
+
151
+ | Token | Purpose |
152
+ |-------|----------|
153
+ | `--dt-bg` | Main background |
154
+ | `--dt-surface` | Elevated surface (cards, popovers) |
155
+ | `--dt-border` | Default border |
156
+ | `--dt-border-day` | Day-column dividers |
157
+ | `--dt-text` | Primary text |
158
+ | `--dt-text-2` | Secondary text |
159
+ | `--dt-text-3` | Tertiary / muted text |
160
+ | `--dt-accent` | Accent color (buttons, now-indicator, highlights) |
161
+ | `--dt-accent-dim` | Accent at ~12% opacity |
162
+ | `--dt-glow` | Accent glow / focus ring |
163
+ | `--dt-today-bg` | Today column highlight |
164
+ | `--dt-btn-text` | Button label color |
165
+ | `--dt-scrollbar` | Scrollbar thumb |
166
+ | `--dt-success` | Success / completed indicator |
167
+ | `--dt-serif` | Serif font stack |
168
+ | `--dt-sans` | Sans-serif font stack |
169
+ | `--dt-mono` | Monospace font stack |
170
+ | `--dt-hm-empty` | Heatmap: empty cell |
171
+ | `--dt-hm-low` | Heatmap: low density |
172
+ | `--dt-hm-mid` | Heatmap: medium density |
173
+ | `--dt-hm-high` | Heatmap: high density |
174
+ | `--dt-hm-max` | Heatmap: maximum density |
175
+
176
+ </details>
177
+
128
178
  ## Architecture
129
179
 
130
180
  ```
@@ -59,6 +59,10 @@
59
59
  showToolbar?: boolean;
60
60
  /** Links to display in toolbar */
61
61
  links?: { href: string; label: string }[];
62
+ /** Text direction: 'ltr' (default), 'rtl', or 'auto' */
63
+ dir?: 'ltr' | 'rtl' | 'auto';
64
+ /** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
65
+ locale?: string;
62
66
 
63
67
  // ── Callbacks ──
64
68
  oneventclick?: (event: TimelineEvent) => void;
@@ -75,11 +79,20 @@
75
79
  height = 600,
76
80
  showToolbar = true,
77
81
  links = [],
82
+ dir,
83
+ locale,
78
84
  oneventclick,
79
85
  oneventcreate,
80
86
  oneventmove,
81
87
  }: Props = $props();
82
88
 
89
+ import { setDefaultLocale } from '../core/locale.js';
90
+
91
+ // ── Set locale when provided ──
92
+ $effect(() => {
93
+ if (locale) setDefaultLocale(locale);
94
+ });
95
+
83
96
  // ── Create reactive state ──
84
97
  const store: EventStore = $derived(createEventStore(adapter));
85
98
  const viewState: ViewState = createViewState(untrack(() => ({
@@ -135,7 +148,14 @@
135
148
  );
136
149
  </script>
137
150
 
138
- <div class="cal" style="{theme}; --cal-h: {height}px">
151
+ <div
152
+ class="cal"
153
+ style="{theme}; --cal-h: {height}px"
154
+ role="region"
155
+ aria-label="Calendar"
156
+ dir={dir}
157
+ lang={locale}
158
+ >
139
159
  {#if showToolbar}
140
160
  <Toolbar {viewState} views={toolbarViews} {links} />
141
161
  {/if}
@@ -33,6 +33,10 @@ interface Props {
33
33
  href: string;
34
34
  label: string;
35
35
  }[];
36
+ /** Text direction: 'ltr' (default), 'rtl', or 'auto' */
37
+ dir?: 'ltr' | 'rtl' | 'auto';
38
+ /** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
39
+ locale?: string;
36
40
  oneventclick?: (event: TimelineEvent) => void;
37
41
  oneventcreate?: (range: {
38
42
  start: Date;
@@ -47,7 +47,7 @@
47
47
  );
48
48
  </script>
49
49
 
50
- <nav class="tb">
50
+ <nav class="tb" aria-label="Calendar navigation">
51
51
  <!-- Nav: ← Today → -->
52
52
  <div class="tb-nav">
53
53
  <button class="tb-btn" onclick={() => viewState.prev()} aria-label="Previous">
@@ -1,6 +1,7 @@
1
1
  export { createClock } from './clock.svelte.js';
2
2
  export type { Clock } from './clock.svelte.js';
3
3
  export { DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, } from './time.js';
4
- export { setDefaultLocale, getDefaultLocale, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, } from './locale.js';
4
+ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, } from './locale.js';
5
+ export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
5
6
  export type { TimelineEvent, WeekTimelineProps, DayTimelineProps, } from './types.js';
6
7
  export { timeToX } from './types.js';
@@ -4,5 +4,7 @@ export { createClock } from './clock.svelte.js';
4
4
  // Time constants & math
5
5
  export { DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, } from './time.js';
6
6
  // Locale-aware formatting
7
- export { setDefaultLocale, getDefaultLocale, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, } from './locale.js';
7
+ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, } from './locale.js';
8
+ // Timezone utilities
9
+ export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
8
10
  export { timeToX } from './types.js';
@@ -11,8 +11,9 @@
11
11
  export declare function setDefaultLocale(tag: string): void;
12
12
  /** Get the current default locale */
13
13
  export declare function getDefaultLocale(): string;
14
- /** Format hour index (0-23) as compact 12h label: 12a, 1a … 12p, 1p … */
15
- export declare function fmtH(h: number): string;
14
+ export declare function is24HourLocale(locale?: string): boolean;
15
+ /** Format hour index (0-23) as compact label: 12h ("12a", "1p") or 24h ("0", "13") */
16
+ export declare function fmtH(h: number, locale?: string): string;
16
17
  /** Short weekday name for a timestamp: "Mon", "Tue", etc. */
17
18
  export declare function weekdayShort(ms: number, locale?: string): string;
18
19
  /** Long weekday name for a timestamp: "Monday", "Tuesday", etc. */
@@ -17,8 +17,25 @@ export function setDefaultLocale(tag) {
17
17
  export function getDefaultLocale() {
18
18
  return defaultLocale;
19
19
  }
20
- /** Format hour index (0-23) as compact 12h label: 12a, 1a … 12p, 1p … */
21
- export function fmtH(h) {
20
+ /**
21
+ * Detect whether the current locale uses 12-hour or 24-hour time.
22
+ * Caches per locale tag for performance.
23
+ */
24
+ const hourCycleCache = new Map();
25
+ export function is24HourLocale(locale) {
26
+ const loc = locale ?? defaultLocale;
27
+ if (hourCycleCache.has(loc))
28
+ return hourCycleCache.get(loc);
29
+ const sample = new Intl.DateTimeFormat(loc, { hour: 'numeric' }).resolvedOptions();
30
+ const is24 = sample.hourCycle === 'h23' || sample.hourCycle === 'h24';
31
+ hourCycleCache.set(loc, is24);
32
+ return is24;
33
+ }
34
+ /** Format hour index (0-23) as compact label: 12h ("12a", "1p") or 24h ("0", "13") */
35
+ export function fmtH(h, locale) {
36
+ if (is24HourLocale(locale)) {
37
+ return String(h);
38
+ }
22
39
  if (h === 0)
23
40
  return '12a';
24
41
  if (h === 12)
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Convert a Date (assumed UTC or local) to a Date representing
3
+ * the same instant in the target timezone.
4
+ *
5
+ * The returned Date's local getters (getHours, getMinutes, etc.)
6
+ * will return the values as they appear in the target timezone.
7
+ */
8
+ export declare function toZonedTime(date: Date | number, timezone: string): Date;
9
+ /**
10
+ * Convert a "zoned" Date (whose local getters represent a specific timezone)
11
+ * back to a true UTC Date. Use this before persisting to a backend.
12
+ */
13
+ export declare function fromZonedTime(date: Date | number, timezone: string): Date;
14
+ /**
15
+ * Get the current time as it appears in the given timezone.
16
+ */
17
+ export declare function nowInZone(timezone: string): Date;
18
+ /**
19
+ * Format a Date in a specific timezone using Intl.DateTimeFormat.
20
+ * Returns a locale-aware string.
21
+ */
22
+ export declare function formatInTimeZone(date: Date | number, timezone: string, options?: Intl.DateTimeFormatOptions, locale?: string): string;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Timezone utilities — convert between IANA timezones and local time.
3
+ *
4
+ * Uses date-fns-tz under the hood. All functions accept an IANA timezone
5
+ * string (e.g. 'America/New_York', 'Europe/Warsaw', 'Asia/Tokyo').
6
+ *
7
+ * Usage:
8
+ * import { toZonedTime, fromZonedTime, nowInZone } from '@nomideusz/svelte-calendar';
9
+ *
10
+ * // Convert a UTC date to display in a specific timezone
11
+ * const localDate = toZonedTime(utcDate, 'America/New_York');
12
+ *
13
+ * // Convert a "display" date back to UTC for storage
14
+ * const utcDate = fromZonedTime(localDate, 'America/New_York');
15
+ *
16
+ * // Get current time in a timezone
17
+ * const now = nowInZone('Asia/Tokyo');
18
+ */
19
+ import { toZonedTime as dfnsToZoned, fromZonedTime as dfnsFromZoned } from 'date-fns-tz';
20
+ /**
21
+ * Convert a Date (assumed UTC or local) to a Date representing
22
+ * the same instant in the target timezone.
23
+ *
24
+ * The returned Date's local getters (getHours, getMinutes, etc.)
25
+ * will return the values as they appear in the target timezone.
26
+ */
27
+ export function toZonedTime(date, timezone) {
28
+ return dfnsToZoned(date, timezone);
29
+ }
30
+ /**
31
+ * Convert a "zoned" Date (whose local getters represent a specific timezone)
32
+ * back to a true UTC Date. Use this before persisting to a backend.
33
+ */
34
+ export function fromZonedTime(date, timezone) {
35
+ return dfnsFromZoned(date, timezone);
36
+ }
37
+ /**
38
+ * Get the current time as it appears in the given timezone.
39
+ */
40
+ export function nowInZone(timezone) {
41
+ return dfnsToZoned(new Date(), timezone);
42
+ }
43
+ /**
44
+ * Format a Date in a specific timezone using Intl.DateTimeFormat.
45
+ * Returns a locale-aware string.
46
+ */
47
+ export function formatInTimeZone(date, timezone, options = {}, locale) {
48
+ const d = typeof date === 'number' ? new Date(date) : date;
49
+ return new Intl.DateTimeFormat(locale ?? 'en-US', {
50
+ ...options,
51
+ timeZone: timezone,
52
+ }).format(d);
53
+ }
@@ -1,7 +1,7 @@
1
1
  export { createEventStore } from './event-store.svelte.js';
2
2
  export type { EventStore } from './event-store.svelte.js';
3
3
  export { createViewState } from './view-state.svelte.js';
4
- export type { ViewState, ViewStateOptions, CalendarViewId, ViewGranularity, DateRange as ViewDateRange, } from './view-state.svelte.js';
4
+ export type { ViewState, ViewStateOptions, CalendarViewId, BuiltInViewId, ViewGranularity, DateRange as ViewDateRange, } from './view-state.svelte.js';
5
5
  export { createSelection } from './selection.svelte.js';
6
6
  export type { Selection } from './selection.svelte.js';
7
7
  export { createDragState } from './drag.svelte.js';
@@ -1,9 +1,19 @@
1
- /** All registered view IDs. Add new ones here as variants are created. */
2
- export type CalendarViewId = 'day-grid' | 'day-agenda' | 'week-grid' | 'week-agenda' | 'week-heatmap';
1
+ /**
2
+ * Built-in view IDs. Custom view IDs are also supported CalendarViewId
3
+ * is typed as `string` so consumers can register any ID.
4
+ */
5
+ export type BuiltInViewId = 'day-grid' | 'day-agenda' | 'week-grid' | 'week-agenda' | 'week-heatmap';
6
+ /**
7
+ * Any view identifier. Use built-in strings like 'day-grid' or your own
8
+ * custom IDs like 'day-kanban', 'week-resource', etc.
9
+ */
10
+ export type CalendarViewId = string;
3
11
  export type ViewGranularity = 'day' | 'week';
4
12
  export interface ViewStateOptions {
5
13
  defaultView?: CalendarViewId;
6
14
  mondayStart?: boolean;
15
+ /** IANA timezone string (e.g. 'America/New_York'). Defaults to local timezone. */
16
+ timezone?: string;
7
17
  }
8
18
  export interface DateRange {
9
19
  start: Date;
@@ -15,6 +25,8 @@ export interface ViewState {
15
25
  readonly range: DateRange;
16
26
  readonly granularity: ViewGranularity;
17
27
  readonly mondayStart: boolean;
28
+ /** IANA timezone, or undefined for local */
29
+ readonly timezone: string | undefined;
18
30
  setView(id: CalendarViewId): void;
19
31
  setFocusDate(date: Date): void;
20
32
  next(): void;
@@ -35,6 +35,7 @@ export function createViewState(options = {}) {
35
35
  let view = $state(options.defaultView ?? 'week-grid');
36
36
  let focusDate = $state(new Date());
37
37
  let mondayStart = $state(options.mondayStart ?? true);
38
+ const timezone = options.timezone;
38
39
  const granularity = $derived(granularityFor(view));
39
40
  const range = $derived(computeRange(focusDate, granularity, mondayStart));
40
41
  return {
@@ -53,6 +54,9 @@ export function createViewState(options = {}) {
53
54
  get mondayStart() {
54
55
  return mondayStart;
55
56
  },
57
+ get timezone() {
58
+ return timezone;
59
+ },
56
60
  setView(id) {
57
61
  view = id;
58
62
  },
package/dist/index.d.ts CHANGED
@@ -4,10 +4,10 @@ export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, } from './p
4
4
  export { Calendar, Toolbar } from './calendar/index.js';
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, ViewGranularity, ViewDateRange, Selection, DragState, DragMode, DragPayload, } from './engine/index.js';
7
+ export type { EventStore, ViewState, ViewStateOptions, CalendarViewId, BuiltInViewId, ViewGranularity, ViewDateRange, Selection, DragState, DragMode, DragPayload, } from './engine/index.js';
8
8
  export { createMemoryAdapter, createRestAdapter } from './adapters/index.js';
9
9
  export type { CalendarAdapter, DateRange, RestAdapterOptions } from './adapters/index.js';
10
- export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, timeToX, } from './core/index.js';
10
+ export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, timeToX, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './core/index.js';
11
11
  export type { Clock, TimelineEvent, WeekTimelineProps, DayTimelineProps, } from './core/index.js';
12
- export { midnight, parchment, indigo, presets, stageBg } from './theme/index.js';
12
+ export { midnight, parchment, indigo, neutral, bare, presets, stageBg } from './theme/index.js';
13
13
  export type { PresetName } from './theme/index.js';
package/dist/index.js CHANGED
@@ -9,6 +9,6 @@ export { createEventStore, createViewState, createSelection, createDragState, }
9
9
  // ─── Adapters ───────────────────────────────────────────
10
10
  export { createMemoryAdapter, createRestAdapter } from './adapters/index.js';
11
11
  // ─── Core: clock, time, locale, types ───────────────────
12
- export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, timeToX, } from './core/index.js';
12
+ export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, timeToX, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './core/index.js';
13
13
  // ─── Themes ─────────────────────────────────────────────
14
- export { midnight, parchment, indigo, presets, stageBg } from './theme/index.js';
14
+ export { midnight, parchment, indigo, neutral, bare, presets, stageBg } from './theme/index.js';
@@ -39,17 +39,26 @@
39
39
  const m = mins % 60;
40
40
  return m > 0 ? `${h}h ${m}m free` : `${h}h free`;
41
41
  });
42
+
43
+ function handleKeydown(e: KeyboardEvent) {
44
+ if (e.key === 'Enter' || e.key === ' ') {
45
+ e.preventDefault();
46
+ onclick?.({ start, end });
47
+ }
48
+ }
42
49
  </script>
43
50
 
44
- <!-- svelte-ignore a11y_click_events_have_key_events -->
45
- <!-- svelte-ignore a11y_no_static_element_interactions -->
46
51
  <div
47
52
  class="es"
48
53
  class:es-v={orientation === 'vertical'}
49
54
  class:es-h={orientation === 'horizontal'}
55
+ role="button"
56
+ tabindex="0"
57
+ aria-label="Create event, {fmtTime(start)} to {fmtTime(end)}, {dur()}"
50
58
  onclick={() => onclick?.({ start, end })}
59
+ onkeydown={handleKeydown}
51
60
  >
52
- <div class="es-hint">
61
+ <div class="es-hint" aria-hidden="true">
53
62
  <span class="es-plus">+</span>
54
63
  <span class="es-range">{fmtTime(start)} – {fmtTime(end)}</span>
55
64
  </div>
@@ -68,6 +77,11 @@
68
77
  border-color: var(--dt-accent-dim, rgba(239, 68, 68, 0.18));
69
78
  background: var(--dt-accent-dim, rgba(239, 68, 68, 0.05));
70
79
  }
80
+ .es:focus-visible {
81
+ outline: 2px solid var(--dt-accent, #ef4444);
82
+ outline-offset: 2px;
83
+ border-color: var(--dt-accent-dim, rgba(239, 68, 68, 0.18));
84
+ }
71
85
 
72
86
  .es-hint {
73
87
  position: absolute;
@@ -61,17 +61,35 @@
61
61
  }
62
62
 
63
63
  const accentColor = $derived(event.color || 'var(--dt-accent, #ef4444)');
64
+
65
+ const ariaLabel = $derived(() => {
66
+ const t = event.title;
67
+ const time = `${fmtTime(event.start)} to ${fmtTime(event.end)}`;
68
+ const dur = fmtDuration(event.start, event.end);
69
+ const status = active ? ', happening now' : past ? ', past' : '';
70
+ return `${t}, ${time}, ${dur}${status}`;
71
+ });
72
+
73
+ function handleKeydown(e: KeyboardEvent) {
74
+ if (e.key === 'Enter' || e.key === ' ') {
75
+ e.preventDefault();
76
+ onclick?.(event);
77
+ }
78
+ }
64
79
  </script>
65
80
 
66
- <!-- svelte-ignore a11y_click_events_have_key_events -->
67
- <!-- svelte-ignore a11y_no_static_element_interactions -->
68
81
  <div
69
82
  class="eb eb-{variant}"
70
83
  class:eb-active={active}
71
84
  class:eb-past={past}
72
85
  class:eb-editable={editable}
73
86
  style="--eb-color: {accentColor}"
87
+ role={onclick ? 'button' : 'article'}
88
+ tabindex={onclick ? 0 : -1}
89
+ aria-label={ariaLabel()}
90
+ aria-current={active ? 'true' : undefined}
74
91
  onclick={() => onclick?.(event)}
92
+ onkeydown={handleKeydown}
75
93
  >
76
94
  {#if children}
77
95
  {@render children(event)}
@@ -115,6 +133,11 @@
115
133
  .eb-editable:hover {
116
134
  transform: translateY(-1px);
117
135
  }
136
+ .eb:focus-visible {
137
+ outline: 2px solid var(--dt-accent, #ef4444);
138
+ outline-offset: 2px;
139
+ border-radius: 4px;
140
+ }
118
141
  .eb-past {
119
142
  opacity: 0.5;
120
143
  }
@@ -53,7 +53,7 @@
53
53
  </script>
54
54
 
55
55
  {#if mode === 'badge'}
56
- <span class="ni-badge" style={colorVar}>
56
+ <span class="ni-badge" style={colorVar} role="status" aria-live="polite" aria-label="Current time: {time}">
57
57
  {#if children}
58
58
  {@render children()}
59
59
  {:else}
@@ -61,7 +61,7 @@
61
61
  {/if}
62
62
  </span>
63
63
  {:else if mode === 'dot'}
64
- <div class="ni ni-dot {orientation}" style="{posStyle}; {colorVar}">
64
+ <div class="ni ni-dot {orientation}" style="{posStyle}; {colorVar}" role="status" aria-label="Current time: {time}">
65
65
  <div class="ni-dot-circle"></div>
66
66
  {#if showLabel && time}
67
67
  <div class="ni-label">
@@ -71,7 +71,7 @@
71
71
  {/if}
72
72
  </div>
73
73
  {:else}
74
- <div class="ni ni-line {orientation}" style="{posStyle}; {colorVar}">
74
+ <div class="ni ni-line {orientation}" style="{posStyle}; {colorVar}" role="status" aria-label="Current time: {time}">
75
75
  <div class="ni-line-bar"></div>
76
76
  {#if showLabel && time}
77
77
  <div class="ni-label">
@@ -1,2 +1,2 @@
1
- export { midnight, parchment, indigo, presets, stageBg, } from './presets.js';
1
+ export { midnight, parchment, indigo, neutral, bare, presets, stageBg, } from './presets.js';
2
2
  export type { PresetName } from './presets.js';
@@ -1,2 +1,2 @@
1
1
  // ─── Theme barrel export ────────────────────────────────
2
- export { midnight, parchment, indigo, presets, stageBg, } from './presets.js';
2
+ export { midnight, parchment, indigo, neutral, bare, presets, stageBg, } from './presets.js';
@@ -10,11 +10,24 @@ export declare const midnight = "\n\t--dt-bg: #0b0e14;\n\t--dt-surface: #10141c;
10
10
  export declare const parchment = "\n\t--dt-bg: #f0e9dd;\n\t--dt-surface: #e8dfd0;\n\t--dt-border: rgba(139, 110, 70, 0.12);\n\t--dt-border-day: rgba(139, 110, 70, 0.22);\n\t--dt-text: rgba(55, 40, 22, 0.88);\n\t--dt-text-2: rgba(110, 85, 52, 0.62);\n\t--dt-text-3: rgba(139, 110, 70, 0.42);\n\t--dt-accent: #b85c2f;\n\t--dt-accent-dim: rgba(184, 92, 47, 0.14);\n\t--dt-glow: rgba(184, 92, 47, 0.25);\n\t--dt-today-bg: rgba(184, 92, 47, 0.04);\n\t--dt-btn-text: #fdf6ee;\n\t--dt-scrollbar: rgba(139, 110, 70, 0.15);\n\t--dt-success: rgba(76, 153, 76, 0.7);\n\t--dt-serif: Georgia, 'Times New Roman', serif;\n\t--dt-mono: 'Courier New', 'Courier', monospace;\n\t--dt-sans: 'Outfit', Georgia, serif;\n\t--dt-hm-empty: rgba(139, 110, 70, 0.06);\n\t--dt-hm-low: rgba(184, 92, 47, 0.15);\n\t--dt-hm-mid: rgba(184, 92, 47, 0.3);\n\t--dt-hm-high: rgba(184, 92, 47, 0.5);\n\t--dt-hm-max: rgba(184, 92, 47, 0.72);\n";
11
11
  /** Soft Indigo — calm productivity, muted lavender with indigo markers */
12
12
  export declare const indigo = "\n\t--dt-bg: #f7f6fc;\n\t--dt-surface: #eeedf8;\n\t--dt-border: rgba(99, 102, 241, 0.08);\n\t--dt-border-day: rgba(99, 102, 241, 0.14);\n\t--dt-text: rgba(30, 27, 55, 0.82);\n\t--dt-text-2: rgba(75, 70, 120, 0.55);\n\t--dt-text-3: rgba(99, 102, 241, 0.3);\n\t--dt-accent: #6366f1;\n\t--dt-accent-dim: rgba(99, 102, 241, 0.12);\n\t--dt-glow: rgba(99, 102, 241, 0.25);\n\t--dt-today-bg: rgba(99, 102, 241, 0.03);\n\t--dt-btn-text: #fff;\n\t--dt-scrollbar: rgba(99, 102, 241, 0.1);\n\t--dt-success: rgba(34, 197, 94, 0.7);\n\t--dt-serif: Georgia, 'Times New Roman', serif;\n\t--dt-sans: 'Outfit', system-ui, sans-serif;\n\t--dt-hm-empty: rgba(99, 102, 241, 0.04);\n\t--dt-hm-low: rgba(99, 102, 241, 0.12);\n\t--dt-hm-mid: rgba(99, 102, 241, 0.28);\n\t--dt-hm-high: rgba(99, 102, 241, 0.48);\n\t--dt-hm-max: rgba(99, 102, 241, 0.7);\n";
13
+ /**
14
+ * Neutral — inherits fonts from host page, uses standard grays + safe blue accent.
15
+ * Drop-in preset that blends into any website without fighting its design.
16
+ */
17
+ export declare const neutral = "\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\t--dt-hm-empty: rgba(0, 0, 0, 0.03);\n\t--dt-hm-low: rgba(37, 99, 235, 0.12);\n\t--dt-hm-mid: rgba(37, 99, 235, 0.28);\n\t--dt-hm-high: rgba(37, 99, 235, 0.48);\n\t--dt-hm-max: rgba(37, 99, 235, 0.7);\n";
18
+ /**
19
+ * Bare — transparent skeleton that inherits everything from the host page.
20
+ * All colors use currentColor/transparent/inherit so the calendar fully absorbs
21
+ * the parent site's design. Override individual --dt-* tokens as needed.
22
+ */
23
+ export declare const bare = "\n\t--dt-bg: transparent;\n\t--dt-surface: transparent;\n\t--dt-border: currentColor;\n\t--dt-border-day: currentColor;\n\t--dt-text: inherit;\n\t--dt-text-2: inherit;\n\t--dt-text-3: inherit;\n\t--dt-accent: currentColor;\n\t--dt-accent-dim: transparent;\n\t--dt-glow: transparent;\n\t--dt-today-bg: transparent;\n\t--dt-btn-text: inherit;\n\t--dt-scrollbar: currentColor;\n\t--dt-success: currentColor;\n\t--dt-serif: inherit;\n\t--dt-sans: inherit;\n\t--dt-mono: inherit;\n\t--dt-hm-empty: transparent;\n\t--dt-hm-low: currentColor;\n\t--dt-hm-mid: currentColor;\n\t--dt-hm-high: currentColor;\n\t--dt-hm-max: currentColor;\n";
13
24
  /** All available presets keyed by name */
14
25
  export declare const presets: {
15
26
  readonly midnight: "\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\t--dt-hm-empty: rgba(80, 80, 120, 0.06);\n\t--dt-hm-low: rgba(239, 68, 68, 0.15);\n\t--dt-hm-mid: rgba(239, 68, 68, 0.3);\n\t--dt-hm-high: rgba(239, 68, 68, 0.5);\n\t--dt-hm-max: rgba(239, 68, 68, 0.72);\n";
16
27
  readonly parchment: "\n\t--dt-bg: #f0e9dd;\n\t--dt-surface: #e8dfd0;\n\t--dt-border: rgba(139, 110, 70, 0.12);\n\t--dt-border-day: rgba(139, 110, 70, 0.22);\n\t--dt-text: rgba(55, 40, 22, 0.88);\n\t--dt-text-2: rgba(110, 85, 52, 0.62);\n\t--dt-text-3: rgba(139, 110, 70, 0.42);\n\t--dt-accent: #b85c2f;\n\t--dt-accent-dim: rgba(184, 92, 47, 0.14);\n\t--dt-glow: rgba(184, 92, 47, 0.25);\n\t--dt-today-bg: rgba(184, 92, 47, 0.04);\n\t--dt-btn-text: #fdf6ee;\n\t--dt-scrollbar: rgba(139, 110, 70, 0.15);\n\t--dt-success: rgba(76, 153, 76, 0.7);\n\t--dt-serif: Georgia, 'Times New Roman', serif;\n\t--dt-mono: 'Courier New', 'Courier', monospace;\n\t--dt-sans: 'Outfit', Georgia, serif;\n\t--dt-hm-empty: rgba(139, 110, 70, 0.06);\n\t--dt-hm-low: rgba(184, 92, 47, 0.15);\n\t--dt-hm-mid: rgba(184, 92, 47, 0.3);\n\t--dt-hm-high: rgba(184, 92, 47, 0.5);\n\t--dt-hm-max: rgba(184, 92, 47, 0.72);\n";
17
28
  readonly indigo: "\n\t--dt-bg: #f7f6fc;\n\t--dt-surface: #eeedf8;\n\t--dt-border: rgba(99, 102, 241, 0.08);\n\t--dt-border-day: rgba(99, 102, 241, 0.14);\n\t--dt-text: rgba(30, 27, 55, 0.82);\n\t--dt-text-2: rgba(75, 70, 120, 0.55);\n\t--dt-text-3: rgba(99, 102, 241, 0.3);\n\t--dt-accent: #6366f1;\n\t--dt-accent-dim: rgba(99, 102, 241, 0.12);\n\t--dt-glow: rgba(99, 102, 241, 0.25);\n\t--dt-today-bg: rgba(99, 102, 241, 0.03);\n\t--dt-btn-text: #fff;\n\t--dt-scrollbar: rgba(99, 102, 241, 0.1);\n\t--dt-success: rgba(34, 197, 94, 0.7);\n\t--dt-serif: Georgia, 'Times New Roman', serif;\n\t--dt-sans: 'Outfit', system-ui, sans-serif;\n\t--dt-hm-empty: rgba(99, 102, 241, 0.04);\n\t--dt-hm-low: rgba(99, 102, 241, 0.12);\n\t--dt-hm-mid: rgba(99, 102, 241, 0.28);\n\t--dt-hm-high: rgba(99, 102, 241, 0.48);\n\t--dt-hm-max: rgba(99, 102, 241, 0.7);\n";
29
+ readonly neutral: "\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\t--dt-hm-empty: rgba(0, 0, 0, 0.03);\n\t--dt-hm-low: rgba(37, 99, 235, 0.12);\n\t--dt-hm-mid: rgba(37, 99, 235, 0.28);\n\t--dt-hm-high: rgba(37, 99, 235, 0.48);\n\t--dt-hm-max: rgba(37, 99, 235, 0.7);\n";
30
+ readonly bare: "\n\t--dt-bg: transparent;\n\t--dt-surface: transparent;\n\t--dt-border: currentColor;\n\t--dt-border-day: currentColor;\n\t--dt-text: inherit;\n\t--dt-text-2: inherit;\n\t--dt-text-3: inherit;\n\t--dt-accent: currentColor;\n\t--dt-accent-dim: transparent;\n\t--dt-glow: transparent;\n\t--dt-today-bg: transparent;\n\t--dt-btn-text: inherit;\n\t--dt-scrollbar: currentColor;\n\t--dt-success: currentColor;\n\t--dt-serif: inherit;\n\t--dt-sans: inherit;\n\t--dt-mono: inherit;\n\t--dt-hm-empty: transparent;\n\t--dt-hm-low: currentColor;\n\t--dt-hm-mid: currentColor;\n\t--dt-hm-high: currentColor;\n\t--dt-hm-max: currentColor;\n";
18
31
  };
19
32
  export type PresetName = keyof typeof presets;
20
33
  /**
@@ -76,8 +76,65 @@ export const indigo = `
76
76
  --dt-hm-high: rgba(99, 102, 241, 0.48);
77
77
  --dt-hm-max: rgba(99, 102, 241, 0.7);
78
78
  `;
79
+ /**
80
+ * Neutral — inherits fonts from host page, uses standard grays + safe blue accent.
81
+ * Drop-in preset that blends into any website without fighting its design.
82
+ */
83
+ export const neutral = `
84
+ --dt-bg: #ffffff;
85
+ --dt-surface: #f9fafb;
86
+ --dt-border: rgba(0, 0, 0, 0.08);
87
+ --dt-border-day: rgba(0, 0, 0, 0.14);
88
+ --dt-text: rgba(0, 0, 0, 0.87);
89
+ --dt-text-2: rgba(0, 0, 0, 0.54);
90
+ --dt-text-3: rgba(0, 0, 0, 0.38);
91
+ --dt-accent: #2563eb;
92
+ --dt-accent-dim: rgba(37, 99, 235, 0.12);
93
+ --dt-glow: rgba(37, 99, 235, 0.25);
94
+ --dt-today-bg: rgba(37, 99, 235, 0.04);
95
+ --dt-btn-text: #fff;
96
+ --dt-scrollbar: rgba(0, 0, 0, 0.1);
97
+ --dt-success: rgba(22, 163, 74, 0.7);
98
+ --dt-serif: inherit;
99
+ --dt-sans: inherit;
100
+ --dt-mono: ui-monospace, 'SFMono-Regular', monospace;
101
+ --dt-hm-empty: rgba(0, 0, 0, 0.03);
102
+ --dt-hm-low: rgba(37, 99, 235, 0.12);
103
+ --dt-hm-mid: rgba(37, 99, 235, 0.28);
104
+ --dt-hm-high: rgba(37, 99, 235, 0.48);
105
+ --dt-hm-max: rgba(37, 99, 235, 0.7);
106
+ `;
107
+ /**
108
+ * Bare — transparent skeleton that inherits everything from the host page.
109
+ * All colors use currentColor/transparent/inherit so the calendar fully absorbs
110
+ * the parent site's design. Override individual --dt-* tokens as needed.
111
+ */
112
+ export const bare = `
113
+ --dt-bg: transparent;
114
+ --dt-surface: transparent;
115
+ --dt-border: currentColor;
116
+ --dt-border-day: currentColor;
117
+ --dt-text: inherit;
118
+ --dt-text-2: inherit;
119
+ --dt-text-3: inherit;
120
+ --dt-accent: currentColor;
121
+ --dt-accent-dim: transparent;
122
+ --dt-glow: transparent;
123
+ --dt-today-bg: transparent;
124
+ --dt-btn-text: inherit;
125
+ --dt-scrollbar: currentColor;
126
+ --dt-success: currentColor;
127
+ --dt-serif: inherit;
128
+ --dt-sans: inherit;
129
+ --dt-mono: inherit;
130
+ --dt-hm-empty: transparent;
131
+ --dt-hm-low: currentColor;
132
+ --dt-hm-mid: currentColor;
133
+ --dt-hm-high: currentColor;
134
+ --dt-hm-max: currentColor;
135
+ `;
79
136
  /** All available presets keyed by name */
80
- export const presets = { midnight, parchment, indigo };
137
+ export const presets = { midnight, parchment, indigo, neutral, bare };
81
138
  /**
82
139
  * Stage background color matching each theme.
83
140
  * Useful for the container behind the timeline component.
@@ -86,4 +143,6 @@ export const stageBg = {
86
143
  midnight: '#080a0f',
87
144
  parchment: '#e4dace',
88
145
  indigo: '#efedf8',
146
+ neutral: '#ffffff',
147
+ bare: 'transparent',
89
148
  };
package/package.json CHANGED
@@ -1,62 +1,65 @@
1
1
  {
2
- "name": "@nomideusz/svelte-calendar",
3
- "version": "0.1.1",
4
- "description": "A themeable, pluggable Svelte 5 calendar with Day and Week views — Grid, Timeline, Agenda, Heatmap.",
5
- "type": "module",
6
- "license": "MIT",
7
- "svelte": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "svelte": "./dist/index.js",
13
- "default": "./dist/index.js"
14
- }
15
- },
16
- "files": [
17
- "dist",
18
- "!dist/**/*.test.*",
19
- "!dist/**/*.spec.*"
20
- ],
21
- "peerDependencies": {
22
- "svelte": "^5.0.0"
23
- },
24
- "devDependencies": {
25
- "@sveltejs/adapter-auto": "^7.0.0",
26
- "@sveltejs/kit": "^2.50.2",
27
- "@sveltejs/package": "^2.5.7",
28
- "@sveltejs/vite-plugin-svelte": "^6.2.4",
29
- "svelte": "^5.51.0",
30
- "svelte-check": "^4.3.6",
31
- "typescript": "^5.9.3",
32
- "vite": "^7.3.1",
33
- "vitest": "^4.0.18"
34
- },
35
- "dependencies": {
36
- "date-fns": "^4.1.0"
37
- },
38
- "keywords": [
39
- "svelte",
40
- "calendar",
41
- "scheduler",
42
- "timeline",
43
- "week",
44
- "day",
45
- "booking",
46
- "svelte5"
47
- ],
48
- "repository": {
49
- "type": "git",
50
- "url": "https://github.com/nomideusz/svelte-calendar"
51
- },
52
- "scripts": {
53
- "dev": "vite dev",
54
- "build": "vite build",
55
- "package": "svelte-kit sync && svelte-package",
56
- "preview": "vite preview",
57
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
58
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
59
- "test": "vitest run",
60
- "test:watch": "vitest"
61
- }
62
- }
2
+ "name": "@nomideusz/svelte-calendar",
3
+ "version": "0.2.1",
4
+ "description": "A themeable, pluggable Svelte 5 calendar with Day and Week views — Grid, Timeline, Agenda, Heatmap.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "svelte": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "svelte": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "!dist/**/*.test.*",
19
+ "!dist/**/*.spec.*"
20
+ ],
21
+ "scripts": {
22
+ "dev": "vite dev",
23
+ "build": "vite build",
24
+ "package": "svelte-kit sync && svelte-package",
25
+ "prepublishOnly": "npm run package",
26
+ "preview": "vite preview",
27
+ "prepare": "svelte-kit sync || echo ''",
28
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
29
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest"
32
+ },
33
+ "peerDependencies": {
34
+ "svelte": "^5.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@sveltejs/adapter-auto": "^7.0.0",
38
+ "@sveltejs/kit": "^2.50.2",
39
+ "@sveltejs/package": "^2.5.7",
40
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
41
+ "svelte": "^5.51.0",
42
+ "svelte-check": "^4.3.6",
43
+ "typescript": "^5.9.3",
44
+ "vite": "^7.3.1",
45
+ "vitest": "^4.0.18"
46
+ },
47
+ "dependencies": {
48
+ "date-fns": "^4.1.0",
49
+ "date-fns-tz": "^3.2.0"
50
+ },
51
+ "keywords": [
52
+ "svelte",
53
+ "calendar",
54
+ "scheduler",
55
+ "timeline",
56
+ "week",
57
+ "day",
58
+ "booking",
59
+ "svelte5"
60
+ ],
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "https://github.com/nomideusz/svelte-calendar"
64
+ }
65
+ }