@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
|
-
|
|
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