@lavalogic/scoria 0.40.32 → 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 (135) 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/InfoAlert.svelte +32 -32
  38. package/dist/Components/LoadingAnimation.svelte +228 -227
  39. package/dist/Components/LoadingModal.svelte +19 -19
  40. package/dist/Components/LoadingOverlay.svelte +20 -20
  41. package/dist/Components/Modal.svelte +127 -127
  42. package/dist/Components/MultiSelect.svelte +126 -126
  43. package/dist/Components/Nav/Nav.svelte +71 -71
  44. package/dist/Components/Nav/NavServiceMenuList.svelte +37 -37
  45. package/dist/Components/Nav/NavTab.svelte +46 -46
  46. package/dist/Components/Nav/ServiceMenuItem.svelte +62 -62
  47. package/dist/Components/Nav/ServicesNav.svelte +50 -50
  48. package/dist/Components/NumberInput.svelte +98 -98
  49. package/dist/Components/OptionCards.svelte +58 -58
  50. package/dist/Components/PageHeading.svelte +21 -21
  51. package/dist/Components/PasswordInput.svelte +61 -61
  52. package/dist/Components/PopoverButton.svelte +53 -53
  53. package/dist/Components/Portal.svelte +7 -7
  54. package/dist/Components/ProgressBubble.svelte +42 -42
  55. package/dist/Components/QuerySelect.svelte +102 -102
  56. package/dist/Components/ResourceCalendar/ResourceCalendar.svelte +241 -241
  57. package/dist/Components/SidebarPanel.svelte +32 -32
  58. package/dist/Components/SingleSelect.svelte +122 -122
  59. package/dist/Components/StepButton.svelte +48 -48
  60. package/dist/Components/StepForm.svelte +58 -58
  61. package/dist/Components/Switch.svelte +43 -43
  62. package/dist/Components/Table/ARCHITECTURE.md +298 -298
  63. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +30 -30
  64. package/dist/Components/Table/Body/Rows/ColumnList.svelte +24 -24
  65. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +165 -165
  66. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +13 -13
  67. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +120 -120
  68. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +81 -81
  69. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +92 -92
  70. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +110 -110
  71. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +54 -54
  72. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +35 -35
  73. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +64 -64
  74. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +82 -82
  75. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +88 -88
  76. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +90 -90
  77. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +96 -96
  78. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +71 -71
  79. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +124 -124
  80. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +91 -91
  81. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +88 -88
  82. package/dist/Components/Table/Body/Rows/TableRow.svelte +127 -127
  83. package/dist/Components/Table/Body/TableBody.svelte +92 -92
  84. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +25 -25
  85. package/dist/Components/Table/Headers/SelectAllHeader.svelte +20 -20
  86. package/dist/Components/Table/Headers/TableHead.svelte +175 -175
  87. package/dist/Components/Table/Misc/ColumnPanel.svelte +32 -32
  88. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +29 -29
  89. package/dist/Components/Table/Misc/FilterInput.svelte +326 -326
  90. package/dist/Components/Table/Misc/FilterPanel.svelte +79 -79
  91. package/dist/Components/Table/Misc/FilterPanelModal.svelte +21 -21
  92. package/dist/Components/Table/Misc/ShareViewModal.svelte +118 -118
  93. package/dist/Components/Table/Misc/SidePanel.svelte +24 -24
  94. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +318 -318
  95. package/dist/Components/Table/Misc/TableFooter.svelte +201 -201
  96. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +112 -112
  97. package/dist/Components/Table/Misc/TableSidebar.svelte +116 -116
  98. package/dist/Components/Table/Misc/TableViewDropdown.svelte +25 -25
  99. package/dist/Components/Table/Misc/ToolboxPanel.svelte +60 -60
  100. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +63 -63
  101. package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +101 -101
  102. package/dist/Components/Table/NestedTable.svelte +29 -29
  103. package/dist/Components/Table/Table.svelte +140 -140
  104. package/dist/Components/Test/TestContextConsumer.svelte +8 -8
  105. package/dist/Components/TextArea.svelte +92 -90
  106. package/dist/Components/TextInput.svelte +128 -117
  107. package/dist/Components/ThreeStateRadio.svelte +101 -101
  108. package/dist/Components/Touch/BoolCard.svelte +27 -27
  109. package/dist/Components/Touch/DateModal.svelte +98 -98
  110. package/dist/Components/Touch/EditCard.svelte +44 -44
  111. package/dist/Components/Touch/InfoCard.svelte +25 -25
  112. package/dist/Components/Touch/InfoTextCard.svelte +22 -22
  113. package/dist/Components/Touch/ModalExplanation.svelte +13 -13
  114. package/dist/Components/Touch/NumberKeyboard.svelte +81 -81
  115. package/dist/Components/Touch/NumberModal.svelte +49 -49
  116. package/dist/Components/Touch/PlusMinusCard.svelte +40 -40
  117. package/dist/Components/Touch/SelectModal.svelte +86 -86
  118. package/dist/Components/Touch/SummaryCard.svelte +74 -74
  119. package/dist/Components/Touch/TextAreaModal.svelte +53 -53
  120. package/dist/Components/Touch/TextModal.svelte +56 -56
  121. package/dist/Components/Touch/TouchCard.svelte +28 -28
  122. package/dist/Components/Touch/TouchModal.svelte +141 -141
  123. package/dist/Components/Touch/UtilityButton.svelte +63 -63
  124. package/dist/Components/VerticalTabGroup.svelte +38 -38
  125. package/dist/Components/VerticalTabGroupButton.svelte +46 -46
  126. package/dist/Helpers/Datamatrix.d.ts +19 -19
  127. package/dist/Helpers/Datamatrix.js +291 -291
  128. package/dist/Types/Examples/ExampleModal.svelte +27 -27
  129. package/dist/paraglide/README.md +1 -1
  130. package/dist/scss/_basics.scss +12 -12
  131. package/dist/scss/_colours.scss +140 -140
  132. package/dist/scss/_motion.scss +57 -57
  133. package/dist/scss/_sizing.scss +85 -85
  134. package/dist/scss/_transitions.scss +8 -8
  135. 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>