@nomideusz/svelte-calendar 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -175,6 +175,79 @@ const custom = `
175
175
 
176
176
  </details>
177
177
 
178
+ ## Accessibility
179
+
180
+ All interactive elements include proper ARIA attributes and keyboard support:
181
+
182
+ - **EventBlock** — `role="button"`, `aria-label` (title + time + duration + status), `tabindex="0"`, Enter/Space activates
183
+ - **EmptySlot** — `role="button"`, `aria-label` (time range + duration), keyboard-accessible for event creation
184
+ - **NowIndicator** — `role="status"`, `aria-live="polite"` announces the current time
185
+ - **Calendar** — `role="region"`, `aria-label="Calendar"`
186
+ - **Toolbar** — `aria-label="Calendar navigation"`
187
+ - Focus-visible outlines on all interactive primitives
188
+
189
+ ## Locale & i18n
190
+
191
+ The calendar uses `Intl.DateTimeFormat` for all date/time formatting. Pass a BCP 47 locale tag to the `Calendar` shell:
192
+
193
+ ```svelte
194
+ <Calendar {views} {adapter} locale="pl-PL" dir="rtl" />
195
+ ```
196
+
197
+ | Prop | Type | Default | Description |
198
+ |------|------|---------|-------------|
199
+ | `locale` | `string` | `'en-US'` | BCP 47 tag — controls weekday names, month names, date formats |
200
+ | `dir` | `'ltr' \| 'rtl' \| 'auto'` | — | Text direction (for Arabic, Hebrew, etc.) |
201
+
202
+ The `locale` prop automatically switches between 12-hour and 24-hour time display based on the locale's hour cycle.
203
+
204
+ ### Programmatic locale control
205
+
206
+ ```ts
207
+ import { setDefaultLocale, getDefaultLocale, is24HourLocale } from '@nomideusz/svelte-calendar';
208
+
209
+ setDefaultLocale('de-DE'); // All formatting functions now use German
210
+ is24HourLocale('en-US'); // false (12h)
211
+ is24HourLocale('de-DE'); // true (24h)
212
+ ```
213
+
214
+ All formatting functions (`weekdayShort`, `monthLong`, `fmtDay`, `fmtWeekRange`, etc.) accept an optional `locale` parameter to override per-call.
215
+
216
+ ## Timezones
217
+
218
+ Timezone utilities are included via `date-fns-tz`:
219
+
220
+ ```ts
221
+ import {
222
+ toZonedTime,
223
+ fromZonedTime,
224
+ nowInZone,
225
+ formatInTimeZone,
226
+ } from '@nomideusz/svelte-calendar';
227
+
228
+ // Display a UTC date in a specific timezone
229
+ const nyTime = toZonedTime(utcDate, 'America/New_York');
230
+
231
+ // Convert back to UTC for storage
232
+ const utc = fromZonedTime(displayDate, 'America/New_York');
233
+
234
+ // Current time in Tokyo
235
+ const tokyoNow = nowInZone('Asia/Tokyo');
236
+
237
+ // Locale-aware formatting in a timezone
238
+ formatInTimeZone(date, 'Europe/Warsaw', { hour: 'numeric', minute: '2-digit' }, 'pl-PL');
239
+ // → "14:30"
240
+ ```
241
+
242
+ The engine's `createViewState` also accepts a `timezone` option:
243
+
244
+ ```ts
245
+ const viewState = createViewState({
246
+ defaultView: 'week-grid',
247
+ timezone: 'America/New_York', // stored on viewState.timezone
248
+ });
249
+ ```
250
+
178
251
  ## Architecture
179
252
 
180
253
  ```
@@ -32,7 +32,7 @@
32
32
  return m === 0 ? `${h12}${suffix}` : `${h12}:${String(m).padStart(2, '0')}${suffix}`;
33
33
  }
34
34
 
