@lavalogic/scoria 0.37.44 → 0.37.45

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