@nomideusz/svelte-calendar 0.1.0 → 0.2.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
@@ -1,6 +1,6 @@
1
- # @svelte-calendar/core
1
+ # @nomideusz/svelte-calendar
2
2
 
3
- A themeable, pluggable **Svelte 5** calendar component library with **Day** and **Week** views ready for yoga studios, tour bookings, concerts, language schools, and more.
3
+ A themeable, pluggable **Svelte 5** calendar component library with **Day** and **Week** viewsready for yoga studios, tour bookings, concerts, language schools, and more.
4
4
 
5
5
  ## Views — Concept-Paired
6
6
 
@@ -9,14 +9,15 @@ Switching between Day and Week preserves the active concept.
9
9
 
10
10
  | Concept | Day View | Week View | Description |
11
11
  |---------|----------|-----------|-------------|
12
- | **Grid** | DayTimeline (horizontal) | WeekGrid (vertical) | The primary planner — time blocks on a scrollable grid. |
13
- | **Agenda** | DayAgenda | WeekAgenda | List / feed — Done, Now, Next (day) or grouped-by-day scroll (week). |
12
+ | **Grid** | DayGrid | WeekGrid | The primary planner — time blocks on a scrollable grid. |
13
+ | **Timeline** | DayTimeline | | Horizontal day timeline. Day-only. |
14
+ | **Agenda** | Agenda `mode="day"` | Agenda `mode="week"` | List / feed — Done, Now, Next (day) or grouped-by-day scroll (week). |
14
15
  | **Heatmap** | — | WeekHeatmap | Density view — 24 cells per day showing busy/free intensity. Week-only. |
15
16
 
16
17
  ## Installation
17
18
 
18
19
  ```bash
19
- npm install @svelte-calendar/core
20
+ pnpm add @nomideusz/svelte-calendar
20
21
  ```
21
22
 
22
23
  > **Peer dependency:** Svelte 5 (`^5.0.0`)
@@ -27,15 +28,15 @@ npm install @svelte-calendar/core
27
28
  <script lang="ts">
28
29
  import {
29
30
  Calendar,
31
+ DayGrid,
30
32
  DayTimeline,
31
- DayAgenda,
33
+ Agenda,
32
34
  WeekGrid,
33
- WeekAgenda,
34
35
  WeekHeatmap,
35
36
  createMemoryAdapter,
36
37
  midnight,
37
- } from '@svelte-calendar/core';
38
- import type { CalendarView, TimelineEvent } from '@svelte-calendar/core';
38
+ } from '@nomideusz/svelte-calendar';
39
+ import type { CalendarView, TimelineEvent } from '@nomideusz/svelte-calendar';
39
40
 
