@spatika/react 1.2.0 → 1.3.1
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/AGENTS.md +2 -2
- package/README.md +6 -2
- package/dist/composites/EventCalendar.d.ts +39 -2
- package/dist/composites/EventCalendar.d.ts.map +1 -1
- package/dist/composites/EventCalendar.js +48 -16
- package/dist/composites/EventTimeline.d.ts +6 -1
- package/dist/composites/EventTimeline.d.ts.map +1 -1
- package/dist/composites/EventTimeline.js +5 -3
- package/dist/composites/scheduler-ui.d.ts +4 -3
- package/dist/composites/scheduler-ui.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lib/scheduler.d.ts +4 -0
- package/dist/lib/scheduler.d.ts.map +1 -1
- package/dist/lib/scheduler.js +1 -0
- package/package.json +4 -4
- package/skills/spatika-ui/SKILL.md +2 -2
- package/src/composites/EventCalendar.tsx +174 -72
- package/src/composites/EventTimeline.tsx +32 -23
- package/src/composites/scheduler-ui.tsx +11 -9
- package/src/composites/scheduler.test.tsx +86 -0
- package/src/index.test.ts +4 -0
- package/src/index.ts +10 -0
- package/src/lib/scheduler.test.ts +13 -0
- package/src/lib/scheduler.ts +5 -0
|
@@ -60,6 +60,37 @@ import {
|
|
|
60
60
|
} from "./scheduler-ui";
|
|
61
61
|
import { DropdownMenuItem } from "../primitives/DropdownMenu";
|
|
62
62
|
|
|
63
|
+
export type EventCalendarEventVariant =
|
|
64
|
+
| "month-bar"
|
|
65
|
+
| "month-timed"
|
|
66
|
+
| "all-day"
|
|
67
|
+
| "time-grid"
|
|
68
|
+
| "agenda";
|
|
69
|
+
|
|
70
|
+
export type EventCalendarEventRenderContext = {
|
|
71
|
+
view: CalendarView;
|
|
72
|
+
variant: EventCalendarEventVariant;
|
|
73
|
+
locale?: string;
|
|
74
|
+
hour12: boolean;
|
|
75
|
+
source?: SchedulerEvent;
|
|
76
|
+
defaultChildren: ReactNode;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type EventCalendarToolbarContext = {
|
|
80
|
+
view: CalendarView;
|
|
81
|
+
date: Date;
|
|
82
|
+
title: string;
|
|
83
|
+
views: CalendarView[];
|
|
84
|
+
locale?: string;
|
|
85
|
+
preferences: Required<SchedulerPreferences>;
|
|
86
|
+
onViewChange: (view: CalendarView) => void;
|
|
87
|
+
onPrev: () => void;
|
|
88
|
+
onNext: () => void;
|
|
89
|
+
onToday: () => void;
|
|
90
|
+
onDateChange: (date: Date) => void;
|
|
91
|
+
onPreferencesChange: (next: Required<SchedulerPreferences>) => void;
|
|
92
|
+
};
|
|
93
|
+
|
|
63
94
|
export type EventCalendarProps = {
|
|
64
95
|
events?: SchedulerEvent[];
|
|
65
96
|
resources?: SchedulerResource[];
|
|
@@ -85,6 +116,22 @@ export type EventCalendarProps = {
|
|
|
85
116
|
onEventCreate?: (event: SchedulerEvent) => void;
|
|
86
117
|
onEventDelete?: (event: SchedulerEvent) => void;
|
|
87
118
|
onSlotClick?: (slot: { start: Date; end: Date; allDay: boolean }) => void;
|
|
119
|
+
/**
|
|
120
|
+
* Built-in create/edit dialog. Defaults to true.
|
|
121
|
+
* Set false when the host owns create/edit (custom schedule modal, etc.).
|
|
122
|
+
*/
|
|
123
|
+
showEventEditor?: boolean;
|
|
124
|
+
/** Preferences gear in the default toolbar. Ignored when `renderToolbar` is set. */
|
|
125
|
+
showPreferences?: boolean;
|
|
126
|
+
/** Extra controls after the view menu. Ignored when `renderToolbar` is set. */
|
|
127
|
+
toolbarTrailing?: ReactNode;
|
|
128
|
+
/** Replace event chip contents. The kit still wraps with the click/drag trigger. */
|
|
129
|
+
renderEvent?: (
|
|
130
|
+
event: NormalizedEvent,
|
|
131
|
+
context: EventCalendarEventRenderContext,
|
|
132
|
+
) => ReactNode;
|
|
133
|
+
/** Replace the toolbar. The default kit toolbar is used when omitted. */
|
|
134
|
+
renderToolbar?: (context: EventCalendarToolbarContext) => ReactNode;
|
|
88
135
|
className?: string;
|
|
89
136
|
};
|
|
90
137
|
|
|
@@ -167,6 +214,11 @@ export function EventCalendar({
|
|
|
167
214
|
onEventCreate,
|
|
168
215
|
onEventDelete,
|
|
169
216
|
onSlotClick,
|
|
217
|
+
showEventEditor = true,
|
|
218
|
+
showPreferences = true,
|
|
219
|
+
toolbarTrailing,
|
|
220
|
+
renderEvent,
|
|
221
|
+
renderToolbar,
|
|
170
222
|
className,
|
|
171
223
|
}: EventCalendarProps) {
|
|
172
224
|
const [view, setView] = useControllableState<CalendarView>({
|
|
@@ -238,6 +290,7 @@ export function EventCalendar({
|
|
|
238
290
|
function openEvent(event: NormalizedEvent) {
|
|
239
291
|
const source = sourceEvent(events, event.id);
|
|
240
292
|
if (source) onEventClick?.(source);
|
|
293
|
+
if (!showEventEditor) return;
|
|
241
294
|
setDraft(source ? draftFromSource(source) : {
|
|
242
295
|
id: event.id,
|
|
243
296
|
title: event.title,
|
|
@@ -253,7 +306,7 @@ export function EventCalendar({
|
|
|
253
306
|
|
|
254
307
|
function openSlot(slot: { start: Date; end: Date; allDay: boolean }) {
|
|
255
308
|
onSlotClick?.(slot);
|
|
256
|
-
if (readOnly) return;
|
|
309
|
+
if (readOnly || !showEventEditor) return;
|
|
257
310
|
setDraft({ start: slot.start, end: slot.end, allDay: slot.allDay });
|
|
258
311
|
}
|
|
259
312
|
|
|
@@ -322,13 +375,28 @@ export function EventCalendar({
|
|
|
322
375
|
function renderEventTrigger(
|
|
323
376
|
event: NormalizedEvent,
|
|
324
377
|
className: string,
|
|
325
|
-
|
|
326
|
-
options?: { resize?: "ns" },
|
|
378
|
+
defaultChildren: ReactNode,
|
|
379
|
+
options?: { resize?: "ns"; variant: EventCalendarEventVariant },
|
|
327
380
|
) {
|
|
381
|
+
const source = sourceEvent(events, event.id);
|
|
382
|
+
const variant = options?.variant ?? "month-timed";
|
|
383
|
+
const children = renderEvent
|
|
384
|
+
? renderEvent(event, {
|
|
385
|
+
view: resolvedView,
|
|
386
|
+
variant,
|
|
387
|
+
locale,
|
|
388
|
+
hour12,
|
|
389
|
+
source,
|
|
390
|
+
defaultChildren,
|
|
391
|
+
})
|
|
392
|
+
: defaultChildren;
|
|
393
|
+
if (renderEvent && children == null) return null;
|
|
394
|
+
|
|
328
395
|
const resizable =
|
|
329
396
|
options?.resize === "ns" &&
|
|
330
397
|
eventCanResize(event, { readOnly, enabled: canResize }) &&
|
|
331
398
|
Boolean(onEventChange);
|
|
399
|
+
const painted = variant === "month-bar" || variant === "all-day" || variant === "time-grid";
|
|
332
400
|
|
|
333
401
|
const button = (
|
|
334
402
|
<button
|
|
@@ -336,9 +404,11 @@ export function EventCalendar({
|
|
|
336
404
|
draggable={false}
|
|
337
405
|
className={className}
|
|
338
406
|
style={
|
|
339
|
-
|
|
340
|
-
? eventColorStyle(event.color,
|
|
341
|
-
:
|
|
407
|
+
variant === "agenda"
|
|
408
|
+
? eventColorStyle(event.color, "wash")
|
|
409
|
+
: painted
|
|
410
|
+
? eventColorStyle(event.color, resolvedView === "month" ? "bar" : "block")
|
|
411
|
+
: undefined
|
|
342
412
|
}
|
|
343
413
|
onClick={() => {
|
|
344
414
|
if (dragRef.current?.active) return;
|
|
@@ -385,24 +455,46 @@ export function EventCalendar({
|
|
|
385
455
|
aria-label="Event calendar"
|
|
386
456
|
onKeyDown={handleKeyDown}
|
|
387
457
|
>
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
458
|
+
{renderToolbar ? (
|
|
459
|
+
renderToolbar({
|
|
460
|
+
view: resolvedView,
|
|
461
|
+
date: resolvedDate,
|
|
462
|
+
title,
|
|
463
|
+
views,
|
|
464
|
+
locale,
|
|
465
|
+
preferences: resolvedPrefs,
|
|
466
|
+
onViewChange: (next) => setView(next),
|
|
467
|
+
onPrev: () => go(-1),
|
|
468
|
+
onNext: () => go(1),
|
|
469
|
+
onToday: () => setDate(new Date()),
|
|
470
|
+
onDateChange: (next) => setDate(next),
|
|
471
|
+
onPreferencesChange: (next) => setPrefs(next),
|
|
472
|
+
})
|
|
473
|
+
) : (
|
|
474
|
+
<SchedulerToolbar
|
|
475
|
+
title={title}
|
|
476
|
+
onPrev={() => go(-1)}
|
|
477
|
+
onNext={() => go(1)}
|
|
478
|
+
onToday={() => setDate(new Date())}
|
|
479
|
+
menuLabel={CALENDAR_VIEW_LABEL[resolvedView]}
|
|
480
|
+
menu={views.map((item) => (
|
|
481
|
+
<DropdownMenuItem key={item} onClick={() => setView(item)}>
|
|
482
|
+
{CALENDAR_VIEW_LABEL[item]}
|
|
483
|
+
</DropdownMenuItem>
|
|
484
|
+
))}
|
|
485
|
+
trailing={
|
|
486
|
+
<>
|
|
487
|
+
{toolbarTrailing}
|
|
488
|
+
{showPreferences ? (
|
|
489
|
+
<SchedulerPreferencesMenu
|
|
490
|
+
value={resolvedPrefs}
|
|
491
|
+
onChange={(next) => setPrefs(next)}
|
|
492
|
+
/>
|
|
493
|
+
) : null}
|
|
494
|
+
</>
|
|
495
|
+
}
|
|
496
|
+
/>
|
|
497
|
+
)}
|
|
406
498
|
|
|
407
499
|
{resolvedView === "month" ? (
|
|
408
500
|
<MonthGrid
|
|
@@ -425,7 +517,7 @@ export function EventCalendar({
|
|
|
425
517
|
locale={locale}
|
|
426
518
|
hour12={hour12}
|
|
427
519
|
onSlotClick={openSlot}
|
|
428
|
-
|
|
520
|
+
renderEventTrigger={renderEventTrigger}
|
|
429
521
|
/>
|
|
430
522
|
) : (
|
|
431
523
|
<TimeGrid
|
|
@@ -446,31 +538,33 @@ export function EventCalendar({
|
|
|
446
538
|
|
|
447
539
|
<ResourceLegend resources={resources} hiddenIds={hiddenIds} onToggle={toggleResource} />
|
|
448
540
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
541
|
+
{showEventEditor ? (
|
|
542
|
+
<EventEditor
|
|
543
|
+
open={draft != null}
|
|
544
|
+
onOpenChange={(open) => {
|
|
545
|
+
if (!open) setDraft(null);
|
|
546
|
+
}}
|
|
547
|
+
draft={draft}
|
|
548
|
+
resources={resources}
|
|
549
|
+
readOnly={editorLocked}
|
|
550
|
+
onSave={(event) => {
|
|
551
|
+
if (event.id && sourceEvent(events, event.id)) {
|
|
552
|
+
const source = sourceEvent(events, event.id)!;
|
|
553
|
+
onEventChange?.(source, event);
|
|
554
|
+
} else {
|
|
555
|
+
onEventCreate?.(event);
|
|
556
|
+
}
|
|
557
|
+
}}
|
|
558
|
+
onDelete={
|
|
559
|
+
onEventDelete
|
|
560
|
+
? (id) => {
|
|
561
|
+
const source = sourceEvent(events, id);
|
|
562
|
+
if (source) onEventDelete(source);
|
|
563
|
+
}
|
|
564
|
+
: undefined
|
|
463
565
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
onEventDelete
|
|
467
|
-
? (id) => {
|
|
468
|
-
const source = sourceEvent(events, id);
|
|
469
|
-
if (source) onEventDelete(source);
|
|
470
|
-
}
|
|
471
|
-
: undefined
|
|
472
|
-
}
|
|
473
|
-
/>
|
|
566
|
+
/>
|
|
567
|
+
) : null}
|
|
474
568
|
</SchedulerShell>
|
|
475
569
|
);
|
|
476
570
|
}
|
|
@@ -500,7 +594,7 @@ function MonthGrid({
|
|
|
500
594
|
event: NormalizedEvent,
|
|
501
595
|
className: string,
|
|
502
596
|
children: ReactNode,
|
|
503
|
-
options?: { resize?: "ns" },
|
|
597
|
+
options?: { resize?: "ns"; variant: EventCalendarEventVariant },
|
|
504
598
|
) => ReactNode;
|
|
505
599
|
}) {
|
|
506
600
|
const weeks = chunkWeeks(getMonthGrid(date, weekStartsOn));
|
|
@@ -585,6 +679,7 @@ function MonthGrid({
|
|
|
585
679
|
slot.event,
|
|
586
680
|
"flex h-5 w-full items-center truncate rounded-md px-1.5 text-[11px] font-semibold leading-none",
|
|
587
681
|
slot.event.title,
|
|
682
|
+
{ variant: "month-bar" },
|
|
588
683
|
)}
|
|
589
684
|
</div>
|
|
590
685
|
),
|
|
@@ -628,6 +723,7 @@ function MonthGrid({
|
|
|
628
723
|
</span>
|
|
629
724
|
<span className="min-w-0 truncate font-medium">{event.title}</span>
|
|
630
725
|
</>,
|
|
726
|
+
{ variant: "month-timed" },
|
|
631
727
|
)}
|
|
632
728
|
</div>
|
|
633
729
|
))}
|
|
@@ -698,7 +794,7 @@ function TimeGrid({
|
|
|
698
794
|
event: NormalizedEvent,
|
|
699
795
|
className: string,
|
|
700
796
|
children: ReactNode,
|
|
701
|
-
options?: { resize?: "ns" },
|
|
797
|
+
options?: { resize?: "ns"; variant: EventCalendarEventVariant },
|
|
702
798
|
) => ReactNode;
|
|
703
799
|
}) {
|
|
704
800
|
const days =
|
|
@@ -756,6 +852,7 @@ function TimeGrid({
|
|
|
756
852
|
event,
|
|
757
853
|
"flex h-5 w-full items-center truncate rounded-md px-1.5 text-[11px] font-semibold",
|
|
758
854
|
event.title,
|
|
855
|
+
{ variant: "all-day" },
|
|
759
856
|
)}
|
|
760
857
|
</div>
|
|
761
858
|
))}
|
|
@@ -824,7 +921,7 @@ function TimeGrid({
|
|
|
824
921
|
{formatTime(item.event.start, locale, hour12)}
|
|
825
922
|
</span>
|
|
826
923
|
</>,
|
|
827
|
-
{ resize: "ns" },
|
|
924
|
+
{ resize: "ns", variant: "time-grid" },
|
|
828
925
|
)}
|
|
829
926
|
</div>
|
|
830
927
|
))}
|
|
@@ -852,7 +949,7 @@ function AgendaList({
|
|
|
852
949
|
locale,
|
|
853
950
|
hour12,
|
|
854
951
|
onSlotClick,
|
|
855
|
-
|
|
952
|
+
renderEventTrigger,
|
|
856
953
|
}: {
|
|
857
954
|
date: Date;
|
|
858
955
|
events: NormalizedEvent[];
|
|
@@ -860,7 +957,12 @@ function AgendaList({
|
|
|
860
957
|
locale?: string;
|
|
861
958
|
hour12: boolean;
|
|
862
959
|
onSlotClick?: EventCalendarProps["onSlotClick"];
|
|
863
|
-
|
|
960
|
+
renderEventTrigger: (
|
|
961
|
+
event: NormalizedEvent,
|
|
962
|
+
className: string,
|
|
963
|
+
children: ReactNode,
|
|
964
|
+
options?: { resize?: "ns"; variant: EventCalendarEventVariant },
|
|
965
|
+
) => ReactNode;
|
|
864
966
|
}) {
|
|
865
967
|
const days = filterWeekendDays(agendaDays(date, AGENDA_LENGTH), showWeekends);
|
|
866
968
|
|
|
@@ -899,24 +1001,24 @@ function AgendaList({
|
|
|
899
1001
|
<p className="pt-2 text-xs text-muted-foreground">No events</p>
|
|
900
1002
|
) : (
|
|
901
1003
|
items.map((event) => (
|
|
902
|
-
<
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
</
|
|
1004
|
+
<div key={occurrenceKey(event)} data-scheduler-day={isoDay(day)}>
|
|
1005
|
+
{renderEventTrigger(
|
|
1006
|
+
event,
|
|
1007
|
+
"flex w-full min-w-0 items-start gap-2 rounded-md px-2 py-1.5 text-left hover:bg-muted/40",
|
|
1008
|
+
<>
|
|
1009
|
+
<span className="mt-1.5 size-2 shrink-0 rounded-full" style={eventColorStyle(event.color, "dot")} />
|
|
1010
|
+
<span className="min-w-0">
|
|
1011
|
+
<span className="block truncate text-sm font-semibold text-foreground">{event.title}</span>
|
|
1012
|
+
<span className="text-[11px] text-muted-foreground">
|
|
1013
|
+
{event.allDay || isMonthBar(event)
|
|
1014
|
+
? "All day"
|
|
1015
|
+
: `${formatTime(event.start, locale, hour12)} – ${formatTime(event.end, locale, hour12)}`}
|
|
1016
|
+
</span>
|
|
1017
|
+
</span>
|
|
1018
|
+
</>,
|
|
1019
|
+
{ variant: "agenda" },
|
|
1020
|
+
)}
|
|
1021
|
+
</div>
|
|
920
1022
|
))
|
|
921
1023
|
)}
|
|
922
1024
|
</div>
|
|
@@ -48,6 +48,11 @@ export type EventTimelineProps = {
|
|
|
48
48
|
onEventChange?: (event: SchedulerEvent, next: Partial<SchedulerEvent>) => void;
|
|
49
49
|
onEventCreate?: (event: SchedulerEvent) => void;
|
|
50
50
|
onEventDelete?: (event: SchedulerEvent) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Built-in create/edit dialog. Defaults to true.
|
|
53
|
+
* Set false when the host owns create/edit.
|
|
54
|
+
*/
|
|
55
|
+
showEventEditor?: boolean;
|
|
51
56
|
className?: string;
|
|
52
57
|
};
|
|
53
58
|
|
|
@@ -103,6 +108,7 @@ export function EventTimeline({
|
|
|
103
108
|
onEventChange,
|
|
104
109
|
onEventCreate,
|
|
105
110
|
onEventDelete,
|
|
111
|
+
showEventEditor = true,
|
|
106
112
|
className,
|
|
107
113
|
}: EventTimelineProps) {
|
|
108
114
|
const [scale, setScale] = useControllableState<TimelineScale>({
|
|
@@ -153,6 +159,7 @@ export function EventTimeline({
|
|
|
153
159
|
function openEvent(event: NormalizedEvent) {
|
|
154
160
|
const source = sourceEvent(events, event.id);
|
|
155
161
|
if (source) onEventClick?.(source);
|
|
162
|
+
if (!showEventEditor) return;
|
|
156
163
|
setDraft(source ? draftFromSource(source) : {
|
|
157
164
|
id: event.id,
|
|
158
165
|
title: event.title,
|
|
@@ -328,30 +335,32 @@ export function EventTimeline({
|
|
|
328
335
|
onToggle={toggleResource}
|
|
329
336
|
/>
|
|
330
337
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
338
|
+
{showEventEditor ? (
|
|
339
|
+
<EventEditor
|
|
340
|
+
open={draft != null}
|
|
341
|
+
onOpenChange={(open) => {
|
|
342
|
+
if (!open) setDraft(null);
|
|
343
|
+
}}
|
|
344
|
+
draft={draft}
|
|
345
|
+
resources={resources}
|
|
346
|
+
readOnly={editorLocked}
|
|
347
|
+
onSave={(event) => {
|
|
348
|
+
if (event.id && sourceEvent(events, event.id)) {
|
|
349
|
+
onEventChange?.(sourceEvent(events, event.id)!, event);
|
|
350
|
+
} else {
|
|
351
|
+
onEventCreate?.(event);
|
|
352
|
+
}
|
|
353
|
+
}}
|
|
354
|
+
onDelete={
|
|
355
|
+
onEventDelete
|
|
356
|
+
? (id) => {
|
|
357
|
+
const source = sourceEvent(events, id);
|
|
358
|
+
if (source) onEventDelete(source);
|
|
359
|
+
}
|
|
360
|
+
: undefined
|
|
344
361
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
onEventDelete
|
|
348
|
-
? (id) => {
|
|
349
|
-
const source = sourceEvent(events, id);
|
|
350
|
-
if (source) onEventDelete(source);
|
|
351
|
-
}
|
|
352
|
-
: undefined
|
|
353
|
-
}
|
|
354
|
-
/>
|
|
362
|
+
/>
|
|
363
|
+
) : null}
|
|
355
364
|
</SchedulerShell>
|
|
356
365
|
);
|
|
357
366
|
}
|
|
@@ -36,6 +36,16 @@ export const TIMELINE_SCALE_LABEL: Record<TimelineScale, string> = {
|
|
|
36
36
|
years: "Years",
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
export type SchedulerToolbarProps = {
|
|
40
|
+
title: ReactNode;
|
|
41
|
+
onPrev: () => void;
|
|
42
|
+
onNext: () => void;
|
|
43
|
+
onToday: () => void;
|
|
44
|
+
menuLabel: string;
|
|
45
|
+
menu: ReactNode;
|
|
46
|
+
trailing?: ReactNode;
|
|
47
|
+
};
|
|
48
|
+
|
|
39
49
|
export function SchedulerToolbar({
|
|
40
50
|
title,
|
|
41
51
|
onPrev,
|
|
@@ -44,15 +54,7 @@ export function SchedulerToolbar({
|
|
|
44
54
|
menuLabel,
|
|
45
55
|
menu,
|
|
46
56
|
trailing,
|
|
47
|
-
}: {
|
|
48
|
-
title: string;
|
|
49
|
-
onPrev: () => void;
|
|
50
|
-
onNext: () => void;
|
|
51
|
-
onToday: () => void;
|
|
52
|
-
menuLabel: string;
|
|
53
|
-
menu: ReactNode;
|
|
54
|
-
trailing?: ReactNode;
|
|
55
|
-
}) {
|
|
57
|
+
}: SchedulerToolbarProps) {
|
|
56
58
|
return (
|
|
57
59
|
<div
|
|
58
60
|
data-slot="scheduler-toolbar"
|
|
@@ -152,6 +152,75 @@ describe("EventCalendar", () => {
|
|
|
152
152
|
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
153
153
|
expect(onEventCreate).toHaveBeenCalledWith(expect.objectContaining({ title: "Focus time" }));
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
it("skips the built-in editor when showEventEditor is false", async () => {
|
|
157
|
+
const user = userEvent.setup();
|
|
158
|
+
const onEventClick = vi.fn();
|
|
159
|
+
const onSlotClick = vi.fn();
|
|
160
|
+
render(
|
|
161
|
+
<EventCalendar
|
|
162
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
163
|
+
events={events}
|
|
164
|
+
resources={resources}
|
|
165
|
+
locale="en-US"
|
|
166
|
+
showEventEditor={false}
|
|
167
|
+
onEventClick={onEventClick}
|
|
168
|
+
onSlotClick={onSlotClick}
|
|
169
|
+
/>,
|
|
170
|
+
);
|
|
171
|
+
await user.click(screen.getByText("Morning Run"));
|
|
172
|
+
expect(onEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: "run" }));
|
|
173
|
+
expect(screen.queryByRole("heading", { name: /event/i })).not.toBeInTheDocument();
|
|
174
|
+
|
|
175
|
+
await user.click(screen.getByRole("button", { name: "15" }));
|
|
176
|
+
expect(onSlotClick).toHaveBeenCalled();
|
|
177
|
+
expect(screen.queryByRole("heading", { name: "New event" })).not.toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("renders custom event chips and toolbar", async () => {
|
|
181
|
+
const user = userEvent.setup();
|
|
182
|
+
const onDateChange = vi.fn();
|
|
183
|
+
render(
|
|
184
|
+
<EventCalendar
|
|
185
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
186
|
+
events={events}
|
|
187
|
+
resources={resources}
|
|
188
|
+
locale="en-US"
|
|
189
|
+
showEventEditor={false}
|
|
190
|
+
showPreferences={false}
|
|
191
|
+
toolbarTrailing={<button type="button">Jump year</button>}
|
|
192
|
+
renderEvent={(event) => <span>★ {event.title}</span>}
|
|
193
|
+
renderToolbar={({ title, onToday }) => (
|
|
194
|
+
<div>
|
|
195
|
+
<p>{title}</p>
|
|
196
|
+
<button type="button" onClick={onToday}>
|
|
197
|
+
Jump today
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
onDateChange={onDateChange}
|
|
202
|
+
/>,
|
|
203
|
+
);
|
|
204
|
+
expect(screen.getByText("★ Morning Run")).toBeInTheDocument();
|
|
205
|
+
expect(screen.queryByRole("button", { name: "Preferences" })).not.toBeInTheDocument();
|
|
206
|
+
expect(screen.queryByRole("button", { name: "Jump year" })).not.toBeInTheDocument();
|
|
207
|
+
await user.click(screen.getByRole("button", { name: "Jump today" }));
|
|
208
|
+
expect(onDateChange).toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("keeps toolbarTrailing on the default toolbar", () => {
|
|
212
|
+
render(
|
|
213
|
+
<EventCalendar
|
|
214
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
215
|
+
events={events}
|
|
216
|
+
locale="en-US"
|
|
217
|
+
showPreferences={false}
|
|
218
|
+
toolbarTrailing={<button type="button">Jump year</button>}
|
|
219
|
+
/>,
|
|
220
|
+
);
|
|
221
|
+
expect(screen.getByRole("button", { name: "Jump year" })).toBeInTheDocument();
|
|
222
|
+
expect(screen.getByRole("heading", { name: /August 2026/i })).toBeInTheDocument();
|
|
223
|
+
});
|
|
155
224
|
});
|
|
156
225
|
|
|
157
226
|
describe("EventTimeline", () => {
|
|
@@ -191,4 +260,21 @@ describe("EventTimeline", () => {
|
|
|
191
260
|
expect(screen.getByText("API V3 Development")).toBeInTheDocument();
|
|
192
261
|
expect(screen.getByText("Mobile App UI/UX")).toBeInTheDocument();
|
|
193
262
|
});
|
|
263
|
+
|
|
264
|
+
it("skips the built-in editor when showEventEditor is false", async () => {
|
|
265
|
+
const user = userEvent.setup();
|
|
266
|
+
const onEventClick = vi.fn();
|
|
267
|
+
render(
|
|
268
|
+
<EventTimeline
|
|
269
|
+
defaultDate={new Date(2026, 7, 1)}
|
|
270
|
+
events={timelineEvents}
|
|
271
|
+
resources={timelineResources}
|
|
272
|
+
showEventEditor={false}
|
|
273
|
+
onEventClick={onEventClick}
|
|
274
|
+
/>,
|
|
275
|
+
);
|
|
276
|
+
await user.click(screen.getByText("API V3 Development"));
|
|
277
|
+
expect(onEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: "api" }));
|
|
278
|
+
expect(screen.queryByRole("heading", { name: /event/i })).not.toBeInTheDocument();
|
|
279
|
+
});
|
|
194
280
|
});
|
package/src/index.test.ts
CHANGED
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
Button,
|
|
6
6
|
ChartContainer,
|
|
7
7
|
ChartDataGrid,
|
|
8
|
+
EventCalendar,
|
|
8
9
|
MapChart,
|
|
10
|
+
SchedulerToolbar,
|
|
9
11
|
mercator,
|
|
10
12
|
} from "./index";
|
|
11
13
|
|
|
@@ -17,6 +19,8 @@ describe("@spatika/react public exports", () => {
|
|
|
17
19
|
expect(ChartDataGrid).toBeTypeOf("function");
|
|
18
20
|
expect(MapChart).toBeTypeOf("function");
|
|
19
21
|
expect(BarChart3D).toBeTypeOf("function");
|
|
22
|
+
expect(EventCalendar).toBeTypeOf("function");
|
|
23
|
+
expect(SchedulerToolbar).toBeTypeOf("function");
|
|
20
24
|
expect(mercator([0, 0])[0]).toBeCloseTo(0.5);
|
|
21
25
|
});
|
|
22
26
|
});
|
package/src/index.ts
CHANGED
|
@@ -486,6 +486,9 @@ export {
|
|
|
486
486
|
export {
|
|
487
487
|
EventCalendar,
|
|
488
488
|
type EventCalendarProps,
|
|
489
|
+
type EventCalendarEventVariant,
|
|
490
|
+
type EventCalendarEventRenderContext,
|
|
491
|
+
type EventCalendarToolbarContext,
|
|
489
492
|
} from "./composites/EventCalendar";
|
|
490
493
|
export {
|
|
491
494
|
EventTimeline,
|
|
@@ -495,12 +498,19 @@ export {
|
|
|
495
498
|
EventEditor,
|
|
496
499
|
type EventEditorDraft,
|
|
497
500
|
} from "./composites/EventEditor";
|
|
501
|
+
export {
|
|
502
|
+
SchedulerToolbar,
|
|
503
|
+
SchedulerPreferencesMenu,
|
|
504
|
+
CALENDAR_VIEW_LABEL,
|
|
505
|
+
type SchedulerToolbarProps,
|
|
506
|
+
} from "./composites/scheduler-ui";
|
|
498
507
|
export {
|
|
499
508
|
DEFAULT_SCHEDULER_PREFERENCES,
|
|
500
509
|
SCHEDULER_COLOR_TONES,
|
|
501
510
|
} from "./lib/scheduler";
|
|
502
511
|
export type {
|
|
503
512
|
CalendarView,
|
|
513
|
+
NormalizedEvent,
|
|
504
514
|
SchedulerColorTone,
|
|
505
515
|
SchedulerEvent,
|
|
506
516
|
SchedulerPreferences,
|
|
@@ -65,6 +65,19 @@ describe("scheduler dates", () => {
|
|
|
65
65
|
expect(occupiesDay(birthday, new Date(2026, 7, 4))).toBe(true);
|
|
66
66
|
expect(occupiesDay(birthday, new Date(2026, 7, 5))).toBe(false);
|
|
67
67
|
});
|
|
68
|
+
|
|
69
|
+
it("preserves host data on normalized events", () => {
|
|
70
|
+
const [event] = normalizeEvents([
|
|
71
|
+
{
|
|
72
|
+
id: "run",
|
|
73
|
+
title: "Morning Run",
|
|
74
|
+
start: "2026-08-03T07:00:00",
|
|
75
|
+
end: "2026-08-03T07:45:00",
|
|
76
|
+
data: { kind: "CATCHUP" },
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
expect(event.data).toEqual({ kind: "CATCHUP" });
|
|
80
|
+
});
|
|
68
81
|
});
|
|
69
82
|
|
|
70
83
|
describe("scheduler layout", () => {
|