@r2digisolutions/components 0.9.2 → 0.9.4

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.
@@ -11,7 +11,7 @@
11
11
  label?: string;
12
12
  placeholder?: string;
13
13
  value?: string;
14
- type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url' | 'date';
14
+ type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url' | 'date' | 'time';
15
15
  status?: InputStatus;
16
16
  helperText?: string;
17
17
  disabled?: boolean;
@@ -7,7 +7,7 @@ interface InputProps {
7
7
  label?: string;
8
8
  placeholder?: string;
9
9
  value?: string;
10
- type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url' | 'date';
10
+ type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url' | 'date' | 'time';
11
11
  status?: InputStatus;
12
12
  helperText?: string;
13
13
  disabled?: boolean;
@@ -25,6 +25,6 @@ interface DateTimePickerProps {
25
25
  value: string;
26
26
  }) => void;
27
27
  }
28
- declare const DateTimePicker: import("svelte").Component<DateTimePickerProps, {}, "date" | "value" | "time" | "open">;
28
+ declare const DateTimePicker: import("svelte").Component<DateTimePickerProps, {}, "date" | "time" | "value" | "open">;
29
29
  type DateTimePicker = ReturnType<typeof DateTimePicker>;
30
30
  export default DateTimePicker;
@@ -18,6 +18,6 @@ interface FormDateTimePickerProps {
18
18
  value: string;
19
19
  }) => void;
20
20
  }
21
- declare const FormDateTimePicker: import("svelte").Component<FormDateTimePickerProps, {}, "date" | "value" | "time">;
21
+ declare const FormDateTimePicker: import("svelte").Component<FormDateTimePickerProps, {}, "date" | "time" | "value">;
22
22
  type FormDateTimePicker = ReturnType<typeof FormDateTimePicker>;
23
23
  export default FormDateTimePicker;
@@ -43,7 +43,7 @@
43
43
  label?: string;
44
44
  placeholder?: string;
45
45
  value?: string;
46
- type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url';
46
+ type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url' | 'date' | 'time';
47
47
  status?: FormFieldStatus;
48
48
  helperText?: string;
49
49
  errorMessage?: string;
