@lavalogic/scoria 0.40.16 → 0.40.17
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.
|
@@ -6,7 +6,7 @@ import Button from "../Button.svelte";
|
|
|
6
6
|
import Icon from "../Icon.svelte";
|
|
7
7
|
import LoadingOverlay from "../LoadingOverlay.svelte";
|
|
8
8
|
import { CalendarViewMode } from "./AppointmentCalendarProps.js";
|
|
9
|
-
let { doors, appointments, selectedDate, viewMode, ondatechange, onviewmodechange, ondragcreate, onappointmentclick, onappointmentmove, startHour = 0, endHour = 24, slotIntervalMinutes = 15, isLoading = false, headerExtra } = $props();
|
|
9
|
+
let { doors, appointments, selectedDate, viewMode, ondatechange, onviewmodechange, ondragcreate, onappointmentclick, onappointmentmove, startHour = 0, endHour = 24, slotIntervalMinutes = 15, availability, centreHour = 12, isLoading = false, headerExtra } = $props();
|
|
10
10
|
/**
|
|
11
11
|
* `selectedDate` is always read by the component, never written. The
|
|
12
12
|
* runtime type may be either a plain `Date` or a `SvelteDate`; both
|
|
@@ -92,6 +92,34 @@ const timeSlots = $derived.by(() => {
|
|
|
92
92
|
}
|
|
93
93
|
return slots;
|
|
94
94
|
});
|
|
95
|
+
// ---- Availability shading ----
|
|
96
|
+
/**
|
|
97
|
+
* Whether the given slot is outside the door's available windows.
|
|
98
|
+
*
|
|
99
|
+
* A door absent from {@link availability} (or no `availability` map at
|
|
100
|
+
* all) is treated as fully available, so nothing is shaded until the
|
|
101
|
+
* caller supplies windows for that door.
|
|
102
|
+
*/
|
|
103
|
+
function isSlotUnavailable(doorId, slotIndex) {
|
|
104
|
+
if (!availability) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const windows = availability.get(doorId);
|
|
108
|
+
if (!windows) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const slot = timeSlots[slotIndex];
|
|
112
|
+
if (!slot) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const minutes = slot.hour * 60 + slot.minute;
|
|
116
|
+
for (const window of windows) {
|
|
117
|
+
if (minutes >= window.startMinutes && minutes < window.endMinutes) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
95
123
|
/**
|
|
96
124
|
* Build a `YYYY-MM-DD` key for date bucketing. Avoids `toISOString`
|
|
97
125
|
* timezone conversions that would smear appointments across days.
|
|
@@ -202,6 +230,10 @@ function onSlotMouseDown(doorId, slotIndex, e) {
|
|
|
202
230
|
if (!ondragcreate || appointmentDragStarted) {
|
|
203
231
|
return;
|
|
204
232
|
}
|
|
233
|
+
// Unavailable slots cannot seed a new appointment.
|
|
234
|
+
if (isSlotUnavailable(doorId, slotIndex)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
205
237
|
isDragging = true;
|
|
206
238
|
dragDoorId = doorId;
|
|
207
239
|
dragStartSlotIndex = slotIndex;
|
|
@@ -221,6 +253,10 @@ function onSlotMouseEnter(doorId, slotIndex, e) {
|
|
|
221
253
|
}
|
|
222
254
|
// Appointment move drag
|
|
223
255
|
if (appointmentDragStarted) {
|
|
256
|
+
// Freeze the drop target rather than land it on an unavailable slot.
|
|
257
|
+
if (isSlotUnavailable(doorId, slotIndex)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
224
260
|
isDraggingAppointment = true;
|
|
225
261
|
moveTargetDoorId = doorId;
|
|
226
262
|
moveTargetSlotIndex = slotIndex;
|
|
@@ -230,6 +266,10 @@ function onSlotMouseEnter(doorId, slotIndex, e) {
|
|
|
230
266
|
if (!isDragging || doorId !== dragDoorId) {
|
|
231
267
|
return;
|
|
232
268
|
}
|
|
269
|
+
// Keep the selection within the door's available windows.
|
|
270
|
+
if (isSlotUnavailable(doorId, slotIndex)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
233
273
|
dragEndSlotIndex = slotIndex;
|
|
234
274
|
}
|
|
235
275
|
function onSlotMouseUp() {
|
|
@@ -417,6 +457,38 @@ const viewModes = [
|
|
|
417
457
|
label: "Month"
|
|
418
458
|
}
|
|
419
459
|
];
|
|
460
|
+
// ---- Centre-on-hour scrolling ----
|
|
461
|
+
/** The whole hour whose row carries the `data-centre-row` marker. */
|
|
462
|
+
const centreRowHour = $derived(centreHour == null ? null : Math.floor(centreHour));
|
|
463
|
+
/** The scrollable calendar body; bound so we can centre it on the marker. */
|
|
464
|
+
let calendarBody = $state();
|
|
465
|
+
/**
|
|
466
|
+
* Whether the grid has been centred for the current spell in a
|
|
467
|
+
* day/week view. Plain (non-reactive) so re-centring only happens when
|
|
468
|
+
* the grid (re)appears, never while the user scrolls or data loads.
|
|
469
|
+
*/
|
|
470
|
+
let hasCentred = false;
|
|
471
|
+
$effect(() => {
|
|
472
|
+
// Re-arm whenever we leave the time-grid views so re-entry recentres.
|
|
473
|
+
if (viewMode !== CalendarViewMode.Day && viewMode !== CalendarViewMode.Week) {
|
|
474
|
+
hasCentred = false;
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (hasCentred || centreRowHour == null || !calendarBody) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const marker = calendarBody.querySelector("[data-centre-row]");
|
|
481
|
+
if (!marker) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
// Centre the marker row in the viewport via rect maths so nested
|
|
485
|
+
// sticky/relative offset parents don't skew the target.
|
|
486
|
+
const bodyRect = calendarBody.getBoundingClientRect();
|
|
487
|
+
const markerRect = marker.getBoundingClientRect();
|
|
488
|
+
const delta = markerRect.top + markerRect.height / 2 - (bodyRect.top + bodyRect.height / 2);
|
|
489
|
+
calendarBody.scrollTop += delta;
|
|
490
|
+
hasCentred = true;
|
|
491
|
+
});
|
|
420
492
|
</script>
|
|
421
493
|
|
|
422
494
|
<!-- A drag can end anywhere on the page (the pointer is often released outside
|
|
@@ -511,7 +583,10 @@ const viewModes = [
|
|
|
511
583
|
{/if}
|
|
512
584
|
|
|
513
585
|
<!-- Calendar body -->
|
|
514
|
-
<div
|
|
586
|
+
<div
|
|
587
|
+
class="calendar-body"
|
|
588
|
+
bind:this={calendarBody}
|
|
589
|
+
>
|
|
515
590
|
{#if isLoading}
|
|
516
591
|
<LoadingOverlay />
|
|
517
592
|
{/if}
|
|
@@ -590,6 +665,9 @@ const viewModes = [
|
|
|
590
665
|
class="time-label"
|
|
591
666
|
class:hour-start={slot.minute === 0}
|
|
592
667
|
class:row-hover={hoveredSlotIndex === slotIdx}
|
|
668
|
+
data-centre-row={slot.hour === centreRowHour && slot.minute === 0
|
|
669
|
+
? ''
|
|
670
|
+
: undefined}
|
|
593
671
|
>
|
|
594
672
|
{#if slot.minute === 0}
|
|
595
673
|
<span>{slot.label}</span>
|
|
@@ -608,6 +686,7 @@ const viewModes = [
|
|
|
608
686
|
class:drag-selected={isSlotInDragRange(door.id, slotIdx)}
|
|
609
687
|
class:move-target={isSlotInMoveTarget(door.id, slotIdx)}
|
|
610
688
|
class:row-hover={hoveredSlotIndex === slotIdx}
|
|
689
|
+
class:unavailable={isSlotUnavailable(door.id, slotIdx)}
|
|
611
690
|
onmousedown={(e) => {
|
|
612
691
|
onSlotMouseDown(door.id, slotIdx, e);
|
|
613
692
|
}}
|
|
@@ -858,6 +937,11 @@ const viewModes = [
|
|
|
858
937
|
border-bottom-color: #b4e2f0;
|
|
859
938
|
z-index: 2;
|
|
860
939
|
}
|
|
940
|
+
.slot-cell.unavailable {
|
|
941
|
+
cursor: not-allowed;
|
|
942
|
+
background-color: #e9ecf1;
|
|
943
|
+
background-image: repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(128, 150, 165, 0.18) 5px, rgba(128, 150, 165, 0.18) 10px);
|
|
944
|
+
}
|
|
861
945
|
|
|
862
946
|
.appointment-block {
|
|
863
947
|
position: absolute;
|
|
@@ -40,6 +40,16 @@ export interface DragSelection {
|
|
|
40
40
|
readonly dateFrom: Date;
|
|
41
41
|
readonly dateTo: Date;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* A span of available time on the currently displayed day, expressed in
|
|
45
|
+
* minutes from midnight (`startMinutes` inclusive, `endMinutes`
|
|
46
|
+
* exclusive). Consumed by {@link AppointmentCalendarProps.availability}
|
|
47
|
+
* to shade out the times a door is not bookable.
|
|
48
|
+
*/
|
|
49
|
+
export interface DoorAvailabilityWindow {
|
|
50
|
+
readonly startMinutes: number;
|
|
51
|
+
readonly endMinutes: number;
|
|
52
|
+
}
|
|
43
53
|
/**
|
|
44
54
|
* Arguments passed to {@link AppointmentCalendarProps.onappointmentmove}.
|
|
45
55
|
*
|
|
@@ -85,6 +95,25 @@ export interface AppointmentCalendarProps {
|
|
|
85
95
|
endHour?: number;
|
|
86
96
|
/** The slot interval in minutes (default: 15). */
|
|
87
97
|
slotIntervalMinutes?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Per-door availability for the currently displayed day, keyed by
|
|
100
|
+
* door (segment) id.
|
|
101
|
+
*
|
|
102
|
+
* When a door id is present, any slot whose start falls outside all
|
|
103
|
+
* of its {@link DoorAvailabilityWindow}s is shaded as unavailable and
|
|
104
|
+
* cannot be used to create or receive an appointment. A door absent
|
|
105
|
+
* from the map is treated as fully available (no shading); map an id
|
|
106
|
+
* to an empty array to mark a door wholly unavailable for the day.
|
|
107
|
+
*
|
|
108
|
+
* Omit the prop entirely to disable availability shading.
|
|
109
|
+
*/
|
|
110
|
+
availability?: ReadonlyMap<number, ReadonlyArray<DoorAvailabilityWindow>>;
|
|
111
|
+
/**
|
|
112
|
+
* Hour of the day (0–23) to vertically centre when the day/week grid
|
|
113
|
+
* first appears (default: 12, i.e. midday). Set to `null` to keep the
|
|
114
|
+
* grid scrolled to the top.
|
|
115
|
+
*/
|
|
116
|
+
centreHour?: number | null;
|
|
88
117
|
/** Whether the calendar is loading data. */
|
|
89
118
|
isLoading?: boolean;
|
|
90
119
|
/** Optional snippet for extra header content (e.g. buttons). */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { default as AccordionGroup } from './Components/AccordionGroup.svelte';
|
|
2
2
|
export { default as AppointmentCalendar } from './Components/AppointmentCalendar/AppointmentCalendar.svelte';
|
|
3
|
-
export type { AppointmentCalendarProps, AppointmentMoveArgs, CalendarAppointment, CalendarDoor, CalendarViewMode, DragSelection, } from './Components/AppointmentCalendar/AppointmentCalendarProps.js';
|
|
3
|
+
export type { AppointmentCalendarProps, AppointmentMoveArgs, CalendarAppointment, CalendarDoor, CalendarViewMode, DoorAvailabilityWindow, DragSelection, } from './Components/AppointmentCalendar/AppointmentCalendarProps.js';
|
|
4
4
|
export type { AccordionGroupButtonProps, AccordionGroupOption, } from './Components/AccordionGroupButtonProps.js';
|
|
5
5
|
export type { AccordionGroupProps } from './Components/AccordionGroupProps.js';
|
|
6
6
|
export { default as Action } from './Components/Action.svelte';
|