@lavalogic/scoria 0.40.13 → 0.40.15

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 (143) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +35 -46
  3. package/dist/Components/AccordionGroupButton.svelte +68 -76
  4. package/dist/Components/Action.svelte +103 -135
  5. package/dist/Components/AlertModal.svelte +171 -190
  6. package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +614 -697
  7. package/dist/Components/Base/BaseModal.svelte +90 -116
  8. package/dist/Components/Base/ContextWrapper.svelte +67 -72
  9. package/dist/Components/Base/Pagination.svelte +173 -196
  10. package/dist/Components/Base/Snippets.svelte +34 -36
  11. package/dist/Components/BigRadioSet.svelte +36 -38
  12. package/dist/Components/Bubble.svelte +78 -101
  13. package/dist/Components/Button.svelte +175 -208
  14. package/dist/Components/Checkbox.svelte +75 -86
  15. package/dist/Components/CollapsibleCard.svelte +72 -87
  16. package/dist/Components/CollapsibleMenuGroup.svelte +53 -57
  17. package/dist/Components/ConditionBuilder/ConditionBuilder.svelte +39 -64
  18. package/dist/Components/Contexts/ContextMenu.svelte +141 -156
  19. package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
  20. package/dist/Components/Contexts/ContextMenuOption.svelte +53 -60
  21. package/dist/Components/Contexts/Toast.svelte +81 -86
  22. package/dist/Components/Contexts/ToastContainer.svelte +58 -62
  23. package/dist/Components/Contexts/Tooltip.svelte +36 -37
  24. package/dist/Components/DataMatrixIcon.svelte +95 -108
  25. package/dist/Components/DateInput.svelte +118 -146
  26. package/dist/Components/DatePicker.svelte +255 -294
  27. package/dist/Components/DesktopModal.svelte +70 -90
  28. package/dist/Components/Dial.svelte +167 -197
  29. package/dist/Components/DimensionInput.svelte +124 -169
  30. package/dist/Components/DragMenu/DragMenuHolder.svelte +98 -114
  31. package/dist/Components/DragMenu/DraggableCard.svelte +64 -76
  32. package/dist/Components/FileCard.svelte +21 -24
  33. package/dist/Components/FileUpload.svelte +190 -225
  34. package/dist/Components/GridInput.svelte +159 -196
  35. package/dist/Components/GridInputSet.svelte +24 -29
  36. package/dist/Components/HorizontalTabGroup.svelte +73 -89
  37. package/dist/Components/HorizontalTabGroupButton.svelte +40 -44
  38. package/dist/Components/Icon.svelte +99 -128
  39. package/dist/Components/Icons.js +357 -357
  40. package/dist/Components/InfoAlert.svelte +35 -38
  41. package/dist/Components/LoadingAnimation.svelte +691 -755
  42. package/dist/Components/LoadingModal.svelte +36 -44
  43. package/dist/Components/LoadingOverlay.svelte +22 -26
  44. package/dist/Components/Modal.svelte +204 -254
  45. package/dist/Components/MultiSelect.svelte +186 -216
  46. package/dist/Components/Nav/Nav.svelte +94 -110
  47. package/dist/Components/Nav/NavServiceMenuList.svelte +57 -61
  48. package/dist/Components/Nav/NavTab.svelte +58 -62
  49. package/dist/Components/Nav/ServiceMenuItem.svelte +103 -111
  50. package/dist/Components/Nav/ServicesNav.svelte +72 -79
  51. package/dist/Components/NumberInput.svelte +121 -155
  52. package/dist/Components/OptionCards.svelte +62 -73
  53. package/dist/Components/PageHeading.svelte +23 -25
  54. package/dist/Components/PasswordInput.svelte +69 -88
  55. package/dist/Components/PopoverButton.svelte +151 -0
  56. package/dist/Components/PopoverButton.svelte.d.ts +9 -0
  57. package/dist/Components/PopoverButtonProps.d.ts +21 -0
  58. package/dist/Components/PopoverButtonProps.js +1 -0
  59. package/dist/Components/Portal.svelte +29 -35
  60. package/dist/Components/ProgressBubble.svelte +91 -106
  61. package/dist/Components/QuerySelect.svelte +260 -346
  62. package/dist/Components/SidebarPanel.svelte +34 -36
  63. package/dist/Components/SingleSelect.svelte +196 -244
  64. package/dist/Components/StepButton.svelte +65 -70
  65. package/dist/Components/StepForm.svelte +115 -135
  66. package/dist/Components/Switch.svelte +57 -69
  67. package/dist/Components/Table/ARCHITECTURE.md +298 -298
  68. package/dist/Components/Table/Body/QuickSearchCell.svelte +578 -655
  69. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +42 -51
  70. package/dist/Components/Table/Body/Rows/ColumnList.svelte +42 -58
  71. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +403 -442
  72. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +51 -78
  73. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +165 -184
  74. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +110 -123
  75. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +134 -160
  76. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +190 -231
  77. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +82 -104
  78. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +52 -87
  79. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +122 -150
  80. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +132 -159
  81. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +190 -236
  82. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +173 -211
  83. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +204 -244
  84. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +140 -177
  85. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +188 -217
  86. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +173 -193
  87. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +166 -187
  88. package/dist/Components/Table/Body/Rows/TableRow.svelte +245 -288
  89. package/dist/Components/Table/Body/TableBody.svelte +141 -154
  90. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +35 -41
  91. package/dist/Components/Table/Headers/SelectAllHeader.svelte +24 -30
  92. package/dist/Components/Table/Headers/TableHead.svelte +348 -402
  93. package/dist/Components/Table/Misc/ColumnPanel.svelte +37 -39
  94. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +35 -39
  95. package/dist/Components/Table/Misc/FilterInput.svelte +556 -619
  96. package/dist/Components/Table/Misc/FilterPanel.svelte +170 -183
  97. package/dist/Components/Table/Misc/FilterPanelModal.svelte +25 -31
  98. package/dist/Components/Table/Misc/ShareViewModal.svelte +252 -281
  99. package/dist/Components/Table/Misc/SidePanel.svelte +32 -35
  100. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +574 -629
  101. package/dist/Components/Table/Misc/TableFooter.svelte +326 -332
  102. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +134 -140
  103. package/dist/Components/Table/Misc/TableSidebar.svelte +183 -198
  104. package/dist/Components/Table/Misc/TableViewDropdown.svelte +83 -103
  105. package/dist/Components/Table/Misc/ToolboxPanel.svelte +83 -100
  106. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +88 -107
  107. package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +163 -190
  108. package/dist/Components/Table/NestedTable.svelte +102 -128
  109. package/dist/Components/Table/Table.svelte +531 -663
  110. package/dist/Components/Test/TestContextConsumer.svelte +11 -13
  111. package/dist/Components/TextArea.svelte +95 -130
  112. package/dist/Components/TextInput.svelte +125 -176
  113. package/dist/Components/ThreeStateRadio.svelte +111 -130
  114. package/dist/Components/Touch/BoolCard.svelte +33 -45
  115. package/dist/Components/Touch/DateModal.svelte +164 -204
  116. package/dist/Components/Touch/EditCard.svelte +48 -59
  117. package/dist/Components/Touch/InfoCard.svelte +28 -31
  118. package/dist/Components/Touch/InfoTextCard.svelte +26 -28
  119. package/dist/Components/Touch/ModalExplanation.svelte +15 -17
  120. package/dist/Components/Touch/NumberKeyboard.svelte +181 -218
  121. package/dist/Components/Touch/NumberModal.svelte +59 -79
  122. package/dist/Components/Touch/PlusMinusCard.svelte +43 -46
  123. package/dist/Components/Touch/SelectModal.svelte +111 -126
  124. package/dist/Components/Touch/SummaryCard.svelte +80 -93
  125. package/dist/Components/Touch/TextAreaModal.svelte +72 -94
  126. package/dist/Components/Touch/TextModal.svelte +77 -105
  127. package/dist/Components/Touch/TouchCard.svelte +31 -33
  128. package/dist/Components/Touch/TouchModal.svelte +214 -270
  129. package/dist/Components/Touch/UtilityButton.svelte +88 -109
  130. package/dist/Components/VerticalTabGroup.svelte +60 -78
  131. package/dist/Components/VerticalTabGroupButton.svelte +54 -58
  132. package/dist/Helpers/Datamatrix.d.ts +19 -19
  133. package/dist/Helpers/Datamatrix.js +291 -291
  134. package/dist/Types/Examples/ExampleModal.svelte +31 -35
  135. package/dist/index.d.ts +2 -0
  136. package/dist/index.js +1 -0
  137. package/dist/scss/_basics.scss +12 -12
  138. package/dist/scss/_colours.scss +134 -134
  139. package/dist/scss/_mixins.scss +3392 -3392
  140. package/dist/scss/_motion.scss +57 -57
  141. package/dist/scss/_sizing.scss +85 -85
  142. package/dist/scss/_transitions.scss +8 -8
  143. package/package.json +1 -1
