@r2digisolutions/components 0.9.2 → 0.9.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/dist/components/atoms/Input/Input.svelte +1 -1
- package/dist/components/atoms/Input/Input.svelte.d.ts +1 -1
- package/dist/components/molecules/DateTimePicker/DateTimePicker.svelte.d.ts +1 -1
- package/dist/components/molecules/FormDateTimePicker/FormDateTimePicker.svelte.d.ts +1 -1
- package/dist/components/molecules/FormField/FormField.svelte +1 -1
- package/dist/components/molecules/FormField/FormField.svelte.d.ts +1 -1
- package/dist/components/organisms/CalendarApp/CalendarApp.svelte +304 -121
- package/dist/components/organisms/CalendarApp/CalendarApp.svelte.d.ts +27 -0
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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" | "
|
|
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" | "
|
|
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,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(
|
|
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(
|
|
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(
|
|
267
|
+
return `${start.toLocaleDateString(locale, { month: 'long' })} ${start.getDate()}–${end.getDate()}, ${end.getFullYear()}`;
|
|
215
268
|
}
|
|
216
|
-
return `${start.toLocaleDateString(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
<
|
|
685
|
-
|
|
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}>
|
|
723
|
-
<IconButton label=
|
|
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=
|
|
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=
|
|
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:
|
|
746
|
-
{ id: 'week', label:
|
|
747
|
-
{ id: 'day', label:
|
|
748
|
-
{ id: 'agenda', label:
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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(
|
|
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">
|
|
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">
|
|
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
|
|
1050
|
-
{#if
|
|
1160
|
+
<!-- Detail panel: day agenda (default) or selected event -->
|
|
1161
|
+
{#if showDetailPanel}
|
|
1051
1162
|
<aside
|
|
1052
|
-
class="hidden w-
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
<
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
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';
|