@lavalogic/scoria 0.37.56 → 0.38.1

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 (50) hide show
  1. package/dist/Components/Table/ARCHITECTURE.md +22 -22
  2. package/dist/Components/Table/Misc/ColumnPanel.svelte +17 -61
  3. package/dist/Components/Table/Misc/ColumnPanel.svelte.d.ts +2 -24
  4. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +3 -92
  5. package/dist/Components/Table/Misc/ColumnPanelModal.svelte.d.ts +2 -24
  6. package/dist/Components/Table/Misc/ColumnPanelModalProps.d.ts +2 -2
  7. package/dist/Components/Table/Misc/FilterPanel.svelte +25 -113
  8. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +626 -0
  9. package/dist/Components/Table/Misc/TableConfigurationModal.svelte.d.ts +26 -0
  10. package/dist/Components/Table/Misc/TableConfigurationModalProps.d.ts +13 -0
  11. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +15 -0
  12. package/dist/Components/Table/Misc/TableSidebar.svelte +45 -2
  13. package/dist/Components/Table/Misc/TableViewDropdown.svelte +97 -0
  14. package/dist/Components/Table/Misc/TableViewDropdown.svelte.d.ts +35 -0
  15. package/dist/Components/Table/SubApis.svelte.js +0 -41
  16. package/dist/Components/Table/Types/Columns/JSONTableLayout.d.ts +5 -5
  17. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.d.ts +5 -5
  18. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.js +5 -5
  19. package/dist/Components/Table/Types/Context/PreferencesState.svelte.d.ts +106 -147
  20. package/dist/Components/Table/Types/Context/PreferencesState.svelte.js +156 -203
  21. package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +323 -44
  22. package/dist/Components/Table/Types/Context/TableContext.svelte.js +687 -187
  23. package/dist/Components/Table/Types/Context/TableInitOptions.d.ts +9 -0
  24. package/dist/Components/Table/Types/Context/ViewState.svelte.d.ts +152 -0
  25. package/dist/Components/Table/Types/Context/ViewState.svelte.js +223 -0
  26. package/dist/Components/Table/Types/ExpandedPanel.d.ts +9 -5
  27. package/dist/Components/Table/Types/ExpandedPanel.js +9 -5
  28. package/dist/Components/Table/Types/Persistence/DatatableView.d.ts +95 -0
  29. package/dist/Components/Table/Types/Persistence/DatatableViewEnvelope.d.ts +21 -0
  30. package/dist/Components/Table/Types/Persistence/DatatableViewEnvelope.js +1 -0
  31. package/dist/Components/Table/Types/Persistence/DatatableViewKind.d.ts +10 -0
  32. package/dist/Components/Table/Types/Persistence/DatatableViewKind.js +1 -0
  33. package/dist/Components/Table/Types/Persistence/JSONActiveView.d.ts +58 -0
  34. package/dist/Components/Table/Types/Persistence/JSONActiveView.js +41 -0
  35. package/dist/Components/Table/Types/Persistence/JSONTableFilter.d.ts +53 -0
  36. package/dist/Components/Table/Types/Persistence/JSONTableFilter.js +42 -0
  37. package/dist/Components/Table/Types/Persistence/RemoteTableLayoutAdapter.d.ts +47 -0
  38. package/dist/Components/Table/Types/Persistence/RemoteTableLayoutAdapter.js +1 -0
  39. package/dist/Components/Table/Types/Public/CreateTableOptions.d.ts +10 -0
  40. package/dist/Components/Table/Types/Public/TablePersistence.d.ts +1 -4
  41. package/dist/Components/Table/Types/Public/TableSubApis.d.ts +0 -18
  42. package/dist/Components/Table/Types/Public/index.d.ts +1 -1
  43. package/dist/Components/Table/createTable.svelte.js +2 -0
  44. package/dist/index.d.ts +8 -1
  45. package/dist/index.js +9 -0
  46. package/package.json +1 -1
  47. package/dist/Components/Table/Types/Columns/Definitions/ColumnDefSet.d.ts +0 -20
  48. package/dist/Components/Table/Types/Columns/Definitions/JSONColumnDefSet.d.ts +0 -25
  49. /package/dist/Components/Table/{Types/Columns/Definitions/ColumnDefSet.js → Misc/TableConfigurationModalProps.js} +0 -0
  50. /package/dist/Components/Table/Types/{Columns/Definitions/JSONColumnDefSet.js → Persistence/DatatableView.js} +0 -0