40
41
  const events: TimelineEvent[] = [
41
42
  { id: '1', title: 'Yoga Flow', start: new Date('2025-03-01T09:00'), end: new Date('2025-03-01T10:00'), color: '#818cf8' },
@@ -47,10 +48,10 @@ npm install @svelte-calendar/core
47
48
 
48
49
  // Concepts are paired by label — switching Day↔Week preserves the concept
49
50
  const views: CalendarView[] = [
50
- { id: 'day-grid', label: 'Grid', granularity: 'day', component: DayTimeline },
51
+ { id: 'day-grid', label: 'Grid', granularity: 'day', component: DayGrid },
51
52
  { id: 'week-grid', label: 'Grid', granularity: 'week', component: WeekGrid },
52
- { id: 'day-agenda', label: 'Agenda', granularity: 'day', component: DayAgenda },
53
- { id: 'week-agenda', label: 'Agenda', granularity: 'week', component: WeekAgenda },
53
+ { id: 'day-agenda', label: 'Agenda', granularity: 'day', component: Agenda, props: { mode: 'day' } },
54
+ { id: 'week-agenda', label: 'Agenda', granularity: 'week', component: Agenda, props: { mode: 'week' } },
54
55
  { id: 'week-heatmap', label: 'Heatmap', granularity: 'week', component: WeekHeatmap },
55
56
  ];
56
57
  </script>
@@ -66,6 +67,27 @@ npm install @svelte-calendar/core
66
67
  />
67
68
  ```
68
69
 
70
+ ## Settings Panel
71
+
72
+ The `Settings` component provides a theme picker and dynamic fields for controlling view parameters:
73
+
74
+ ```svelte
75
+ <script lang="ts">
76
+ import { Settings } from '@nomideusz/svelte-calendar';
77
+ import type { SettingsField, PresetName } from '@nomideusz/svelte-calendar';
78
+
79
+ let theme: PresetName = $state('midnight');
80
+ let values = $state({ hourHeight: 60, elastic: true });
81
+
82
+ const fields: SettingsField[] = [
83
+ { key: 'hourHeight', label: 'Hour Height', type: 'range', min: 40, max: 120, step: 5 },
84
+ { key: 'elastic', label: 'Elastic Compression', type: 'toggle' },
85
+ ];
86
+ </script>
87
+
88
+ <Settings {fields} bind:values bind:theme />
89
+ ```
90
+
69
91
  ## Themes
70
92
 
71
93
  Three built-in presets each view reads from the same `--dt-*` CSS custom property contract:
@@ -78,7 +100,7 @@ Three built-in presets each view reads from the same `--dt-*` CSS custom proper
78
100
 
79
101
  ```svelte
80
102
  <script>
81
- import { midnight, parchment, indigo, presets } from '@svelte-calendar/core';
103
+ import { midnight, parchment, indigo, presets } from '@nomideusz/svelte-calendar';
82
104
  </script>
83
105
 
84
106
  <!-- Apply directly -->
@@ -107,13 +129,13 @@ const custom = `
107
129
 
108
130
  ```
109
131
  src/lib/
110
- +-- core/ # Clock, time utils, locale, types
111
- +-- engine/ # Reactive state: event-store, view-state, selection, drag
112
- +-- adapters/ # Data layer: memory adapter, REST adapter
113
- +-- primitives/ # Low-level UI atoms: NowIndicator, EventBlock, TimeGutter...
114
- +-- calendar/ # Calendar shell, Toolbar
115
- +-- views/ # The 6 view components (day/ + week/)
116
- +-- theme/ # Preset themes and token definitions
132
+ ├── core/ # Clock, time utils, locale, types
133
+ ├── engine/ # Reactive state: event-store, view-state, selection, drag
134
+ ├── adapters/ # Data layer: memory adapter, REST adapter
135
+ ├── primitives/ # Low-level UI atoms: NowIndicator, EventBlock, TimeGutter...
136
+ ├── calendar/ # Calendar shell, Toolbar
137
+ ├── views/ # View components (day/, week/, agenda/, settings/)
138
+ └── theme/ # Preset themes and token definitions
117
139
  ```
118
140
 
119
141
  ## Engine
@@ -124,19 +146,19 @@ import {
124
146
  createViewState,
125
147
  createSelection,
126
148
  createDragState,
127
- } from '@svelte-calendar/core';
149
+ } from '@nomideusz/svelte-calendar';
128
150
  ```
129
151
 
130
- - **`createEventStore(adapter)`** reactive event list with fetch/add/update/remove
131
- - **`createViewState(options)`** current view, date range, navigation (prev/next/today)
132
- - **`createSelection()`** selected event tracking
133
- - **`createDragState()`** drag-to-create and drag-to-move state machine
152
+ - **`createEventStore(adapter)`**reactive event list with fetch/add/update/remove
153
+ - **`createViewState(options)`**current view, date range, navigation (prev/next/today)
154
+ - **`createSelection()`**selected event tracking
155
+ - **`createDragState()`**drag-to-create and drag-to-move state machine
134
156
 
135
157
  ## Adapters
136
158
 
137
159
  | Adapter | Use |
138
160
  |---------|-----|
139
- | `createMemoryAdapter(events)` | In-memory great for demos and prototyping |
161
+ | `createMemoryAdapter(events)` | In-memorygreat for demos and prototyping |
140
162
  | `createRestAdapter(options)` | Fetch from a REST API with configurable endpoints |
141
163
 
142
164
  ## Standalone Views
@@ -150,6 +172,9 @@ Each view works independently without the Calendar shell:
150
172
  <!-- Vertical day grid with elastic night compression -->
151
173
  <DayGrid style={midnight} events={events} height={600} elastic />
152
174
 
175
+ <!-- Agenda in day mode -->
176
+ <Agenda mode="day" events={events} height={520} />
177
+
153
178
  <!-- Week density heatmap -->
154
179
  <WeekHeatmap style={parchment} events={events} height={320} />
155
180
  ```
@@ -157,10 +182,10 @@ Each view works independently without the Calendar shell:
157
182
  ## Development
158
183
 
159
184
  ```bash
160
- npm install
161
- npm run dev # SvelteKit dev server (demo app)
162
- npm run check # Type check
163
- npm run package # Build the library into dist/
185
+ pnpm install
186
+ pnpm dev # SvelteKit dev server (demo app)
187
+ pnpm check # Type check
188
+ pnpm run package # Build the library into dist/
164
189
  ```
165
190
 
166
191
  ## License
@@ -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
12
  export { midnight, parchment, indigo, 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
14
  export { midnight, parchment, indigo, 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">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomideusz/svelte-calendar",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A themeable, pluggable Svelte 5 calendar with Day and Week views — Grid, Timeline, Agenda, Heatmap.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,8 @@
33
33
  "vitest": "^4.0.18"
34
34
  },
35
35
  "dependencies": {
36
- "date-fns": "^4.1.0"
36
+ "date-fns": "^4.1.0",
37
+ "date-fns-tz": "^3.2.0"
37
38
  },
38
39
  "keywords": [
39
40
  "svelte",