@lavalogic/scoria 0.40.31 → 0.40.33

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 (137) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +28 -28
  3. package/dist/Components/AccordionGroupButton.svelte +60 -60
  4. package/dist/Components/Action.svelte +48 -48
  5. package/dist/Components/AlertModal.svelte +102 -102
  6. package/dist/Components/Base/ContextWrapper.svelte +45 -45
  7. package/dist/Components/Base/Pagination.svelte +135 -135
  8. package/dist/Components/Base/Snippets.svelte +25 -25
  9. package/dist/Components/BigRadioSet.svelte +34 -34
  10. package/dist/Components/Bubble.svelte +38 -38
  11. package/dist/Components/Button.svelte +83 -83
  12. package/dist/Components/Checkbox.svelte +71 -71
  13. package/dist/Components/CollapsibleCard.svelte +59 -59
  14. package/dist/Components/CollapsibleMenuGroup.svelte +43 -43
  15. package/dist/Components/ConditionBuilder/ConditionBuilder.svelte +101 -101
  16. package/dist/Components/Contexts/ContextMenu.svelte +76 -76
  17. package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
  18. package/dist/Components/Contexts/ContextMenuOption.svelte +38 -38
  19. package/dist/Components/Contexts/Toast.svelte +71 -71
  20. package/dist/Components/Contexts/ToastContainer.svelte +34 -34
  21. package/dist/Components/Contexts/Tooltip.svelte +32 -32
  22. package/dist/Components/DataMatrixIcon.svelte +37 -37
  23. package/dist/Components/DateInput.svelte +80 -80
  24. package/dist/Components/DatePicker.svelte +86 -86
  25. package/dist/Components/DesktopModal.svelte +65 -65
  26. package/dist/Components/Dial.svelte +35 -35
  27. package/dist/Components/DimensionInput.svelte +95 -95
  28. package/dist/Components/DragMenu/DragMenuHolder.svelte +53 -53
  29. package/dist/Components/DragMenu/DraggableCard.svelte +49 -49
  30. package/dist/Components/FileCard.svelte +18 -18
  31. package/dist/Components/FileUpload.svelte +115 -115
  32. package/dist/Components/GridInput.svelte +123 -123
  33. package/dist/Components/GridInputSet.svelte +21 -21
  34. package/dist/Components/HorizontalTabGroup.svelte +54 -54
  35. package/dist/Components/HorizontalTabGroupButton.svelte +32 -32
  36. package/dist/Components/Icon.svelte +68 -68
  37. package/dist/Components/Icons.d.ts +3 -3
  38. package/dist/Components/Icons.js +11 -11
  39. package/dist/Components/InfoAlert.svelte +32 -32
  40. package/dist/Components/LoadingAnimation.svelte +228 -227
  41. package/dist/Components/LoadingModal.svelte +19 -19
  42. package/dist/Components/LoadingOverlay.svelte +20 -20
  43. package/dist/Components/Modal.svelte +127 -127
  44. package/dist/Components/MultiSelect.svelte +126 -126
  45. package/dist/Components/Nav/Nav.svelte +71 -71
  46. package/dist/Components/Nav/NavServiceMenuList.svelte +37 -37
  47. package/dist/Components/Nav/NavTab.svelte +46 -46
  48. package/dist/Components/Nav/ServiceMenuItem.svelte +62 -62
  49. package/dist/Components/Nav/ServicesNav.svelte +50 -50
  50. package/dist/Components/NumberInput.svelte +98 -98
  51. package/dist/Components/OptionCards.svelte +58 -58
  52. package/dist/Components/PageHeading.svelte +21 -21
  53. package/dist/Components/PasswordInput.svelte +61 -61
  54. package/dist/Components/PopoverButton.svelte +53 -53
  55. package/dist/Components/Portal.svelte +7 -7
  56. package/dist/Components/ProgressBubble.svelte +42 -42
  57. package/dist/Components/QuerySelect.svelte +102 -102
  58. package/dist/Components/ResourceCalendar/ResourceCalendar.svelte +241 -241
  59. package/dist/Components/SidebarPanel.svelte +32 -32
  60. package/dist/Components/SingleSelect.svelte +122 -122
  61. package/dist/Components/StepButton.svelte +48 -48
  62. package/dist/Components/StepForm.svelte +58 -58
  63. package/dist/Components/Switch.svelte +43 -43
  64. package/dist/Components/Table/ARCHITECTURE.md +298 -298
  65. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +30 -30
  66. package/dist/Components/Table/Body/Rows/ColumnList.svelte +24 -24
  67. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +165 -165
  68. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +13 -13
  69. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +120 -120
  70. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +81 -81
  71. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +92 -92
  72. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +110 -110
  73. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +54 -54
  74. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +35 -35
  75. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +64 -64
  76. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +82 -82
  77. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +88 -88
  78. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +90 -90
  79. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +96 -96
  80. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +71 -71
  81. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +124 -124
  82. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +91 -91
  83. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +88 -88
  84. package/dist/Components/Table/Body/Rows/TableRow.svelte +127 -127
  85. package/dist/Components/Table/Body/TableBody.svelte +92 -92
  86. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +25 -25
  87. package/dist/Components/Table/Headers/SelectAllHeader.svelte +20 -20
  88. package/dist/Components/Table/Headers/TableHead.svelte +175 -175
  89. package/dist/Components/Table/Misc/ColumnPanel.svelte +32 -32
  90. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +29 -29
  91. package/dist/Components/Table/Misc/FilterInput.svelte +326 -326
  92. package/dist/Components/Table/Misc/FilterPanel.svelte +79 -79
  93. package/dist/Components/Table/Misc/FilterPanelModal.svelte +21 -21
  94. package/dist/Components/Table/Misc/ShareViewModal.svelte +118 -118
  95. package/dist/Components/Table/Misc/SidePanel.svelte +24 -24
  96. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +318 -318
  97. package/dist/Components/Table/Misc/TableFooter.svelte +201 -201
  98. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +112 -112
  99. package/dist/Components/Table/Misc/TableSidebar.svelte +116 -116
  100. package/dist/Components/Table/Misc/TableViewDropdown.svelte +25 -25
  101. package/dist/Components/Table/Misc/ToolboxPanel.svelte +60 -60
  102. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +63 -63
  103. package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +101 -101
  104. package/dist/Components/Table/NestedTable.svelte +29 -29
  105. package/dist/Components/Table/Table.svelte +140 -140
  106. package/dist/Components/Test/TestContextConsumer.svelte +8 -8
  107. package/dist/Components/TextArea.svelte +92 -90
  108. package/dist/Components/TextInput.svelte +128 -117
  109. package/dist/Components/ThreeStateRadio.svelte +101 -101
  110. package/dist/Components/Touch/BoolCard.svelte +27 -27
  111. package/dist/Components/Touch/DateModal.svelte +98 -98
  112. package/dist/Components/Touch/EditCard.svelte +44 -44
  113. package/dist/Components/Touch/InfoCard.svelte +25 -25
  114. package/dist/Components/Touch/InfoTextCard.svelte +22 -22
  115. package/dist/Components/Touch/ModalExplanation.svelte +13 -13
  116. package/dist/Components/Touch/NumberKeyboard.svelte +81 -81
  117. package/dist/Components/Touch/NumberModal.svelte +49 -49
  118. package/dist/Components/Touch/PlusMinusCard.svelte +40 -40
  119. package/dist/Components/Touch/SelectModal.svelte +86 -86
  120. package/dist/Components/Touch/SummaryCard.svelte +74 -74
  121. package/dist/Components/Touch/TextAreaModal.svelte +53 -53
  122. package/dist/Components/Touch/TextModal.svelte +56 -56
  123. package/dist/Components/Touch/TouchCard.svelte +28 -28
  124. package/dist/Components/Touch/TouchModal.svelte +141 -141
  125. package/dist/Components/Touch/UtilityButton.svelte +63 -63
  126. package/dist/Components/VerticalTabGroup.svelte +38 -38
  127. package/dist/Components/VerticalTabGroupButton.svelte +46 -46
  128. package/dist/Helpers/Datamatrix.d.ts +19 -19
  129. package/dist/Helpers/Datamatrix.js +291 -291
  130. package/dist/Types/Examples/ExampleModal.svelte +27 -27
  131. package/dist/paraglide/README.md +1 -1
  132. package/dist/scss/_basics.scss +12 -12
  133. package/dist/scss/_colours.scss +140 -140
  134. package/dist/scss/_motion.scss +57 -57
  135. package/dist/scss/_sizing.scss +85 -85
  136. package/dist/scss/_transitions.scss +8 -8
  137. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
