@lavalogic/scoria 0.37.44 → 0.37.46

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.
Files changed (239) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +46 -46
  3. package/dist/Components/AccordionGroupButton.svelte +76 -76
  4. package/dist/Components/Action.svelte +136 -136
  5. package/dist/Components/ActionProps.d.ts +2 -6
  6. package/dist/Components/AlertModal.svelte +195 -183
  7. package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +702 -702
  8. package/dist/Components/Base/BaseModal.svelte +125 -125
  9. package/dist/Components/Base/ContextWrapper.svelte +72 -72
  10. package/dist/Components/Base/Pagination.svelte +198 -198
  11. package/dist/Components/Base/Snippets.svelte +36 -36
  12. package/dist/Components/BigRadioSet.svelte +38 -38
  13. package/dist/Components/Bubble.svelte +101 -101
  14. package/dist/Components/Button.svelte +209 -209
  15. package/dist/Components/ButtonProps.d.ts +2 -6
  16. package/dist/Components/Checkbox.svelte +86 -86
  17. package/dist/Components/CollapsibleCard.svelte +78 -78
  18. package/dist/Components/Contexts/ContextMenu.svelte +164 -164
  19. package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
  20. package/dist/Components/Contexts/ContextMenuOption.svelte +60 -60
  21. package/dist/Components/Contexts/Toast.svelte +86 -83
  22. package/dist/Components/Contexts/Toast.svelte.d.ts +8 -5
  23. package/dist/Components/Contexts/ToastContainer.svelte +65 -65
  24. package/dist/Components/Contexts/Tooltip.svelte +37 -37
  25. package/dist/Components/DataMatrixIcon.svelte +112 -112
  26. package/dist/Components/DataMatrixIconProps.d.ts +3 -13
  27. package/dist/Components/DateInput.svelte +148 -148
  28. package/dist/Components/DatePicker.svelte +300 -300
  29. package/dist/Components/DesktopModal.svelte +90 -90
  30. package/dist/Components/Dial.svelte +204 -204
  31. package/dist/Components/DialProps.d.ts +1 -18
  32. package/dist/Components/DimensionInput.svelte +170 -170
  33. package/dist/Components/DragMenu/DragMenuHolder.svelte +119 -119
  34. package/dist/Components/DragMenu/DraggableCard.svelte +77 -77
  35. package/dist/Components/FileCard.svelte +24 -24
  36. package/dist/Components/FileUpload.svelte +236 -236
  37. package/dist/Components/GridInput.svelte +196 -200
  38. package/dist/Components/GridInputSet.svelte +29 -29
  39. package/dist/Components/HorizontalTabGroup.svelte +82 -82
  40. package/dist/Components/HorizontalTabGroupButton.svelte +44 -44
  41. package/dist/Components/Icon.svelte +128 -128
  42. package/dist/Components/IconProps.d.ts +2 -7
  43. package/dist/Components/Icons.js +357 -357
  44. package/dist/Components/LoadingAnimation.svelte +29 -29
  45. package/dist/Components/LoadingModal.svelte +45 -45
  46. package/dist/Components/LoadingOverlay.svelte +25 -25
  47. package/dist/Components/LoadingOverlayProps.d.ts +3 -7
  48. package/dist/Components/Modal.svelte +258 -258
  49. package/dist/Components/ModalProps.d.ts +0 -3
  50. package/dist/Components/MultiSelect.svelte +220 -220
  51. package/dist/Components/MultiSelectCommonProps.d.ts +6 -17
  52. package/dist/Components/Nav/Nav.svelte +110 -99
  53. package/dist/Components/Nav/NavProps.d.ts +4 -11
  54. package/dist/Components/Nav/NavServiceMenuList.svelte +43 -43
  55. package/dist/Components/Nav/NavTab.svelte +62 -62
  56. package/dist/Components/Nav/ServiceMenuItem.svelte +100 -100
  57. package/dist/Components/Nav/ServicesNav.svelte +80 -80
  58. package/dist/Components/NumberInput.svelte +157 -157
  59. package/dist/Components/PageHeading.svelte +25 -25
  60. package/dist/Components/PasswordInput.svelte +88 -88
  61. package/dist/Components/Portal.svelte +36 -36
  62. package/dist/Components/ProgressBubble.svelte +107 -108
  63. package/dist/Components/ProgressBubbleProps.d.ts +3 -3
  64. package/dist/Components/QuerySelect.svelte +366 -366
  65. package/dist/Components/QuerySelectProps.d.ts +4 -14
  66. package/dist/Components/SingleSelect.svelte +250 -250
  67. package/dist/Components/SingleSelectCommonProps.d.ts +3 -19
  68. package/dist/Components/StepButton.svelte +70 -70
  69. package/dist/Components/StepForm.svelte +140 -140
  70. package/dist/Components/Switch.svelte +69 -69
  71. package/dist/Components/Table/ARCHITECTURE.md +298 -0
  72. package/dist/Components/Table/Body/QuickSearchCell.svelte +683 -694
  73. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +51 -51
  74. package/dist/Components/Table/Body/Rows/ColumnList.svelte +59 -59
  75. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +456 -478
  76. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +79 -79
  77. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +185 -188
  78. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +124 -124
  79. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +120 -120
  80. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +230 -230
  81. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +105 -105
  82. package/dist/Components/Table/Body/Rows/Columns/SelectColumn.d.ts +12 -9
  83. package/dist/Components/Table/Body/Rows/Columns/SelectTableCell.d.ts +5 -5
  84. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +87 -87
  85. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +126 -126
  86. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +159 -159
  87. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +246 -246
  88. package/dist/Components/Table/Body/Rows/Columns/TableDatePickerProps.d.ts +4 -4
  89. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +216 -216
  90. package/dist/Components/Table/Body/Rows/Columns/TableQueryCell.d.ts +6 -5
  91. package/dist/Components/Table/Body/Rows/Columns/TableQueryColumn.d.ts +7 -6
  92. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +252 -257
  93. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +179 -179
  94. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +221 -229
  95. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte.d.ts +11 -11
  96. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +197 -197
  97. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +190 -190
  98. package/dist/Components/Table/Body/Rows/TableRow.svelte +290 -290
  99. package/dist/Components/Table/Body/TableBody.svelte +155 -155
  100. package/dist/Components/Table/ColumnDefinitions/ExampleItemColumnDefRepository.svelte.js +2 -3
  101. package/dist/Components/Table/ColumnFactory.svelte.d.ts +9 -11
  102. package/dist/Components/Table/ColumnFactory.svelte.js +110 -143
  103. package/dist/Components/Table/ColumnKindRegistry.d.ts +112 -0
  104. package/dist/Components/Table/ColumnKindRegistry.js +63 -0
  105. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +41 -41
  106. package/dist/Components/Table/Headers/SelectAllHeader.svelte +30 -30
  107. package/dist/Components/Table/Headers/TableHead.svelte +415 -413
  108. package/dist/Components/Table/Misc/ColumnPanel.svelte +94 -94
  109. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +112 -112
  110. package/dist/Components/Table/Misc/FilterInput.svelte +620 -644
  111. package/dist/Components/Table/Misc/FilterInput.svelte.d.ts +12 -12
  112. package/dist/Components/Table/Misc/FilterInputProps.d.ts +2 -2
  113. package/dist/Components/Table/Misc/FilterPanel.svelte +257 -257
  114. package/dist/Components/Table/Misc/FilterPanelModal.svelte +30 -30
  115. package/dist/Components/Table/Misc/SidePanel.svelte +35 -35
  116. package/dist/Components/Table/Misc/TableFooter.svelte +311 -311
  117. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +125 -125
  118. package/dist/Components/Table/Misc/TableSidebar.svelte +157 -157
  119. package/dist/Components/Table/Misc/ToolboxPanel.svelte +100 -100
  120. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +107 -107
  121. package/dist/Components/Table/NestedTable.svelte +130 -133
  122. package/dist/Components/Table/SubApis.svelte.d.ts +1 -5
  123. package/dist/Components/Table/SubApis.svelte.js +33 -32
  124. package/dist/Components/Table/Table.svelte +692 -745
  125. package/dist/Components/Table/TableHelpers.svelte.d.ts +6 -19
  126. package/dist/Components/Table/TableHelpers.svelte.js +6 -14
  127. package/dist/Components/Table/Types/Columns/Column.d.ts +2 -2
  128. package/dist/Components/Table/Types/Columns/DateColumn.d.ts +3 -3
  129. package/dist/Components/Table/Types/Columns/DateTableCell.d.ts +7 -7
  130. package/dist/Components/Table/Types/Columns/Definitions/Accessors/AccessorDef.svelte.d.ts +7 -8
  131. package/dist/Components/Table/Types/Columns/Definitions/Accessors/AccessorDef.svelte.js +4 -5
  132. package/dist/Components/Table/Types/Columns/Definitions/Accessors/CheckboxDef.svelte.d.ts +11 -14
  133. package/dist/Components/Table/Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js +3 -4
  134. package/dist/Components/Table/Types/Columns/Definitions/Accessors/DateInputDef.svelte.d.ts +7 -11
  135. package/dist/Components/Table/Types/Columns/Definitions/Accessors/DateInputDef.svelte.js +3 -3
  136. package/dist/Components/Table/Types/Columns/Definitions/Accessors/NumberInputDef.svelte.d.ts +13 -18
  137. package/dist/Components/Table/Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js +4 -5
  138. package/dist/Components/Table/Types/Columns/Definitions/Accessors/QueryDef.svelte.d.ts +5 -5
  139. package/dist/Components/Table/Types/Columns/Definitions/Accessors/QueryDef.svelte.js +2 -2
  140. package/dist/Components/Table/Types/Columns/Definitions/Accessors/SelectDef.svelte.d.ts +15 -12
  141. package/dist/Components/Table/Types/Columns/Definitions/Accessors/SelectDef.svelte.js +4 -5
  142. package/dist/Components/Table/Types/Columns/Definitions/Accessors/TextInputDef.svelte.d.ts +8 -12
  143. package/dist/Components/Table/Types/Columns/Definitions/Accessors/TextInputDef.svelte.js +3 -3
  144. package/dist/Components/Table/Types/Columns/Definitions/Actions/ActionsDef.svelte.d.ts +6 -6
  145. package/dist/Components/Table/Types/Columns/Definitions/Actions/ActionsDef.svelte.js +3 -3
  146. package/dist/Components/Table/Types/Columns/Definitions/ColumnDef.svelte.d.ts +49 -16
  147. package/dist/Components/Table/Types/Columns/Definitions/ColumnDef.svelte.js +27 -5
  148. package/dist/Components/Table/Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.d.ts +7 -7
  149. package/dist/Components/Table/Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.js +4 -4
  150. package/dist/Components/Table/Types/Columns/Definitions/Display/BubbleDef.svelte.d.ts +5 -5
  151. package/dist/Components/Table/Types/Columns/Definitions/Display/BubbleDef.svelte.js +2 -2
  152. package/dist/Components/Table/Types/Columns/Definitions/Display/DisplayDef.svelte.d.ts +5 -5
  153. package/dist/Components/Table/Types/Columns/Definitions/Display/DisplayDef.svelte.js +2 -2
  154. package/dist/Components/Table/Types/Columns/Definitions/Display/ProgressBarDef.svelte.d.ts +5 -5
  155. package/dist/Components/Table/Types/Columns/Definitions/Display/ProgressBarDef.svelte.js +2 -2
  156. package/dist/Components/Table/Types/Columns/Definitions/Display/ValidityDef.svelte.d.ts +5 -5
  157. package/dist/Components/Table/Types/Columns/Definitions/Display/ValidityDef.svelte.js +2 -2
  158. package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.d.ts +11 -19
  159. package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.js +4 -8
  160. package/dist/Components/Table/Types/Columns/Definitions/FilterOnly/FilterOnlyDef.svelte.d.ts +6 -7
  161. package/dist/Components/Table/Types/Columns/Definitions/FilterOnly/FilterOnlyDef.svelte.js +3 -4
  162. package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.d.ts +9 -9
  163. package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.js +4 -4
  164. package/dist/Components/Table/Types/Columns/ExpandTableCell.d.ts +5 -5
  165. package/dist/Components/Table/Types/Columns/ProgressBarTableCell.d.ts +6 -6
  166. package/dist/Components/Table/Types/Columns/TableCell.d.ts +7 -6
  167. package/dist/Components/Table/Types/Context/ClipboardState.svelte.d.ts +98 -0
  168. package/dist/Components/Table/Types/Context/ClipboardState.svelte.js +290 -0
  169. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.d.ts +17 -45
  170. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.js +17 -45
  171. package/dist/Components/Table/Types/Context/FilteringState.svelte.d.ts +9 -20
  172. package/dist/Components/Table/Types/Context/FilteringState.svelte.js +9 -20
  173. package/dist/Components/Table/Types/Context/FocusState.svelte.d.ts +23 -42
  174. package/dist/Components/Table/Types/Context/FocusState.svelte.js +32 -59
  175. package/dist/Components/Table/Types/Context/ITable.d.ts +5 -8
  176. package/dist/Components/Table/Types/Context/PreferencesState.svelte.d.ts +29 -56
  177. package/dist/Components/Table/Types/Context/PreferencesState.svelte.js +28 -55
  178. package/dist/Components/Table/Types/Context/QueryValuesState.svelte.d.ts +50 -0
  179. package/dist/Components/Table/Types/Context/QueryValuesState.svelte.js +62 -0
  180. package/dist/Components/Table/Types/Context/SelectionState.svelte.d.ts +17 -28
  181. package/dist/Components/Table/Types/Context/SelectionState.svelte.js +17 -28
  182. package/dist/Components/Table/Types/Context/SortingState.svelte.d.ts +15 -29
  183. package/dist/Components/Table/Types/Context/SortingState.svelte.js +17 -31
  184. package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +212 -325
  185. package/dist/Components/Table/Types/Context/TableContext.svelte.js +292 -706
  186. package/dist/Components/Table/Types/Context/VirtualisationState.svelte.d.ts +62 -79
  187. package/dist/Components/Table/Types/Context/VirtualisationState.svelte.js +54 -73
  188. package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.d.ts +16 -27
  189. package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.js +16 -27
  190. package/dist/Components/Table/Types/DataRepository/IDataRepository.d.ts +12 -15
  191. package/dist/Components/Table/Types/DataRepository/TableDataRepository.svelte.d.ts +15 -30
  192. package/dist/Components/Table/Types/DataRepository/TableDataRepository.svelte.js +16 -28
  193. package/dist/Components/Table/Types/Header.d.ts +4 -4
  194. package/dist/Components/Table/Types/HeaderGroup.d.ts +5 -5
  195. package/dist/Components/Table/Types/Public/index.d.ts +2 -4
  196. package/dist/Components/Table/Types/Public/index.js +2 -4
  197. package/dist/Components/Table/clipboardEscape.d.ts +32 -0
  198. package/dist/Components/Table/clipboardEscape.js +45 -0
  199. package/dist/Components/Table/createTable.svelte.d.ts +8 -18
  200. package/dist/Components/Table/createTable.svelte.js +19 -47
  201. package/dist/Components/Test/TestContextConsumer.svelte +13 -13
  202. package/dist/Components/TextArea.svelte +130 -130
  203. package/dist/Components/TextInput.svelte +176 -176
  204. package/dist/Components/ThreeStateRadio.svelte +131 -131
  205. package/dist/Components/Touch/BoolCard.svelte +45 -45
  206. package/dist/Components/Touch/DateModal.svelte +209 -209
  207. package/dist/Components/Touch/EditCard.svelte +59 -59
  208. package/dist/Components/Touch/InfoCard.svelte +31 -31
  209. package/dist/Components/Touch/InfoTextCard.svelte +28 -28
  210. package/dist/Components/Touch/ModalExplanation.svelte +17 -17
  211. package/dist/Components/Touch/NumberKeyboard.svelte +225 -225
  212. package/dist/Components/Touch/NumberModal.svelte +79 -79
  213. package/dist/Components/Touch/PlusMinusCard.svelte +46 -46
  214. package/dist/Components/Touch/SelectModal.svelte +123 -123
  215. package/dist/Components/Touch/SummaryCard.svelte +93 -93
  216. package/dist/Components/Touch/TextAreaModal.svelte +95 -95
  217. package/dist/Components/Touch/TextModal.svelte +106 -106
  218. package/dist/Components/Touch/TouchCard.svelte +33 -33
  219. package/dist/Components/Touch/TouchModal.svelte +274 -274
  220. package/dist/Components/Touch/UtilityButton.svelte +109 -109
  221. package/dist/Components/VerticalTabGroup.svelte +79 -79
  222. package/dist/Components/VerticalTabGroupButton.svelte +58 -58
  223. package/dist/Contexts/ToastContext.svelte.js +3 -4
  224. package/dist/Helpers/Datamatrix.d.ts +19 -19
  225. package/dist/Helpers/Datamatrix.js +291 -291
  226. package/dist/Helpers/Helpers.svelte.d.ts +47 -4
  227. package/dist/Helpers/Helpers.svelte.js +97 -4
  228. package/dist/Helpers/dropdownPortal.d.ts +26 -41
  229. package/dist/Helpers/dropdownPortal.js +62 -116
  230. package/dist/Types/Examples/ExampleModal.svelte +35 -35
  231. package/dist/index.d.ts +1 -2
  232. package/dist/index.js +1 -1
  233. package/dist/scss/_basics.scss +12 -12
  234. package/dist/scss/_colours.scss +134 -134
  235. package/dist/scss/_mixins.scss +3358 -3360
  236. package/dist/scss/_motion.scss +57 -57
  237. package/dist/scss/_sizing.scss +85 -85
  238. package/dist/scss/_transitions.scss +8 -8
  239. package/package.json +8 -6