35
- const dur = $derived(() => {
35
+ const dur = $derived.by(() => {
36
36
  const mins = Math.round((end.getTime() - start.getTime()) / 60_000);
37
37
  if (mins < 60) return `${mins}m free`;
38
38
  const h = Math.floor(mins / 60);
@@ -54,7 +54,7 @@
54
54
  class:es-h={orientation === 'horizontal'}
55
55
  role="button"
56
56
  tabindex="0"
57
- aria-label="Create event, {fmtTime(start)} to {fmtTime(end)}, {dur()}"
57
+ aria-label="Create event, {fmtTime(start)} to {fmtTime(end)}, {dur}"
58
58
  onclick={() => onclick?.({ start, end })}
59
59
  onkeydown={handleKeydown}
60
60
  >
@@ -62,7 +62,7 @@
62
62
 
63
63
  const accentColor = $derived(event.color || 'var(--dt-accent, #ef4444)');
64
64
 
65
- const ariaLabel = $derived(() => {
65
+ const ariaLabel = $derived.by(() => {
66
66
  const t = event.title;
67
67
  const time = `${fmtTime(event.start)} to ${fmtTime(event.end)}`;
68
68
  const dur = fmtDuration(event.start, event.end);
@@ -78,19 +78,7 @@
78
78
  }
79
79
  </script>
80
80
 
81
- <div
82
- class="eb eb-{variant}"
83
- class:eb-active={active}
84
- class:eb-past={past}
85
- class:eb-editable={editable}
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}
91
- onclick={() => onclick?.(event)}
92
- onkeydown={handleKeydown}
93
- >
81
+ {#snippet content()}
94
82
  {#if children}
95
83
  {@render children(event)}
96
84
  {:else if variant === 'chip'}
@@ -119,7 +107,38 @@
119
107
  {/if}
120
108
  {#if active}<span class="eb-live-badge">now</span>{/if}
121
109
  {/if}
110
+ {/snippet}
111
+
112
+ {#if onclick}
113
+ <div
114
+ class="eb eb-{variant}"
115
+ class:eb-active={active}
116
+ class:eb-past={past}
117
+ class:eb-editable={editable}
118
+ style="--eb-color: {accentColor}"
119
+ role="button"
120
+ tabindex="0"
121
+ aria-label={ariaLabel}
122
+ aria-current={active ? 'true' : undefined}
123
+ onclick={() => onclick?.(event)}
124
+ onkeydown={handleKeydown}
125
+ >
126
+ {@render content()}
127
+ </div>
128
+ {:else}
129
+ <div
130
+ class="eb eb-{variant}"
131
+ class:eb-active={active}
132
+ class:eb-past={past}
133
+ class:eb-editable={editable}
134
+ style="--eb-color: {accentColor}"
135
+ role="article"
136
+ aria-label={ariaLabel}
137
+ aria-current={active ? 'true' : undefined}
138
+ >
139
+ {@render content()}
122
140
  </div>
141
+ {/if}
123
142
 
124
143
  <style>
125
144
  .eb {
@@ -754,7 +754,7 @@
754
754
  transition: width 1s linear;
755
755
  }
756
756
 
757
- .ag-empty {
757
+ :global(.ag-empty) {
758
758
  display: flex;
759
759
  align-items: center;
760
760
  justify-content: center;
@@ -880,7 +880,7 @@
880
880
  color: var(--dt-text-3, rgba(255, 255, 255, 0.25));
881
881
  margin-bottom: 2px;
882
882
  }
883
- .ag-q-free-next {
883
+ :global(.ag-q-free-next) {
884
884
  font-size: 10px;
885
885
  font-weight: 500;
886
886
  line-height: 1.2;
@@ -890,7 +890,7 @@
890
890
  text-overflow: ellipsis;
891
891
  margin-bottom: 2px;
892
892
  }
893
- .ag-q-free-until {
893
+ :global(.ag-q-free-until) {
894
894
  font-size: 9px;
895
895
  font-family: var(--dt-mono, monospace);
896
896
  color: var(--dt-accent, #ff6b4a);
@@ -30,16 +30,20 @@
30
30
 
31
31
  let { fields, values = $bindable(), theme = $bindable() }: Props = $props();
32
32
 
33
- const themeKeys: PresetName[] = ['midnight', 'parchment', 'indigo'];
33
+ const themeKeys: PresetName[] = ['midnight', 'parchment', 'indigo', 'neutral', 'bare'];
34
34
  const themeLabels: Record<PresetName, string> = {
35
35
  midnight: 'Midnight',
36
36
  parchment: 'Parchment',
37
37
  indigo: 'Indigo',
38
+ neutral: 'Neutral',
39
+ bare: 'Bare',
38
40
  };
39
41
  const themeAccents: Record<PresetName, string> = {
40
42
  midnight: '#ef4444',
41
43
  parchment: '#b85c2f',
42
44
  indigo: '#6366f1',
45
+ neutral: '#94a0b4',
46
+ bare: '#94a0b4',
43
47
  };
44
48
 
45
49
  let open = $state(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomideusz/svelte-calendar",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",