- <svelte:options runes />
2
-
3
- <script
4
- lang="ts"
5
- generics="T extends object, IdType extends Primitive"
1
+ <svelte:options runes />
2
+
3
+ <script
4
+ lang="ts"
5
+ generics="T extends object, IdType extends Primitive"
6
6
  >import Button from "../../Button.svelte";
7
7
  import DesktopModal from "../../DesktopModal.svelte";
8
8
  import HorizontalTabGroup from "../../HorizontalTabGroup.svelte";
@@ -284,318 +284,318 @@ const sectionTabs = [{
284
284
  }
285
285
  }];
286
286
  const selectedSectionTab = $derived(sectionTabs.find((t) => t.args.kind === section) ?? sectionTabs[0]);
287
- </script>
288
-
289
- <!-- Left-nav section pane: the top horizontal tabs for the active kind. -->
290
- {#snippet sectionPane(_args: { kind: DatatableViewKind })}
291
- <div class="pane">
292
- <VerticalTabGroup
293
- tabs={innerTabs}
294
- selected={selectedInnerTab}
295
- nohistory
296
- />
297
- </div>
298
- {/snippet}
299
-
300
- <!-- Body of a single top tab (My / Shared / Save). -->
301
- {#snippet tabBody(args: { tab: ConfigTab })}
302
- <div class="tab-body">
303
- {#if errorMessage}
304
- <div
305
- class="error-banner"
306
- role="alert"
307
- >
308
- <Icon
309
- name="circle-exclamation"
310
- size={Size.Small}
311
- />
312
- <span>{errorMessage}</span>
313
- </div>
314
- {/if}
315
-
316
- {#if args.tab === 'mine'}
317
- {#if loadingViews}
318
- <InfoAlert>{pair(i18n['scoria.message.loading_layouts'], i18n['scoria.message.loading_filters'])}</InfoAlert>
319
- {:else if myViews.length === 0}
320
- <InfoAlert>
321
- {pair(i18n['scoria.text.no_layouts_saved'], i18n['scoria.text.no_filters_saved'])}
322
- </InfoAlert>
323
- {:else}
324
- <ul class="card-list">
325
- {#each myViews as view (view.id)}
326
- <li class="view-card">
327
- <div class="view-card-head">
328
- <span class="view-name">{view.name}</span>
329
- <div class="view-actions">
330
- <Button
331
- variant={Variant.Secondary}
332
- size={Size.MediumSmall}
333
- nowrap
334
- onclick={() => {
335
- applyView(view);
336
- }}>{i18n['scoria.action.apply']()}</Button
337
- >
338
- <!--
339
- "Save Layout" / "Save Filter" overwrites THIS saved
340
- view's stored body with the table's current live state.
341
- Typical flow: user applies the view, tweaks the
342
- columns or filter inputs, then clicks Save to make
343
- the tweaks part of the saved view. Distinct from
344
- "Save a new …" (which always creates a fresh record).
345
- -->
346
- <Button
347
- variant={Variant.Secondary}
348
- size={Size.MediumSmall}
349
- nowrap
350
- iconLeft={{ name: 'circle-tick', size: Size.Small }}
351
- onclick={() => {
352
- saveLiveStateInto(view);
353
- }}>{pair(i18n['scoria.action.save_layout'], i18n['scoria.action.save_filter'])}</Button
354
- >
355
- <!--
356
- "Update Options" opens a dedicated modal that edits
357
- the view's name and sharing visibility (private /
358
- public). Kept separate from the inline body-save
359
- above so a quick name fix never accidentally
360
- overwrites the saved layout / filter.
361
- -->
362
- <Button
363
- variant={Variant.Secondary}
364
- size={Size.MediumSmall}
365
- nowrap
366
- iconLeft={{ name: 'edit', size: Size.Small }}
367
- onclick={() => {
368
- updatingOptionsView = view;
369
- }}>{i18n['scoria.action.update_options']()}</Button
370
- >
371
- <Button
372
- variant={Variant.Secondary}
373
- size={Size.MediumSmall}
374
- nowrap
375
- iconLeft={{ name: 'external-link', size: Size.Small }}
376
- onclick={() => {
377
- // Open the share dialog rather than switching to
378
- // the "Shared" tab - subscribing OTHER users to
379
- // this view is a distinct flow from "Subscribe
380
- // to Additional" (which is the current user
381
- // joining a public view).
382
- sharingView = view;
383
- }}>{i18n['scoria.action.share']()}</Button
384
- >
385
- <Button
386
- variant={Variant.Secondary}
387
- size={Size.MediumSmall}
388
- nowrap
389
- iconLeft={{ name: 'circle-cross', size: Size.Small }}
390
- onclick={() => {
391
- deleteView(view);
392
- }}>{i18n['scoria.action.delete']()}</Button
393
- >
394
- </div>
395
- </div>
396
- {#if view.subscribers && view.subscribers.length > 0}
397
- <ul class="subscriber-list">
398
- {#each view.subscribers as subscriber (subscriber.userId)}
399
- <li class="subscriber">
400
- <Icon
401
- name="user-3"
402
- size={Size.Small}
403
- />
404
- <span>{subscriber.displayName ?? subscriber.userId}</span>
405
- <Button
406
- variant={Variant.Secondary}
407
- size={Size.Tiny}
408
- nowrap
409
- onclick={() => {
410
- removeSubscriber(view, subscriber.userId);
411
- }}>{i18n['scoria.action.remove']()}</Button
412
- >
413
- </li>
414
- {/each}
415
- </ul>
416
- {/if}
417
- </li>
418
- {/each}
419
- </ul>
420
- {/if}
421
- {:else if args.tab === 'shared'}
422
- <div class="shared-head">
423
- <Button
424
- variant={Variant.Secondary}
425
- size={Size.MediumSmall}
426
- nowrap
427
- disabled={loadingPublicViews}
428
- iconLeft={{ name: 'circle-plus', size: Size.Small }}
429
- onclick={loadPublicViews}>{i18n['scoria.action.subscribe_additional']()}</Button
430
- >
431
- </div>
432
-
433
- {#if publicViews !== null}
434
- {#if publicViews.length === 0}
435
- <InfoAlert>
436
- {pair(i18n['scoria.text.no_further_layouts'], i18n['scoria.text.no_further_filters'])}
437
- </InfoAlert>
438
- {:else}
439
- <ul class="card-list">
440
- {#each publicViews as view (view.id)}
441
- <li class="view-card">
442
- <div class="view-card-head">
443
- <span class="view-name">{view.name}</span>
444
- <div class="view-actions">
445
- <Button
446
- variant={Variant.Secondary}
447
- size={Size.MediumSmall}
448
- nowrap
449
- disabled={currentUserId === undefined}
450
- onclick={() => {
451
- subscribe(view);
452
- }}>{i18n['scoria.action.subscribe']()}</Button
453
- >
454
- </div>
455
- </div>
456
- </li>
457
- {/each}
458
- </ul>
459
- {/if}
460
- {/if}
461
-
462
- {#if sharedViews.length === 0}
463
- <InfoAlert>
464
- {pair(i18n['scoria.text.no_layouts_shared'], i18n['scoria.text.no_filters_shared'])}
465
- </InfoAlert>
466
- {:else}
467
- <ul class="card-list">
468
- {#each sharedViews as view (view.id)}
469
- <li class="view-card">
470
- <div class="view-card-head">
471
- <span class="view-name">{view.name}</span>
472
- <div class="view-actions">
473
- <Button
474
- variant={Variant.Secondary}
475
- size={Size.MediumSmall}
476
- nowrap
477
- onclick={() => {
478
- applyView(view);
479
- }}>{i18n['scoria.action.apply']()}</Button
480
- >
481
- <Button
482
- variant={Variant.Secondary}
483
- size={Size.MediumSmall}
484
- nowrap
485
- disabled={currentUserId === undefined}
486
- onclick={() => {
487
- unsubscribe(view);
488
- }}>{i18n['scoria.action.unsubscribe']()}</Button
489
- >
490
- </div>
491
- </div>
492
- </li>
493
- {/each}
494
- </ul>
495
- {/if}
496
- {:else}
497
- <div class="save-tab">
498
- <InfoAlert>
499
- {pair(i18n['scoria.text.save_layout_intro'], i18n['scoria.text.save_filter_intro'])}
500
- </InfoAlert>
501
- <!-- The InfoAlert above spans the full tab body so the
502
- empty-state visuals are consistent across all three tabs.
503
- The name input + Save button below stay grouped in a
504
- narrower form column (`.save-tab-form`) so they keep
505
- comfortable form proportions instead of stretching across
506
- the whole 50vw modal. -->
507
- <div class="save-tab-form">
508
- <TextInput
509
- placeholder={pair(i18n['scoria.label.layout_name_placeholder'], i18n['scoria.label.filter_name_placeholder'])}
510
- bind:value={newViewName}
511
- onkeydown={(e: KeyboardEvent) => {
512
- if (e.key === 'Enter') {
513
- e.preventDefault();
514
- saveNewView();
515
- }
516
- }}
517
- />
518
- <!-- Wrap the button in a row so the flex column's default
519
- `align-items: stretch` does not blow it up to the input's
520
- width. The row itself can stretch; the button inside sits
521
- at its natural content width like every other Button. -->
522
- <div class="save-tab-actions">
523
- <Button
524
- disabled={!newViewName.trim()}
525
- onclick={saveNewView}>{i18n['scoria.action.save_current']()}</Button
526
- >
527
- </div>
528
- </div>
529
- </div>
530
- {/if}
531
- </div>
532
- {/snippet}
533
-
534
- <DesktopModal
535
- headerLabel={i18n['scoria.action.table_configuration']()}
536
- headerSubtitle={tableContext.datatableUuid
537
- ? i18n['scoria.label.table_identifier']({ id: tableContext.datatableUuid })
538
- : undefined}
539
- onclose={() => onclose?.()}
540
- buttons={[{ label: i18n['scoria.action.close'](), callback: () => onclose?.() }]}
541
- width={cssLength('50vw')}
542
- minWidth={cssLength('40rem')}
543
- >
544
- {#if !remoteAvailable}
545
- <div class="empty-state">
546
- <Icon
547
- name="circle-information"
548
- size={Size.Large}
549
- />
550
- <p>{i18n['scoria.text.views_unavailable']()}</p>
551
- </div>
552
- {:else}
553
- <div class="config">
554
- <HorizontalTabGroup
555
- tabs={sectionTabs}
556
- selected={selectedSectionTab}
557
- grow
558
- />
559
- </div>
560
- {/if}
561
- </DesktopModal>
562
-
563
- <!--
564
- Share dialog stacked on top of the Configuration modal. Rendered as a
565
- sibling (not nested in the modal body) so the second modal's overlay
566
- covers the first - matching how every other layered modal in scoria
567
- stacks. After a successful Save we refresh the saved-views list so the
568
- owner's card's subscriber strip updates immediately, then close.
569
- -->
570
- {#if sharingView}
571
- <ShareViewModal
572
- view={sharingView}
573
- onclose={() => {
574
- sharingView = null;
575
- }}
576
- onsaved={async () => {
577
- await tableContext.refreshSavedViews();
578
- }}
579
- />
580
- {/if}
581
-
582
- <!--
583
- Update-Options dialog stacked the same way as the Share dialog.
584
- After a successful Save we refresh the saved views so the My-* card
585
- picks up the new name / visibility immediately.
586
- -->
587
- {#if updatingOptionsView}
588
- <UpdateViewOptionsModal
589
- view={updatingOptionsView}
590
- onclose={() => {
591
- updatingOptionsView = null;
592
- }}
593
- onsaved={async () => {
594
- await tableContext.refreshSavedViews();
595
- }}
596
- />
597
- {/if}
598
-
287
+ </script>
288
+
289
+ <!-- Left-nav section pane: the top horizontal tabs for the active kind. -->
290
+ {#snippet sectionPane(_args: { kind: DatatableViewKind })}
291
+ <div class="pane">
292
+ <VerticalTabGroup
293
+ tabs={innerTabs}
294
+ selected={selectedInnerTab}
295
+ nohistory
296
+ />
297
+ </div>
298
+ {/snippet}
299
+
300
+ <!-- Body of a single top tab (My / Shared / Save). -->
301
+ {#snippet tabBody(args: { tab: ConfigTab })}
302
+ <div class="tab-body">
303
+ {#if errorMessage}
304
+ <div
305
+ class="error-banner"
306
+ role="alert"
307
+ >
308
+ <Icon
309
+ name="circle-exclamation"
310
+ size={Size.Small}
311
+ />
312
+ <span>{errorMessage}</span>
313
+ </div>
314
+ {/if}
315
+
316
+ {#if args.tab === 'mine'}
317
+ {#if loadingViews}
318
+ <InfoAlert>{pair(i18n['scoria.message.loading_layouts'], i18n['scoria.message.loading_filters'])}</InfoAlert>
319
+ {:else if myViews.length === 0}
320
+ <InfoAlert>
321
+ {pair(i18n['scoria.text.no_layouts_saved'], i18n['scoria.text.no_filters_saved'])}
322
+ </InfoAlert>
323
+ {:else}
324
+ <ul class="card-list">
325
+ {#each myViews as view (view.id)}
326
+ <li class="view-card">
327
+ <div class="view-card-head">
328
+ <span class="view-name">{view.name}</span>
329
+ <div class="view-actions">
330
+ <Button
331
+ variant={Variant.Secondary}
332
+ size={Size.MediumSmall}
333
+ nowrap
334
+ onclick={() => {
335
+ applyView(view);
336
+ }}>{i18n['scoria.action.apply']()}</Button
337
+ >
338
+ <!--
339
+ "Save Layout" / "Save Filter" overwrites THIS saved
340
+ view's stored body with the table's current live state.
341
+ Typical flow: user applies the view, tweaks the
342
+ columns or filter inputs, then clicks Save to make
343
+ the tweaks part of the saved view. Distinct from
344
+ "Save a new …" (which always creates a fresh record).
345
+ -->
346
+ <Button
347
+ variant={Variant.Secondary}
348
+ size={Size.MediumSmall}
349
+ nowrap
350
+ iconLeft={{ name: 'circle-tick', size: Size.Small }}
351
+ onclick={() => {
352
+ saveLiveStateInto(view);
353
+ }}>{pair(i18n['scoria.action.save_layout'], i18n['scoria.action.save_filter'])}</Button
354
+ >
355
+ <!--
356
+ "Update Options" opens a dedicated modal that edits
357
+ the view's name and sharing visibility (private /
358
+ public). Kept separate from the inline body-save
359
+ above so a quick name fix never accidentally
360
+ overwrites the saved layout / filter.
361
+ -->
362
+ <Button
363
+ variant={Variant.Secondary}
364
+ size={Size.MediumSmall}
365
+ nowrap
366
+ iconLeft={{ name: 'edit', size: Size.Small }}
367
+ onclick={() => {
368
+ updatingOptionsView = view;
369
+ }}>{i18n['scoria.action.update_options']()}</Button
370
+ >
371
+ <Button
372
+ variant={Variant.Secondary}
373
+ size={Size.MediumSmall}
374
+ nowrap
375
+ iconLeft={{ name: 'external-link', size: Size.Small }}
376
+ onclick={() => {
377
+ // Open the share dialog rather than switching to
378
+ // the "Shared" tab - subscribing OTHER users to
379
+ // this view is a distinct flow from "Subscribe
380
+ // to Additional" (which is the current user
381
+ // joining a public view).
382
+ sharingView = view;
383
+ }}>{i18n['scoria.action.share']()}</Button
384
+ >
385
+ <Button
386
+ variant={Variant.Secondary}
387
+ size={Size.MediumSmall}
388
+ nowrap
389
+ iconLeft={{ name: 'circle-cross', size: Size.Small }}
390
+ onclick={() => {
391
+ deleteView(view);
392
+ }}>{i18n['scoria.action.delete']()}</Button
393
+ >
394
+ </div>
395
+ </div>
396
+ {#if view.subscribers && view.subscribers.length > 0}
397
+ <ul class="subscriber-list">
398
+ {#each view.subscribers as subscriber (subscriber.userId)}
399
+ <li class="subscriber">
400
+ <Icon
401
+ name="user-3"
402
+ size={Size.Small}
403
+ />
404
+ <span>{subscriber.displayName ?? subscriber.userId}</span>
405
+ <Button
406
+ variant={Variant.Secondary}
407
+ size={Size.Tiny}
408
+ nowrap
409
+ onclick={() => {
410
+ removeSubscriber(view, subscriber.userId);
411
+ }}>{i18n['scoria.action.remove']()}</Button
412
+ >
413
+ </li>
414
+ {/each}
415
+ </ul>
416
+ {/if}
417
+ </li>
418
+ {/each}
419
+ </ul>
420
+ {/if}
421
+ {:else if args.tab === 'shared'}
422
+ <div class="shared-head">
423
+ <Button
424
+ variant={Variant.Secondary}
425
+ size={Size.MediumSmall}
426
+ nowrap
427
+ disabled={loadingPublicViews}
428
+ iconLeft={{ name: 'circle-plus', size: Size.Small }}
429
+ onclick={loadPublicViews}>{i18n['scoria.action.subscribe_additional']()}</Button
430
+ >
431
+ </div>
432
+
433
+ {#if publicViews !== null}
434
+ {#if publicViews.length === 0}
435
+ <InfoAlert>
436
+ {pair(i18n['scoria.text.no_further_layouts'], i18n['scoria.text.no_further_filters'])}
437
+ </InfoAlert>
438
+ {:else}
439
+ <ul class="card-list">
440
+ {#each publicViews as view (view.id)}
441
+ <li class="view-card">
442
+ <div class="view-card-head">
443
+ <span class="view-name">{view.name}</span>
444
+ <div class="view-actions">
445
+ <Button
446
+ variant={Variant.Secondary}
447
+ size={Size.MediumSmall}
448
+ nowrap
449
+ disabled={currentUserId === undefined}
450
+ onclick={() => {
451
+ subscribe(view);
452
+ }}>{i18n['scoria.action.subscribe']()}</Button
453
+ >
454
+ </div>
455
+ </div>
456
+ </li>
457
+ {/each}
458
+ </ul>
459
+ {/if}
460
+ {/if}
461
+
462
+ {#if sharedViews.length === 0}
463
+ <InfoAlert>
464
+ {pair(i18n['scoria.text.no_layouts_shared'], i18n['scoria.text.no_filters_shared'])}
465
+ </InfoAlert>
466
+ {:else}
467
+ <ul class="card-list">
468
+ {#each sharedViews as view (view.id)}
469
+ <li class="view-card">
470
+ <div class="view-card-head">
471
+ <span class="view-name">{view.name}</span>
472
+ <div class="view-actions">
473
+ <Button
474
+ variant={Variant.Secondary}
475
+ size={Size.MediumSmall}
476
+ nowrap
477
+ onclick={() => {
478
+ applyView(view);
479
+ }}>{i18n['scoria.action.apply']()}</Button
480
+ >
481
+ <Button
482
+ variant={Variant.Secondary}
483
+ size={Size.MediumSmall}
484
+ nowrap
485
+ disabled={currentUserId === undefined}
486
+ onclick={() => {
487
+ unsubscribe(view);
488
+ }}>{i18n['scoria.action.unsubscribe']()}</Button
489
+ >
490
+ </div>
491
+ </div>
492
+ </li>
493
+ {/each}
494
+ </ul>
495
+ {/if}
496
+ {:else}
497
+ <div class="save-tab">
498
+ <InfoAlert>
499
+ {pair(i18n['scoria.text.save_layout_intro'], i18n['scoria.text.save_filter_intro'])}
500
+ </InfoAlert>
501
+ <!-- The InfoAlert above spans the full tab body so the
502
+ empty-state visuals are consistent across all three tabs.
503
+ The name input + Save button below stay grouped in a
504
+ narrower form column (`.save-tab-form`) so they keep
505
+ comfortable form proportions instead of stretching across
506
+ the whole 50vw modal. -->
507
+ <div class="save-tab-form">
508
+ <TextInput
509
+ placeholder={pair(i18n['scoria.label.layout_name_placeholder'], i18n['scoria.label.filter_name_placeholder'])}
510
+ bind:value={newViewName}
511
+ onkeydown={(e: KeyboardEvent) => {
512
+ if (e.key === 'Enter') {
513
+ e.preventDefault();
514
+ saveNewView();
515
+ }
516
+ }}
517
+ />
518
+ <!-- Wrap the button in a row so the flex column's default
519
+ `align-items: stretch` does not blow it up to the input's
520
+ width. The row itself can stretch; the button inside sits
521
+ at its natural content width like every other Button. -->
522
+ <div class="save-tab-actions">
523
+ <Button
524
+ disabled={!newViewName.trim()}
525
+ onclick={saveNewView}>{i18n['scoria.action.save_current']()}</Button
526
+ >
527
+ </div>
528
+ </div>
529
+ </div>
530
+ {/if}
531
+ </div>
532
+ {/snippet}
533
+
534
+ <DesktopModal
535
+ headerLabel={i18n['scoria.action.table_configuration']()}
536
+ headerSubtitle={tableContext.datatableUuid
537
+ ? i18n['scoria.label.table_identifier']({ id: tableContext.datatableUuid })
538
+ : undefined}
539
+ onclose={() => onclose?.()}
540
+ buttons={[{ label: i18n['scoria.action.close'](), callback: () => onclose?.() }]}
541
+ width={cssLength('50vw')}
542
+ minWidth={cssLength('40rem')}
543
+ >
544
+ {#if !remoteAvailable}
545
+ <div class="empty-state">
546
+ <Icon
547
+ name="circle-information"
548
+ size={Size.Large}
549
+ />
550
+ <p>{i18n['scoria.text.views_unavailable']()}</p>
551
+ </div>
552
+ {:else}
553
+ <div class="config">
554
+ <HorizontalTabGroup
555
+ tabs={sectionTabs}
556
+ selected={selectedSectionTab}
557
+ grow
558
+ />
559
+ </div>
560
+ {/if}
561
+ </DesktopModal>
562
+
563
+ <!--
564
+ Share dialog stacked on top of the Configuration modal. Rendered as a
565
+ sibling (not nested in the modal body) so the second modal's overlay
566
+ covers the first - matching how every other layered modal in scoria
567
+ stacks. After a successful Save we refresh the saved-views list so the
568
+ owner's card's subscriber strip updates immediately, then close.
569
+ -->
570
+ {#if sharingView}
571
+ <ShareViewModal
572
+ view={sharingView}
573
+ onclose={() => {
574
+ sharingView = null;
575
+ }}
576
+ onsaved={async () => {
577
+ await tableContext.refreshSavedViews();
578
+ }}
579
+ />
580
+ {/if}
581
+
582
+ <!--
583
+ Update-Options dialog stacked the same way as the Share dialog.
584
+ After a successful Save we refresh the saved views so the My-* card
585
+ picks up the new name / visibility immediately.
586
+ -->
587
+ {#if updatingOptionsView}
588
+ <UpdateViewOptionsModal
589
+ view={updatingOptionsView}
590
+ onclose={() => {
591
+ updatingOptionsView = null;
592
+ }}
593
+ onsaved={async () => {
594
+ await tableContext.refreshSavedViews();
595
+ }}
596
+ />
597
+ {/if}
598
+
599
599
  <style>.empty-state {
600
600
  display: flex;
601
601
  flex-flow: column nowrap;
@@ -754,4 +754,4 @@ const selectedSectionTab = $derived(sectionTabs.find((t) => t.args.kind === sect
754
754
  display: flex;
755
755
  flex-flow: row nowrap;
756
756
  justify-content: flex-start;
757
- }</style>
757
+ }</style>