@@ -1,704 +1,704 @@
1
- <svelte:options runes />
2
-
3
- <script lang="ts">
4
- import { Colour } from '../../scss/colours.js';
5
- import { Size } from '../../Types/Internal/Size.js';
6
- import Button from '../Button.svelte';
7
- import Icon from '../Icon.svelte';
8
- import LoadingOverlay from '../LoadingOverlay.svelte';
9
- import { CalendarViewMode } from './AppointmentCalendarProps.js';
10
- import type {
11
- AppointmentCalendarProps,
12
- CalendarAppointment,
13
- DragSelection,
14
- } from './AppointmentCalendarProps.js';
15
-
16
- let {
17
- doors,
18
- appointments,
19
- selectedDate,
20
- viewMode,
21
- ondatechange,
22
- onviewmodechange,
23
- ondragcreate,
24
- onappointmentclick,
25
- onappointmentmove,
26
- startHour = 0,
27
- endHour = 24,
28
- slotIntervalMinutes = 15,
29
- isLoading = false,
30
- headerExtra,
31
- }: AppointmentCalendarProps = $props();
32
-
33
- /**
34
- * `selectedDate` is always read by the component, never written. The
35
- * runtime type may be either a plain `Date` or a `SvelteDate`; both
36
- * expose the same `Date` interface that we use here. Cast to `Date`
37
- * for narrowing inside the script.
38
- */
39
- const selected = $derived(selectedDate as Date);
40
-
41
- /**
42
- * Today's reference Date hoisted out of the per-cell loops so that
43
- * rendering the month grid does not allocate 42 fresh `Date`
44
- * instances on every state change. `$derived` does not invalidate
45
- * over time on its own; if you need real day-rollover semantics
46
- * track it with a `SvelteDate` and a `setInterval`.
47
- */
48
- const today = $derived(new Date());
49
-
50
- // ---- Date helpers ----
51
-
52
- const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
53
- const monthNames = [
54
- 'January',
55
- 'February',
56
- 'March',
57
- 'April',
58
- 'May',
59
- 'June',
60
- 'July',
61
- 'August',
62
- 'September',
63
- 'October',
64
- 'November',
65
- 'December',
66
- ];
67
-
68
- function formatDate(d: Date): string {
69
- if (viewMode === CalendarViewMode.Month) {
70
- return `${monthNames[d.getMonth()]} ${d.getFullYear()}`;
71
- }
72
- return `${dayNames[d.getDay()]} ${d.getDate()} ${monthNames[d.getMonth()]} ${d.getFullYear()}`;
73
- }
74
-
75
- function sameDay(a: Date, b: Date): boolean {
76
- return (
77
- a.getFullYear() === b.getFullYear() &&
78
- a.getMonth() === b.getMonth() &&
79
- a.getDate() === b.getDate()
80
- );
81
- }
82
-
83
- function addDays(d: Date, n: number): Date {
84
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
85
- const result = new Date(d);
86
- result.setDate(result.getDate() + n);
87
- return result;
88
- }
89
-
90
- function navigateDate(direction: number): void {
91
- if (viewMode === CalendarViewMode.Day) {
92
- ondatechange(addDays(selected, direction));
93
- } else if (viewMode === CalendarViewMode.Week) {
94
- ondatechange(addDays(selected, direction * 7));
95
- } else {
96
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
97
- const d = new Date(selected);
98
- d.setMonth(d.getMonth() + direction);
99
- ondatechange(d);
100
- }
101
- }
102
-
103
- function goToToday(): void {
104
- ondatechange(new Date());
105
- }
106
-
107
- // ---- Time slot generation ----
108
-
109
- const timeSlots: Array<{ hour: number; minute: number; label: string }> = $derived.by(() => {
110
- const slots: Array<{ hour: number; minute: number; label: string }> = [];
111
- for (let h = startHour; h < endHour; h++) {
112
- for (let m = 0; m < 60; m += slotIntervalMinutes) {
113
- const hh = String(h).padStart(2, '0');
114
- const mm = String(m).padStart(2, '0');
115
- slots.push({ hour: h, minute: m, label: `${hh}:${mm}` });
116
- }
117
- }
118
- return slots;
119
- });
120
-
121
- // ---- Appointment positioning (day view) ----
122
-
123
- type PositionedAppointment = CalendarAppointment & {
124
- readonly topPercent: number;
125
- readonly heightPercent: number;
126
- };
127
-
128
- /**
129
- * Build a `YYYY-MM-DD` key for date bucketing. Avoids `toISOString`
130
- * timezone conversions that would smear appointments across days.
131
- */
132
- function dayKey(d: Date): string {
133
- const y = d.getFullYear();
134
- const m = String(d.getMonth() + 1).padStart(2, '0');
135
- const day = String(d.getDate()).padStart(2, '0');
136
- return `${y}-${m}-${day}`;
137
- }
138
-
139
- /**
140
- * Precomputed `Map<doorId, Map<dayKey, PositionedAppointment[]>>` for
141
- * day and week views. Replaces the previous per-cell
142
- * `appointments.filter(...)` calls which were `O(doors * appointments)`
143
- * per render and allocated a fresh `Date` per appointment per pass.
144
- */
145
- const positionedByDoorAndDay = $derived.by(() => {
146
- const totalMinutes = (endHour - startHour) * 60;
147
- // Local indexes built inside a $derived; outer reactivity already covers reads.
148
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
149
- const byDoor = new Map<number, Map<string, Array<PositionedAppointment>>>();
150
- for (const apt of appointments) {
151
- const from = new Date(apt.dateFrom);
152
- const to = new Date(apt.dateTo);
153
- const startMin = (from.getHours() - startHour) * 60 + from.getMinutes();
154
- const endMin = (to.getHours() - startHour) * 60 + to.getMinutes();
155
- const durationMin = Math.max(endMin - startMin, slotIntervalMinutes);
156
- const positioned: PositionedAppointment = {
157
- ...apt,
158
- topPercent: (Math.max(0, startMin) / totalMinutes) * 100,
159
- heightPercent: (Math.min(durationMin, totalMinutes - startMin) / totalMinutes) * 100,
160
- };
161
- const key = dayKey(from);
162
- let perDay = byDoor.get(apt.segmentId);
163
- if (!perDay) {
164
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
165
- perDay = new Map();
166
- byDoor.set(apt.segmentId, perDay);
167
- }
168
- const list = perDay.get(key);
169
- if (list) {
170
- list.push(positioned);
171
- } else {
172
- perDay.set(key, [positioned]);
173
- }
174
- }
175
- return byDoor;
176
- });
177
-
178
- function appointmentsAt(doorId: number, day: Date): ReadonlyArray<PositionedAppointment> {
179
- return positionedByDoorAndDay.get(doorId)?.get(dayKey(day)) ?? [];
180
- }
181
-
182
- /**
183
- * Precomputed appointment-count badges per day for the month view.
184
- * Replaces the previous `O(cells * doors * appointments)` recomputation
185
- * which scaled to ~42 000 ops on a single render with 10 doors and
186
- * 100 appointments.
187
- */
188
- const countsByDay = $derived.by(() => {
189
- // Local indexes built inside a $derived; outer reactivity already covers reads.
190
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
191
- const map = new Map<string, Array<{ doorId: number; doorName: string; count: number }>>();
192
- // One scan over appointments builds a nested Map<dayKey, Map<doorId, count>>.
193
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
194
- const intermediate = new Map<string, Map<number, number>>();
195
- for (const apt of appointments) {
196
- const key = dayKey(new Date(apt.dateFrom));
197
- let perDay = intermediate.get(key);
198
- if (!perDay) {
199
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
200
- perDay = new Map();
201
- intermediate.set(key, perDay);
202
- }
203
- perDay.set(apt.segmentId, (perDay.get(apt.segmentId) ?? 0) + 1);
204
- }
205
- // Flatten to the door-ordered list the markup expects.
206
- for (const [key, perDay] of intermediate) {
207
- const list: Array<{ doorId: number; doorName: string; count: number }> = [];
208
- for (const door of doors) {
209
- const count = perDay.get(door.id);
210
- if (count) {
211
- list.push({ doorId: door.id, doorName: door.name, count });
212
- }
213
- }
214
- if (list.length > 0) {
215
- map.set(key, list);
216
- }
217
- }
218
- return map;
219
- });
220
-
221
- function countsAt(day: Date): ReadonlyArray<{ doorId: number; doorName: string; count: number }> {
222
- return countsByDay.get(dayKey(day)) ?? [];
223
- }
224
-
225
- // ---- Click and drag logic ----
226
-
227
- let isDragging = $state(false);
228
- let dragDoorId: number | null = $state(null);
229
- let dragStartSlotIndex: number | null = $state(null);
230
- let dragEndSlotIndex: number | null = $state(null);
231
-
232
- function onSlotMouseDown(doorId: number, slotIndex: number): void {
233
- if (!ondragcreate || appointmentDragStarted) {
234
- return;
235
- }
236
- isDragging = true;
237
- dragDoorId = doorId;
238
- dragStartSlotIndex = slotIndex;
239
- dragEndSlotIndex = slotIndex;
240
- }
241
-
242
- function onSlotMouseEnter(doorId: number, slotIndex: number): void {
243
- // Appointment move drag
244
- if (appointmentDragStarted) {
245
- isDraggingAppointment = true;
246
- moveTargetDoorId = doorId;
247
- moveTargetSlotIndex = slotIndex;
248
- return;
249
- }
250
- // Slot drag-create
251
- if (!isDragging || doorId !== dragDoorId) {
252
- return;
253
- }
254
- dragEndSlotIndex = slotIndex;
255
- }
256
-
257
- function onSlotMouseUp(): void {
258
- // ---- Handle appointment drag-move ----
259
- if (
260
- isDraggingAppointment &&
261
- draggedAppointment &&
262
- moveTargetDoorId != null &&
263
- moveTargetSlotIndex != null
264
- ) {
265
- const slot = timeSlots[moveTargetSlotIndex];
266
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
267
- const newDateFrom = new Date(selected);
268
- newDateFrom.setHours(slot.hour, slot.minute, 0, 0);
269
- const newDateTo = new Date(
270
- newDateFrom.getTime() + draggedAptSlotCount * slotIntervalMinutes * 60000
271
- );
272
-
273
- // Check whether the position actually changed
274
- const origFrom = new Date(draggedAppointment.dateFrom);
275
- const samePosition =
276
- moveTargetDoorId === draggedAppointment.segmentId &&
277
- newDateFrom.getTime() === origFrom.getTime();
278
-
279
- if (samePosition) {
280
- // Dropped in the same spot - treat as a click
281
- onappointmentclick?.(draggedAppointment);
282
- } else if (!hasOverlap(moveTargetDoorId, newDateFrom, newDateTo, draggedAppointment.id)) {
283
- const door = doors.find((d) => d.id === moveTargetDoorId);
284
- onappointmentmove?.({
285
- appointment: draggedAppointment,
286
- newDoorId: moveTargetDoorId,
287
- newDoorName: door?.name ?? '',
288
- newDateFrom,
289
- newDateTo,
290
- });
291
- }
292
-
293
- resetAppointmentDrag();
294
- return;
295
- }
296
-
297
- // ---- Handle appointment click (mousedown + mouseup without leaving the block) ----
298
- if (appointmentDragStarted && !isDraggingAppointment && draggedAppointment) {
299
- onappointmentclick?.(draggedAppointment);
300
- resetAppointmentDrag();
301
- return;
302
- }
303
-
304
- resetAppointmentDrag();
305
-
306
- // ---- Handle drag-create ----
307
- if (
308
- !isDragging ||
309
- dragDoorId == null ||
310
- dragStartSlotIndex == null ||
311
- dragEndSlotIndex == null
312
- ) {
313
- return;
314
- }
315
-
316
- const startIdx = Math.min(dragStartSlotIndex, dragEndSlotIndex);
317
- const endIdx = Math.max(dragStartSlotIndex, dragEndSlotIndex);
318
-
319
- const startSlot = timeSlots[startIdx];
320
- const endSlot = timeSlots[endIdx];
321
-
322
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
323
- const dateFrom = new Date(selected);
324
- dateFrom.setHours(startSlot.hour, startSlot.minute, 0, 0);
325
-
326
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
327
- const dateTo = new Date(selected);
328
- // End slot is the start of the last selected slot; add one interval for the end time
329
- dateTo.setHours(endSlot.hour, endSlot.minute + slotIntervalMinutes, 0, 0);
330
-
331
- const door = doors.find((d) => d.id === dragDoorId);
332
-
333
- const selection: DragSelection = {
334
- doorId: dragDoorId,
335
- doorName: door?.name ?? '',
336
- dateFrom,
337
- dateTo,
338
- };
339
-
340
- isDragging = false;
341
- dragDoorId = null;
342
- dragStartSlotIndex = null;
343
- dragEndSlotIndex = null;
344
-
345
- ondragcreate?.(selection);
346
- }
347
-
348
- function isSlotInDragRange(doorId: number, slotIndex: number): boolean {
349
- if (!isDragging || doorId !== dragDoorId) {
350
- return false;
351
- }
352
- if (dragStartSlotIndex == null || dragEndSlotIndex == null) {
353
- return false;
354
- }
355
- const min = Math.min(dragStartSlotIndex, dragEndSlotIndex);
356
- const max = Math.max(dragStartSlotIndex, dragEndSlotIndex);
357
- return slotIndex >= min && slotIndex <= max;
358
- }
359
-
360
- // ---- Row hover tracking ----
361
-
362
- let hoveredSlotIndex: number | null = $state(null);
363
-
364
- // ---- Appointment drag-move logic ----
365
-
366
- let appointmentDragStarted = $state(false);
367
- let isDraggingAppointment = $state(false);
368
- let draggedAppointment: CalendarAppointment | null = $state(null);
369
- let draggedAptSlotCount = $state(0);
370
- let moveTargetDoorId: number | null = $state(null);
371
- let moveTargetSlotIndex: number | null = $state(null);
372
-
373
- function onAppointmentMouseDown(apt: CalendarAppointment, e: MouseEvent): void {
374
- if (!onappointmentmove) {
375
- return;
376
- }
377
- e.preventDefault();
378
- appointmentDragStarted = true;
379
- draggedAppointment = apt;
380
-
381
- const from = new Date(apt.dateFrom);
382
- const to = new Date(apt.dateTo);
383
- const durationMin = (to.getTime() - from.getTime()) / 60000;
384
- draggedAptSlotCount = Math.max(1, Math.ceil(durationMin / slotIntervalMinutes));
385
- }
386
-
387
- function resetAppointmentDrag(): void {
388
- appointmentDragStarted = false;
389
- isDraggingAppointment = false;
390
- draggedAppointment = null;
391
- draggedAptSlotCount = 0;
392
- moveTargetDoorId = null;
393
- moveTargetSlotIndex = null;
394
- }
395
-
396
- function isSlotInMoveTarget(doorId: number, slotIndex: number): boolean {
397
- if (!isDraggingAppointment || moveTargetDoorId !== doorId) {
398
- return false;
399
- }
400
- if (moveTargetSlotIndex == null) {
401
- return false;
402
- }
403
- return (
404
- slotIndex >= moveTargetSlotIndex && slotIndex < moveTargetSlotIndex + draggedAptSlotCount
405
- );
406
- }
407
-
408
- function hasOverlap(doorId: number, dateFrom: Date, dateTo: Date, excludeAptId: number): boolean {
409
- return appointments.some((apt) => {
410
- if (apt.id === excludeAptId) {
411
- return false;
412
- }
413
- if (apt.segmentId !== doorId) {
414
- return false;
415
- }
416
- const aptFrom = new Date(apt.dateFrom);
417
- const aptTo = new Date(apt.dateTo);
418
- return dateFrom < aptTo && dateTo > aptFrom;
419
- });
420
- }
421
-
422
- // ---- Month view helpers ----
423
-
424
- function getMonthDays(date: Date): Array<{ date: Date; isCurrentMonth: boolean }> {
425
- const year = date.getFullYear();
426
- const month = date.getMonth();
427
-
428
- const firstDayOfMonth = new Date(year, month, 1);
429
-
430
- // Start from Monday of the week containing the first day
431
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
432
- const startDate = new Date(firstDayOfMonth);
433
- const dayOfWeek = startDate.getDay();
434
- const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
435
- startDate.setDate(startDate.getDate() - daysFromMonday);
436
-
437
- const days: Array<{ date: Date; isCurrentMonth: boolean }> = [];
438
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
439
- const current = new Date(startDate);
440
-
441
- // Generate 6 weeks (42 days) to fill the grid
442
- for (let i = 0; i < 42; i++) {
443
- days.push({
444
- date: new Date(current),
445
- isCurrentMonth: current.getMonth() === month,
446
- });
447
- current.setDate(current.getDate() + 1);
448
- }
449
-
450
- return days;
451
- }
452
-
453
- /** Cached month grid; rebuilt only when `selected` changes. */
454
- const monthDays = $derived(getMonthDays(selected));
455
-
456
- // ---- Week view helpers ----
457
-
458
- function getWeekDays(date: Date): Array<Date> {
459
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
460
- const d = new Date(date);
461
- const day = d.getDay();
462
- const mondayOffset = day === 0 ? -6 : 1 - day;
463
- d.setDate(d.getDate() + mondayOffset);
464
-
465
- const week: Array<Date> = [];
466
- for (let i = 0; i < 7; i++) {
467
- week.push(new Date(d));
468
- d.setDate(d.getDate() + 1);
469
- }
470
- return week;
471
- }
472
-
473
- const weekDays = $derived(getWeekDays(selected));
474
-
475
- // ---- View mode buttons ----
476
-
477
- const viewModes: ReadonlyArray<{ mode: CalendarViewMode; label: string }> = [
478
- { mode: CalendarViewMode.Day, label: 'Day' },
479
- { mode: CalendarViewMode.Week, label: 'Week' },
480
- { mode: CalendarViewMode.Month, label: 'Month' },
481
- ];
482
- </script>
483
-
484
- <!-- svelte-ignore a11y_no_static_element_interactions -->
485
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
486
- <div
487
- class="appointment-calendar"
488
- class:is-moving-appointment={isDraggingAppointment}
489
- onmouseup={onSlotMouseUp}
490
- role="application"
491
- >
492
- <!-- Header: navigation + view toggle -->
493
- <div class="calendar-header">
494
- <div class="header-left">
495
- <Button
496
- label="Today"
497
- size={Size.Small}
498
- onclick={goToToday}
499
- />
500
- <div class="nav-arrows">
501
- <button
502
- class="nav-btn"
503
- onclick={() => {
504
- navigateDate(-1);
505
- }}
506
- aria-label="Previous"
507
- >
508
- <Icon
509
- name="chevron-left"
510
- size={Size.Small}
511
- colour={Colour['$ui-blue-14']}
512
- />
513
- </button>
514
- <button
515
- class="nav-btn"
516
- onclick={() => {
517
- navigateDate(1);
518
- }}
519
- aria-label="Next"
520
- >
521
- <Icon
522
- name="chevron-right"
523
- size={Size.Small}
524
- colour={Colour['$ui-blue-14']}
525
- />
526
- </button>
527
- </div>
528
- <h2 class="current-date-label">{formatDate(selected)}</h2>
529
- </div>
530
- <div class="header-right">
531
- {#if headerExtra}
532
- {@render headerExtra()}
533
- {/if}
534
- <div class="view-toggle">
535
- {#each viewModes as { mode, label } (mode)}
536
- <button
537
- class="view-btn"
538
- class:active={viewMode === mode}
539
- onclick={() => {
540
- onviewmodechange(mode);
541
- }}
542
- >
543
- {label}
544
- </button>
545
- {/each}
546
- </div>
547
- </div>
548
- </div>
549
-
550
- <!-- Week day tabs rendered OUTSIDE the scrollable body so they stay frozen -->
551
- {#if viewMode === CalendarViewMode.Week}
552
- <div class="week-day-tabs">
553
- {#each weekDays as day (day.getTime())}
554
- <button
555
- class="week-day-tab"
556
- class:active={sameDay(day, selected)}
557
- class:today={sameDay(day, today)}
558
- onclick={() => {
559
- ondatechange(day);
560
- }}
561
- >
562
- <span class="day-name">{dayNames[day.getDay()].slice(0, 3)}</span>
563
- <span class="day-num">{day.getDate()}</span>
564
- </button>
565
- {/each}
566
- </div>
567
- {/if}
568
-
569
- <!-- Calendar body -->
570
- <div class="calendar-body">
571
- {#if isLoading}
572
- <LoadingOverlay />
573
- {/if}
574
-
575
- {#if viewMode === CalendarViewMode.Day}
576
- <!-- DAY VIEW -->
577
- <!-- svelte-ignore a11y_no_static_element_interactions -->
578
- <div
579
- class="day-view"
580
- onmouseleave={() => {
581
- hoveredSlotIndex = null;
582
- }}
583
- >
584
- <!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
585
- {@render doorGrid(selected)}
586
- </div>
587
- {:else if viewMode === CalendarViewMode.Week}
588
- <!-- WEEK VIEW: day grid only, tabs are above calendar-body -->
589
- <!-- svelte-ignore a11y_no_static_element_interactions -->
590
- <div
591
- class="day-view"
592
- onmouseleave={() => {
593
- hoveredSlotIndex = null;
594
- }}
595
- >
596
- <!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
597
- {@render doorGrid(selected)}
598
- </div>
599
- {:else}
600
- <!-- MONTH VIEW -->
601
- <div class="month-view">
602
- <div class="month-header-row">
603
- {#each ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as dayLabel (dayLabel)}
604
- <div class="month-day-header">{dayLabel}</div>
605
- {/each}
606
- </div>
607
- <div class="month-grid">
608
- {#each monthDays as { date: dayDate, isCurrentMonth } (dayDate.getTime())}
609
- <button
610
- class="month-day-cell"
611
- class:other-month={!isCurrentMonth}
612
- class:today={sameDay(dayDate, today)}
613
- class:selected={sameDay(dayDate, selected)}
614
- onclick={() => {
615
- ondatechange(dayDate);
616
- onviewmodechange(CalendarViewMode.Day);
617
- }}
618
- >
619
- <span class="day-number">{dayDate.getDate()}</span>
620
- <div class="day-badges">
621
- {#each countsAt(dayDate) as { doorId, doorName, count } (doorId)}
622
- <span class="door-badge">
623
- {doorName}: {count}
624
- </span>
625
- {/each}
626
- </div>
627
- </button>
628
- {/each}
629
- </div>
630
- </div>
631
- {/if}
632
- </div>
633
- </div>
634
-
635
- {#snippet doorGrid(day: Date)}
636
- <div class="grid-header">
637
- <div class="time-header">Time</div>
638
- {#each doors as door (door.id)}
639
- <div class="door-header">{door.name}</div>
640
- {/each}
641
- </div>
642
- <div class="grid-body">
643
- <div class="time-column">
644
- {#each timeSlots as slot, slotIdx (slot.hour * 60 + slot.minute)}
645
- <div
646
- class="time-label"
647
- class:hour-start={slot.minute === 0}
648
- class:row-hover={hoveredSlotIndex === slotIdx}
649
- >
650
- {#if slot.minute === 0}
651
- <span>{slot.label}</span>
652
- {/if}
653
- </div>
654
- {/each}
655
- </div>
656
- {#each doors as door (door.id)}
657
- <div class="door-column">
658
- <!-- Slot cells for drag interaction (rendered first, sit below appointments) -->
659
- {#each timeSlots as slot, slotIdx (slot.hour * 60 + slot.minute)}
660
- <!-- svelte-ignore a11y_no_static_element_interactions -->
661
- <div
662
- class="slot-cell"
663
- class:hour-start={slot.minute === 0}
664
- class:drag-selected={isSlotInDragRange(door.id, slotIdx)}
665
- class:move-target={isSlotInMoveTarget(door.id, slotIdx)}
666
- class:row-hover={hoveredSlotIndex === slotIdx}
667
- onmousedown={() => {
668
- onSlotMouseDown(door.id, slotIdx);
669
- }}
670
- onmouseenter={() => {
671
- hoveredSlotIndex = slotIdx;
672
- onSlotMouseEnter(door.id, slotIdx);
673
- }}
674
- ></div>
675
- {/each}
676
- <!-- Appointment blocks positioned absolutely (rendered after, sit above slots) -->
677
- {#each appointmentsAt(door.id, day) as apt (apt.id)}
678
- <!-- svelte-ignore a11y_no_static_element_interactions -->
679
- <div
680
- class="appointment-block"
681
- class:inbound={apt.typeCode === 'INBOUND'}
682
- class:outbound={apt.typeCode === 'OUTBOUND'}
683
- class:dragging={isDraggingAppointment && draggedAppointment?.id === apt.id}
684
- class:movable={!!onappointmentmove}
685
- style="top: {apt.topPercent}%; height: {apt.heightPercent}%;"
686
- onmousedown={(e) => {
687
- onAppointmentMouseDown(apt, e);
688
- }}
689
- >
690
- <span class="apt-type">{apt.typeCode}</span>
691
- <span class="apt-label">{apt.label}</span>
692
- {#if apt.sublabel}
693
- <span class="apt-sublabel">{apt.sublabel}</span>
694
- {/if}
695
- </div>
696
- {/each}
697
- </div>
698
- {/each}
699
- </div>
700
- {/snippet}
701
-
1
+ <svelte:options runes />
2
+
3
+ <script lang="ts">
4
+ import { Colour } from '../../scss/colours.js';
5
+ import { Size } from '../../Types/Internal/Size.js';
6
+ import Button from '../Button.svelte';
7
+ import Icon from '../Icon.svelte';
8
+ import LoadingOverlay from '../LoadingOverlay.svelte';
9
+ import { CalendarViewMode } from './AppointmentCalendarProps.js';
10
+ import type {
11
+ AppointmentCalendarProps,
12
+ CalendarAppointment,
13
+ DragSelection,
14
+ } from './AppointmentCalendarProps.js';
15
+
16
+ let {
17
+ doors,
18
+ appointments,
19
+ selectedDate,
20
+ viewMode,
21
+ ondatechange,
22
+ onviewmodechange,
23
+ ondragcreate,
24
+ onappointmentclick,
25
+ onappointmentmove,
26
+ startHour = 0,
27
+ endHour = 24,
28
+ slotIntervalMinutes = 15,
29
+ isLoading = false,
30
+ headerExtra,
31
+ }: AppointmentCalendarProps = $props();
32
+
33
+ /**
34
+ * `selectedDate` is always read by the component, never written. The
35
+ * runtime type may be either a plain `Date` or a `SvelteDate`; both
36
+ * expose the same `Date` interface that we use here. Cast to `Date`
37
+ * for narrowing inside the script.
38
+ */
39
+ const selected = $derived(selectedDate as Date);
40
+
41
+ /**
42
+ * Today's reference Date hoisted out of the per-cell loops so that
43
+ * rendering the month grid does not allocate 42 fresh `Date`
44
+ * instances on every state change. `$derived` does not invalidate
45
+ * over time on its own; if you need real day-rollover semantics
46
+ * track it with a `SvelteDate` and a `setInterval`.
47
+ */
48
+ const today = $derived(new Date());
49
+
50
+ // ---- Date helpers ----
51
+
52
+ const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
53
+ const monthNames = [
54
+ 'January',
55
+ 'February',
56
+ 'March',
57
+ 'April',
58
+ 'May',
59
+ 'June',
60
+ 'July',
61
+ 'August',
62
+ 'September',
63
+ 'October',
64
+ 'November',
65
+ 'December',
66
+ ];
67
+
68
+ function formatDate(d: Date): string {
69
+ if (viewMode === CalendarViewMode.Month) {
70
+ return `${monthNames[d.getMonth()]} ${d.getFullYear()}`;
71
+ }
72
+ return `${dayNames[d.getDay()]} ${d.getDate()} ${monthNames[d.getMonth()]} ${d.getFullYear()}`;
73
+ }
74
+
75
+ function sameDay(a: Date, b: Date): boolean {
76
+ return (
77
+ a.getFullYear() === b.getFullYear() &&
78
+ a.getMonth() === b.getMonth() &&
79
+ a.getDate() === b.getDate()
80
+ );
81
+ }
82
+
83
+ function addDays(d: Date, n: number): Date {
84
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
85
+ const result = new Date(d);
86
+ result.setDate(result.getDate() + n);
87
+ return result;
88
+ }
89
+
90
+ function navigateDate(direction: number): void {
91
+ if (viewMode === CalendarViewMode.Day) {
92
+ ondatechange(addDays(selected, direction));
93
+ } else if (viewMode === CalendarViewMode.Week) {
94
+ ondatechange(addDays(selected, direction * 7));
95
+ } else {
96
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
97
+ const d = new Date(selected);
98
+ d.setMonth(d.getMonth() + direction);
99
+ ondatechange(d);
100
+ }
101
+ }
102
+
103
+ function goToToday(): void {
104
+ ondatechange(new Date());
105
+ }
106
+
107
+ // ---- Time slot generation ----
108
+
109
+ const timeSlots: Array<{ hour: number; minute: number; label: string }> = $derived.by(() => {
110
+ const slots: Array<{ hour: number; minute: number; label: string }> = [];
111
+ for (let h = startHour; h < endHour; h++) {
112
+ for (let m = 0; m < 60; m += slotIntervalMinutes) {
113
+ const hh = String(h).padStart(2, '0');
114
+ const mm = String(m).padStart(2, '0');
115
+ slots.push({ hour: h, minute: m, label: `${hh}:${mm}` });
116
+ }
117
+ }
118
+ return slots;
119
+ });
120
+
121
+ // ---- Appointment positioning (day view) ----
122
+
123
+ type PositionedAppointment = CalendarAppointment & {
124
+ readonly topPercent: number;
125
+ readonly heightPercent: number;
126
+ };
127
+
128
+ /**
129
+ * Build a `YYYY-MM-DD` key for date bucketing. Avoids `toISOString`
130
+ * timezone conversions that would smear appointments across days.
131
+ */
132
+ function dayKey(d: Date): string {
133
+ const y = d.getFullYear();
134
+ const m = String(d.getMonth() + 1).padStart(2, '0');
135
+ const day = String(d.getDate()).padStart(2, '0');
136
+ return `${y}-${m}-${day}`;
137
+ }
138
+
139
+ /**
140
+ * Precomputed `Map<doorId, Map<dayKey, PositionedAppointment[]>>` for
141
+ * day and week views. Replaces the previous per-cell
142
+ * `appointments.filter(...)` calls which were `O(doors * appointments)`
143
+ * per render and allocated a fresh `Date` per appointment per pass.
144
+ */
145
+ const positionedByDoorAndDay = $derived.by(() => {
146
+ const totalMinutes = (endHour - startHour) * 60;
147
+ // Local indexes built inside a $derived; outer reactivity already covers reads.
148
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
149
+ const byDoor = new Map<number, Map<string, Array<PositionedAppointment>>>();
150
+ for (const apt of appointments) {
151
+ const from = new Date(apt.dateFrom);
152
+ const to = new Date(apt.dateTo);
153
+ const startMin = (from.getHours() - startHour) * 60 + from.getMinutes();
154
+ const endMin = (to.getHours() - startHour) * 60 + to.getMinutes();
155
+ const durationMin = Math.max(endMin - startMin, slotIntervalMinutes);
156
+ const positioned: PositionedAppointment = {
157
+ ...apt,
158
+ topPercent: (Math.max(0, startMin) / totalMinutes) * 100,
159
+ heightPercent: (Math.min(durationMin, totalMinutes - startMin) / totalMinutes) * 100,
160
+ };
161
+ const key = dayKey(from);
162
+ let perDay = byDoor.get(apt.segmentId);
163
+ if (!perDay) {
164
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
165
+ perDay = new Map();
166
+ byDoor.set(apt.segmentId, perDay);
167
+ }
168
+ const list = perDay.get(key);
169
+ if (list) {
170
+ list.push(positioned);
171
+ } else {
172
+ perDay.set(key, [positioned]);
173
+ }
174
+ }
175
+ return byDoor;
176
+ });
177
+
178
+ function appointmentsAt(doorId: number, day: Date): ReadonlyArray<PositionedAppointment> {
179
+ return positionedByDoorAndDay.get(doorId)?.get(dayKey(day)) ?? [];
180
+ }
181
+
182
+ /**
183
+ * Precomputed appointment-count badges per day for the month view.
184
+ * Replaces the previous `O(cells * doors * appointments)` recomputation
185
+ * which scaled to ~42 000 ops on a single render with 10 doors and
186
+ * 100 appointments.
187
+ */
188
+ const countsByDay = $derived.by(() => {
189
+ // Local indexes built inside a $derived; outer reactivity already covers reads.
190
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
191
+ const map = new Map<string, Array<{ doorId: number; doorName: string; count: number }>>();
192
+ // One scan over appointments builds a nested Map<dayKey, Map<doorId, count>>.
193
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
194
+ const intermediate = new Map<string, Map<number, number>>();
195
+ for (const apt of appointments) {
196
+ const key = dayKey(new Date(apt.dateFrom));
197
+ let perDay = intermediate.get(key);
198
+ if (!perDay) {
199
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
200
+ perDay = new Map();
201
+ intermediate.set(key, perDay);
202
+ }
203
+ perDay.set(apt.segmentId, (perDay.get(apt.segmentId) ?? 0) + 1);
204
+ }
205
+ // Flatten to the door-ordered list the markup expects.
206
+ for (const [key, perDay] of intermediate) {
207
+ const list: Array<{ doorId: number; doorName: string; count: number }> = [];
208
+ for (const door of doors) {
209
+ const count = perDay.get(door.id);
210
+ if (count) {
211
+ list.push({ doorId: door.id, doorName: door.name, count });
212
+ }
213
+ }
214
+ if (list.length > 0) {
215
+ map.set(key, list);
216
+ }
217
+ }
218
+ return map;
219
+ });
220
+
221
+ function countsAt(day: Date): ReadonlyArray<{ doorId: number; doorName: string; count: number }> {
222
+ return countsByDay.get(dayKey(day)) ?? [];
223
+ }
224
+
225
+ // ---- Click and drag logic ----
226
+
227
+ let isDragging = $state(false);
228
+ let dragDoorId: number | null = $state(null);
229
+ let dragStartSlotIndex: number | null = $state(null);
230
+ let dragEndSlotIndex: number | null = $state(null);
231
+
232
+ function onSlotMouseDown(doorId: number, slotIndex: number): void {
233
+ if (!ondragcreate || appointmentDragStarted) {
234
+ return;
235
+ }
236
+ isDragging = true;
237
+ dragDoorId = doorId;
238
+ dragStartSlotIndex = slotIndex;
239
+ dragEndSlotIndex = slotIndex;
240
+ }
241
+
242
+ function onSlotMouseEnter(doorId: number, slotIndex: number): void {
243
+ // Appointment move drag
244
+ if (appointmentDragStarted) {
245
+ isDraggingAppointment = true;
246
+ moveTargetDoorId = doorId;
247
+ moveTargetSlotIndex = slotIndex;
248
+ return;
249
+ }
250
+ // Slot drag-create
251
+ if (!isDragging || doorId !== dragDoorId) {
252
+ return;
253
+ }
254
+ dragEndSlotIndex = slotIndex;
255
+ }
256
+
257
+ function onSlotMouseUp(): void {
258
+ // ---- Handle appointment drag-move ----
259
+ if (
260
+ isDraggingAppointment &&
261
+ draggedAppointment &&
262
+ moveTargetDoorId != null &&
263
+ moveTargetSlotIndex != null
264
+ ) {
265
+ const slot = timeSlots[moveTargetSlotIndex];
266
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
267
+ const newDateFrom = new Date(selected);
268
+ newDateFrom.setHours(slot.hour, slot.minute, 0, 0);
269
+ const newDateTo = new Date(
270
+ newDateFrom.getTime() + draggedAptSlotCount * slotIntervalMinutes * 60000
271
+ );
272
+
273
+ // Check whether the position actually changed
274
+ const origFrom = new Date(draggedAppointment.dateFrom);
275
+ const samePosition =
276
+ moveTargetDoorId === draggedAppointment.segmentId &&
277
+ newDateFrom.getTime() === origFrom.getTime();
278
+
279
+ if (samePosition) {
280
+ // Dropped in the same spot - treat as a click
281
+ onappointmentclick?.(draggedAppointment);
282
+ } else if (!hasOverlap(moveTargetDoorId, newDateFrom, newDateTo, draggedAppointment.id)) {
283
+ const door = doors.find((d) => d.id === moveTargetDoorId);
284
+ onappointmentmove?.({
285
+ appointment: draggedAppointment,
286
+ newDoorId: moveTargetDoorId,
287
+ newDoorName: door?.name ?? '',
288
+ newDateFrom,
289
+ newDateTo,
290
+ });
291
+ }
292
+
293
+ resetAppointmentDrag();
294
+ return;
295
+ }
296
+
297
+ // ---- Handle appointment click (mousedown + mouseup without leaving the block) ----
298
+ if (appointmentDragStarted && !isDraggingAppointment && draggedAppointment) {
299
+ onappointmentclick?.(draggedAppointment);
300
+ resetAppointmentDrag();
301
+ return;
302
+ }
303
+
304
+ resetAppointmentDrag();
305
+
306
+ // ---- Handle drag-create ----
307
+ if (
308
+ !isDragging ||
309
+ dragDoorId == null ||
310
+ dragStartSlotIndex == null ||
311
+ dragEndSlotIndex == null
312
+ ) {
313
+ return;
314
+ }
315
+
316
+ const startIdx = Math.min(dragStartSlotIndex, dragEndSlotIndex);
317
+ const endIdx = Math.max(dragStartSlotIndex, dragEndSlotIndex);
318
+
319
+ const startSlot = timeSlots[startIdx];
320
+ const endSlot = timeSlots[endIdx];
321
+
322
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
323
+ const dateFrom = new Date(selected);
324
+ dateFrom.setHours(startSlot.hour, startSlot.minute, 0, 0);
325
+
326
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
327
+ const dateTo = new Date(selected);
328
+ // End slot is the start of the last selected slot; add one interval for the end time
329
+ dateTo.setHours(endSlot.hour, endSlot.minute + slotIntervalMinutes, 0, 0);
330
+
331
+ const door = doors.find((d) => d.id === dragDoorId);
332
+
333
+ const selection: DragSelection = {
334
+ doorId: dragDoorId,
335
+ doorName: door?.name ?? '',
336
+ dateFrom,
337
+ dateTo,
338
+ };
339
+
340
+ isDragging = false;
341
+ dragDoorId = null;
342
+ dragStartSlotIndex = null;
343
+ dragEndSlotIndex = null;
344
+
345
+ ondragcreate?.(selection);
346
+ }
347
+
348
+ function isSlotInDragRange(doorId: number, slotIndex: number): boolean {
349
+ if (!isDragging || doorId !== dragDoorId) {
350
+ return false;
351
+ }
352
+ if (dragStartSlotIndex == null || dragEndSlotIndex == null) {
353
+ return false;
354
+ }
355
+ const min = Math.min(dragStartSlotIndex, dragEndSlotIndex);
356
+ const max = Math.max(dragStartSlotIndex, dragEndSlotIndex);
357
+ return slotIndex >= min && slotIndex <= max;
358
+ }
359
+
360
+ // ---- Row hover tracking ----
361
+
362
+ let hoveredSlotIndex: number | null = $state(null);
363
+
364
+ // ---- Appointment drag-move logic ----
365
+
366
+ let appointmentDragStarted = $state(false);
367
+ let isDraggingAppointment = $state(false);
368
+ let draggedAppointment: CalendarAppointment | null = $state(null);
369
+ let draggedAptSlotCount = $state(0);
370
+ let moveTargetDoorId: number | null = $state(null);
371
+ let moveTargetSlotIndex: number | null = $state(null);
372
+
373
+ function onAppointmentMouseDown(apt: CalendarAppointment, e: MouseEvent): void {
374
+ if (!onappointmentmove) {
375
+ return;
376
+ }
377
+ e.preventDefault();
378
+ appointmentDragStarted = true;
379
+ draggedAppointment = apt;
380
+
381
+ const from = new Date(apt.dateFrom);
382
+ const to = new Date(apt.dateTo);
383
+ const durationMin = (to.getTime() - from.getTime()) / 60000;
384
+ draggedAptSlotCount = Math.max(1, Math.ceil(durationMin / slotIntervalMinutes));
385
+ }
386
+
387
+ function resetAppointmentDrag(): void {
388
+ appointmentDragStarted = false;
389
+ isDraggingAppointment = false;
390
+ draggedAppointment = null;
391
+ draggedAptSlotCount = 0;
392
+ moveTargetDoorId = null;
393
+ moveTargetSlotIndex = null;
394
+ }
395
+
396
+ function isSlotInMoveTarget(doorId: number, slotIndex: number): boolean {
397
+ if (!isDraggingAppointment || moveTargetDoorId !== doorId) {
398
+ return false;
399
+ }
400
+ if (moveTargetSlotIndex == null) {
401
+ return false;
402
+ }
403
+ return (
404
+ slotIndex >= moveTargetSlotIndex && slotIndex < moveTargetSlotIndex + draggedAptSlotCount
405
+ );
406
+ }
407
+
408
+ function hasOverlap(doorId: number, dateFrom: Date, dateTo: Date, excludeAptId: number): boolean {
409
+ return appointments.some((apt) => {
410
+ if (apt.id === excludeAptId) {
411
+ return false;
412
+ }
413
+ if (apt.segmentId !== doorId) {
414
+ return false;
415
+ }
416
+ const aptFrom = new Date(apt.dateFrom);
417
+ const aptTo = new Date(apt.dateTo);
418
+ return dateFrom < aptTo && dateTo > aptFrom;
419
+ });
420
+ }
421
+
422
+ // ---- Month view helpers ----
423
+
424
+ function getMonthDays(date: Date): Array<{ date: Date; isCurrentMonth: boolean }> {
425
+ const year = date.getFullYear();
426
+ const month = date.getMonth();
427
+
428
+ const firstDayOfMonth = new Date(year, month, 1);
429
+
430
+ // Start from Monday of the week containing the first day
431
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
432
+ const startDate = new Date(firstDayOfMonth);
433
+ const dayOfWeek = startDate.getDay();
434
+ const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
435
+ startDate.setDate(startDate.getDate() - daysFromMonday);
436
+
437
+ const days: Array<{ date: Date; isCurrentMonth: boolean }> = [];
438
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
439
+ const current = new Date(startDate);
440
+
441
+ // Generate 6 weeks (42 days) to fill the grid
442
+ for (let i = 0; i < 42; i++) {
443
+ days.push({
444
+ date: new Date(current),
445
+ isCurrentMonth: current.getMonth() === month,
446
+ });
447
+ current.setDate(current.getDate() + 1);
448
+ }
449
+
450
+ return days;
451
+ }
452
+
453
+ /** Cached month grid; rebuilt only when `selected` changes. */
454
+ const monthDays = $derived(getMonthDays(selected));
455
+
456
+ // ---- Week view helpers ----
457
+
458
+ function getWeekDays(date: Date): Array<Date> {
459
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
460
+ const d = new Date(date);
461
+ const day = d.getDay();
462
+ const mondayOffset = day === 0 ? -6 : 1 - day;
463
+ d.setDate(d.getDate() + mondayOffset);
464
+
465
+ const week: Array<Date> = [];
466
+ for (let i = 0; i < 7; i++) {
467
+ week.push(new Date(d));
468
+ d.setDate(d.getDate() + 1);
469
+ }
470
+ return week;
471
+ }
472
+
473
+ const weekDays = $derived(getWeekDays(selected));
474
+
475
+ // ---- View mode buttons ----
476
+
477
+ const viewModes: ReadonlyArray<{ mode: CalendarViewMode; label: string }> = [
478
+ { mode: CalendarViewMode.Day, label: 'Day' },
479
+ { mode: CalendarViewMode.Week, label: 'Week' },
480
+ { mode: CalendarViewMode.Month, label: 'Month' },
481
+ ];
482
+ </script>
483
+
484
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
485
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
486
+ <div
487
+ class="appointment-calendar"
488
+ class:is-moving-appointment={isDraggingAppointment}
489
+ onmouseup={onSlotMouseUp}
490
+ role="application"
491
+ >
492
+ <!-- Header: navigation + view toggle -->
493
+ <div class="calendar-header">
494
+ <div class="header-left">
495
+ <Button
496
+ label="Today"
497
+ size={Size.Small}
498
+ onclick={goToToday}
499
+ />
500
+ <div class="nav-arrows">
501
+ <button
502
+ class="nav-btn"
503
+ onclick={() => {
504
+ navigateDate(-1);
505
+ }}
506
+ aria-label="Previous"
507
+ >
508
+ <Icon
509
+ name="chevron-left"
510
+ size={Size.Small}
511
+ colour={Colour['$ui-blue-14']}
512
+ />
513
+ </button>
514
+ <button
515
+ class="nav-btn"
516
+ onclick={() => {
517
+ navigateDate(1);
518
+ }}
519
+ aria-label="Next"
520
+ >
521
+ <Icon
522
+ name="chevron-right"
523
+ size={Size.Small}
524
+ colour={Colour['$ui-blue-14']}
525
+ />
526
+ </button>
527
+ </div>
528
+ <h2 class="current-date-label">{formatDate(selected)}</h2>
529
+ </div>
530
+ <div class="header-right">
531
+ {#if headerExtra}
532
+ {@render headerExtra()}
533
+ {/if}
534
+ <div class="view-toggle">
535
+ {#each viewModes as { mode, label } (mode)}
536
+ <button
537
+ class="view-btn"
538
+ class:active={viewMode === mode}
539
+ onclick={() => {
540
+ onviewmodechange(mode);
541
+ }}
542
+ >
543
+ {label}
544
+ </button>
545
+ {/each}
546
+ </div>
547
+ </div>
548
+ </div>
549
+
550
+ <!-- Week day tabs rendered OUTSIDE the scrollable body so they stay frozen -->
551
+ {#if viewMode === CalendarViewMode.Week}
552
+ <div class="week-day-tabs">
553
+ {#each weekDays as day (day.getTime())}
554
+ <button
555
+ class="week-day-tab"
556
+ class:active={sameDay(day, selected)}
557
+ class:today={sameDay(day, today)}
558
+ onclick={() => {
559
+ ondatechange(day);
560
+ }}
561
+ >
562
+ <span class="day-name">{dayNames[day.getDay()].slice(0, 3)}</span>
563
+ <span class="day-num">{day.getDate()}</span>
564
+ </button>
565
+ {/each}
566
+ </div>
567
+ {/if}
568
+
569
+ <!-- Calendar body -->
570
+ <div class="calendar-body">
571
+ {#if isLoading}
572
+ <LoadingOverlay />
573
+ {/if}
574
+
575
+ {#if viewMode === CalendarViewMode.Day}
576
+ <!-- DAY VIEW -->
577
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
578
+ <div
579
+ class="day-view"
580
+ onmouseleave={() => {
581
+ hoveredSlotIndex = null;
582
+ }}
583
+ >
584
+ <!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
585
+ {@render doorGrid(selected)}
586
+ </div>
587
+ {:else if viewMode === CalendarViewMode.Week}
588
+ <!-- WEEK VIEW: day grid only, tabs are above calendar-body -->
589
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
590
+ <div
591
+ class="day-view"
592
+ onmouseleave={() => {
593
+ hoveredSlotIndex = null;
594
+ }}
595
+ >
596
+ <!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
597
+ {@render doorGrid(selected)}
598
+ </div>
599
+ {:else}
600
+ <!-- MONTH VIEW -->
601
+ <div class="month-view">
602
+ <div class="month-header-row">
603
+ {#each ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as dayLabel (dayLabel)}
604
+ <div class="month-day-header">{dayLabel}</div>
605
+ {/each}
606
+ </div>
607
+ <div class="month-grid">
608
+ {#each monthDays as { date: dayDate, isCurrentMonth } (dayDate.getTime())}
609
+ <button
610
+ class="month-day-cell"
611
+ class:other-month={!isCurrentMonth}
612
+ class:today={sameDay(dayDate, today)}
613
+ class:selected={sameDay(dayDate, selected)}
614
+ onclick={() => {
615
+ ondatechange(dayDate);
616
+ onviewmodechange(CalendarViewMode.Day);
617
+ }}
618
+ >
619
+ <span class="day-number">{dayDate.getDate()}</span>
620
+ <div class="day-badges">
621
+ {#each countsAt(dayDate) as { doorId, doorName, count } (doorId)}
622
+ <span class="door-badge">
623
+ {doorName}: {count}
624
+ </span>
625
+ {/each}
626
+ </div>
627
+ </button>
628
+ {/each}
629
+ </div>
630
+ </div>
631
+ {/if}
632
+ </div>
633
+ </div>
634
+
635
+ {#snippet doorGrid(day: Date)}
636
+ <div class="grid-header">
637
+ <div class="time-header">Time</div>
638
+ {#each doors as door (door.id)}
639
+ <div class="door-header">{door.name}</div>
640
+ {/each}
641
+ </div>
642
+ <div class="grid-body">
643
+ <div class="time-column">
644
+ {#each timeSlots as slot, slotIdx (slot.hour * 60 + slot.minute)}
645
+ <div
646
+ class="time-label"
647
+ class:hour-start={slot.minute === 0}
648
+ class:row-hover={hoveredSlotIndex === slotIdx}
649
+ >
650
+ {#if slot.minute === 0}
651
+ <span>{slot.label}</span>
652
+ {/if}
653
+ </div>
654
+ {/each}
655
+ </div>
656
+ {#each doors as door (door.id)}
657
+ <div class="door-column">
658
+ <!-- Slot cells for drag interaction (rendered first, sit below appointments) -->
659
+ {#each timeSlots as slot, slotIdx (slot.hour * 60 + slot.minute)}
660
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
661
+ <div
662
+ class="slot-cell"
663
+ class:hour-start={slot.minute === 0}
664
+ class:drag-selected={isSlotInDragRange(door.id, slotIdx)}
665
+ class:move-target={isSlotInMoveTarget(door.id, slotIdx)}
666
+ class:row-hover={hoveredSlotIndex === slotIdx}
667
+ onmousedown={() => {
668
+ onSlotMouseDown(door.id, slotIdx);
669
+ }}
670
+ onmouseenter={() => {
671
+ hoveredSlotIndex = slotIdx;
672
+ onSlotMouseEnter(door.id, slotIdx);
673
+ }}
674
+ ></div>
675
+ {/each}
676
+ <!-- Appointment blocks positioned absolutely (rendered after, sit above slots) -->
677
+ {#each appointmentsAt(door.id, day) as apt (apt.id)}
678
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
679
+ <div
680
+ class="appointment-block"
681
+ class:inbound={apt.typeCode === 'INBOUND'}
682
+ class:outbound={apt.typeCode === 'OUTBOUND'}
683
+ class:dragging={isDraggingAppointment && draggedAppointment?.id === apt.id}
684
+ class:movable={!!onappointmentmove}
685
+ style="top: {apt.topPercent}%; height: {apt.heightPercent}%;"
686
+ onmousedown={(e) => {
687
+ onAppointmentMouseDown(apt, e);
688
+ }}
689
+ >
690
+ <span class="apt-type">{apt.typeCode}</span>
691
+ <span class="apt-label">{apt.label}</span>
692
+ {#if apt.sublabel}
693
+ <span class="apt-sublabel">{apt.sublabel}</span>
694
+ {/if}
695
+ </div>
696
+ {/each}
697
+ </div>
698
+ {/each}
699
+ </div>
700
+ {/snippet}
701
+
702
702
  <style>.appointment-calendar {
703
703
  display: flex;
704
704
  flex-flow: column nowrap;
@@ -1123,4 +1123,4 @@
1123
1123
  white-space: nowrap;
1124
1124
  overflow: hidden;
1125
1125
  text-overflow: ellipsis;
1126
- }</style>
1126
+ }</style>