@@ -1,456 +1,417 @@
1
- <svelte:options runes />
2
-
3
- <script
4
- lang="ts"
5
- generics="T extends object, IdType extends Primitive"
6
- >
7
- import Action from '../../../Action.svelte';
8
- import Checkbox from '../../../Checkbox.svelte';
9
- import ThreeStateRadio from '../../../ThreeStateRadio.svelte';
10
- import { TableContext } from '../../Types/Context/TableContext.svelte.js';
11
- import type { Primitive } from '../../Types/Context/TableInitOptions.js';
12
- import { Size } from '../../../../Types/Internal/Size.js';
13
- import { Ternary } from '../../../../Types/Internal/Ternary.js';
14
- import { getContext } from 'svelte';
15
- import type { ColumnListRowProps } from './ColumnListRowProps.js';
16
- const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
17
-
18
- const { columnDef }: ColumnListRowProps<T> = $props();
19
-
20
- /**
21
- * Position of this column inside the sorted column list. Drives the
22
- * disabled state of the "move up" / "move down" buttons and supplies
23
- * the target index to `moveColumnTo`. Sort by `index` (the stable
24
- * layout identity) rather than relying on `columnDefs` iteration order.
25
- */
26
- const sortedColumns = $derived(tableContext.columnDefs.slice().sort((a, b) => a.index - b.index));
27
- const currentIndex = $derived(
28
- columnDef == null ? -1 : sortedColumns.findIndex((it) => it.id === columnDef.id)
29
- );
30
- const isAtTop = $derived(currentIndex <= 0);
31
- const isAtBottom = $derived(currentIndex === -1 || currentIndex >= sortedColumns.length - 1);
32
-
33
- function moveUp(): void {
34
- if (columnDef == null || isAtTop) {
35
- return;
36
- }
37
- preserveListScroll(() => {
38
- tableContext.moveColumnTo(currentIndex, currentIndex - 1);
39
- });
1
+ <svelte:options runes />
2
+
3
+ <script
4
+ lang="ts"
5
+ generics="T extends object, IdType extends Primitive"
6
+ >import Action from "../../../Action.svelte";
7
+ import Checkbox from "../../../Checkbox.svelte";
8
+ import ThreeStateRadio from "../../../ThreeStateRadio.svelte";
9
+ import { TableContext } from "../../Types/Context/TableContext.svelte.js";
10
+ import { Size } from "../../../../Types/Internal/Size.js";
11
+ import { Ternary } from "../../../../Types/Internal/Ternary.js";
12
+ import { getContext } from "svelte";
13
+ const tableContext = getContext(TableContext.identifier);
14
+ const { columnDef } = $props();
15
+ /**
16
+ * Position of this column inside the sorted column list. Drives the
17
+ * disabled state of the "move up" / "move down" buttons and supplies
18
+ * the target index to `moveColumnTo`. Sort by `index` (the stable
19
+ * layout identity) rather than relying on `columnDefs` iteration order.
20
+ */
21
+ const sortedColumns = $derived(tableContext.columnDefs.slice().sort((a, b) => a.index - b.index));
22
+ const currentIndex = $derived(columnDef == null ? -1 : sortedColumns.findIndex((it) => it.id === columnDef.id));
23
+ const isAtTop = $derived(currentIndex <= 0);
24
+ const isAtBottom = $derived(currentIndex === -1 || currentIndex >= sortedColumns.length - 1);
25
+ function moveUp() {
26
+ if (columnDef == null || isAtTop) {
27
+ return;
40
28
  }
41
-
42
- function moveDown(): void {
43
- if (columnDef == null || isAtBottom) {
44
- return;
45
- }
46
- preserveListScroll(() => {
47
- tableContext.moveColumnTo(currentIndex, currentIndex + 1);
48
- });
29
+ preserveListScroll(() => {
30
+ tableContext.moveColumnTo(currentIndex, currentIndex - 1);
31
+ });
32
+ }
33
+ function moveDown() {
34
+ if (columnDef == null || isAtBottom) {
35
+ return;
49
36
  }
50
-
51
- let isBeingTargeted = $state(false);
52
- /** Bound `<li>` element used by `preserveListScroll` to find the
53
- * enclosing `.column-list` even when the event target is the
54
- * visibility checkbox's hidden `<input>` (which is
55
- * `position: absolute` with no positioned ancestor and therefore
56
- * drifts away from the row in the layout tree). */
57
- let rowEl: HTMLLIElement | undefined = $state();
58
-
59
- function disableDragVisual() {
60
- isBeingTargeted = false;
37
+ preserveListScroll(() => {
38
+ tableContext.moveColumnTo(currentIndex, currentIndex + 1);
39
+ });
40
+ }
41
+ let isBeingTargeted = $state(false);
42
+ /** Bound `<li>` element used by `preserveListScroll` to find the
43
+ * enclosing `.column-list` even when the event target is the
44
+ * visibility checkbox's hidden `<input>` (which is
45
+ * `position: absolute` with no positioned ancestor and therefore
46
+ * drifts away from the row in the layout tree). */
47
+ let rowEl = $state();
48
+ function disableDragVisual() {
49
+ isBeingTargeted = false;
50
+ }
51
+ function makeKVPairs(item, index) {
52
+ return [item, index];
53
+ }
54
+ /**
55
+ * Pre-click scroll snapshot.
56
+ *
57
+ * When the user clicks a checkbox label, the browser fires a synthetic
58
+ * click on the hidden `<input>` BEFORE `onchange` fires. That synthetic
59
+ * click focuses the input, and the focus side effect scrolls the
60
+ * column-list (and sometimes the window) to bring the 1x1px,
61
+ * `clip-path`-ed input's bounding box into view. Reading `scrollTop`
62
+ * inside `onchange` therefore captures the post-jump position and
63
+ * "restoring" to it freezes the list at the wrong spot.
64
+ *
65
+ * Snapshotting on `mousedown` (which fires before the focus step)
66
+ * captures the genuine pre-click position. The snapshot is re-applied
67
+ * at multiple time points after the mutation so any later focus /
68
+ * layout shift gets undone.
69
+ */
70
+ let preClickListScroll = 0;
71
+ let preClickWindowScrollX = 0;
72
+ let preClickWindowScrollY = 0;
73
+ /** Milliseconds `Date.now()` stamp of the last `snapshotScrollPosition`
74
+ * call. Lets `preserveListScroll` tell apart "click triggered toggle
75
+ * whose mousedown just snapshotted real pre-click state" from
76
+ * "keyboard or other path where the mousedown snapshot is stale". */
77
+ let lastSnapshotAt = 0;
78
+ /** Stale snapshots more than this many ms old are ignored. Tuned
79
+ * loose enough to survive any focus / click latency the browser
80
+ * might insert, but tight enough that an old click from minutes
81
+ * ago can't poison a later keyboard toggle. */
82
+ const SNAPSHOT_FRESHNESS_MS = 500;
83
+ function snapshotScrollPosition() {
84
+ const list = rowEl?.closest(".column-list");
85
+ preClickListScroll = list?.scrollTop ?? 0;
86
+ preClickWindowScrollX = typeof window !== "undefined" ? window.scrollX : 0;
87
+ preClickWindowScrollY = typeof window !== "undefined" ? window.scrollY : 0;
88
+ lastSnapshotAt = Date.now();
89
+ }
90
+ function restoreScrollPosition() {
91
+ const list = rowEl?.closest(".column-list");
92
+ if (list != null) {
93
+ list.scrollTop = preClickListScroll;
61
94
  }
62
-
63
- function makeKVPairs(item: string, index: number): readonly [string, number] {
64
- return [item, index];
95
+ if (typeof window !== "undefined") {
96
+ window.scrollTo(preClickWindowScrollX, preClickWindowScrollY);
65
97
  }
66
-
67
- /**
68
- * Pre-click scroll snapshot.
69
- *
70
- * When the user clicks a checkbox label, the browser fires a synthetic
71
- * click on the hidden `<input>` BEFORE `onchange` fires. That synthetic
72
- * click focuses the input, and the focus side effect scrolls the
73
- * column-list (and sometimes the window) to bring the 1x1px,
74
- * `clip-path`-ed input's bounding box into view. Reading `scrollTop`
75
- * inside `onchange` therefore captures the post-jump position and
76
- * "restoring" to it freezes the list at the wrong spot.
77
- *
78
- * Snapshotting on `mousedown` (which fires before the focus step)
79
- * captures the genuine pre-click position. The snapshot is re-applied
80
- * at multiple time points after the mutation so any later focus /
81
- * layout shift gets undone.
82
- */
83
- let preClickListScroll = 0;
84
- let preClickWindowScrollX = 0;
85
- let preClickWindowScrollY = 0;
86
- /** Milliseconds `Date.now()` stamp of the last `snapshotScrollPosition`
87
- * call. Lets `preserveListScroll` tell apart "click triggered toggle
88
- * whose mousedown just snapshotted real pre-click state" from
89
- * "keyboard or other path where the mousedown snapshot is stale". */
90
- let lastSnapshotAt = 0;
91
-
92
- /** Stale snapshots more than this many ms old are ignored. Tuned
93
- * loose enough to survive any focus / click latency the browser
94
- * might insert, but tight enough that an old click from minutes
95
- * ago can't poison a later keyboard toggle. */
96
- const SNAPSHOT_FRESHNESS_MS = 500;
97
-
98
- function snapshotScrollPosition(): void {
99
- const list = rowEl?.closest('.column-list') as HTMLElement | null;
100
- preClickListScroll = list?.scrollTop ?? 0;
101
- preClickWindowScrollX = typeof window !== 'undefined' ? window.scrollX : 0;
102
- preClickWindowScrollY = typeof window !== 'undefined' ? window.scrollY : 0;
103
- lastSnapshotAt = Date.now();
98
+ }
99
+ /**
100
+ * Run `mutate` and then re-apply the pre-click scroll snapshot
101
+ * aggressively across the next ~400ms so any deferred focus / layout
102
+ * work that fights us at the tail of the event loop still ends up at
103
+ * the user's original scroll position.
104
+ *
105
+ * The snapshot is taken in capture phase from the row's `{@attach}`
106
+ * listener below, before the focus side effect can scroll. If invoked
107
+ * without a recent mousedown (keyboard toggle, programmatic call) a
108
+ * fresh snapshot is taken here.
109
+ *
110
+ * A temporary scroll listener on list + window forcibly reverts any
111
+ * scroll event during the lock window, complementing the discrete
112
+ * `rAF` / `setTimeout` restores. The listener no-ops when already at
113
+ * the snapshot, so the restore call inside it does not re-trigger.
114
+ *
115
+ * The active hidden input is blurred immediately after the mutation
116
+ * so the browser stops treating it as the "currently focused element
117
+ * to keep visible" on subsequent layout passes.
118
+ */
119
+ function preserveListScroll(mutate) {
120
+ if (Date.now() - lastSnapshotAt > SNAPSHOT_FRESHNESS_MS) {
121
+ snapshotScrollPosition();
104
122
  }
105
-
106
- function restoreScrollPosition(): void {
107
- const list = rowEl?.closest('.column-list') as HTMLElement | null;
108
- if (list != null) {
109
- list.scrollTop = preClickListScroll;
110
- }
111
- if (typeof window !== 'undefined') {
112
- window.scrollTo(preClickWindowScrollX, preClickWindowScrollY);
113
- }
123
+ const focused = typeof document !== "undefined" && document.activeElement instanceof HTMLElement ? document.activeElement : null;
124
+ const list = rowEl?.closest(".column-list");
125
+ mutate();
126
+ if (focused != null && rowEl?.contains(focused) === true) {
127
+ focused.blur();
114
128
  }
115
-
116
- /**
117
- * Run `mutate` and then re-apply the pre-click scroll snapshot
118
- * aggressively across the next ~400ms so any deferred focus / layout
119
- * work that fights us at the tail of the event loop still ends up at
120
- * the user's original scroll position.
121
- *
122
- * The snapshot is taken in capture phase from the row's `{@attach}`
123
- * listener below, before the focus side effect can scroll. If invoked
124
- * without a recent mousedown (keyboard toggle, programmatic call) a
125
- * fresh snapshot is taken here.
126
- *
127
- * A temporary scroll listener on list + window forcibly reverts any
128
- * scroll event during the lock window, complementing the discrete
129
- * `rAF` / `setTimeout` restores. The listener no-ops when already at
130
- * the snapshot, so the restore call inside it does not re-trigger.
131
- *
132
- * The active hidden input is blurred immediately after the mutation
133
- * so the browser stops treating it as the "currently focused element
134
- * to keep visible" on subsequent layout passes.
135
- */
136
- function preserveListScroll(mutate: () => void): void {
137
- if (Date.now() - lastSnapshotAt > SNAPSHOT_FRESHNESS_MS) {
138
- snapshotScrollPosition();
139
- }
140
- const focused =
141
- typeof document !== 'undefined' && document.activeElement instanceof HTMLElement
142
- ? document.activeElement
143
- : null;
144
- const list = rowEl?.closest('.column-list') as HTMLElement | null;
145
- mutate();
146
- if (focused != null && rowEl?.contains(focused) === true) {
147
- focused.blur();
148
- }
149
- if (typeof requestAnimationFrame !== 'undefined') {
150
- requestAnimationFrame(restoreScrollPosition);
151
- }
152
- setTimeout(restoreScrollPosition, 0);
153
- setTimeout(restoreScrollPosition, 60);
154
- setTimeout(restoreScrollPosition, 120);
155
- setTimeout(restoreScrollPosition, 240);
156
-
157
- // Scroll-lock: forcibly revert any rogue scroll for the next ~400ms.
158
- // Anything in this window that scrolls the list or page (focus
159
- // auto-scroll, scrollIntoView from a deferred reactive effect,
160
- // etc.) is snapped back to the pre-click snapshot. The handler
161
- // no-ops when already at the snapshot, so the restore inside it
162
- // does not re-trigger.
163
- if (list != null && typeof window !== 'undefined') {
164
- const lockListHandler = (): void => {
165
- if (list.scrollTop !== preClickListScroll) {
166
- list.scrollTop = preClickListScroll;
167
- }
168
- };
169
- const lockWindowHandler = (): void => {
170
- if (window.scrollX !== preClickWindowScrollX || window.scrollY !== preClickWindowScrollY) {
171
- window.scrollTo(preClickWindowScrollX, preClickWindowScrollY);
172
- }
173
- };
174
- list.addEventListener('scroll', lockListHandler, { passive: true });
175
- window.addEventListener('scroll', lockWindowHandler, { passive: true });
176
- setTimeout(() => {
177
- list.removeEventListener('scroll', lockListHandler);
178
- window.removeEventListener('scroll', lockWindowHandler);
179
- }, 400);
180
- }
129
+ if (typeof requestAnimationFrame !== "undefined") {
130
+ requestAnimationFrame(restoreScrollPosition);
181
131
  }
182
-
183
- /**
184
- * Current pin state of this column expressed as a `Ternary` so the
185
- * three states (`pinned-left` / `unpinned` / `pinned-right`) map
186
- * cleanly onto `ThreeStateRadio`:
187
- *
188
- * - `Ternary.False` → pinned to the LEFT side (leftmost segment)
189
- * - `Ternary.Neutral` unpinned (middle segment)
190
- * - `Ternary.True` → pinned to the RIGHT side (rightmost segment)
191
- */
192
- const pinSide: Ternary = $derived(
193
- columnDef == null
194
- ? Ternary.Neutral
195
- : tableContext.columnPinning.left?.has(columnDef.id)
196
- ? Ternary.False
197
- : tableContext.columnPinning.right?.has(columnDef.id)
198
- ? Ternary.True
199
- : Ternary.Neutral
200
- );
201
-
202
- /**
203
- * Set this column's pin state from a `Ternary`. The previous side (if
204
- * any) is removed first so the column is never pinned to both sides.
205
- * Wrapped in `preserveListScroll` so the column-list scroll position
206
- * survives any focus side effect inside the ThreeStateRadio's hidden
207
- * inputs.
208
- */
209
- function setPin(next: Ternary): void {
210
- if (columnDef == null) {
211
- return;
212
- }
213
- preserveListScroll(() => {
214
- tableContext.setColumnPinning((pinning) => {
215
- const leftKeys = [...(pinning.left?.keys() ?? [])].filter((id) => id !== columnDef.id);
216
- const rightKeys = [...(pinning.right?.keys() ?? [])].filter((id) => id !== columnDef.id);
217
- if (next === Ternary.False) {
218
- leftKeys.push(columnDef.id);
219
- } else if (next === Ternary.True) {
220
- rightKeys.push(columnDef.id);
221
- }
222
- return {
223
- ...pinning,
224
- left: new Map(leftKeys.map(makeKVPairs)),
225
- right: new Map(rightKeys.map(makeKVPairs)),
226
- };
227
- });
132
+ setTimeout(restoreScrollPosition, 0);
133
+ setTimeout(restoreScrollPosition, 60);
134
+ setTimeout(restoreScrollPosition, 120);
135
+ setTimeout(restoreScrollPosition, 240);
136
+ // Scroll-lock: forcibly revert any rogue scroll for the next ~400ms.
137
+ // Anything in this window that scrolls the list or page (focus
138
+ // auto-scroll, scrollIntoView from a deferred reactive effect,
139
+ // etc.) is snapped back to the pre-click snapshot. The handler
140
+ // no-ops when already at the snapshot, so the restore inside it
141
+ // does not re-trigger.
142
+ if (list != null && typeof window !== "undefined") {
143
+ const lockListHandler = () => {
144
+ if (list.scrollTop !== preClickListScroll) {
145
+ list.scrollTop = preClickListScroll;
146
+ }
147
+ };
148
+ const lockWindowHandler = () => {
149
+ if (window.scrollX !== preClickWindowScrollX || window.scrollY !== preClickWindowScrollY) {
150
+ window.scrollTo(preClickWindowScrollX, preClickWindowScrollY);
151
+ }
152
+ };
153
+ list.addEventListener("scroll", lockListHandler, { passive: true });
154
+ window.addEventListener("scroll", lockWindowHandler, { passive: true });
155
+ setTimeout(() => {
156
+ list.removeEventListener("scroll", lockListHandler);
157
+ window.removeEventListener("scroll", lockWindowHandler);
158
+ }, 400);
159
+ }
160
+ }
161
+ /**
162
+ * Current pin state of this column expressed as a `Ternary` so the
163
+ * three states (`pinned-left` / `unpinned` / `pinned-right`) map
164
+ * cleanly onto `ThreeStateRadio`:
165
+ *
166
+ * - `Ternary.False` → pinned to the LEFT side (leftmost segment)
167
+ * - `Ternary.Neutral` → unpinned (middle segment)
168
+ * - `Ternary.True` → pinned to the RIGHT side (rightmost segment)
169
+ */
170
+ const pinSide = $derived(columnDef == null ? Ternary.Neutral : tableContext.columnPinning.left?.has(columnDef.id) ? Ternary.False : tableContext.columnPinning.right?.has(columnDef.id) ? Ternary.True : Ternary.Neutral);
171
+ /**
172
+ * Set this column's pin state from a `Ternary`. The previous side (if
173
+ * any) is removed first so the column is never pinned to both sides.
174
+ * Wrapped in `preserveListScroll` so the column-list scroll position
175
+ * survives any focus side effect inside the ThreeStateRadio's hidden
176
+ * inputs.
177
+ */
178
+ function setPin(next) {
179
+ if (columnDef == null) {
180
+ return;
181
+ }
182
+ preserveListScroll(() => {
183
+ tableContext.setColumnPinning((pinning) => {
184
+ const leftKeys = [...pinning.left?.keys() ?? []].filter((id) => id !== columnDef.id);
185
+ const rightKeys = [...pinning.right?.keys() ?? []].filter((id) => id !== columnDef.id);
186
+ if (next === Ternary.False) {
187
+ leftKeys.push(columnDef.id);
188
+ } else if (next === Ternary.True) {
189
+ rightKeys.push(columnDef.id);
190
+ }
191
+ return {
192
+ ...pinning,
193
+ left: new Map(leftKeys.map(makeKVPairs)),
194
+ right: new Map(rightKeys.map(makeKVPairs))
195
+ };
228
196
  });
197
+ });
198
+ }
199
+ const onVisibilityChange = (e) => {
200
+ if (!columnDef) {
201
+ console.warn("no def found", e);
202
+ return;
229
203
  }
230
-
231
- const onVisibilityChange = (e: Event) => {
232
- if (!columnDef) {
233
- console.warn('no def found', e);
234
-
235
- return;
236
- }
237
- const checked = (e.target as HTMLInputElement).checked;
238
- preserveListScroll(() => {
239
- tableContext.updateVisibility((defs) => {
240
- defs[columnDef.id] = checked;
241
- return defs;
242
- });
204
+ const checked = e.target.checked;
205
+ preserveListScroll(() => {
206
+ tableContext.updateVisibility((defs) => {
207
+ defs[columnDef.id] = checked;
208
+ return defs;
243
209
  });
244
- };
245
-
246
- /**
247
- * Keyboard reorder support. Alt+ArrowUp/Down nudges the column,
248
- * Alt+Home/End jumps to the ends. Bound on the row so any focusable
249
- * child (eye, pin selector, move buttons) can trigger it.
250
- *
251
- * `Alt` is required so plain arrow keys still navigate radio groups
252
- * (ThreeStateRadio) and the row's own scroll container.
253
- */
254
- function onRowKeydown(e: KeyboardEvent) {
255
- if (!columnDef || !tableContext.allowColumnReordering) {
256
- return;
257
- }
258
- if (!e.altKey) {
259
- return;
260
- }
261
- if (currentIndex === -1) {
262
- return;
263
- }
264
-
265
- const lastIndex = sortedColumns.length - 1;
266
- let targetIndex: number;
267
-
268
- switch (e.key) {
269
- case 'ArrowUp':
270
- targetIndex = Math.max(0, currentIndex - 1);
271
- break;
272
- case 'ArrowDown':
273
- targetIndex = Math.min(lastIndex, currentIndex + 1);
274
- break;
275
- case 'Home':
276
- targetIndex = 0;
277
- break;
278
- case 'End':
279
- targetIndex = lastIndex;
280
- break;
281
- default:
282
- return;
283
- }
284
-
285
- if (targetIndex === currentIndex) {
286
- e.preventDefault();
287
- return;
288
- }
289
-
210
+ });
211
+ };
212
+ /**
213
+ * Keyboard reorder support. Alt+ArrowUp/Down nudges the column,
214
+ * Alt+Home/End jumps to the ends. Bound on the row so any focusable
215
+ * child (eye, pin selector, move buttons) can trigger it.
216
+ *
217
+ * `Alt` is required so plain arrow keys still navigate radio groups
218
+ * (ThreeStateRadio) and the row's own scroll container.
219
+ */
220
+ function onRowKeydown(e) {
221
+ if (!columnDef || !tableContext.allowColumnReordering) {
222
+ return;
223
+ }
224
+ if (!e.altKey) {
225
+ return;
226
+ }
227
+ if (currentIndex === -1) {
228
+ return;
229
+ }
230
+ const lastIndex = sortedColumns.length - 1;
231
+ let targetIndex;
232
+ switch (e.key) {
233
+ case "ArrowUp":
234
+ targetIndex = Math.max(0, currentIndex - 1);
235
+ break;
236
+ case "ArrowDown":
237
+ targetIndex = Math.min(lastIndex, currentIndex + 1);
238
+ break;
239
+ case "Home":
240
+ targetIndex = 0;
241
+ break;
242
+ case "End":
243
+ targetIndex = lastIndex;
244
+ break;
245
+ default: return;
246
+ }
247
+ if (targetIndex === currentIndex) {
290
248
  e.preventDefault();
291
- preserveListScroll(() => {
292
- tableContext.moveColumnTo(currentIndex, targetIndex);
293
- });
249
+ return;
294
250
  }
295
- </script>
296
-
297
- {#if columnDef}
298
- {@const reorderLabel = typeof columnDef.header === 'string' ? columnDef.header : columnDef.id}
299
- <!--
300
- The `<li>` is a structural container for the eye, pin selector,
301
- label, and reorder buttons - none activated by clicking the `<li>`
302
- itself. The `onkeydown` here only captures Alt+Arrow / Alt+Home /
303
- Alt+End bubbling up from whichever inner control has focus to
304
- drive the reorder shortcut; the `<li>` is not an interactive
305
- widget. The structure is a free-form list of cards (not a tree
306
- or listbox), so the right call is to silence the warning rather
307
- than fake `role="treeitem"` / `role="option"`.
308
- -->
309
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
310
- <li
311
- bind:this={rowEl}
312
- class="single-column"
313
- class:targeted={isBeingTargeted}
314
- class:draggable={tableContext.allowColumnReordering}
315
- id="{tableContext.tableName}-column-settings-{columnDef.id}"
316
- draggable={tableContext.allowColumnReordering}
317
- onkeydown={onRowKeydown}
318
- {@attach (node: HTMLLIElement) => {
319
- // Capture-phase `mousedown` and `pointerdown` listeners. Both
320
- // must fire BEFORE the synthetic click on the visibility /
321
- // pin checkbox focuses its hidden input, because that focus
322
- // triggers an auto-scroll to bring the 1x1px clip-pathed
323
- // input into view. Capture phase runs before target / bubble,
324
- // so this listener reliably snapshots the user's true
325
- // pre-click scroll position.
326
- //
327
- // Both event types are wired because browsers differ on which
328
- // fires first; the second is a cheap no-op snapshot.
329
- const snapshot = (): void => {
330
- snapshotScrollPosition();
331
- };
332
- node.addEventListener('pointerdown', snapshot, { capture: true });
333
- node.addEventListener('mousedown', snapshot, { capture: true });
334
- return () => {
335
- node.removeEventListener('pointerdown', snapshot, { capture: true });
336
- node.removeEventListener('mousedown', snapshot, { capture: true });
337
- };
338
- }}
339
- ondragstart={() => {
340
- tableContext.dragTarget = columnDef;
341
- }}
342
- ondragend={disableDragVisual}
343
- ondragover={(e) => {
344
- e.preventDefault();
345
- if (tableContext.dragTarget?.id !== columnDef.id) {
346
- isBeingTargeted = true;
347
- }
348
- }}
349
- ondragleave={disableDragVisual}
350
- ondrop={disableDragVisual}
351
- >
352
- <!--
353
- `.row-main` stacks the column label on top and the visibility /
354
- pin controls underneath so the label gets the full card width
355
- to wrap into. `.reorder-buttons` is a sibling outside
356
- `.row-main` so the up / down arrows stay vertically centred on
357
- the right edge of the card.
358
- -->
359
- <div class="row-main">
360
- <span
361
- class="column-label"
362
- title={typeof columnDef.header === 'string' ? columnDef.header : columnDef.id}
363
- >
364
- {#if typeof columnDef.header === 'function'}
365
- <!-- FIXME this needs testing -->
366
- {@render columnDef.header()}
367
- {:else}
368
- {columnDef.header}
369
- {/if}
370
- </span>
371
-
372
- <div class="row-controls">
373
- <div class="visibility-wrapper">
374
- <Checkbox
375
- icon={{ name: 'assessment', size: Size.MediumSmall }}
376
- size={Size.FillWidth}
377
- id={`is-visible-${tableContext.tableName}-${columnDef.id}`}
378
- value={tableContext.columnVisibility[columnDef.id] !== false}
379
- onchange={onVisibilityChange}
380
- />
381
- </div>
382
-
383
- {#if tableContext.allowColumnReordering}
384
- <!--
385
- Tri-state pin selector. The three `Ternary` slots map to:
386
- False -> pinned LEFT (pin-left icon, leftmost)
387
- Neutral -> unpinned (circle-cross icon, middle)
388
- True -> pinned RIGHT (pin-right icon, rightmost)
389
- Expand columns can never be pinned and disable the group.
390
- -->
391
- <div class="pin-selector">
392
- <ThreeStateRadio
393
- value={pinSide}
394
- noLabel="Pin to left"
395
- neutralLabel="Unpinned"
396
- yesLabel="Pin to right"
397
- noIcon={{ name: 'pin-left', size: Size.MediumSmall }}
398
- neutralIcon={{ name: 'circle-cross', size: Size.MediumSmall }}
399
- yesIcon={{ name: 'pin-right', size: Size.MediumSmall }}
400
- onchange={(next: Ternary) => {
401
- setPin(next);
402
- }}
403
- />
404
- </div>
405
- {/if}
406
- </div>
407
- </div>
408
-
409
- {#if tableContext.allowColumnReordering}
410
- <!--
411
- Up / down "move" buttons make column reordering
412
- discoverable without relying on the user noticing a
413
- drag-cursor. The whole row remains draggable for power
414
- users. Buttons disable at the list boundaries so a no-op
415
- `moveColumnTo` is never dispatched.
416
- -->
417
- <div class="reorder-buttons">
418
- <Action
419
- icon={{ name: 'chevron-up', size: Size.Medium }}
420
- size={Size.Medium}
421
- paddingSize={Size.Small}
422
- label="Move {reorderLabel} up"
423
- disabled={isAtTop}
424
- onclick={moveUp}
425
- />
426
- <Action
427
- icon={{ name: 'chevron-down', size: Size.Medium }}
428
- size={Size.Medium}
429
- paddingSize={Size.Small}
430
- label="Move {reorderLabel} down"
431
- disabled={isAtBottom}
432
- onclick={moveDown}
433
- />
434
- </div>
435
- {/if}
436
- </li>
437
- {:else}
438
- <li
439
- id="{tableContext.tableName}-column-settings-top-anchor"
440
- class="single-column anchor"
441
- class:targeted={isBeingTargeted}
442
- ondragover={(e) => {
443
- e.preventDefault();
444
-
445
- if (tableContext.dragTarget?.id !== undefined) {
446
- isBeingTargeted = true;
447
- }
448
- }}
449
- ondragleave={disableDragVisual}
450
- ondrop={disableDragVisual}
451
- ></li>
452
- {/if}
453
-
251
+ e.preventDefault();
252
+ preserveListScroll(() => {
253
+ tableContext.moveColumnTo(currentIndex, targetIndex);
254
+ });
255
+ }
256
+ </script>
257
+
258
+ {#if columnDef}
259
+ {@const reorderLabel = typeof columnDef.header === 'string' ? columnDef.header : columnDef.id}
260
+ <!--
261
+ The `<li>` is a structural container for the eye, pin selector,
262
+ label, and reorder buttons - none activated by clicking the `<li>`
263
+ itself. The `onkeydown` here only captures Alt+Arrow / Alt+Home /
264
+ Alt+End bubbling up from whichever inner control has focus to
265
+ drive the reorder shortcut; the `<li>` is not an interactive
266
+ widget. The structure is a free-form list of cards (not a tree
267
+ or listbox), so the right call is to silence the warning rather
268
+ than fake `role="treeitem"` / `role="option"`.
269
+ -->
270
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
271
+ <li
272
+ bind:this={rowEl}
273
+ class="single-column"
274
+ class:targeted={isBeingTargeted}
275
+ class:draggable={tableContext.allowColumnReordering}
276
+ id="{tableContext.tableName}-column-settings-{columnDef.id}"
277
+ draggable={tableContext.allowColumnReordering}
278
+ onkeydown={onRowKeydown}
279
+ {@attach (node: HTMLLIElement) => {
280
+ // Capture-phase `mousedown` and `pointerdown` listeners. Both
281
+ // must fire BEFORE the synthetic click on the visibility /
282
+ // pin checkbox focuses its hidden input, because that focus
283
+ // triggers an auto-scroll to bring the 1x1px clip-pathed
284
+ // input into view. Capture phase runs before target / bubble,
285
+ // so this listener reliably snapshots the user's true
286
+ // pre-click scroll position.
287
+ //
288
+ // Both event types are wired because browsers differ on which
289
+ // fires first; the second is a cheap no-op snapshot.
290
+ const snapshot = (): void => {
291
+ snapshotScrollPosition();
292
+ };
293
+ node.addEventListener('pointerdown', snapshot, { capture: true });
294
+ node.addEventListener('mousedown', snapshot, { capture: true });
295
+ return () => {
296
+ node.removeEventListener('pointerdown', snapshot, { capture: true });
297
+ node.removeEventListener('mousedown', snapshot, { capture: true });
298
+ };
299
+ }}
300
+ ondragstart={() => {
301
+ tableContext.dragTarget = columnDef;
302
+ }}
303
+ ondragend={disableDragVisual}
304
+ ondragover={(e) => {
305
+ e.preventDefault();
306
+ if (tableContext.dragTarget?.id !== columnDef.id) {
307
+ isBeingTargeted = true;
308
+ }
309
+ }}
310
+ ondragleave={disableDragVisual}
311
+ ondrop={disableDragVisual}
312
+ >
313
+ <!--
314
+ `.row-main` stacks the column label on top and the visibility /
315
+ pin controls underneath so the label gets the full card width
316
+ to wrap into. `.reorder-buttons` is a sibling outside
317
+ `.row-main` so the up / down arrows stay vertically centred on
318
+ the right edge of the card.
319
+ -->
320
+ <div class="row-main">
321
+ <span
322
+ class="column-label"
323
+ title={typeof columnDef.header === 'string' ? columnDef.header : columnDef.id}
324
+ >
325
+ {#if typeof columnDef.header === 'function'}
326
+ <!-- FIXME this needs testing -->
327
+ {@render columnDef.header()}
328
+ {:else}
329
+ {columnDef.header}
330
+ {/if}
331
+ </span>
332
+
333
+ <div class="row-controls">
334
+ <div class="visibility-wrapper">
335
+ <Checkbox
336
+ icon={{ name: 'assessment', size: Size.MediumSmall }}
337
+ size={Size.FillWidth}
338
+ id={`is-visible-${tableContext.tableName}-${columnDef.id}`}
339
+ value={tableContext.columnVisibility[columnDef.id] !== false}
340
+ onchange={onVisibilityChange}
341
+ />
342
+ </div>
343
+
344
+ {#if tableContext.allowColumnReordering}
345
+ <!--
346
+ Tri-state pin selector. The three `Ternary` slots map to:
347
+ False -> pinned LEFT (pin-left icon, leftmost)
348
+ Neutral -> unpinned (circle-cross icon, middle)
349
+ True -> pinned RIGHT (pin-right icon, rightmost)
350
+ Expand columns can never be pinned and disable the group.
351
+ -->
352
+ <div class="pin-selector">
353
+ <ThreeStateRadio
354
+ value={pinSide}
355
+ noLabel="Pin to left"
356
+ neutralLabel="Unpinned"
357
+ yesLabel="Pin to right"
358
+ noIcon={{ name: 'pin-left', size: Size.MediumSmall }}
359
+ neutralIcon={{ name: 'circle-cross', size: Size.MediumSmall }}
360
+ yesIcon={{ name: 'pin-right', size: Size.MediumSmall }}
361
+ onchange={(next: Ternary) => {
362
+ setPin(next);
363
+ }}
364
+ />
365
+ </div>
366
+ {/if}
367
+ </div>
368
+ </div>
369
+
370
+ {#if tableContext.allowColumnReordering}
371
+ <!--
372
+ Up / down "move" buttons make column reordering
373
+ discoverable without relying on the user noticing a
374
+ drag-cursor. The whole row remains draggable for power
375
+ users. Buttons disable at the list boundaries so a no-op
376
+ `moveColumnTo` is never dispatched.
377
+ -->
378
+ <div class="reorder-buttons">
379
+ <Action
380
+ icon={{ name: 'chevron-up', size: Size.Medium }}
381
+ size={Size.Medium}
382
+ paddingSize={Size.Small}
383
+ label="Move {reorderLabel} up"
384
+ disabled={isAtTop}
385
+ onclick={moveUp}
386
+ />
387
+ <Action
388
+ icon={{ name: 'chevron-down', size: Size.Medium }}
389
+ size={Size.Medium}
390
+ paddingSize={Size.Small}
391
+ label="Move {reorderLabel} down"
392
+ disabled={isAtBottom}
393
+ onclick={moveDown}
394
+ />
395
+ </div>
396
+ {/if}
397
+ </li>
398
+ {:else}
399
+ <li
400
+ id="{tableContext.tableName}-column-settings-top-anchor"
401
+ class="single-column anchor"
402
+ class:targeted={isBeingTargeted}
403
+ ondragover={(e) => {
404
+ e.preventDefault();
405
+
406
+ if (tableContext.dragTarget?.id !== undefined) {
407
+ isBeingTargeted = true;
408
+ }
409
+ }}
410
+ ondragleave={disableDragVisual}
411
+ ondrop={disableDragVisual}
412
+ ></li>
413
+ {/if}
414
+
454
415
  <style>li {
455
416
  box-sizing: border-box;
456
417
  }
@@ -561,4 +522,4 @@ li.single-column:not(.anchor):last-of-type {
561
522
  --colour: #1f81a0;
562
523
  color: #1f81a0;
563
524
  background-color: transparent;
564
- }</style>
525
+ }</style>