@@ -0,0 +1,626 @@
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 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 type { TabOption } from '../../../Types/Internal/TabOption.js';
16
+ import { Variant } from '../../../Types/Internal/Variant.js';
17
+ import { getContext } from 'svelte';
18
+ import { TableContext } from '../Types/Context/TableContext.svelte.js';
19
+ import type { Primitive } from '../Types/Context/TableInitOptions.js';
20
+ import type { DatatableView } from '../Types/Persistence/DatatableView.js';
21
+ import type { DatatableViewKind } from '../Types/Persistence/DatatableViewKind.js';
22
+ import type { TableConfigurationModalProps } from './TableConfigurationModalProps.js';
23
+
24
+ const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
25
+
26
+ const { onclose }: TableConfigurationModalProps = $props();
27
+
28
+ // ── Left-nav section + right-pane tab selection ──────────────────────
29
+ // `section` picks the kind (layout vs filter); `tab` picks which of the
30
+ // three horizontal panes is visible for that section. Both are plain
31
+ // `$state` since they are pure local UI selection. They drive the
32
+ // `selected` prop of the two scoria tab groups below.
33
+ let section = $state<DatatableViewKind>('layout');
34
+ type ConfigTab = 'mine' | 'shared' | 'save';
35
+ let tab = $state<ConfigTab>('mine');
36
+
37
+ // Name field for the "Save a new ..." tab.
38
+ let newViewName = $state('');
39
+
40
+ // Public-views discovery state for the "Subscribe to Additional" flow.
41
+ // `null` = not yet loaded; an array = the last successful fetch.
42
+ let publicViews = $state<ReadonlyArray<DatatableView> | null>(null);
43
+ let loadingPublicViews = $state(false);
44
+
45
+ // User-visible error banner. Every adapter call is wrapped so a
46
+ // rejection lands here instead of throwing into the UI. scoria cannot
47
+ // assume a host-provided toast context exists, so the modal surfaces
48
+ // errors inline in its own body.
49
+ let errorMessage = $state<string | null>(null);
50
+
51
+ // Whether remote saved views are available at all. A table created
52
+ // without `datatableUuid` / `remoteLayouts` opts out of remote views;
53
+ // the modal then renders a graceful "unavailable" empty state and
54
+ // makes zero adapter calls.
55
+ const remoteAvailable = $derived(
56
+ tableContext.datatableUuid !== undefined && tableContext.remoteLayouts !== undefined
57
+ );
58
+
59
+ // The opaque current-user id, used to scope "mine" vs "shared" and to
60
+ // drive subscribe / unsubscribe. `undefined` when the host supplied
61
+ // `() => undefined` - sharing controls then degrade gracefully.
62
+ const currentUserId = $derived(tableContext.getUserId());
63
+
64
+ // All saved views of the active section, split by ownership. A view the
65
+ // current user owns is "mine"; anything else they can see is "shared"
66
+ // (a subscription). When no user id is available every view falls into
67
+ // "mine" so the user can still manage what they have.
68
+ const sectionViews = $derived(tableContext._viewState.savedViews(section));
69
+ const myViews = $derived(
70
+ sectionViews.filter((view) => currentUserId === undefined || view.ownerUserId === currentUserId)
71
+ );
72
+ const sharedViews = $derived(
73
+ sectionViews.filter((view) => currentUserId !== undefined && view.ownerUserId !== currentUserId)
74
+ );
75
+
76
+ // Human labels for the active section, so the three tabs and headings
77
+ // read "My Layouts" / "My Filters" etc. without branching everywhere.
78
+ const sectionNoun = $derived(section === 'filter' ? 'Filter' : 'Layout');
79
+ const sectionNounPlural = $derived(section === 'filter' ? 'Filters' : 'Layouts');
80
+
81
+ /**
82
+ * Run an adapter-backed action, clearing any prior error first and
83
+ * routing a rejection to the inline error banner. Keeps every async
84
+ * branch below a one-liner and guarantees no rejection escapes to the
85
+ * UI (the modal never throws).
86
+ */
87
+ async function guard(action: () => Promise<void>, failureMessage: string): Promise<void> {
88
+ errorMessage = null;
89
+ try {
90
+ await action();
91
+ } catch {
92
+ errorMessage = failureMessage;
93
+ }
94
+ }
95
+
96
+ /** Apply a saved view to the live table. Pure local call - no await. */
97
+ function applyView(view: DatatableView): void {
98
+ errorMessage = null;
99
+ tableContext.applyDatatableView(view);
100
+ }
101
+
102
+ /** Delete a view the current user owns, then refresh the lists. */
103
+ function deleteView(view: DatatableView): void {
104
+ const adapter = tableContext.remoteLayouts;
105
+ if (!adapter) {
106
+ return;
107
+ }
108
+ void guard(async () => {
109
+ await adapter.deleteView(view.id);
110
+ await tableContext.refreshSavedViews();
111
+ }, `Could not delete "${view.name}". Please try again.`);
112
+ }
113
+
114
+ /** Save the live state of the active section as a brand-new view. */
115
+ function saveNewView(): void {
116
+ const trimmed = newViewName.trim();
117
+ if (!trimmed) {
118
+ return;
119
+ }
120
+ void guard(async () => {
121
+ await tableContext.saveCurrentAsNewView(trimmed, section);
122
+ newViewName = '';
123
+ }, `Could not save the new ${sectionNoun.toLowerCase()}. Please try again.`);
124
+ }
125
+
126
+ /** Remove the current user as a subscriber of a shared view. */
127
+ function unsubscribe(view: DatatableView): void {
128
+ const adapter = tableContext.remoteLayouts;
129
+ if (!adapter || currentUserId === undefined) {
130
+ return;
131
+ }
132
+ void guard(async () => {
133
+ await adapter.removeSubscriber(view.id, currentUserId);
134
+ await tableContext.refreshSavedViews();
135
+ }, `Could not unsubscribe from "${view.name}". Please try again.`);
136
+ }
137
+
138
+ /** Remove a named subscriber from a view the current user owns. */
139
+ function removeSubscriber(view: DatatableView, userId: string): void {
140
+ const adapter = tableContext.remoteLayouts;
141
+ if (!adapter) {
142
+ return;
143
+ }
144
+ void guard(async () => {
145
+ await adapter.removeSubscriber(view.id, userId);
146
+ await tableContext.refreshSavedViews();
147
+ }, 'Could not remove the subscriber. Please try again.');
148
+ }
149
+
150
+ /** Fetch the public views the user could subscribe to. */
151
+ function loadPublicViews(): void {
152
+ const adapter = tableContext.remoteLayouts;
153
+ const datatableUuid = tableContext.datatableUuid;
154
+ if (!adapter || datatableUuid === undefined) {
155
+ return;
156
+ }
157
+ loadingPublicViews = true;
158
+ void guard(async () => {
159
+ const all = await adapter.listPublicViews(datatableUuid);
160
+ publicViews = all.filter((view) => view.kind === section);
161
+ }, 'Could not load views available to subscribe to. Please try again.').finally(() => {
162
+ loadingPublicViews = false;
163
+ });
164
+ }
165
+
166
+ /** Subscribe the current user to a public view, then refresh. */
167
+ function subscribe(view: DatatableView): void {
168
+ const adapter = tableContext.remoteLayouts;
169
+ if (!adapter || currentUserId === undefined) {
170
+ return;
171
+ }
172
+ void guard(async () => {
173
+ await adapter.addSubscriber(view.id, currentUserId);
174
+ await tableContext.refreshSavedViews();
175
+ loadPublicViews();
176
+ }, `Could not subscribe to "${view.name}". Please try again.`);
177
+ }
178
+
179
+ /** Switch the active left-nav section, resetting the tab + scratch UI. */
180
+ function selectSection(next: DatatableViewKind): void {
181
+ section = next;
182
+ tab = 'mine';
183
+ newViewName = '';
184
+ publicViews = null;
185
+ errorMessage = null;
186
+ }
187
+
188
+ /** Switch the active right-pane tab, resetting tab-local scratch UI. */
189
+ function selectTab(next: ConfigTab): void {
190
+ tab = next;
191
+ errorMessage = null;
192
+ if (next !== 'save') {
193
+ newViewName = '';
194
+ }
195
+ if (next !== 'shared') {
196
+ publicViews = null;
197
+ }
198
+ }
199
+
200
+ // ── scoria tab-group wiring ──────────────────────────────────────────
201
+ // The left-hand vertical nav is a `HorizontalTabGroup` (tabs strip on
202
+ // the left, content on the right); the top horizontal tabs are a
203
+ // `VerticalTabGroup` (tabs strip on top, content below). Same two
204
+ // scoria primitives the flowms order-allocation modal uses, so the
205
+ // look is consistent across the app.
206
+
207
+ /** The three top tabs for the active section. Rebuilt when the section
208
+ * flips so the labels track "My Layouts" / "My Filters" etc. */
209
+ const innerTabs: Array<TabOption<{ tab: ConfigTab }>> = $derived([
210
+ {
211
+ label: `My ${sectionNounPlural}`,
212
+ content: tabBody,
213
+ args: { tab: 'mine' },
214
+ onclick: () => {
215
+ selectTab('mine');
216
+ },
217
+ },
218
+ {
219
+ // DESIGN-REVIEW: the mockups were internally inconsistent
220
+ // ("Subscribed to Filters" on one, "Shared Filters" on
221
+ // another). Resolved to "Shared" consistently for both the
222
+ // layout and filter sections.
223
+ label: `Shared ${sectionNounPlural}`,
224
+ content: tabBody,
225
+ args: { tab: 'shared' },
226
+ onclick: () => {
227
+ selectTab('shared');
228
+ },
229
+ },
230
+ {
231
+ label: `Save a new ${sectionNoun}`,
232
+ content: tabBody,
233
+ args: { tab: 'save' },
234
+ onclick: () => {
235
+ selectTab('save');
236
+ },
237
+ },
238
+ ]);
239
+
240
+ const selectedInnerTab = $derived(innerTabs.find((t) => t.args.tab === tab) ?? innerTabs[0]);
241
+
242
+ /** The two left-nav sections. Each renders the inner `VerticalTabGroup`
243
+ * via the shared `sectionPane` snippet. */
244
+ const sectionTabs: Array<TabOption<{ kind: DatatableViewKind }>> = [
245
+ {
246
+ label: 'Table Layouts',
247
+ content: sectionPane,
248
+ args: { kind: 'layout' },
249
+ onclick: () => {
250
+ selectSection('layout');
251
+ },
252
+ },
253
+ {
254
+ label: 'Table Filters',
255
+ content: sectionPane,
256
+ args: { kind: 'filter' },
257
+ onclick: () => {
258
+ selectSection('filter');
259
+ },
260
+ },
261
+ ];
262
+
263
+ const selectedSectionTab = $derived(
264
+ sectionTabs.find((t) => t.args.kind === section) ?? sectionTabs[0]
265
+ );
266
+ </script>
267
+
268
+ <!-- Left-nav section pane: the top horizontal tabs for the active kind. -->
269
+ {#snippet sectionPane(_args: { kind: DatatableViewKind })}
270
+ <div class="pane">
271
+ <VerticalTabGroup
272
+ tabs={innerTabs}
273
+ selected={selectedInnerTab}
274
+ nohistory
275
+ />
276
+ </div>
277
+ {/snippet}
278
+
279
+ <!-- Body of a single top tab (My / Shared / Save). -->
280
+ {#snippet tabBody(args: { tab: ConfigTab })}
281
+ <div class="tab-body">
282
+ {#if errorMessage}
283
+ <div
284
+ class="error-banner"
285
+ role="alert"
286
+ >
287
+ <Icon
288
+ name="circle-exclamation"
289
+ size={Size.Small}
290
+ />
291
+ <span>{errorMessage}</span>
292
+ </div>
293
+ {/if}
294
+
295
+ {#if args.tab === 'mine'}
296
+ {#if myViews.length === 0}
297
+ <p class="muted">
298
+ You have not saved any {sectionNounPlural.toLowerCase()} yet. Use the "Save a new {sectionNoun}"
299
+ tab to create one.
300
+ </p>
301
+ {:else}
302
+ <ul class="card-list">
303
+ {#each myViews as view (view.id)}
304
+ <li class="view-card">
305
+ <div class="view-card-head">
306
+ <span class="view-name">{view.name}</span>
307
+ <div class="view-actions">
308
+ <Button
309
+ variant={Variant.Secondary}
310
+ size={Size.MediumSmall}
311
+ nowrap
312
+ onclick={() => {
313
+ applyView(view);
314
+ }}>Apply</Button
315
+ >
316
+ <Button
317
+ variant={Variant.Secondary}
318
+ size={Size.MediumSmall}
319
+ nowrap
320
+ iconLeft={{ name: 'external-link', size: Size.Small }}
321
+ onclick={() => {
322
+ selectTab('shared');
323
+ }}>Share</Button
324
+ >
325
+ <Button
326
+ variant={Variant.Secondary}
327
+ size={Size.MediumSmall}
328
+ nowrap
329
+ iconLeft={{ name: 'circle-cross', size: Size.Small }}
330
+ onclick={() => {
331
+ deleteView(view);
332
+ }}>Delete</Button
333
+ >
334
+ </div>
335
+ </div>
336
+ {#if view.subscribers && view.subscribers.length > 0}
337
+ <ul class="subscriber-list">
338
+ {#each view.subscribers as subscriber (subscriber.userId)}
339
+ <li class="subscriber">
340
+ <Icon
341
+ name="user-3"
342
+ size={Size.Small}
343
+ />
344
+ <span>{subscriber.displayName ?? subscriber.userId}</span>
345
+ <Button
346
+ variant={Variant.Secondary}
347
+ size={Size.Tiny}
348
+ nowrap
349
+ onclick={() => {
350
+ removeSubscriber(view, subscriber.userId);
351
+ }}>Remove</Button
352
+ >
353
+ </li>
354
+ {/each}
355
+ </ul>
356
+ {/if}
357
+ </li>
358
+ {/each}
359
+ </ul>
360
+ {/if}
361
+ {:else if args.tab === 'shared'}
362
+ <div class="shared-head">
363
+ <Button
364
+ variant={Variant.Secondary}
365
+ size={Size.MediumSmall}
366
+ nowrap
367
+ disabled={loadingPublicViews}
368
+ iconLeft={{ name: 'circle-plus', size: Size.Small }}
369
+ onclick={loadPublicViews}>Subscribe to Additional...</Button
370
+ >
371
+ </div>
372
+
373
+ {#if publicViews !== null}
374
+ {#if publicViews.length === 0}
375
+ <p class="muted">
376
+ There are no further {sectionNounPlural.toLowerCase()} available to subscribe to.
377
+ </p>
378
+ {:else}
379
+ <ul class="card-list">
380
+ {#each publicViews as view (view.id)}
381
+ <li class="view-card">
382
+ <div class="view-card-head">
383
+ <span class="view-name">{view.name}</span>
384
+ <div class="view-actions">
385
+ <Button
386
+ variant={Variant.Secondary}
387
+ size={Size.MediumSmall}
388
+ nowrap
389
+ disabled={currentUserId === undefined}
390
+ onclick={() => {
391
+ subscribe(view);
392
+ }}>Subscribe</Button
393
+ >
394
+ </div>
395
+ </div>
396
+ </li>
397
+ {/each}
398
+ </ul>
399
+ {/if}
400
+ {/if}
401
+
402
+ {#if sharedViews.length === 0}
403
+ <p class="muted">
404
+ No {sectionNounPlural.toLowerCase()} are currently shared with you.
405
+ </p>
406
+ {:else}
407
+ <ul class="card-list">
408
+ {#each sharedViews as view (view.id)}
409
+ <li class="view-card">
410
+ <div class="view-card-head">
411
+ <span class="view-name">{view.name}</span>
412
+ <div class="view-actions">
413
+ <Button
414
+ variant={Variant.Secondary}
415
+ size={Size.MediumSmall}
416
+ nowrap
417
+ onclick={() => {
418
+ applyView(view);
419
+ }}>Apply</Button
420
+ >
421
+ <Button
422
+ variant={Variant.Secondary}
423
+ size={Size.MediumSmall}
424
+ nowrap
425
+ disabled={currentUserId === undefined}
426
+ onclick={() => {
427
+ unsubscribe(view);
428
+ }}>Unsubscribe</Button
429
+ >
430
+ </div>
431
+ </div>
432
+ </li>
433
+ {/each}
434
+ </ul>
435
+ {/if}
436
+ {:else}
437
+ <div class="save-tab">
438
+ <p class="muted">
439
+ Save the table's current {sectionNoun.toLowerCase()} as a reusable named
440
+ {sectionNoun.toLowerCase()}.
441
+ </p>
442
+ <TextInput
443
+ placeholder={`${sectionNoun} name`}
444
+ bind:value={newViewName}
445
+ onkeydown={(e: KeyboardEvent) => {
446
+ if (e.key === 'Enter') {
447
+ e.preventDefault();
448
+ saveNewView();
449
+ }
450
+ }}
451
+ />
452
+ <Button
453
+ type="submit"
454
+ variant={Variant.Primary}
455
+ size={Size.MediumSmall}
456
+ nowrap
457
+ disabled={!newViewName.trim()}
458
+ onclick={saveNewView}>Save Current</Button
459
+ >
460
+ </div>
461
+ {/if}
462
+ </div>
463
+ {/snippet}
464
+
465
+ <DesktopModal
466
+ headerLabel="Table Configuration"
467
+ headerSubtitle={tableContext.datatableUuid
468
+ ? `Table Identifier: ${tableContext.datatableUuid}`
469
+ : undefined}
470
+ onclose={() => onclose?.()}
471
+ buttons={[{ label: 'Close', callback: () => onclose?.() }]}
472
+ width={cssLength('50vw')}
473
+ minWidth={cssLength('40rem')}
474
+ >
475
+ {#if !remoteAvailable}
476
+ <div class="empty-state">
477
+ <Icon
478
+ name="circle-information"
479
+ size={Size.Large}
480
+ />
481
+ <p>Saved layouts and filters are unavailable for this table.</p>
482
+ </div>
483
+ {:else}
484
+ <div class="config">
485
+ <HorizontalTabGroup
486
+ tabs={sectionTabs}
487
+ selected={selectedSectionTab}
488
+ grow
489
+ />
490
+ </div>
491
+ {/if}
492
+ </DesktopModal>
493
+
494
+ <style>.empty-state {
495
+ display: flex;
496
+ flex-flow: column nowrap;
497
+ align-items: center;
498
+ justify-content: center;
499
+ gap: 0.75rem;
500
+ padding: 3rem 1rem;
501
+ text-align: center;
502
+ color: #3a4952;
503
+ }
504
+
505
+ /* Fixed-frame wrapper. The modal is a fixed 50vw (see the `width`
506
+ prop on `DesktopModal`); this `.config` lets the tab group fill
507
+ that frame and the tab body scroll inside it, so switching nav
508
+ section or top tab never reflows the dialog. */
509
+ .config {
510
+ display: flex;
511
+ flex-flow: column nowrap;
512
+ flex: 1;
513
+ min-height: 0;
514
+ width: 100%;
515
+ }
516
+
517
+ /* The right-hand content pane that each left-nav section renders:
518
+ holds the top horizontal tabs and their scrolling body. */
519
+ .pane {
520
+ display: flex;
521
+ flex-flow: column nowrap;
522
+ flex: 1;
523
+ min-width: 0;
524
+ min-height: 0;
525
+ width: 100%;
526
+ }
527
+
528
+ .error-banner {
529
+ display: flex;
530
+ flex-flow: row nowrap;
531
+ align-items: center;
532
+ gap: 0.5rem;
533
+ padding: 0.5rem 0.75rem;
534
+ border: solid 1px #aa1414;
535
+ border-radius: 4px;
536
+ background-color: #f9e9e9;
537
+ color: #aa1414;
538
+ }
539
+
540
+ /* The body of a single top tab. Scrolls within the fixed modal
541
+ frame rather than growing the dialog. */
542
+ .tab-body {
543
+ display: flex;
544
+ flex-flow: column nowrap;
545
+ gap: 0.75rem;
546
+ padding: 1rem;
547
+ min-height: 0;
548
+ max-height: 60vh;
549
+ overflow-y: auto;
550
+ }
551
+
552
+ .muted {
553
+ color: #647d8e;
554
+ }
555
+
556
+ .card-list {
557
+ list-style: none;
558
+ margin: 0;
559
+ padding: 0;
560
+ display: flex;
561
+ flex-flow: column nowrap;
562
+ gap: 0.5rem;
563
+ }
564
+
565
+ .view-card {
566
+ border: solid 1px #cbd4da;
567
+ border-radius: 4px;
568
+ padding: 0.6rem 0.75rem;
569
+ background-color: #ffffff;
570
+ display: flex;
571
+ flex-flow: column nowrap;
572
+ gap: 0.5rem;
573
+ }
574
+
575
+ .view-card-head {
576
+ display: flex;
577
+ flex-flow: row nowrap;
578
+ align-items: center;
579
+ justify-content: space-between;
580
+ gap: 0.75rem;
581
+ }
582
+
583
+ .view-name {
584
+ font-weight: 600;
585
+ overflow: hidden;
586
+ text-overflow: ellipsis;
587
+ white-space: nowrap;
588
+ }
589
+
590
+ .view-actions {
591
+ display: flex;
592
+ flex-flow: row nowrap;
593
+ gap: 0.375rem;
594
+ flex-shrink: 0;
595
+ }
596
+
597
+ .subscriber-list {
598
+ list-style: none;
599
+ margin: 0;
600
+ padding: 0.25rem 0 0;
601
+ display: flex;
602
+ flex-flow: column nowrap;
603
+ gap: 0.25rem;
604
+ border-top: solid 1px #dae0e5;
605
+ }
606
+
607
+ .subscriber {
608
+ display: flex;
609
+ flex-flow: row nowrap;
610
+ align-items: center;
611
+ gap: 0.5rem;
612
+ font-size: 0.875rem;
613
+ }
614
+
615
+ .shared-head {
616
+ display: flex;
617
+ flex-flow: row nowrap;
618
+ margin-bottom: 0.5rem;
619
+ }
620
+
621
+ .save-tab {
622
+ display: flex;
623
+ flex-flow: column nowrap;
624
+ gap: 0.75rem;
625
+ max-width: 24rem;
626
+ }</style>
@@ -0,0 +1,26 @@
1
+ import type { Primitive } from '../Types/Context/TableInitOptions.js';
2
+ import type { TableConfigurationModalProps } from './TableConfigurationModalProps.js';
3
+ declare function $$render<T extends object, IdType extends Primitive>(): {
4
+ props: TableConfigurationModalProps;
5
+ exports: {};
6
+ bindings: "";
7
+ slots: {};
8
+ events: {};
9
+ };
10
+ declare class __sveltets_Render<T extends object, IdType extends Primitive> {
11
+ props(): ReturnType<typeof $$render<T, IdType>>['props'];
12
+ events(): ReturnType<typeof $$render<T, IdType>>['events'];
13
+ slots(): ReturnType<typeof $$render<T, IdType>>['slots'];
14
+ bindings(): "";
15
+ exports(): {};
16
+ }
17
+ interface $$IsomorphicComponent {
18
+ new <T extends object, IdType extends Primitive>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T, IdType>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T, IdType>['props']>, ReturnType<__sveltets_Render<T, IdType>['events']>, ReturnType<__sveltets_Render<T, IdType>['slots']>> & {
19
+ $$bindings?: ReturnType<__sveltets_Render<T, IdType>['bindings']>;
20
+ } & ReturnType<__sveltets_Render<T, IdType>['exports']>;
21
+ <T extends object, IdType extends Primitive>(internal: unknown, props: ReturnType<__sveltets_Render<T, IdType>['props']> & {}): ReturnType<__sveltets_Render<T, IdType>['exports']>;
22
+ z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
23
+ }
24
+ declare const TableConfigurationModal: $$IsomorphicComponent;
25
+ type TableConfigurationModal<T extends object, IdType extends Primitive> = InstanceType<typeof TableConfigurationModal<T, IdType>>;
26
+ export default TableConfigurationModal;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Props for `TableConfigurationModal` - the FPM 403 "Table Configuration"
3
+ * modal that manages saved datatable layouts and filters (the new view
4
+ * system that supersedes the old named-preset box).
5
+ *
6
+ * The modal is driven entirely by the `TableContext` it reads from
7
+ * Svelte context; it has no data props of its own. It only needs a way
8
+ * to tell its host the panel should close.
9
+ */
10
+ export interface TableConfigurationModalProps {
11
+ /** Called when the modal requests close (footer button / backdrop). */
12
+ onclose?: () => void;
13
+ }
@@ -15,6 +15,7 @@
15
15
  import { ExpandedPanel } from '../Types/ExpandedPanel.js';
16
16
  import ColumnPanelModal from './ColumnPanelModal.svelte';
17
17
  import FilterPanelModal from './FilterPanelModal.svelte';
18
+ import TableConfigurationModal from './TableConfigurationModal.svelte';
18
19
  import ToolboxPanelModal from './ToolboxPanelModal.svelte';
19
20
 
20
21
  const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
@@ -24,6 +25,7 @@
24
25
  [ExpandedPanel.Column]: ColumnPanelModal,
25
26
  [ExpandedPanel.Filter]: FilterPanelModal,
26
27
  [ExpandedPanel.Toolbox]: ToolboxPanelModal,
28
+ [ExpandedPanel.Configuration]: TableConfigurationModal,
27
29
  } as const;
28
30
 
29
31
  let expandedPanel = $state<ExpandedPanel | null>(null);
@@ -89,6 +91,19 @@
89
91
  name: 'category',
90
92
  }}>My Tool Box</Button
91
93
  >
94
+ <Button
95
+ size={Size.Small}
96
+ side={Side.Bottom}
97
+ toggled={expandedPanel === ExpandedPanel.Configuration}
98
+ onclick={() => {
99
+ expandedPanel =
100
+ expandedPanel === ExpandedPanel.Configuration ? null : ExpandedPanel.Configuration;
101
+ }}
102
+ variant={Variant.Toolbar}
103
+ iconTop={{
104
+ name: 'setting-cog',
105
+ }}>Table Configuration</Button
106
+ >
92
107
  {#if tableContext.dataRepo.addEmptyRow}
93
108
  <Button
94
109
  size={Size.Small}