@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,646 +1,591 @@
1
- <svelte:options runes />
2
-
3
- <script
4
- lang="ts"
5
- generics="T extends object, IdType extends Primitive"
6
- >
7
- import Button from '../../Button.svelte';
8
- import DesktopModal from '../../DesktopModal.svelte';
9
- import HorizontalTabGroup from '../../HorizontalTabGroup.svelte';
10
- import Icon from '../../Icon.svelte';
11
- import InfoAlert from '../../InfoAlert.svelte';
12
- import TextInput from '../../TextInput.svelte';
13
- import VerticalTabGroup from '../../VerticalTabGroup.svelte';
14
- import { cssLength } from '../../../Helpers/Helpers.svelte.js';
15
- import { Size } from '../../../Types/Internal/Size.js';
16
- import type { TabOption } from '../../../Types/Internal/TabOption.js';
17
- import { Variant } from '../../../Types/Internal/Variant.js';
18
- import { getContext, onMount } from 'svelte';
19
- import { TableContext } from '../Types/Context/TableContext.svelte.js';
20
- import type { Primitive } from '../Types/Context/TableInitOptions.js';
21
- import type { DatatableView } from '../Types/Persistence/DatatableView.js';
22
- import type { DatatableViewKind } from '../Types/Persistence/DatatableViewKind.js';
23
- import ShareViewModal from './ShareViewModal.svelte';
24
- import type { TableConfigurationModalProps } from './TableConfigurationModalProps.js';
25
- import UpdateViewOptionsModal from './UpdateViewOptionsModal.svelte';
26
-
27
- const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
28
-
29
- const { onclose }: TableConfigurationModalProps = $props();
30
-
31
- // ── Left-nav section + right-pane tab selection ──────────────────────
32
- // `section` picks the kind (layout vs filter); `tab` picks which of the
33
- // three horizontal panes is visible for that section. Both are plain
34
- // `$state` since they are pure local UI selection. They drive the
35
- // `selected` prop of the two scoria tab groups below.
36
- let section = $state<DatatableViewKind>('layout');
37
- type ConfigTab = 'mine' | 'shared' | 'save';
38
- let tab = $state<ConfigTab>('mine');
39
-
40
- // Name field for the "Save a new ..." tab.
41
- let newViewName = $state('');
42
-
43
- // Public-views discovery state for the "Subscribe to Additional" flow.
44
- // `null` = not yet loaded; an array = the last successful fetch.
45
- let publicViews = $state<ReadonlyArray<DatatableView> | null>(null);
46
- let loadingPublicViews = $state(false);
47
-
48
- // User-visible error banner. Every adapter call is wrapped so a
49
- // rejection lands here instead of throwing into the UI. scoria cannot
50
- // assume a host-provided toast context exists, so the modal surfaces
51
- // errors inline in its own body.
52
- let errorMessage = $state<string | null>(null);
53
-
54
- // True while the open-time fetch of saved views is in flight, so the
55
- // My-tab shows "Loading…" rather than the "nothing saved yet" message.
56
- let loadingViews = $state(false);
57
-
58
- // The view whose Share modal is currently open. `null` when no share
59
- // modal is rendered. Owner cards (rendered on the My-tab) wire their
60
- // Share button to open `ShareViewModal` against the view they belong
61
- // to; the modal closes itself on Cancel / Save.
62
- let sharingView = $state<DatatableView | null>(null);
63
-
64
- // The view whose Update Options modal is currently open. `null` when
65
- // no options modal is rendered. Same pattern as `sharingView`.
66
- let updatingOptionsView = $state<DatatableView | null>(null);
67
-
68
- /**
69
- * Overwrite a saved view's stored body with the table's current
70
- * live state. Triggered by the "Save Layout" / "Save Filter"
71
- * button on an owner card - the typical flow is "apply view A,
72
- * tweak the columns / filters, then click Save on A". Refreshes
73
- * the saved-views list afterwards so the card label and dropdown
74
- * pick up the new body, and surfaces any failure in the inline
75
- * error banner.
76
- */
77
- function saveLiveStateInto(view: DatatableView): void {
78
- void guard(async () => {
79
- await tableContext.saveLiveStateIntoView(view);
80
- await tableContext.refreshSavedViews();
81
- }, `Could not save changes to "${view.name}". Please try again.`);
1
+ <svelte:options runes />
2
+
3
+ <script
4
+ lang="ts"
5
+ generics="T extends object, IdType extends Primitive"
6
+ >import Button from "../../Button.svelte";
7
+ import DesktopModal from "../../DesktopModal.svelte";
8
+ import HorizontalTabGroup from "../../HorizontalTabGroup.svelte";
9
+ import Icon from "../../Icon.svelte";
10
+ import InfoAlert from "../../InfoAlert.svelte";
11
+ import TextInput from "../../TextInput.svelte";
12
+ import VerticalTabGroup from "../../VerticalTabGroup.svelte";
13
+ import { cssLength } from "../../../Helpers/Helpers.svelte.js";
14
+ import { Size } from "../../../Types/Internal/Size.js";
15
+ import { Variant } from "../../../Types/Internal/Variant.js";
16
+ import { getContext, onMount } from "svelte";
17
+ import { TableContext } from "../Types/Context/TableContext.svelte.js";
18
+ import ShareViewModal from "./ShareViewModal.svelte";
19
+ import UpdateViewOptionsModal from "./UpdateViewOptionsModal.svelte";
20
+ const tableContext = getContext(TableContext.identifier);
21
+ const { onclose } = $props();
22
+ // ── Left-nav section + right-pane tab selection ──────────────────────
23
+ // `section` picks the kind (layout vs filter); `tab` picks which of the
24
+ // three horizontal panes is visible for that section. Both are plain
25
+ // `$state` since they are pure local UI selection. They drive the
26
+ // `selected` prop of the two scoria tab groups below.
27
+ let section = $state("layout");
28
+ let tab = $state("mine");
29
+ // Name field for the "Save a new ..." tab.
30
+ let newViewName = $state("");
31
+ // Public-views discovery state for the "Subscribe to Additional" flow.
32
+ // `null` = not yet loaded; an array = the last successful fetch.
33
+ let publicViews = $state(null);
34
+ let loadingPublicViews = $state(false);
35
+ // User-visible error banner. Every adapter call is wrapped so a
36
+ // rejection lands here instead of throwing into the UI. scoria cannot
37
+ // assume a host-provided toast context exists, so the modal surfaces
38
+ // errors inline in its own body.
39
+ let errorMessage = $state(null);
40
+ // True while the open-time fetch of saved views is in flight, so the
41
+ // My-tab shows "Loading…" rather than the "nothing saved yet" message.
42
+ let loadingViews = $state(false);
43
+ // The view whose Share modal is currently open. `null` when no share
44
+ // modal is rendered. Owner cards (rendered on the My-tab) wire their
45
+ // Share button to open `ShareViewModal` against the view they belong
46
+ // to; the modal closes itself on Cancel / Save.
47
+ let sharingView = $state(null);
48
+ // The view whose Update Options modal is currently open. `null` when
49
+ // no options modal is rendered. Same pattern as `sharingView`.
50
+ let updatingOptionsView = $state(null);
51
+ /**
52
+ * Overwrite a saved view's stored body with the table's current
53
+ * live state. Triggered by the "Save Layout" / "Save Filter"
54
+ * button on an owner card - the typical flow is "apply view A,
55
+ * tweak the columns / filters, then click Save on A". Refreshes
56
+ * the saved-views list afterwards so the card label and dropdown
57
+ * pick up the new body, and surfaces any failure in the inline
58
+ * error banner.
59
+ */
60
+ function saveLiveStateInto(view) {
61
+ void guard(async () => {
62
+ await tableContext.saveLiveStateIntoView(view);
63
+ await tableContext.refreshSavedViews();
64
+ }, `Could not save changes to "${view.name}". Please try again.`);
65
+ }
66
+ // Whether remote saved views are available at all. A table created
67
+ // without `datatableUuid` / `remoteLayouts` opts out of remote views;
68
+ // the modal then renders a graceful "unavailable" empty state and
69
+ // makes zero adapter calls.
70
+ const remoteAvailable = $derived(tableContext.datatableUuid !== undefined && tableContext.remoteLayouts !== undefined);
71
+ // The opaque current-user id, used to scope "mine" vs "shared" and to
72
+ // drive subscribe / unsubscribe. `undefined` when the host supplied
73
+ // `() => undefined` - sharing controls then degrade gracefully.
74
+ const currentUserId = $derived(tableContext.getUserId());
75
+ // All saved views of the active section, split by ownership. A view the
76
+ // current user owns is "mine"; anything else they can see is "shared"
77
+ // (a subscription). When no user id is available every view falls into
78
+ // "mine" so the user can still manage what they have.
79
+ const sectionViews = $derived(tableContext._viewState.savedViews(section));
80
+ const myViews = $derived(sectionViews.filter((view) => currentUserId === undefined || view.ownerUserId === currentUserId));
81
+ const sharedViews = $derived(sectionViews.filter((view) => currentUserId !== undefined && view.ownerUserId !== currentUserId));
82
+ // Human labels for the active section, so the three tabs and headings
83
+ // read "My Layouts" / "My Filters" etc. without branching everywhere.
84
+ const sectionNoun = $derived(section === "filter" ? "Filter" : "Layout");
85
+ const sectionNounPlural = $derived(section === "filter" ? "Filters" : "Layouts");
86
+ /**
87
+ * Run an adapter-backed action, clearing any prior error first and
88
+ * routing a rejection to the inline error banner. Keeps every async
89
+ * branch below a one-liner and guarantees no rejection escapes to the
90
+ * UI (the modal never throws).
91
+ */
92
+ async function guard(action, failureMessage) {
93
+ errorMessage = null;
94
+ try {
95
+ await action();
96
+ } catch (e) {
97
+ // Surface the underlying error rather than swallowing it: the
98
+ // generic message alone has made failures hard to diagnose. The
99
+ // full error (with stack) goes to the console; its message is
100
+ // appended to the banner so it is visible without devtools.
101
+ console.error("[TableConfigurationModal] action failed:", e);
102
+ errorMessage = e instanceof Error ? `${failureMessage} (${e.message})` : failureMessage;
82
103
  }
83
-
84
- // Whether remote saved views are available at all. A table created
85
- // without `datatableUuid` / `remoteLayouts` opts out of remote views;
86
- // the modal then renders a graceful "unavailable" empty state and
87
- // makes zero adapter calls.
88
- const remoteAvailable = $derived(
89
- tableContext.datatableUuid !== undefined && tableContext.remoteLayouts !== undefined
90
- );
91
-
92
- // The opaque current-user id, used to scope "mine" vs "shared" and to
93
- // drive subscribe / unsubscribe. `undefined` when the host supplied
94
- // `() => undefined` - sharing controls then degrade gracefully.
95
- const currentUserId = $derived(tableContext.getUserId());
96
-
97
- // All saved views of the active section, split by ownership. A view the
98
- // current user owns is "mine"; anything else they can see is "shared"
99
- // (a subscription). When no user id is available every view falls into
100
- // "mine" so the user can still manage what they have.
101
- const sectionViews = $derived(tableContext._viewState.savedViews(section));
102
- const myViews = $derived(
103
- sectionViews.filter((view) => currentUserId === undefined || view.ownerUserId === currentUserId)
104
- );
105
- const sharedViews = $derived(
106
- sectionViews.filter((view) => currentUserId !== undefined && view.ownerUserId !== currentUserId)
107
- );
108
-
109
- // Human labels for the active section, so the three tabs and headings
110
- // read "My Layouts" / "My Filters" etc. without branching everywhere.
111
- const sectionNoun = $derived(section === 'filter' ? 'Filter' : 'Layout');
112
- const sectionNounPlural = $derived(section === 'filter' ? 'Filters' : 'Layouts');
113
-
114
- /**
115
- * Run an adapter-backed action, clearing any prior error first and
116
- * routing a rejection to the inline error banner. Keeps every async
117
- * branch below a one-liner and guarantees no rejection escapes to the
118
- * UI (the modal never throws).
119
- */
120
- async function guard(action: () => Promise<void>, failureMessage: string): Promise<void> {
121
- errorMessage = null;
122
- try {
123
- await action();
124
- } catch (e) {
125
- // Surface the underlying error rather than swallowing it: the
126
- // generic message alone has made failures hard to diagnose. The
127
- // full error (with stack) goes to the console; its message is
128
- // appended to the banner so it is visible without devtools.
129
- console.error('[TableConfigurationModal] action failed:', e);
130
- errorMessage = e instanceof Error ? `${failureMessage} (${e.message})` : failureMessage;
131
- }
104
+ }
105
+ // Fetch the saved views fresh every time the modal opens. The boot-time
106
+ // fetch can miss (it races table construction / auth readiness), so the
107
+ // modal cannot rely on `_viewState.savedViews` already being populated -
108
+ // it pulls `GET /datatable-state` itself on open. A failure surfaces in
109
+ // the inline error banner via `guard` rather than being swallowed.
110
+ onMount(() => {
111
+ if (!remoteAvailable) {
112
+ return;
132
113
  }
133
-
134
- // Fetch the saved views fresh every time the modal opens. The boot-time
135
- // fetch can miss (it races table construction / auth readiness), so the
136
- // modal cannot rely on `_viewState.savedViews` already being populated -
137
- // it pulls `GET /datatable-state` itself on open. A failure surfaces in
138
- // the inline error banner via `guard` rather than being swallowed.
139
- onMount(() => {
140
- if (!remoteAvailable) {
141
- return;
142
- }
143
- loadingViews = true;
144
- void guard(
145
- () => tableContext.refreshSavedViews(),
146
- 'Could not load saved layouts and filters. Please try again.'
147
- ).finally(() => {
148
- loadingViews = false;
149
- });
114
+ loadingViews = true;
115
+ void guard(() => tableContext.refreshSavedViews(), "Could not load saved layouts and filters. Please try again.").finally(() => {
116
+ loadingViews = false;
150
117
  });
151
-
152
- /** Apply a saved view to the live table. Pure local call - no await. */
153
- function applyView(view: DatatableView): void {
154
- errorMessage = null;
155
- tableContext.applyDatatableView(view);
118
+ });
119
+ /** Apply a saved view to the live table. Pure local call - no await. */
120
+ function applyView(view) {
121
+ errorMessage = null;
122
+ tableContext.applyDatatableView(view);
123
+ }
124
+ /** Delete a view the current user owns, then refresh the lists. */
125
+ function deleteView(view) {
126
+ const adapter = tableContext.remoteLayouts;
127
+ if (!adapter) {
128
+ return;
156
129
  }
157
-
158
- /** Delete a view the current user owns, then refresh the lists. */
159
- function deleteView(view: DatatableView): void {
160
- const adapter = tableContext.remoteLayouts;
161
- if (!adapter) {
162
- return;
163
- }
164
- void guard(async () => {
165
- await adapter.deleteView(view.id);
166
- await tableContext.refreshSavedViews();
167
- }, `Could not delete "${view.name}". Please try again.`);
130
+ void guard(async () => {
131
+ await adapter.deleteView(view.id);
132
+ await tableContext.refreshSavedViews();
133
+ }, `Could not delete "${view.name}". Please try again.`);
134
+ }
135
+ /**
136
+ * Save the live state of the active section as a brand-new view.
137
+ * `saveCurrentAsNewView` re-fetches the saved views and makes the new
138
+ * one active; the `My …` list and the panel dropdowns are all driven
139
+ * off the reactive `_viewState.savedViews(section)` so they update
140
+ * without a reopen. On success we also switch to the `My …` tab so
141
+ * the user immediately sees the view they just saved.
142
+ */
143
+ function saveNewView() {
144
+ const trimmed = newViewName.trim();
145
+ if (!trimmed) {
146
+ return;
168
147
  }
169
-
170
- /**
171
- * Save the live state of the active section as a brand-new view.
172
- * `saveCurrentAsNewView` re-fetches the saved views and makes the new
173
- * one active; the `My …` list and the panel dropdowns are all driven
174
- * off the reactive `_viewState.savedViews(section)` so they update
175
- * without a reopen. On success we also switch to the `My …` tab so
176
- * the user immediately sees the view they just saved.
177
- */
178
- function saveNewView(): void {
179
- const trimmed = newViewName.trim();
180
- if (!trimmed) {
181
- return;
182
- }
183
- void guard(async () => {
184
- await tableContext.saveCurrentAsNewView(trimmed, section);
185
- newViewName = '';
186
- selectTab('mine');
187
- }, `Could not save the new ${sectionNoun.toLowerCase()}. Please try again.`);
148
+ void guard(async () => {
149
+ await tableContext.saveCurrentAsNewView(trimmed, section);
150
+ newViewName = "";
151
+ selectTab("mine");
152
+ }, `Could not save the new ${sectionNoun.toLowerCase()}. Please try again.`);
153
+ }
154
+ /** Remove the current user as a subscriber of a shared view. */
155
+ function unsubscribe(view) {
156
+ const adapter = tableContext.remoteLayouts;
157
+ if (!adapter || currentUserId === undefined) {
158
+ return;
188
159
  }
189
-
190
- /** Remove the current user as a subscriber of a shared view. */
191
- function unsubscribe(view: DatatableView): void {
192
- const adapter = tableContext.remoteLayouts;
193
- if (!adapter || currentUserId === undefined) {
194
- return;
195
- }
196
- void guard(async () => {
197
- await adapter.removeSubscriber(view.id, currentUserId);
198
- await tableContext.refreshSavedViews();
199
- }, `Could not unsubscribe from "${view.name}". Please try again.`);
160
+ void guard(async () => {
161
+ await adapter.removeSubscriber(view.id, currentUserId);
162
+ await tableContext.refreshSavedViews();
163
+ }, `Could not unsubscribe from "${view.name}". Please try again.`);
164
+ }
165
+ /** Remove a named subscriber from a view the current user owns. */
166
+ function removeSubscriber(view, userId) {
167
+ const adapter = tableContext.remoteLayouts;
168
+ if (!adapter) {
169
+ return;
200
170
  }
201
-
202
- /** Remove a named subscriber from a view the current user owns. */
203
- function removeSubscriber(view: DatatableView, userId: string): void {
204
- const adapter = tableContext.remoteLayouts;
205
- if (!adapter) {
206
- return;
207
- }
208
- void guard(async () => {
209
- await adapter.removeSubscriber(view.id, userId);
210
- await tableContext.refreshSavedViews();
211
- }, 'Could not remove the subscriber. Please try again.');
171
+ void guard(async () => {
172
+ await adapter.removeSubscriber(view.id, userId);
173
+ await tableContext.refreshSavedViews();
174
+ }, "Could not remove the subscriber. Please try again.");
175
+ }
176
+ /** Fetch the public views the user could subscribe to. */
177
+ function loadPublicViews() {
178
+ const adapter = tableContext.remoteLayouts;
179
+ const datatableUuid = tableContext.datatableUuid;
180
+ if (!adapter || datatableUuid === undefined) {
181
+ return;
212
182
  }
213
-
214
- /** Fetch the public views the user could subscribe to. */
215
- function loadPublicViews(): void {
216
- const adapter = tableContext.remoteLayouts;
217
- const datatableUuid = tableContext.datatableUuid;
218
- if (!adapter || datatableUuid === undefined) {
219
- return;
220
- }
221
- loadingPublicViews = true;
222
- void guard(async () => {
223
- const all = await adapter.listPublicViews(datatableUuid);
224
- publicViews = all.filter((view) => view.kind === section);
225
- }, 'Could not load views available to subscribe to. Please try again.').finally(() => {
226
- loadingPublicViews = false;
227
- });
183
+ loadingPublicViews = true;
184
+ void guard(async () => {
185
+ const all = await adapter.listPublicViews(datatableUuid);
186
+ publicViews = all.filter((view) => view.kind === section);
187
+ }, "Could not load views available to subscribe to. Please try again.").finally(() => {
188
+ loadingPublicViews = false;
189
+ });
190
+ }
191
+ /** Subscribe the current user to a public view, then refresh. */
192
+ function subscribe(view) {
193
+ const adapter = tableContext.remoteLayouts;
194
+ if (!adapter || currentUserId === undefined) {
195
+ return;
228
196
  }
229
-
230
- /** Subscribe the current user to a public view, then refresh. */
231
- function subscribe(view: DatatableView): void {
232
- const adapter = tableContext.remoteLayouts;
233
- if (!adapter || currentUserId === undefined) {
234
- return;
235
- }
236
- void guard(async () => {
237
- await adapter.addSubscriber(view.id, currentUserId);
238
- await tableContext.refreshSavedViews();
239
- loadPublicViews();
240
- }, `Could not subscribe to "${view.name}". Please try again.`);
197
+ void guard(async () => {
198
+ await adapter.addSubscriber(view.id, currentUserId);
199
+ await tableContext.refreshSavedViews();
200
+ loadPublicViews();
201
+ }, `Could not subscribe to "${view.name}". Please try again.`);
202
+ }
203
+ /** Switch the active left-nav section, resetting the tab + scratch UI. */
204
+ function selectSection(next) {
205
+ section = next;
206
+ tab = "mine";
207
+ newViewName = "";
208
+ publicViews = null;
209
+ errorMessage = null;
210
+ }
211
+ /** Switch the active right-pane tab, resetting tab-local scratch UI. */
212
+ function selectTab(next) {
213
+ tab = next;
214
+ errorMessage = null;
215
+ if (next !== "save") {
216
+ newViewName = "";
241
217
  }
242
-
243
- /** Switch the active left-nav section, resetting the tab + scratch UI. */
244
- function selectSection(next: DatatableViewKind): void {
245
- section = next;
246
- tab = 'mine';
247
- newViewName = '';
218
+ if (next !== "shared") {
248
219
  publicViews = null;
249
- errorMessage = null;
250
220
  }
251
-
252
- /** Switch the active right-pane tab, resetting tab-local scratch UI. */
253
- function selectTab(next: ConfigTab): void {
254
- tab = next;
255
- errorMessage = null;
256
- if (next !== 'save') {
257
- newViewName = '';
221
+ }
222
+ // ── scoria tab-group wiring ──────────────────────────────────────────
223
+ // The left-hand vertical nav is a `HorizontalTabGroup` (tabs strip on
224
+ // the left, content on the right); the top horizontal tabs are a
225
+ // `VerticalTabGroup` (tabs strip on top, content below). Same two
226
+ // scoria primitives the flowms order-allocation modal uses, so the
227
+ // look is consistent across the app.
228
+ /** The three top tabs for the active section. Rebuilt when the section
229
+ * flips so the labels track "My Layouts" / "My Filters" etc. */
230
+ const innerTabs = $derived([
231
+ {
232
+ label: `My ${sectionNounPlural}`,
233
+ content: tabBody,
234
+ args: { tab: "mine" },
235
+ onclick: () => {
236
+ selectTab("mine");
258
237
  }
259
- if (next !== 'shared') {
260
- publicViews = null;
238
+ },
239
+ {
240
+ label: `Shared ${sectionNounPlural}`,
241
+ content: tabBody,
242
+ args: { tab: "shared" },
243
+ onclick: () => {
244
+ selectTab("shared");
245
+ }
246
+ },
247
+ {
248
+ label: `Save a new ${sectionNoun}`,
249
+ content: tabBody,
250
+ args: { tab: "save" },
251
+ onclick: () => {
252
+ selectTab("save");
261
253
  }
262
254
  }
263
-
264
- // ── scoria tab-group wiring ──────────────────────────────────────────
265
- // The left-hand vertical nav is a `HorizontalTabGroup` (tabs strip on
266
- // the left, content on the right); the top horizontal tabs are a
267
- // `VerticalTabGroup` (tabs strip on top, content below). Same two
268
- // scoria primitives the flowms order-allocation modal uses, so the
269
- // look is consistent across the app.
270
-
271
- /** The three top tabs for the active section. Rebuilt when the section
272
- * flips so the labels track "My Layouts" / "My Filters" etc. */
273
- const innerTabs: Array<TabOption<{ tab: ConfigTab }>> = $derived([
274
- {
275
- label: `My ${sectionNounPlural}`,
276
- content: tabBody,
277
- args: { tab: 'mine' },
278
- onclick: () => {
279
- selectTab('mine');
280
- },
281
- },
282
- {
283
- // DESIGN-REVIEW: the mockups were internally inconsistent
284
- // ("Subscribed to Filters" on one, "Shared Filters" on
285
- // another). Resolved to "Shared" consistently for both the
286
- // layout and filter sections.
287
- label: `Shared ${sectionNounPlural}`,
288
- content: tabBody,
289
- args: { tab: 'shared' },
290
- onclick: () => {
291
- selectTab('shared');
292
- },
293
- },
294
- {
295
- label: `Save a new ${sectionNoun}`,
296
- content: tabBody,
297
- args: { tab: 'save' },
298
- onclick: () => {
299
- selectTab('save');
300
- },
301
- },
302
- ]);
303
-
304
- const selectedInnerTab = $derived(innerTabs.find((t) => t.args.tab === tab) ?? innerTabs[0]);
305
-
306
- /** The two left-nav sections. Each renders the inner `VerticalTabGroup`
307
- * via the shared `sectionPane` snippet. */
308
- const sectionTabs: Array<TabOption<{ kind: DatatableViewKind }>> = [
309
- {
310
- label: 'Table Layouts',
311
- content: sectionPane,
312
- args: { kind: 'layout' },
313
- onclick: () => {
314
- selectSection('layout');
315
- },
316
- },
317
- {
318
- label: 'Table Filters',
319
- content: sectionPane,
320
- args: { kind: 'filter' },
321
- onclick: () => {
322
- selectSection('filter');
323
- },
324
- },
325
- ];
326
-
327
- const selectedSectionTab = $derived(
328
- sectionTabs.find((t) => t.args.kind === section) ?? sectionTabs[0]
329
- );
330
- </script>
331
-
332
- <!-- Left-nav section pane: the top horizontal tabs for the active kind. -->
333
- {#snippet sectionPane(_args: { kind: DatatableViewKind })}
334
- <div class="pane">
335
- <VerticalTabGroup
336
- tabs={innerTabs}
337
- selected={selectedInnerTab}
338
- nohistory
339
- />
340
- </div>
341
- {/snippet}
342
-
343
- <!-- Body of a single top tab (My / Shared / Save). -->
344
- {#snippet tabBody(args: { tab: ConfigTab })}
345
- <div class="tab-body">
346
- {#if errorMessage}
347
- <div
348
- class="error-banner"
349
- role="alert"
350
- >
351
- <Icon
352
- name="circle-exclamation"
353
- size={Size.Small}
354
- />
355
- <span>{errorMessage}</span>
356
- </div>
357
- {/if}
358
-
359
- {#if args.tab === 'mine'}
360
- {#if loadingViews}
361
- <InfoAlert>Loading saved {sectionNounPlural.toLowerCase()}…</InfoAlert>
362
- {:else if myViews.length === 0}
363
- <InfoAlert>
364
- You have not saved any {sectionNounPlural.toLowerCase()} yet. Use the "Save a new {sectionNoun}"
365
- tab to create one.
366
- </InfoAlert>
367
- {:else}
368
- <ul class="card-list">
369
- {#each myViews as view (view.id)}
370
- <li class="view-card">
371
- <div class="view-card-head">
372
- <span class="view-name">{view.name}</span>
373
- <div class="view-actions">
374
- <Button
375
- variant={Variant.Secondary}
376
- size={Size.MediumSmall}
377
- nowrap
378
- onclick={() => {
379
- applyView(view);
380
- }}>Apply</Button
381
- >
382
- <!--
383
- "Save Layout" / "Save Filter" overwrites THIS saved
384
- view's stored body with the table's current live state.
385
- Typical flow: user applies the view, tweaks the
386
- columns or filter inputs, then clicks Save to make
387
- the tweaks part of the saved view. Distinct from
388
- "Save a new …" (which always creates a fresh record).
389
- -->
390
- <Button
391
- variant={Variant.Secondary}
392
- size={Size.MediumSmall}
393
- nowrap
394
- iconLeft={{ name: 'circle-tick', size: Size.Small }}
395
- onclick={() => {
396
- saveLiveStateInto(view);
397
- }}>Save {sectionNoun}</Button
398
- >
399
- <!--
400
- "Update Options" opens a dedicated modal that edits
401
- the view's name and sharing visibility (private /
402
- public). Kept separate from the inline body-save
403
- above so a quick name fix never accidentally
404
- overwrites the saved layout / filter.
405
- -->
406
- <Button
407
- variant={Variant.Secondary}
408
- size={Size.MediumSmall}
409
- nowrap
410
- iconLeft={{ name: 'edit', size: Size.Small }}
411
- onclick={() => {
412
- updatingOptionsView = view;
413
- }}>Update Options</Button
414
- >
415
- <Button
416
- variant={Variant.Secondary}
417
- size={Size.MediumSmall}
418
- nowrap
419
- iconLeft={{ name: 'external-link', size: Size.Small }}
420
- onclick={() => {
421
- // Open the share dialog rather than switching to
422
- // the "Shared" tab - subscribing OTHER users to
423
- // this view is a distinct flow from "Subscribe
424
- // to Additional" (which is the current user
425
- // joining a public view).
426
- sharingView = view;
427
- }}>Share</Button
428
- >
429
- <Button
430
- variant={Variant.Secondary}
431
- size={Size.MediumSmall}
432
- nowrap
433
- iconLeft={{ name: 'circle-cross', size: Size.Small }}
434
- onclick={() => {
435
- deleteView(view);
436
- }}>Delete</Button
437
- >
438
- </div>
439
- </div>
440
- {#if view.subscribers && view.subscribers.length > 0}
441
- <ul class="subscriber-list">
442
- {#each view.subscribers as subscriber (subscriber.userId)}
443
- <li class="subscriber">
444
- <Icon
445
- name="user-3"
446
- size={Size.Small}
447
- />
448
- <span>{subscriber.displayName ?? subscriber.userId}</span>
449
- <Button
450
- variant={Variant.Secondary}
451
- size={Size.Tiny}
452
- nowrap
453
- onclick={() => {
454
- removeSubscriber(view, subscriber.userId);
455
- }}>Remove</Button
456
- >
457
- </li>
458
- {/each}
459
- </ul>
460
- {/if}
461
- </li>
462
- {/each}
463
- </ul>
464
- {/if}
465
- {:else if args.tab === 'shared'}
466
- <div class="shared-head">
467
- <Button
468
- variant={Variant.Secondary}
469
- size={Size.MediumSmall}
470
- nowrap
471
- disabled={loadingPublicViews}
472
- iconLeft={{ name: 'circle-plus', size: Size.Small }}
473
- onclick={loadPublicViews}>Subscribe to Additional...</Button
474
- >
475
- </div>
476
-
477
- {#if publicViews !== null}
478
- {#if publicViews.length === 0}
479
- <InfoAlert>
480
- There are no further {sectionNounPlural.toLowerCase()} available to subscribe to.
481
- </InfoAlert>
482
- {:else}
483
- <ul class="card-list">
484
- {#each publicViews as view (view.id)}
485
- <li class="view-card">
486
- <div class="view-card-head">
487
- <span class="view-name">{view.name}</span>
488
- <div class="view-actions">
489
- <Button
490
- variant={Variant.Secondary}
491
- size={Size.MediumSmall}
492
- nowrap
493
- disabled={currentUserId === undefined}
494
- onclick={() => {
495
- subscribe(view);
496
- }}>Subscribe</Button
497
- >
498
- </div>
499
- </div>
500
- </li>
501
- {/each}
502
- </ul>
503
- {/if}
504
- {/if}
505
-
506
- {#if sharedViews.length === 0}
507
- <InfoAlert>
508
- No {sectionNounPlural.toLowerCase()} are currently shared with you.
509
- </InfoAlert>
510
- {:else}
511
- <ul class="card-list">
512
- {#each sharedViews as view (view.id)}
513
- <li class="view-card">
514
- <div class="view-card-head">
515
- <span class="view-name">{view.name}</span>
516
- <div class="view-actions">
517
- <Button
518
- variant={Variant.Secondary}
519
- size={Size.MediumSmall}
520
- nowrap
521
- onclick={() => {
522
- applyView(view);
523
- }}>Apply</Button
524
- >
525
- <Button
526
- variant={Variant.Secondary}
527
- size={Size.MediumSmall}
528
- nowrap
529
- disabled={currentUserId === undefined}
530
- onclick={() => {
531
- unsubscribe(view);
532
- }}>Unsubscribe</Button
533
- >
534
- </div>
535
- </div>
536
- </li>
537
- {/each}
538
- </ul>
539
- {/if}
540
- {:else}
541
- <div class="save-tab">
542
- <InfoAlert>
543
- Save the table's current {sectionNoun.toLowerCase()} as a reusable named
544
- {sectionNoun.toLowerCase()}.
545
- </InfoAlert>
546
- <!-- The InfoAlert above spans the full tab body so the
547
- empty-state visuals are consistent across all three tabs.
548
- The name input + Save button below stay grouped in a
549
- narrower form column (`.save-tab-form`) so they keep
550
- comfortable form proportions instead of stretching across
551
- the whole 50vw modal. -->
552
- <div class="save-tab-form">
553
- <TextInput
554
- placeholder={`${sectionNoun} name`}
555
- bind:value={newViewName}
556
- onkeydown={(e: KeyboardEvent) => {
557
- if (e.key === 'Enter') {
558
- e.preventDefault();
559
- saveNewView();
560
- }
561
- }}
562
- />
563
- <!-- Wrap the button in a row so the flex column's default
564
- `align-items: stretch` does not blow it up to the input's
565
- width. The row itself can stretch; the button inside sits
566
- at its natural content width like every other Button. -->
567
- <div class="save-tab-actions">
568
- <Button
569
- disabled={!newViewName.trim()}
570
- onclick={saveNewView}>Save Current</Button
571
- >
572
- </div>
573
- </div>
574
- </div>
575
- {/if}
576
- </div>
577
- {/snippet}
578
-
579
- <DesktopModal
580
- headerLabel="Table Configuration"
581
- headerSubtitle={tableContext.datatableUuid
582
- ? `Table Identifier: ${tableContext.datatableUuid}`
583
- : undefined}
584
- onclose={() => onclose?.()}
585
- buttons={[{ label: 'Close', callback: () => onclose?.() }]}
586
- width={cssLength('50vw')}
587
- minWidth={cssLength('40rem')}
588
- >
589
- {#if !remoteAvailable}
590
- <div class="empty-state">
591
- <Icon
592
- name="circle-information"
593
- size={Size.Large}
594
- />
595
- <p>Saved layouts and filters are unavailable for this table.</p>
596
- </div>
597
- {:else}
598
- <div class="config">
599
- <HorizontalTabGroup
600
- tabs={sectionTabs}
601
- selected={selectedSectionTab}
602
- grow
603
- />
604
- </div>
605
- {/if}
606
- </DesktopModal>
607
-
608
- <!--
609
- Share dialog stacked on top of the Configuration modal. Rendered as a
610
- sibling (not nested in the modal body) so the second modal's overlay
611
- covers the first - matching how every other layered modal in scoria
612
- stacks. After a successful Save we refresh the saved-views list so the
613
- owner's card's subscriber strip updates immediately, then close.
614
- -->
615
- {#if sharingView}
616
- <ShareViewModal
617
- view={sharingView}
618
- onclose={() => {
619
- sharingView = null;
620
- }}
621
- onsaved={async () => {
622
- await tableContext.refreshSavedViews();
623
- }}
624
- />
625
- {/if}
626
-
627
- <!--
628
- Update-Options dialog stacked the same way as the Share dialog.
629
- After a successful Save we refresh the saved views so the My-* card
630
- picks up the new name / visibility immediately.
631
- -->
632
- {#if updatingOptionsView}
633
- <UpdateViewOptionsModal
634
- view={updatingOptionsView}
635
- onclose={() => {
636
- updatingOptionsView = null;
637
- }}
638
- onsaved={async () => {
639
- await tableContext.refreshSavedViews();
640
- }}
641
- />
642
- {/if}
643
-
255
+ ]);
256
+ const selectedInnerTab = $derived(innerTabs.find((t) => t.args.tab === tab) ?? innerTabs[0]);
257
+ /** The two left-nav sections. Each renders the inner `VerticalTabGroup`
258
+ * via the shared `sectionPane` snippet. */
259
+ const sectionTabs = [{
260
+ label: "Table Layouts",
261
+ content: sectionPane,
262
+ args: { kind: "layout" },
263
+ onclick: () => {
264
+ selectSection("layout");
265
+ }
266
+ }, {
267
+ label: "Table Filters",
268
+ content: sectionPane,
269
+ args: { kind: "filter" },
270
+ onclick: () => {
271
+ selectSection("filter");
272
+ }
273
+ }];
274
+ const selectedSectionTab = $derived(sectionTabs.find((t) => t.args.kind === section) ?? sectionTabs[0]);
275
+ </script>
276
+
277
+ <!-- Left-nav section pane: the top horizontal tabs for the active kind. -->
278
+ {#snippet sectionPane(_args: { kind: DatatableViewKind })}
279
+ <div class="pane">
280
+ <VerticalTabGroup
281
+ tabs={innerTabs}
282
+ selected={selectedInnerTab}
283
+ nohistory
284
+ />
285
+ </div>
286
+ {/snippet}
287
+
288
+ <!-- Body of a single top tab (My / Shared / Save). -->
289
+ {#snippet tabBody(args: { tab: ConfigTab })}
290
+ <div class="tab-body">
291
+ {#if errorMessage}
292
+ <div
293
+ class="error-banner"
294
+ role="alert"
295
+ >
296
+ <Icon
297
+ name="circle-exclamation"
298
+ size={Size.Small}
299
+ />
300
+ <span>{errorMessage}</span>
301
+ </div>
302
+ {/if}
303
+
304
+ {#if args.tab === 'mine'}
305
+ {#if loadingViews}
306
+ <InfoAlert>Loading saved {sectionNounPlural.toLowerCase()}…</InfoAlert>
307
+ {:else if myViews.length === 0}
308
+ <InfoAlert>
309
+ You have not saved any {sectionNounPlural.toLowerCase()} yet. Use the "Save a new {sectionNoun}"
310
+ tab to create one.
311
+ </InfoAlert>
312
+ {:else}
313
+ <ul class="card-list">
314
+ {#each myViews as view (view.id)}
315
+ <li class="view-card">
316
+ <div class="view-card-head">
317
+ <span class="view-name">{view.name}</span>
318
+ <div class="view-actions">
319
+ <Button
320
+ variant={Variant.Secondary}
321
+ size={Size.MediumSmall}
322
+ nowrap
323
+ onclick={() => {
324
+ applyView(view);
325
+ }}>Apply</Button
326
+ >
327
+ <!--
328
+ "Save Layout" / "Save Filter" overwrites THIS saved
329
+ view's stored body with the table's current live state.
330
+ Typical flow: user applies the view, tweaks the
331
+ columns or filter inputs, then clicks Save to make
332
+ the tweaks part of the saved view. Distinct from
333
+ "Save a new …" (which always creates a fresh record).
334
+ -->
335
+ <Button
336
+ variant={Variant.Secondary}
337
+ size={Size.MediumSmall}
338
+ nowrap
339
+ iconLeft={{ name: 'circle-tick', size: Size.Small }}
340
+ onclick={() => {
341
+ saveLiveStateInto(view);
342
+ }}>Save {sectionNoun}</Button
343
+ >
344
+ <!--
345
+ "Update Options" opens a dedicated modal that edits
346
+ the view's name and sharing visibility (private /
347
+ public). Kept separate from the inline body-save
348
+ above so a quick name fix never accidentally
349
+ overwrites the saved layout / filter.
350
+ -->
351
+ <Button
352
+ variant={Variant.Secondary}
353
+ size={Size.MediumSmall}
354
+ nowrap
355
+ iconLeft={{ name: 'edit', size: Size.Small }}
356
+ onclick={() => {
357
+ updatingOptionsView = view;
358
+ }}>Update Options</Button
359
+ >
360
+ <Button
361
+ variant={Variant.Secondary}
362
+ size={Size.MediumSmall}
363
+ nowrap
364
+ iconLeft={{ name: 'external-link', size: Size.Small }}
365
+ onclick={() => {
366
+ // Open the share dialog rather than switching to
367
+ // the "Shared" tab - subscribing OTHER users to
368
+ // this view is a distinct flow from "Subscribe
369
+ // to Additional" (which is the current user
370
+ // joining a public view).
371
+ sharingView = view;
372
+ }}>Share</Button
373
+ >
374
+ <Button
375
+ variant={Variant.Secondary}
376
+ size={Size.MediumSmall}
377
+ nowrap
378
+ iconLeft={{ name: 'circle-cross', size: Size.Small }}
379
+ onclick={() => {
380
+ deleteView(view);
381
+ }}>Delete</Button
382
+ >
383
+ </div>
384
+ </div>
385
+ {#if view.subscribers && view.subscribers.length > 0}
386
+ <ul class="subscriber-list">
387
+ {#each view.subscribers as subscriber (subscriber.userId)}
388
+ <li class="subscriber">
389
+ <Icon
390
+ name="user-3"
391
+ size={Size.Small}
392
+ />
393
+ <span>{subscriber.displayName ?? subscriber.userId}</span>
394
+ <Button
395
+ variant={Variant.Secondary}
396
+ size={Size.Tiny}
397
+ nowrap
398
+ onclick={() => {
399
+ removeSubscriber(view, subscriber.userId);
400
+ }}>Remove</Button
401
+ >
402
+ </li>
403
+ {/each}
404
+ </ul>
405
+ {/if}
406
+ </li>
407
+ {/each}
408
+ </ul>
409
+ {/if}
410
+ {:else if args.tab === 'shared'}
411
+ <div class="shared-head">
412
+ <Button
413
+ variant={Variant.Secondary}
414
+ size={Size.MediumSmall}
415
+ nowrap
416
+ disabled={loadingPublicViews}
417
+ iconLeft={{ name: 'circle-plus', size: Size.Small }}
418
+ onclick={loadPublicViews}>Subscribe to Additional...</Button
419
+ >
420
+ </div>
421
+
422
+ {#if publicViews !== null}
423
+ {#if publicViews.length === 0}
424
+ <InfoAlert>
425
+ There are no further {sectionNounPlural.toLowerCase()} available to subscribe to.
426
+ </InfoAlert>
427
+ {:else}
428
+ <ul class="card-list">
429
+ {#each publicViews as view (view.id)}
430
+ <li class="view-card">
431
+ <div class="view-card-head">
432
+ <span class="view-name">{view.name}</span>
433
+ <div class="view-actions">
434
+ <Button
435
+ variant={Variant.Secondary}
436
+ size={Size.MediumSmall}
437
+ nowrap
438
+ disabled={currentUserId === undefined}
439
+ onclick={() => {
440
+ subscribe(view);
441
+ }}>Subscribe</Button
442
+ >
443
+ </div>
444
+ </div>
445
+ </li>
446
+ {/each}
447
+ </ul>
448
+ {/if}
449
+ {/if}
450
+
451
+ {#if sharedViews.length === 0}
452
+ <InfoAlert>
453
+ No {sectionNounPlural.toLowerCase()} are currently shared with you.
454
+ </InfoAlert>
455
+ {:else}
456
+ <ul class="card-list">
457
+ {#each sharedViews as view (view.id)}
458
+ <li class="view-card">
459
+ <div class="view-card-head">
460
+ <span class="view-name">{view.name}</span>
461
+ <div class="view-actions">
462
+ <Button
463
+ variant={Variant.Secondary}
464
+ size={Size.MediumSmall}
465
+ nowrap
466
+ onclick={() => {
467
+ applyView(view);
468
+ }}>Apply</Button
469
+ >
470
+ <Button
471
+ variant={Variant.Secondary}
472
+ size={Size.MediumSmall}
473
+ nowrap
474
+ disabled={currentUserId === undefined}
475
+ onclick={() => {
476
+ unsubscribe(view);
477
+ }}>Unsubscribe</Button
478
+ >
479
+ </div>
480
+ </div>
481
+ </li>
482
+ {/each}
483
+ </ul>
484
+ {/if}
485
+ {:else}
486
+ <div class="save-tab">
487
+ <InfoAlert>
488
+ Save the table's current {sectionNoun.toLowerCase()} as a reusable named
489
+ {sectionNoun.toLowerCase()}.
490
+ </InfoAlert>
491
+ <!-- The InfoAlert above spans the full tab body so the
492
+ empty-state visuals are consistent across all three tabs.
493
+ The name input + Save button below stay grouped in a
494
+ narrower form column (`.save-tab-form`) so they keep
495
+ comfortable form proportions instead of stretching across
496
+ the whole 50vw modal. -->
497
+ <div class="save-tab-form">
498
+ <TextInput
499
+ placeholder={`${sectionNoun} name`}
500
+ bind:value={newViewName}
501
+ onkeydown={(e: KeyboardEvent) => {
502
+ if (e.key === 'Enter') {
503
+ e.preventDefault();
504
+ saveNewView();
505
+ }
506
+ }}
507
+ />
508
+ <!-- Wrap the button in a row so the flex column's default
509
+ `align-items: stretch` does not blow it up to the input's
510
+ width. The row itself can stretch; the button inside sits
511
+ at its natural content width like every other Button. -->
512
+ <div class="save-tab-actions">
513
+ <Button
514
+ disabled={!newViewName.trim()}
515
+ onclick={saveNewView}>Save Current</Button
516
+ >
517
+ </div>
518
+ </div>
519
+ </div>
520
+ {/if}
521
+ </div>
522
+ {/snippet}
523
+
524
+ <DesktopModal
525
+ headerLabel="Table Configuration"
526
+ headerSubtitle={tableContext.datatableUuid
527
+ ? `Table Identifier: ${tableContext.datatableUuid}`
528
+ : undefined}
529
+ onclose={() => onclose?.()}
530
+ buttons={[{ label: 'Close', callback: () => onclose?.() }]}
531
+ width={cssLength('50vw')}
532
+ minWidth={cssLength('40rem')}
533
+ >
534
+ {#if !remoteAvailable}
535
+ <div class="empty-state">
536
+ <Icon
537
+ name="circle-information"
538
+ size={Size.Large}
539
+ />
540
+ <p>Saved layouts and filters are unavailable for this table.</p>
541
+ </div>
542
+ {:else}
543
+ <div class="config">
544
+ <HorizontalTabGroup
545
+ tabs={sectionTabs}
546
+ selected={selectedSectionTab}
547
+ grow
548
+ />
549
+ </div>
550
+ {/if}
551
+ </DesktopModal>
552
+
553
+ <!--
554
+ Share dialog stacked on top of the Configuration modal. Rendered as a
555
+ sibling (not nested in the modal body) so the second modal's overlay
556
+ covers the first - matching how every other layered modal in scoria
557
+ stacks. After a successful Save we refresh the saved-views list so the
558
+ owner's card's subscriber strip updates immediately, then close.
559
+ -->
560
+ {#if sharingView}
561
+ <ShareViewModal
562
+ view={sharingView}
563
+ onclose={() => {
564
+ sharingView = null;
565
+ }}
566
+ onsaved={async () => {
567
+ await tableContext.refreshSavedViews();
568
+ }}
569
+ />
570
+ {/if}
571
+
572
+ <!--
573
+ Update-Options dialog stacked the same way as the Share dialog.
574
+ After a successful Save we refresh the saved views so the My-* card
575
+ picks up the new name / visibility immediately.
576
+ -->
577
+ {#if updatingOptionsView}
578
+ <UpdateViewOptionsModal
579
+ view={updatingOptionsView}
580
+ onclose={() => {
581
+ updatingOptionsView = null;
582
+ }}
583
+ onsaved={async () => {
584
+ await tableContext.refreshSavedViews();
585
+ }}
586
+ />
587
+ {/if}
588
+
644
589
  <style>.empty-state {
645
590
  display: flex;
646
591
  flex-flow: column nowrap;
@@ -799,4 +744,4 @@
799
744
  display: flex;
800
745
  flex-flow: row nowrap;
801
746
  justify-content: flex-start;
802
- }</style>
747
+ }</style>