@@ -32,7 +32,7 @@ interface FormFieldProps {
32
32
  label?: string;
33
33
  placeholder?: string;
34
34
  value?: string;
35
- type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url';
35
+ type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url' | 'date' | 'time';
36
36
  status?: FormFieldStatus;
37
37
  helperText?: string;
38
38
  errorMessage?: string;
@@ -1,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import { on } from 'svelte/events';
3
+ import { createId } from '../../../utils/id.js';
4
+
2
5
  export type TimeFormat = '24h' | '12h';
3
6
 
4
7
  interface TimePickerProps {
@@ -33,9 +36,17 @@
33
36
  onchange
34
37
  }: TimePickerProps = $props();
35
38
 
36
- let rootEl = $state<HTMLDivElement | null>(null);
39
+ let triggerEl = $state<HTMLElement | null>(null);
40
+ let panelEl = $state<HTMLDivElement | null>(null);
37
41
  let hourListEl = $state<HTMLDivElement | null>(null);
38
42
  let minuteListEl = $state<HTMLDivElement | null>(null);
43
+ /** Hide the native popover until top/left are applied (UA default is 0,0). */
44
+ let placed = $state(false);
45
+ let panelId = $state('');
46
+
47
+ $effect(() => {
48
+ panelId ||= createId('timepicker');
49
+ });
39
50
 
40
51
  const hours24 = $derived(Array.from({ length: 24 }, (_, i) => i));
41
52
  const minutes = $derived(
@@ -87,11 +98,122 @@
87
98
  return false;
88
99
  }
89
100
 
101
+ function positionPanel(opts: { measure?: boolean; show?: boolean } = {}) {
102
+ if (!triggerEl) return;
103
+ if (opts.show) placed = true;
104
+
105
+ const trigger = triggerEl.getBoundingClientRect();
106
+ if (trigger.width < 2 && trigger.height < 2) return;
107
+
108
+ const vv = window.visualViewport;
109
+ const viewW = vv?.width ?? window.innerWidth;
110
+ const viewH = vv?.height ?? window.innerHeight;
111
+ const viewLeft = vv?.offsetLeft ?? 0;
112
+ const viewTop = vv?.offsetTop ?? 0;
113
+ const gap = 8;
114
+ const pad = 8;
115
+ const estimated = { panelW: Math.max(trigger.width, 192), panelH: 280 };
116
+ const canMeasure = opts.measure !== false && !!panelEl?.matches(':popover-open');
117
+ const panelW = Math.min(
118
+ canMeasure && panelEl?.offsetWidth ? panelEl.offsetWidth : estimated.panelW,
119
+ viewW - pad * 2
120
+ );
121
+ const panelH = Math.min(
122
+ canMeasure && panelEl?.offsetHeight ? panelEl.offsetHeight : estimated.panelH,
123
+ 320
124
+ );
125
+
126
+ const spaceBelow = viewTop + viewH - trigger.bottom - gap - pad;
127
+ const spaceAbove = trigger.top - viewTop - gap - pad;
128
+ const side: 'top' | 'bottom' =
129
+ spaceBelow < panelH && spaceAbove > spaceBelow ? 'top' : 'bottom';
130
+
131
+ let top = side === 'bottom' ? trigger.bottom + gap : trigger.top - panelH - gap;
132
+ let left = trigger.left;
133
+
134
+ left = Math.min(Math.max(viewLeft + pad, left), viewLeft + viewW - panelW - pad);
135
+ top = Math.min(Math.max(viewTop + pad, top), viewTop + viewH - panelH - pad);
136
+
137
+ // Imperative styles in `beforetoggle`: a Svelte `style={}` binding
138
+ // flushes too late and the UA popover paints at 0,0 first.
139
+ if (panelEl) {
140
+ const s = panelEl.style;
141
+ s.setProperty('margin', '0');
142
+ s.setProperty('inset', 'auto');
143
+ s.setProperty('top', `${Math.round(top)}px`);
144
+ s.setProperty('left', `${Math.round(left)}px`);
145
+ s.setProperty('right', 'auto');
146
+ s.setProperty('bottom', 'auto');
147
+ s.setProperty('width', `${Math.round(panelW)}px`);
148
+ s.setProperty('visibility', placed ? 'visible' : 'hidden');
149
+ }
150
+ }
151
+
152
+ function schedulePosition() {
153
+ queueMicrotask(() => {
154
+ positionPanel();
155
+ requestAnimationFrame(() => {
156
+ positionPanel();
157
+ requestAnimationFrame(() => positionPanel({ show: true }));
158
+ });
159
+ });
160
+ }
161
+
162
+ function syncNative() {
163
+ if (!panelEl) return;
164
+ const isOpen = panelEl.matches(':popover-open');
165
+ try {
166
+ if (open && !isOpen) panelEl.showPopover();
167
+ else if (!open && isOpen) panelEl.hidePopover();
168
+ } catch {
169
+ /* ignore */
170
+ }
171
+ }
172
+
90
173
  function setOpen(next: boolean) {
91
- if (disabled) return;
174
+ if (disabled && next) return;
175
+ open = next;
176
+ }
177
+
178
+ function handleBeforeToggle(event: ToggleEvent) {
179
+ if (event.newState === 'open') {
180
+ if (disabled) {
181
+ event.preventDefault();
182
+ return;
183
+ }
184
+ placed = false;
185
+ // Set top/left before the first paint — UA popover defaults to 0,0.
186
+ positionPanel({ measure: false });
187
+ } else {
188
+ placed = false;
189
+ }
190
+ }
191
+
192
+ function handleToggle(event: ToggleEvent) {
193
+ const next = event.newState === 'open';
92
194
  open = next;
195
+ if (next) schedulePosition();
196
+ else placed = false;
93
197
  }
94
198
 
199
+ $effect(() => {
200
+ open;
201
+ queueMicrotask(() => {
202
+ syncNative();
203
+ if (open) positionPanel();
204
+ });
205
+ });
206
+
207
+ $effect(() => {
208
+ if (!open) return;
209
+ const offResize = on(window, 'resize', () => positionPanel());
210
+ const offScroll = on(window, 'scroll', () => positionPanel(), { capture: true });
211
+ return () => {
212
+ offResize();
213
+ offScroll();
214
+ };
215
+ });
216
+
95
217
  function emit(next: string) {
96
218
  value = next;
97
219
  onchange?.(next);
@@ -117,17 +239,6 @@
117
239
  onchange?.('');
118
240
  }
119
241
 
120
- function onDocPointerDown(e: PointerEvent) {
121
- if (!open || !rootEl) return;
122
- const path = typeof e.composedPath === 'function' ? e.composedPath() : [];
123
- if (path.includes(rootEl) || rootEl.contains(e.target as Node)) return;
124
- setOpen(false);
125
- }
126
-
127
- function onKey(e: KeyboardEvent) {
128
- if (e.key === 'Escape' && open) setOpen(false);
129
- }
130
-
131
242
  function hourLabel(h: number) {
132
243
  if (format === '24h') return pad(h);
133
244
  const period = h >= 12 ? 'PM' : 'AM';
@@ -138,22 +249,21 @@
138
249
  $effect(() => {
139
250
  if (!open) return;
140
251
  requestAnimationFrame(() => {
141
- const hEl = hourListEl?.querySelector('[aria-pressed="true"]') as HTMLElement | null;
142
- const mEl = minuteListEl?.querySelector('[aria-pressed="true"]') as HTMLElement | null;
252
+ const hEl = hourListEl?.querySelector('[aria-selected="true"]') as HTMLElement | null;
253
+ const mEl = minuteListEl?.querySelector('[aria-selected="true"]') as HTMLElement | null;
143
254
  hEl?.scrollIntoView({ block: 'center' });
144
255
  mEl?.scrollIntoView({ block: 'center' });
145
256
  });
146
257
  });
147
258
  </script>
148
259
 
149
- <svelte:document onpointerdown={onDocPointerDown} onkeydown={onKey} />
150
-
151
- <div class={['relative w-full min-w-[12rem] max-w-[16rem]', className]} bind:this={rootEl}>
260
+ <div class={['w-full min-w-[12rem] max-w-[16rem]', className]}>
152
261
  {#if label}
153
262
  <span class="mb-1.5 block text-sm font-medium text-primary">{label}</span>
154
263
  {/if}
155
264
 
156
265
  <div
266
+ bind:this={triggerEl}
157
267
  class={[
158
268
  'flex h-10 w-full items-center overflow-hidden rounded-xl border border-border bg-surface-elevated transition-colors',
159
269
  open && 'border-brand-500 ring-2 ring-brand-500/20',
@@ -163,8 +273,11 @@
163
273
  <button
164
274
  type="button"
165
275
  {disabled}
166
- onclick={() => setOpen(!open)}
276
+ popovertarget={panelId}
277
+ popovertargetaction="toggle"
167
278
  aria-expanded={open}
279
+ aria-haspopup="dialog"
280
+ aria-controls={panelId}
168
281
  class={[
169
282
  'flex h-full min-w-0 flex-1 items-center gap-2 px-3.5 text-left text-sm',
170
283
  'hover:bg-surface-overlay focus-visible:outline-none',
@@ -203,76 +316,111 @@
203
316
  {/if}
204
317
  </div>
205
318
 
206
- {#if open}
207
- <div
208
- role="dialog"
209
- aria-label="Choose time"
210
- class="absolute left-0 right-0 z-50 mt-2 overflow-hidden rounded-2xl border border-border bg-surface-elevated shadow-xl"
211
- onpointerdown={(e) => e.stopPropagation()}
212
- >
213
- <div class="grid grid-cols-2 divide-x divide-border">
214
- <div class="flex flex-col">
215
- <span class="border-b border-border px-3 py-2 text-center text-[11px] font-medium uppercase tracking-wide text-muted">
216
- Hour
217
- </span>
218
- <div bind:this={hourListEl} class="max-h-52 overflow-y-auto p-1.5" role="listbox" aria-label="Hours">
219
- {#each hours24 as h (h)}
220
- {@const disabledHour = minutes.every((m) => isDisabled(h, m))}
221
- <button
222
- type="button"
223
- role="option"
224
- disabled={disabledHour}
225
- aria-pressed={selectedH === h}
226
- aria-selected={selectedH === h}
227
- onclick={() => pickHour(h)}
228
- class={[
229
- 'w-full rounded-lg px-2 py-1.5 text-sm transition-colors',
230
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
231
- disabledHour && 'cursor-not-allowed opacity-30',
232
- selectedH === h
233
- ? 'bg-brand-500 font-semibold text-white'
234
- : 'text-primary hover:bg-surface-overlay'
235
- ]}
236
- >
237
- {hourLabel(h)}
238
- </button>
239
- {/each}
240
- </div>
319
+ <!-- Native popover (top layer + light dismiss). Not <dialog>: time picker is non-modal. -->
320
+ <div
321
+ bind:this={panelEl}
322
+ id={panelId}
323
+ popover="auto"
324
+ role="dialog"
325
+ aria-label="Choose time"
326
+ onbeforetoggle={handleBeforeToggle}
327
+ ontoggle={handleToggle}
328
+ data-placed={placed ? true : undefined}
329
+ class="timepicker-popover m-0 overflow-hidden rounded-2xl border border-border bg-surface-elevated shadow-xl outline-none"
330
+ >
331
+ <div class="grid grid-cols-2 divide-x divide-border">
332
+ <div class="flex flex-col">
333
+ <span
334
+ class="border-b border-border px-3 py-2 text-center text-[11px] font-medium tracking-wide text-muted uppercase"
335
+ >
336
+ Hour
337
+ </span>
338
+ <div
339
+ bind:this={hourListEl}
340
+ class="max-h-52 overflow-y-auto p-1.5"
341
+ role="listbox"
342
+ aria-label="Hours"
343
+ >
344
+ {#each hours24 as h (h)}
345
+ {@const disabledHour = minutes.every((m) => isDisabled(h, m))}
346
+ <button
347
+ type="button"
348
+ role="option"
349
+ disabled={disabledHour}
350
+ aria-selected={selectedH === h}
351
+ onclick={() => pickHour(h)}
352
+ class={[
353
+ 'w-full rounded-lg px-2 py-1.5 text-sm transition-colors',
354
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
355
+ disabledHour && 'cursor-not-allowed opacity-30',
356
+ selectedH === h
357
+ ? 'bg-brand-500 font-semibold text-white'
358
+ : 'text-primary hover:bg-surface-overlay'
359
+ ]}
360
+ >
361
+ {hourLabel(h)}
362
+ </button>
363
+ {/each}
241
364
  </div>
242
- <div class="flex flex-col">
243
- <span class="border-b border-border px-3 py-2 text-center text-[11px] font-medium uppercase tracking-wide text-muted">
244
- Min
245
- </span>
246
- <div
247
- bind:this={minuteListEl}
248
- class="max-h-52 overflow-y-auto p-1.5"
249
- role="listbox"
250
- aria-label="Minutes"
251
- >
252
- {#each minutes as m (m)}
253
- {@const disabledMin = selectedH === null ? false : isDisabled(selectedH, m)}
254
- <button
255
- type="button"
256
- role="option"
257
- disabled={disabledMin}
258
- aria-pressed={selectedM === m}
259
- aria-selected={selectedM === m}
260
- onclick={() => pickMinute(m)}
261
- class={[
262
- 'w-full rounded-lg px-2 py-1.5 text-sm transition-colors',
263
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
264
- disabledMin && 'cursor-not-allowed opacity-30',
265
- selectedM === m
266
- ? 'bg-brand-500 font-semibold text-white'
267
- : 'text-primary hover:bg-surface-overlay'
268
- ]}
269
- >
270
- {pad(m)}
271
- </button>
272
- {/each}
273
- </div>
365
+ </div>
366
+ <div class="flex flex-col">
367
+ <span
368
+ class="border-b border-border px-3 py-2 text-center text-[11px] font-medium tracking-wide text-muted uppercase"
369
+ >
370
+ Min
371
+ </span>
372
+ <div
373
+ bind:this={minuteListEl}
374
+ class="max-h-52 overflow-y-auto p-1.5"
375
+ role="listbox"
376
+ aria-label="Minutes"
377
+ >
378
+ {#each minutes as m (m)}
379
+ {@const disabledMin = selectedH === null ? false : isDisabled(selectedH, m)}
380
+ <button
381
+ type="button"
382
+ role="option"
383
+ disabled={disabledMin}
384
+ aria-selected={selectedM === m}
385
+ onclick={() => pickMinute(m)}
386
+ class={[
387
+ 'w-full rounded-lg px-2 py-1.5 text-sm transition-colors',
388
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
389
+ disabledMin && 'cursor-not-allowed opacity-30',
390
+ selectedM === m
391
+ ? 'bg-brand-500 font-semibold text-white'
392
+ : 'text-primary hover:bg-surface-overlay'
393
+ ]}
394
+ >
395
+ {pad(m)}
396
+ </button>
397
+ {/each}
274
398
  </div>
275
399
  </div>
276
400
  </div>
277
- {/if}
401
+ </div>
278
402
  </div>
403
+
404
+ <style>
405
+ /* UA popover is inset:0 + margin:auto (viewport-centered and stretched). */
406
+ .timepicker-popover {
407
+ position: fixed;
408
+ inset: unset;
409
+ margin: 0;
410
+ width: max-content;
411
+ height: max-content;
412
+ }
413
+
414
+ .timepicker-popover:popover-open {
415
+ display: block;
416
+ }
417
+
418
+ .timepicker-popover:popover-open:not([data-placed]) {
419
+ visibility: hidden;
420
+ pointer-events: none;
421
+ }
422
+
423
+ .timepicker-popover:not(:popover-open) {
424
+ display: none;
425
+ }
426
+ </style>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { onDestroy } from 'svelte';
2
+ import { onDestroy, type Snippet } from 'svelte';
3
3
  import Button from '../../atoms/Button/Button.svelte';
4
4
  import IconButton from '../../atoms/IconButton/IconButton.svelte';
5
5
  import Badge from '../../atoms/Badge/Badge.svelte';
@@ -7,6 +7,7 @@
7
7
  import SegmentedControl from '../../molecules/SegmentedControl/SegmentedControl.svelte';
8
8
  import SearchInput from '../../molecules/SearchInput/SearchInput.svelte';
9
9
  import Text from '../../atoms/Text/Text.svelte';
10
+ import Stack from '../../atoms/Stack/Stack.svelte';
10
11
  import ChevronLeft from '@lucide/svelte/icons/chevron-left';
11
12
  import ChevronRight from '@lucide/svelte/icons/chevron-right';
12
13
  import Plus from '@lucide/svelte/icons/plus';
@@ -40,6 +41,25 @@
40
41
  description?: string;
41
42
  }
42
43
 
44
+ export interface CalendarAppLabels {
45
+ create?: string;
46
+ myCalendars?: string;
47
+ today?: string;
48
+ search?: string;
49
+ month?: string;
50
+ week?: string;
51
+ day?: string;
52
+ agenda?: string;
53
+ allDay?: string;
54
+ eventsVisible?: string;
55
+ noEvents?: string;
56
+ dayEvents?: string;
57
+ close?: string;
58
+ previous?: string;
59
+ next?: string;
60
+ back?: string;
61
+ }
62
+
43
63
  interface CalendarAppProps {
44
64
  events?: CalendarAppEvent[];
45
65
  calendars?: CalendarSource[];
@@ -51,6 +71,12 @@
51
71
  showSidebar?: boolean;
52
72
  showSearch?: boolean;
53
73
  showCreate?: boolean;
74
+ /** Right detail panel (day agenda / event detail) on lg+ */
75
+ showDetailPanel?: boolean;
76
+ /** i18n-friendly UI strings; English defaults when omitted */
77
+ labels?: CalendarAppLabels;
78
+ /** Locale for toLocaleDateString (default `'en'`) */
79
+ locale?: string;
54
80
  weekStartsOn?: 0 | 1;
55
81
  /** Allow resizing timed events in week / day views */
56
82
  resizable?: boolean;
@@ -61,6 +87,8 @@
61
87
  /** Minimum event duration when resizing (minutes) */
62
88
  resizeMinMinutes?: number;
63
89
  class?: string;
90
+ /** Host actions (Edit / Delete / Open) under event detail */
91
+ eventActions?: Snippet<[CalendarAppEvent]>;
64
92
  onviewchange?: (view: CalendarView) => void;
65
93
  ondatechange?: (date: Date) => void;
66
94
  oneventclick?: (event: CalendarAppEvent) => void;
@@ -77,6 +105,25 @@
77
105
  ) => void;
78
106
  }
79
107
 
108
+ const DEFAULT_LABELS: Required<CalendarAppLabels> = {
109
+ create: 'Create',
110
+ myCalendars: 'My calendars',
111
+ today: 'Today',
112
+ search: 'Search events…',
113
+ month: 'Month',
114
+ week: 'Week',
115
+ day: 'Day',
116
+ agenda: 'Agenda',
117
+ allDay: 'All day',
118
+ eventsVisible: '{n} events visible',
119
+ noEvents: 'No events',
120
+ dayEvents: 'Day events',
121
+ close: 'Close',
122
+ previous: 'Previous',
123
+ next: 'Next',
124
+ back: 'Back'
125
+ };
126
+
80
127
  let {
81
128
  events = $bindable([] as CalendarAppEvent[]),
82
129
  calendars = $bindable([
@@ -91,12 +138,16 @@
91
138
  showSidebar = true,
92
139
  showSearch = true,
93
140
  showCreate = true,
141
+ showDetailPanel = true,
142
+ labels = undefined,
143
+ locale = 'en',
94
144
  weekStartsOn = 1,
95
145
  resizable = true,
96
146
  draggableEvents = true,
97
147
  resizeSnapMinutes = 15,
98
148
  resizeMinMinutes = 15,
99
149
  class: className = '',
150
+ eventActions,
100
151
  onviewchange,
101
152
  ondatechange,
102
153
  oneventclick,
@@ -107,6 +158,8 @@
107
158
  onmove
108
159
  }: CalendarAppProps = $props();
109
160
 
161
+ const ui = $derived({ ...DEFAULT_LABELS, ...labels });
162
+
110
163
  const weekdayLabels = $derived(
111
164
  weekStartsOn === 1
112
165
  ? ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
@@ -194,12 +247,12 @@
194
247
  }
195
248
 
196
249
  const monthTitle = $derived(
197
- date.toLocaleDateString('en', { month: 'long', year: 'numeric' })
250
+ date.toLocaleDateString(locale, { month: 'long', year: 'numeric' })
198
251
  );
199
252
 
200
253
  const headerTitle = $derived.by(() => {
201
254
  if (view === 'day') {
202
- return date.toLocaleDateString('en', {
255
+ return date.toLocaleDateString(locale, {
203
256
  weekday: 'long',
204
257
  month: 'long',
205
258
  day: 'numeric',
@@ -211,14 +264,26 @@
211
264
  const end = addDays(start, 6);
212
265
  const sameMonth = start.getMonth() === end.getMonth();
213
266
  if (sameMonth) {
214
- return `${start.toLocaleDateString('en', { month: 'long' })} ${start.getDate()}–${end.getDate()}, ${end.getFullYear()}`;
267
+ return `${start.toLocaleDateString(locale, { month: 'long' })} ${start.getDate()}–${end.getDate()}, ${end.getFullYear()}`;
215
268
  }
216
- return `${start.toLocaleDateString('en', { month: 'short', day: 'numeric' })} – ${end.toLocaleDateString('en', { month: 'short', day: 'numeric', year: 'numeric' })}`;
269
+ return `${start.toLocaleDateString(locale, { month: 'short', day: 'numeric' })} – ${end.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' })}`;
217
270
  }
218
- if (view === 'agenda') return 'Agenda';
271
+ if (view === 'agenda') return ui.agenda;
219
272
  return monthTitle;
220
273
  });
221
274
 
275
+ const focusDayHeading = $derived(
276
+ date.toLocaleDateString(locale, {
277
+ weekday: 'long',
278
+ month: 'long',
279
+ day: 'numeric'
280
+ })
281
+ );
282
+
283
+ const focusDayEvents = $derived(eventsOn(focusKey));
284
+
285
+ const eventsVisibleLabel = $derived(ui.eventsVisible.replace('{n}', String(filteredEvents.length)));
286
+
222
287
  const monthCells = $derived.by(() => {
223
288
  const y = date.getFullYear();
224
289
  const m = date.getMonth();
@@ -255,7 +320,7 @@
255
320
  return keys
256
321
  .map((key) => ({
257
322
  key,
258
- label: parseIso(key).toLocaleDateString('en', {
323
+ label: parseIso(key).toLocaleDateString(locale, {
259
324
  weekday: 'long',
260
325
  month: 'short',
261
326
  day: 'numeric'
@@ -265,9 +330,7 @@
265
330
  .filter((g) => g.events.length > 0);
266
331
  });
267
332
 
268
- const selectedEvent = $derived(
269
- filteredEvents.find((e) => e.id === selectedEventId) ?? null
270
- );
333
+ let selectedEvent = $state<CalendarAppEvent | null>(null);
271
334
 
272
335
  function setView(v: CalendarView) {
273
336
  view = v;
@@ -294,17 +357,34 @@
294
357
  }
295
358
 
296
359
  function selectEvent(e: CalendarAppEvent) {
360
+ selectedEvent = {
361
+ id: e.id,
362
+ title: e.title,
363
+ date: e.date,
364
+ endDate: e.endDate,
365
+ startTime: e.startTime,
366
+ endTime: e.endTime,
367
+ allDay: e.allDay,
368
+ calendarId: e.calendarId,
369
+ color: e.color,
370
+ location: e.location,
371
+ description: e.description
372
+ };
297
373
  selectedEventId = e.id;
298
374
  oneventclick?.(e);
299
375
  }
300
376
 
377
+ function clearSelectedEvent() {
378
+ selectedEvent = null;
379
+ selectedEventId = null;
380
+ }
301
381
  function toggleCal(id: string, visible: boolean) {
302
382
  calendars = calendars.map((c) => (c.id === id ? { ...c, visible } : c));
303
383
  oncalendartoggle?.(id, visible);
304
384
  }
305
385
 
306
386
  function timeLabel(e: CalendarAppEvent) {
307
- if (e.allDay || (!e.startTime && !e.endTime)) return 'All day';
387
+ if (e.allDay || (!e.startTime && !e.endTime)) return ui.allDay;
308
388
  if (e.startTime && e.endTime) return `${e.startTime}–${e.endTime}`;
309
389
  return e.startTime ?? '';
310
390
  }
@@ -560,12 +640,14 @@
560
640
  }
561
641
 
562
642
  function attachDragListeners() {
643
+ if (typeof document === 'undefined') return;
563
644
  document.addEventListener('pointermove', onDocDragMove, true);
564
645
  document.addEventListener('pointerup', onDocDragUp, true);
565
646
  document.addEventListener('pointercancel', onDocDragCancel, true);
566
647
  }
567
648
 
568
649
  function detachDragListeners() {
650
+ if (typeof document === 'undefined') return;
569
651
  document.removeEventListener('pointermove', onDocDragMove, true);
570
652
  document.removeEventListener('pointerup', onDocDragUp, true);
571
653
  document.removeEventListener('pointercancel', onDocDragCancel, true);
@@ -664,66 +746,86 @@
664
746
 
665
747
  <div
666
748
  class={[
667
- 'flex min-h-[36rem] overflow-hidden rounded-2xl border border-border bg-surface shadow-sm',
749
+ 'flex h-full min-h-0 overflow-hidden rounded-2xl border border-border bg-surface shadow-sm',
668
750
  className
669
751
  ]}
670
752
  >
671
753
  {#if showSidebar}
672
- <aside class="hidden w-56 shrink-0 flex-col border-r border-border bg-surface-elevated p-3 md:flex">
673
- {#if showCreate}
674
- <Button
675
- size="sm"
676
- class="mb-4 w-full"
677
- onclick={() => oncreate?.(focusKey)}
754
+ <aside
755
+ class="hidden w-56 shrink-0 flex-col overflow-hidden border-r border-border bg-surface-elevated md:flex"
756
+ >
757
+ <div class="min-h-0 flex-1 overflow-y-auto overscroll-contain p-3">
758
+ {#if showCreate}
759
+ <Button
760
+ size="sm"
761
+ class="mb-4 w-full"
762
+ onclick={() => oncreate?.(focusKey)}
763
+ >
764
+ <Plus class="h-4 w-4" strokeWidth={2} />
765
+ {ui.create}
766
+ </Button>
767
+ {/if}
768
+
769
+ <Text
770
+ as="p"
771
+ size="xs"
772
+ tone="muted"
773
+ class="mb-2 px-1 font-semibold tracking-wider uppercase"
678
774
  >
679
- <Plus class="h-4 w-4" strokeWidth={2} />
680
- Create
681
- </Button>
682
- {/if}
775
+ {ui.myCalendars}
776
+ </Text>
777
+ <ul class="space-y-0.5">
778
+ {#each calendars as cal (cal.id)}
779
+ <li>
780
+ <div
781
+ class="flex cursor-pointer items-center gap-2.5 rounded-lg px-1.5 py-1.5 hover:bg-surface-overlay"
782
+ role="button"
783
+ tabindex="0"
784
+ onclick={() => toggleCal(cal.id, cal.visible === false)}
785
+ onkeydown={(e) => {
786
+ if (e.key === 'Enter' || e.key === ' ') {
787
+ e.preventDefault();
788
+ toggleCal(cal.id, cal.visible === false);
789
+ }
790
+ }}
791
+ >
792
+ <Checkbox
793
+ size="md"
794
+ checked={cal.visible !== false}
795
+ aria-label={cal.label}
796
+ onchange={(v) => toggleCal(cal.id, v)}
797
+ onclick={(e) => e.stopPropagation()}
798
+ />
799
+ <span
800
+ class={['h-2.5 w-2.5 shrink-0 rounded-full', toneClass[cal.color ?? 'brand']]}
801
+ aria-hidden="true"
802
+ ></span>
803
+ <Text as="span" size="sm" tone="primary" class="min-w-0 flex-1 truncate font-medium">
804
+ {cal.label}
805
+ </Text>
806
+ </div>
807
+ </li>
808
+ {/each}
809
+ </ul>
810
+ </div>
683
811
 
684
- <p class="mb-2 px-1 text-[10px] font-semibold tracking-wider text-muted uppercase">
685
- My calendars
686
- </p>
687
- <ul class="space-y-1">
688
- {#each calendars as cal (cal.id)}
689
- <li>
690
- <label
691
- class="flex cursor-pointer items-center gap-2 rounded-lg px-1.5 py-1.5 hover:bg-surface-overlay"
692
- >
693
- <Checkbox
694
- size="sm"
695
- checked={cal.visible !== false}
696
- onchange={(v) => toggleCal(cal.id, v)}
697
- />
698
- <span
699
- class={['h-2.5 w-2.5 rounded-full', toneClass[cal.color ?? 'brand']]}
700
- aria-hidden="true"
701
- ></span>
702
- <span class="truncate text-xs font-medium text-primary">{cal.label}</span>
703
- </label>
704
- </li>
705
- {/each}
706
- </ul>
707
-
708
- <div class="mt-auto border-t border-border pt-3">
709
- <p class="px-1 text-[10px] text-muted">
710
- {filteredEvents.length} event{filteredEvents.length === 1 ? '' : 's'} visible
711
- </p>
812
+ <div class="shrink-0 border-t border-border px-3 py-3">
813
+ <Text size="sm" tone="muted">{eventsVisibleLabel}</Text>
712
814
  </div>
713
815
  </aside>
714
816
  {/if}
715
817
 
716
- <div class="flex min-w-0 flex-1 flex-col">
818
+ <div class="flex min-h-0 min-w-0 flex-1 flex-col">
717
819
  <!-- Header -->
718
820
  <header
719
- class="flex flex-wrap items-center gap-2 border-b border-border bg-surface-elevated px-3 py-2.5 sm:gap-3 sm:px-4"
821
+ class="flex shrink-0 flex-wrap items-center gap-2 border-b border-border bg-surface-elevated px-3 py-2.5 sm:gap-3 sm:px-4"
720
822
  >
721
823
  <div class="flex items-center gap-1">
722
- <Button size="xs" variant="secondary" onclick={goToday}>Today</Button>
723
- <IconButton label="Previous" size="sm" variant="ghost" onclick={() => shift(-1)}>
824
+ <Button size="xs" variant="secondary" onclick={goToday}>{ui.today}</Button>
825
+ <IconButton label={ui.previous} size="sm" variant="ghost" onclick={() => shift(-1)}>
724
826
  <ChevronLeft class="h-4 w-4" />
725
827
  </IconButton>
726
- <IconButton label="Next" size="sm" variant="ghost" onclick={() => shift(1)}>
828
+ <IconButton label={ui.next} size="sm" variant="ghost" onclick={() => shift(1)}>
727
829
  <ChevronRight class="h-4 w-4" />
728
830
  </IconButton>
729
831
  </div>
@@ -734,7 +836,7 @@
734
836
 
735
837
  {#if showSearch}
736
838
  <div class="order-last w-full sm:order-none sm:w-44 lg:w-56">
737
- <SearchInput bind:value={query} size="sm" placeholder="Search events…" />
839
+ <SearchInput bind:value={query} size="sm" placeholder={ui.search} />
738
840
  </div>
739
841
  {/if}
740
842
 
@@ -742,10 +844,10 @@
742
844
  size="sm"
743
845
  bind:value={view}
744
846
  items={[
745
- { id: 'month', label: 'Month' },
746
- { id: 'week', label: 'Week' },
747
- { id: 'day', label: 'Day' },
748
- { id: 'agenda', label: 'Agenda' }
847
+ { id: 'month', label: ui.month },
848
+ { id: 'week', label: ui.week },
849
+ { id: 'day', label: ui.day },
850
+ { id: 'agenda', label: ui.agenda }
749
851
  ]}
750
852
  onchange={(v) => onviewchange?.(v as CalendarView)}
751
853
  />
@@ -753,12 +855,20 @@
753
855
 
754
856
  <div class="flex min-h-0 flex-1">
755
857
  <!-- Main views -->
756
- <div class="min-w-0 flex-1 overflow-auto p-2 sm:p-3">
858
+ <div
859
+ class={[
860
+ 'min-h-0 min-w-0 flex-1 p-2 sm:p-3',
861
+ view === 'month' ? 'overflow-hidden' : 'overflow-auto'
862
+ ]}
863
+ >
757
864
  {#if view === 'month'}
758
- <div class="grid grid-cols-7 gap-px overflow-hidden rounded-xl border border-border bg-border">
865
+ <div
866
+ class="grid h-full min-h-0 grid-cols-7 gap-px overflow-hidden rounded-xl border border-border bg-border"
867
+ style={`grid-template-rows: auto repeat(${Math.ceil(monthCells.length / 7)}, minmax(0, 1fr));`}
868
+ >
759
869
  {#each weekdayLabels as wd}
760
870
  <div
761
- class="bg-surface-overlay/80 px-1 py-1.5 text-center text-[10px] font-semibold tracking-wide text-muted uppercase"
871
+ class="bg-surface-overlay/80 px-1 py-1.5 text-center text-xs font-semibold tracking-wide text-muted uppercase"
762
872
  >
763
873
  {wd}
764
874
  </div>
@@ -767,7 +877,7 @@
767
877
  {@const dayEvents = eventsOn(cell.key)}
768
878
  <div
769
879
  class={[
770
- 'flex min-h-[5.5rem] flex-col gap-0.5 bg-surface p-1 sm:min-h-[6.5rem]',
880
+ 'flex min-h-0 flex-col gap-0.5 overflow-hidden bg-surface p-1',
771
881
  !cell.inMonth && 'bg-surface-overlay/30 text-muted',
772
882
  cell.key === todayKey && 'ring-1 ring-inset ring-brand-500/40'
773
883
  ]}
@@ -780,6 +890,7 @@
780
890
  cell.key === focusKey && cell.key !== todayKey && 'bg-surface-overlay'
781
891
  ]}
782
892
  onclick={() => {
893
+ clearSelectedEvent();
783
894
  setDate(parseIso(cell.key));
784
895
  ondayclick?.(cell.key);
785
896
  }}
@@ -837,7 +948,7 @@
837
948
  }}
838
949
  >
839
950
  <p class="text-[10px] font-semibold tracking-wide text-muted uppercase">
840
- {(col.date ?? parseIso(col.key)).toLocaleDateString('en', { weekday: 'short' })}
951
+ {(col.date ?? parseIso(col.key)).toLocaleDateString(locale, { weekday: 'short' })}
841
952
  </p>
842
953
  <p
843
954
  class={[
@@ -991,7 +1102,7 @@
991
1102
  <!-- Agenda -->
992
1103
  {#if agendaGroups.length === 0}
993
1104
  <div class="flex h-48 items-center justify-center rounded-xl border border-dashed border-border">
994
- <Text size="sm" tone="muted">No upcoming events</Text>
1105
+ <Text size="sm" tone="muted">{ui.noEvents}</Text>
995
1106
  </div>
996
1107
  {:else}
997
1108
  <ul class="space-y-4">
@@ -1005,7 +1116,7 @@
1005
1116
  >
1006
1117
  {group.label}
1007
1118
  {#if group.key === todayKey}
1008
- <Badge size="sm" variant="primary" class="ml-1">Today</Badge>
1119
+ <Badge size="sm" variant="primary" class="ml-1">{ui.today}</Badge>
1009
1120
  {/if}
1010
1121
  </p>
1011
1122
  <ul class="space-y-1.5">
@@ -1046,65 +1157,137 @@
1046
1157
  {/if}
1047
1158
  </div>
1048
1159
 
1049
- <!-- Detail drawer -->
1050
- {#if selectedEvent}
1160
+ <!-- Detail panel: day agenda (default) or selected event -->
1161
+ {#if showDetailPanel}
1051
1162
  <aside
1052
- class="hidden w-64 shrink-0 flex-col border-l border-border bg-surface-elevated p-4 lg:flex"
1163
+ class="hidden min-h-0 w-80 shrink-0 flex-col overflow-hidden border-l border-border bg-surface-elevated lg:flex"
1053
1164
  >
1054
- <div class="mb-3 flex items-start justify-between gap-2">
1055
- <span
1056
- class={['mt-1 h-2.5 w-2.5 rounded-full', toneClass[eventTone(selectedEvent)]]}
1057
- ></span>
1058
- <div class="min-w-0 flex-1">
1059
- <h3 class="text-sm font-semibold text-primary">{selectedEvent.title}</h3>
1060
- <p class="mt-1 text-xs text-muted">
1061
- {parseIso(selectedEvent.date).toLocaleDateString('en', {
1062
- weekday: 'short',
1063
- month: 'short',
1064
- day: 'numeric'
1065
- })}
1066
- {#if selectedEvent.endDate && selectedEvent.endDate !== selectedEvent.date}
1067
- – {parseIso(selectedEvent.endDate).toLocaleDateString('en', {
1068
- month: 'short',
1069
- day: 'numeric'
1070
- })}
1071
- {/if}
1072
- </p>
1165
+ {#if selectedEvent}
1166
+ {@const tone = eventTone(selectedEvent)}
1167
+ {@const cal = selectedEvent.calendarId
1168
+ ? calendars.find((c) => c.id === selectedEvent.calendarId)
1169
+ : undefined}
1170
+ <div class="flex min-h-0 flex-1 flex-col">
1171
+ <div class="min-h-0 flex-1 overflow-y-auto overscroll-contain p-4">
1172
+ <div class="mb-3 flex items-center gap-1">
1173
+ <IconButton
1174
+ label={ui.back}
1175
+ size="sm"
1176
+ variant="ghost"
1177
+ onclick={clearSelectedEvent}
1178
+ >
1179
+ <ChevronLeft class="h-4 w-4" />
1180
+ </IconButton>
1181
+ <Text as="span" size="xs" tone="muted" class="font-medium">
1182
+ {ui.back}
1183
+ </Text>
1184
+ <div class="flex-1"></div>
1185
+ <IconButton
1186
+ label={ui.close}
1187
+ size="sm"
1188
+ variant="ghost"
1189
+ onclick={clearSelectedEvent}
1190
+ >
1191
+ <X class="h-4 w-4" />
1192
+ </IconButton>
1193
+ </div>
1194
+
1195
+ <div class={['mb-4 rounded-xl px-3.5 py-3', toneSoft[tone]]}>
1196
+ <h3 class="text-base font-semibold text-primary">{selectedEvent.title}</h3>
1197
+ <p class="mt-1 text-xs opacity-80">
1198
+ {parseIso(selectedEvent.date).toLocaleDateString(locale, {
1199
+ weekday: 'short',
1200
+ month: 'short',
1201
+ day: 'numeric'
1202
+ })}
1203
+ {#if selectedEvent.endDate && selectedEvent.endDate !== selectedEvent.date}
1204
+ – {parseIso(selectedEvent.endDate).toLocaleDateString(locale, {
1205
+ month: 'short',
1206
+ day: 'numeric'
1207
+ })}
1208
+ {/if}
1209
+ </p>
1210
+ </div>
1211
+
1212
+ <Stack gap="sm">
1213
+ <div class="flex items-center gap-2 text-sm text-secondary">
1214
+ <Clock class="h-4 w-4 shrink-0 text-muted" />
1215
+ <span>{timeLabel(selectedEvent)}</span>
1216
+ </div>
1217
+ {#if selectedEvent.location}
1218
+ <div class="flex items-center gap-2 text-sm text-secondary">
1219
+ <MapPin class="h-4 w-4 shrink-0 text-muted" />
1220
+ <span class="min-w-0 truncate">{selectedEvent.location}</span>
1221
+ </div>
1222
+ {/if}
1223
+ {#if cal}
1224
+ <div class="flex items-center gap-2">
1225
+ <span
1226
+ class={['h-2.5 w-2.5 shrink-0 rounded-full', toneClass[cal.color ?? 'brand']]}
1227
+ aria-hidden="true"
1228
+ ></span>
1229
+ <Badge size="sm" variant="secondary">{cal.label}</Badge>
1230
+ </div>
1231
+ {/if}
1232
+ {#if selectedEvent.description}
1233
+ <Text as="p" size="sm" tone="secondary" class="leading-relaxed">
1234
+ {selectedEvent.description}
1235
+ </Text>
1236
+ {/if}
1237
+ </Stack>
1238
+ </div>
1239
+
1240
+ {#if eventActions}
1241
+ <div class="shrink-0 border-t border-border bg-surface-elevated p-4">
1242
+ {@render eventActions(selectedEvent)}
1243
+ </div>
1244
+ {/if}
1073
1245
  </div>
1074
- <IconButton
1075
- label="Close"
1076
- size="sm"
1077
- variant="ghost"
1078
- onclick={() => (selectedEventId = null)}
1079
- >
1080
- <X class="h-4 w-4" />
1081
- </IconButton>
1082
- </div>
1246
+ {:else}
1247
+ <div class="flex min-h-0 flex-1 flex-col p-4">
1248
+ <div class="mb-3 flex shrink-0 items-start justify-between gap-2">
1249
+ <div class="min-w-0 flex-1">
1250
+ <p class="text-xs font-semibold tracking-wider text-muted uppercase">
1251
+ {ui.dayEvents}
1252
+ </p>
1253
+ <h3 class="mt-0.5 text-sm font-semibold text-primary">{focusDayHeading}</h3>
1254
+ </div>
1255
+ {#if showCreate}
1256
+ <Button size="xs" variant="secondary" onclick={() => oncreate?.(focusKey)}>
1257
+ <Plus class="h-3.5 w-3.5" strokeWidth={2} />
1258
+ {ui.create}
1259
+ </Button>
1260
+ {/if}
1261
+ </div>
1083
1262
 
1084
- <div class="space-y-3 text-xs text-secondary">
1085
- <p class="inline-flex items-center gap-1.5">
1086
- <Clock class="h-3.5 w-3.5 text-muted" />
1087
- {timeLabel(selectedEvent)}
1088
- </p>
1089
- {#if selectedEvent.location}
1090
- <p class="inline-flex items-center gap-1.5">
1091
- <MapPin class="h-3.5 w-3.5 text-muted" />
1092
- {selectedEvent.location}
1093
- </p>
1094
- {/if}
1095
- {#if selectedEvent.calendarId}
1096
- {@const cal = calendars.find((c) => c.id === selectedEvent.calendarId)}
1097
- {#if cal}
1098
- <p class="inline-flex items-center gap-1.5">
1099
- <span class={['h-2 w-2 rounded-full', toneClass[cal.color ?? 'brand']]}></span>
1100
- {cal.label}
1101
- </p>
1263
+ {#if focusDayEvents.length === 0}
1264
+ <div class="flex min-h-0 flex-1 items-center justify-center px-3 py-8">
1265
+ <Text size="sm" tone="muted">{ui.noEvents}</Text>
1266
+ </div>
1267
+ {:else}
1268
+ <ul class="min-h-0 flex-1 space-y-1.5 overflow-auto overscroll-contain">
1269
+ {#each focusDayEvents as ev (ev.id)}
1270
+ <li>
1271
+ <button
1272
+ type="button"
1273
+ class={[
1274
+ 'flex w-full flex-col gap-0.5 rounded-lg px-2.5 py-2 text-left transition hover:opacity-90',
1275
+ toneSoft[eventTone(ev)]
1276
+ ]}
1277
+ onclick={() => selectEvent(ev)}
1278
+ >
1279
+ <span class="text-xs tabular-nums opacity-80">{timeLabel(ev)}</span>
1280
+ <span class="text-sm font-semibold">{ev.title}</span>
1281
+ {#if ev.location}
1282
+ <span class="truncate text-xs opacity-70">{ev.location}</span>
1283
+ {/if}
1284
+ </button>
1285
+ </li>
1286
+ {/each}
1287
+ </ul>
1102
1288
  {/if}
1103
- {/if}
1104
- {#if selectedEvent.description}
1105
- <p class="leading-relaxed text-secondary">{selectedEvent.description}</p>
1106
- {/if}
1107
- </div>
1289
+ </div>
1290
+ {/if}
1108
1291
  </aside>
1109
1292
  {/if}
1110
1293
  </div>
@@ -1,3 +1,4 @@
1
+ import { type Snippet } from 'svelte';
1
2
  export type CalendarView = 'month' | 'week' | 'day' | 'agenda';
2
3
  export type CalendarTone = 'brand' | 'success' | 'warning' | 'error' | 'info' | 'violet' | 'rose';
3
4
  export interface CalendarSource {
@@ -21,6 +22,24 @@ export interface CalendarAppEvent {
21
22
  location?: string;
22
23
  description?: string;
23
24
  }
25
+ export interface CalendarAppLabels {
26
+ create?: string;
27
+ myCalendars?: string;
28
+ today?: string;
29
+ search?: string;
30
+ month?: string;
31
+ week?: string;
32
+ day?: string;
33
+ agenda?: string;
34
+ allDay?: string;
35
+ eventsVisible?: string;
36
+ noEvents?: string;
37
+ dayEvents?: string;
38
+ close?: string;
39
+ previous?: string;
40
+ next?: string;
41
+ back?: string;
42
+ }
24
43
  interface CalendarAppProps {
25
44
  events?: CalendarAppEvent[];
26
45
  calendars?: CalendarSource[];
@@ -32,6 +51,12 @@ interface CalendarAppProps {
32
51
  showSidebar?: boolean;
33
52
  showSearch?: boolean;
34
53
  showCreate?: boolean;
54
+ /** Right detail panel (day agenda / event detail) on lg+ */
55
+ showDetailPanel?: boolean;
56
+ /** i18n-friendly UI strings; English defaults when omitted */
57
+ labels?: CalendarAppLabels;
58
+ /** Locale for toLocaleDateString (default `'en'`) */
59
+ locale?: string;
35
60
  weekStartsOn?: 0 | 1;
36
61
  /** Allow resizing timed events in week / day views */
37
62
  resizable?: boolean;
@@ -42,6 +67,8 @@ interface CalendarAppProps {
42
67
  /** Minimum event duration when resizing (minutes) */
43
68
  resizeMinMinutes?: number;
44
69
  class?: string;
70
+ /** Host actions (Edit / Delete / Open) under event detail */
71
+ eventActions?: Snippet<[CalendarAppEvent]>;
45
72
  onviewchange?: (view: CalendarView) => void;
46
73
  ondatechange?: (date: Date) => void;
47
74
  oneventclick?: (event: CalendarAppEvent) => void;
package/dist/index.d.ts CHANGED
@@ -751,7 +751,7 @@ export type { RoadmapItem, RoadmapStatus } from './components/organisms/Roadmap/
751
751
  export { default as FAQ } from './components/organisms/FAQ/FAQ.svelte';
752
752
  export type { FaqItem } from './components/organisms/FAQ/FAQ.svelte';
753
753
  export { default as CalendarApp } from './components/organisms/CalendarApp/CalendarApp.svelte';
754
- export type { CalendarView, CalendarTone, CalendarSource, CalendarAppEvent } from './components/organisms/CalendarApp/CalendarApp.svelte';
754
+ export type { CalendarView, CalendarTone, CalendarSource, CalendarAppEvent, CalendarAppLabels } from './components/organisms/CalendarApp/CalendarApp.svelte';
755
755
  export { default as PermissionsMatrix } from './components/organisms/PermissionsMatrix/PermissionsMatrix.svelte';
756
756
  export type { PermissionLevel, PermissionRole, PermissionResource, PermissionMap } from './components/organisms/PermissionsMatrix/PermissionsMatrix.svelte';
757
757
  export { default as RolesPage } from './components/organisms/RolesPage/RolesPage.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",