@lavalogic/scoria 0.37.41 → 0.37.43

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.
@@ -15,6 +15,7 @@
15
15
  selected,
16
16
  noRadius = false,
17
17
  grow = false,
18
+ dense = false,
18
19
  filterable = false,
19
20
  filterFn,
20
21
  }: HorizontalTabGroupProps<CommonDenominator> = $props();
@@ -42,6 +43,7 @@
42
43
  class="wrapper"
43
44
  class:no-radius={noRadius}
44
45
  class:grow
46
+ class:dense
45
47
  >
46
48
  <div class="tabs">
47
49
  <div class="sticky">
@@ -104,6 +106,9 @@
104
106
  .wrapper.grow {
105
107
  flex-grow: 1;
106
108
  }
109
+ .wrapper.dense {
110
+ align-items: flex-start;
111
+ }
107
112
  .wrapper:not(.grow) .tabs {
108
113
  padding-top: 1px;
109
114
  }
@@ -15,6 +15,18 @@ interface HorizontalTabGroupPropsBase<CommonDenominator> {
15
15
  noRadius?: boolean;
16
16
  /** Stretch to fill the parent's flex axis. */
17
17
  grow?: boolean;
18
+ /**
19
+ * Anchor the tab strip to the top of the group rather than letting
20
+ * it stretch to the height of the active tab's content. In the
21
+ * default `align-items: stretch` row layout, the tabs strip and the
22
+ * content pane stretch each other to the tallest natural height -
23
+ * which is fine for full-page tables, but leaves a tall column of
24
+ * empty `.ui-blue-2` background next to a short tab body inside a
25
+ * dialog. With `dense`, the wrapper switches to `align-items:
26
+ * flex-start` so both children sit at their natural heights and
27
+ * the group collapses onto whichever is taller.
28
+ */
29
+ dense?: boolean;
18
30
  /** The tabs to render. Keyed by `option.label`. */
19
31
  tabs: ReadonlyArray<TabOption<CommonDenominator>>;
20
32
  /** Currently selected tab, matched by reference. */
@@ -74,10 +74,18 @@ function makeSpec(kind, def, internals = {}) {
74
74
  * to roughly match what the legacy `<table>` element produced via the
75
75
  * browser auto-layout algorithm for typical FloWMS column content. */
76
76
  const KIND_DEFAULT_WIDTHS = {
77
- text: 200,
78
- number: 100,
77
+ text: 180,
78
+ // Bumped from 100 → 120 so 7-8 digit IDs / quantities (e.g. shipment
79
+ // IDs, order IDs) fit cleanly alongside the sort / filter affordances
80
+ // without forcing the value to truncate or wrap.
81
+ number: 120,
79
82
  checkbox: 80,
80
- date: 140,
83
+ // Bumped from 140 → 180 so the formatted "DD/MM/YYYY HH:mm" string
84
+ // fits inside the cell. The legacy 140px floor cut off the time
85
+ // portion for the typical date format used across FloWMS pages and
86
+ // forced every consumer to set a per-column `width` override (see
87
+ // OrderManagement, which had to pin date columns at 210).
88
+ date: 180,
81
89
  select: 160,
82
90
  query: 160,
83
91
  display: 150,
@@ -257,6 +257,43 @@
257
257
  let showFromDateModal = $state(false);
258
258
  let showToDateModal = $state(false);
259
259
 
260
+ /**
261
+ * Debounce window for typed filter inputs (Text / Number / Between
262
+ * min+max). Discrete selections (Svelecte selects, date pickers,
263
+ * filter-mode dropdowns) bypass this and fire `filterBy` immediately.
264
+ * Combined with the 400 ms debounce in `createRemoteRowSource`, the
265
+ * server-side lookup lands roughly one second after the last keystroke,
266
+ * which matches the "wait a couple of seconds" expectation users have
267
+ * when typing into a free-text filter.
268
+ */
269
+ const FILTER_INPUT_DEBOUNCE_MS = 500;
270
+
271
+ let typingDebounceTimer: ReturnType<typeof setTimeout> | undefined;
272
+
273
+ /** Fire the table's `filterBy` after the user stops typing for
274
+ * `FILTER_INPUT_DEBOUNCE_MS`. Repeated calls coalesce; the most
275
+ * recent value wins. */
276
+ function debouncedFilterBy(id: string, newValue: unknown): void {
277
+ if (typingDebounceTimer !== undefined) {
278
+ clearTimeout(typingDebounceTimer);
279
+ }
280
+ typingDebounceTimer = setTimeout(() => {
281
+ typingDebounceTimer = undefined;
282
+ tableContext.paginationRepo?.filterBy(id, newValue);
283
+ }, FILTER_INPUT_DEBOUNCE_MS);
284
+ }
285
+
286
+ // Clear any pending debounce when the row unmounts so we do not
287
+ // fire `filterBy` against a torn-down `paginationRepo`.
288
+ $effect(() => {
289
+ return () => {
290
+ if (typingDebounceTimer !== undefined) {
291
+ clearTimeout(typingDebounceTimer);
292
+ typingDebounceTimer = undefined;
293
+ }
294
+ };
295
+ });
296
+
260
297
  function generateOnChangeObject(label: string) {
261
298
  return (newValue: SelectOption<unknown> | null) => {
262
299
  tableContext.paginationRepo?.filterBy(def.id, newValue?.value ?? null);
@@ -406,6 +443,24 @@
406
443
  />
407
444
  {/if}
408
445
  {:else}
446
+ {@const selectedPrimitives = selectedOptions
447
+ .map((it) => it.value)
448
+ .filter(
449
+ (v): v is string | number => typeof v === 'string' || typeof v === 'number'
450
+ )}
451
+ <!--
452
+ MultiSelect with `valueAsObject={false}` matches
453
+ options against the primitive value, not the whole
454
+ SelectOption shape. Previously the remote-select
455
+ branch passed the SelectOption objects directly
456
+ (the `selectedOptions` $state), meaning the inner
457
+ `selectedKeys` derived saw
458
+ `[{label, value}, ...]` rows where it expected
459
+ `[id1, id2, ...]`, so no chips ever rendered even
460
+ though the filter was active server-side. Pull the
461
+ primitive `value` off each option to match the
462
+ local-select branch above.
463
+ -->
409
464
  <MultiSelect
410
465
  label={displayLabel}
411
466
  labelProp="label"
@@ -418,7 +473,7 @@
418
473
  // primitives directly.
419
474
  tableContext.paginationRepo?.filterBy(def.id, e);
420
475
  }}
421
- selectedValues={selectedOptions}
476
+ selectedValues={selectedPrimitives}
422
477
  collapseSelection
423
478
  />
424
479
  {/if}
@@ -501,7 +556,7 @@
501
556
  id={inputId}
502
557
  bind:value={valueMin}
503
558
  oninput={() => {
504
- tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMax]);
559
+ debouncedFilterBy(def.id, [valueMin, valueMax]);
505
560
  }}
506
561
  onkeydown={(e: KeyboardEvent) => {
507
562
  e.stopPropagation();
@@ -512,7 +567,7 @@
512
567
  id={inputId}
513
568
  bind:value={valueMax}
514
569
  oninput={() => {
515
- tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMax]);
570
+ debouncedFilterBy(def.id, [valueMin, valueMax]);
516
571
  }}
517
572
  onkeydown={(e: KeyboardEvent) => {
518
573
  e.stopPropagation();
@@ -523,7 +578,7 @@
523
578
  id={inputId}
524
579
  bind:value
525
580
  oninput={() => {
526
- tableContext.paginationRepo?.filterBy(def.id, [value]);
581
+ debouncedFilterBy(def.id, [value]);
527
582
  }}
528
583
  onkeydown={(e: KeyboardEvent) => {
529
584
  e.stopPropagation();
@@ -561,7 +616,7 @@
561
616
  id={inputId}
562
617
  bind:value
563
618
  oninput={() => {
564
- tableContext.paginationRepo?.filterBy(def.id, value);
619
+ debouncedFilterBy(def.id, value);
565
620
  }}
566
621
  onkeydown={(e: KeyboardEvent) => {
567
622
  e.stopPropagation();
@@ -16,6 +16,7 @@
16
16
  noscroll,
17
17
  noTabMessage,
18
18
  nohistory = false,
19
+ dense = false,
19
20
  }: VerticalTabGroupProps<CommonDenominator> = $props();
20
21
 
21
22
  const isNonEmptyString = (raw: unknown): raw is string =>
@@ -48,6 +49,7 @@
48
49
  <div
49
50
  class="wrapper"
50
51
  class:noscroll
52
+ class:dense
51
53
  {style}
52
54
  >
53
55
  <div class="tabs">
@@ -62,6 +64,7 @@
62
64
  <div
63
65
  class="content"
64
66
  class:noscroll
67
+ class:dense
65
68
  >
66
69
  {#if selected}
67
70
  {@render selected.content(selected.args)}
@@ -88,6 +91,10 @@
88
91
  .wrapper.noscroll {
89
92
  overflow: visible;
90
93
  }
94
+ .wrapper.dense {
95
+ flex: 0 0 auto;
96
+ overflow: visible;
97
+ }
91
98
  .wrapper .tabs {
92
99
  border-radius: 0 4px 0 0;
93
100
  display: flex;
@@ -113,4 +120,8 @@
113
120
  }
114
121
  .wrapper .content.noscroll {
115
122
  overflow: visible;
123
+ }
124
+ .wrapper .content.dense {
125
+ flex-grow: 0;
126
+ overflow: visible;
116
127
  }</style>
@@ -20,4 +20,16 @@ export interface VerticalTabGroupProps<CommonDenominator> {
20
20
  noTabMessage?: string;
21
21
  /** Suppress on-mount history restore even when `name` is set. */
22
22
  nohistory?: boolean;
23
+ /**
24
+ * Size the group to the height of the selected tab's content rather
25
+ * than filling whatever vertical space the parent offers. Use this
26
+ * inside dialogs / forms where the tab body is a short, content-
27
+ * driven panel and the default `flex-grow: 1` would leave a
28
+ * stretch of empty background below the rendered content.
29
+ *
30
+ * Concretely this disables the `flex: 1` on the wrapper and the
31
+ * `flex-grow: 1` on the inner `.content`, so the group collapses
32
+ * vertically onto whatever the active tab renders.
33
+ */
34
+ dense?: boolean;
23
35
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lavalogic/scoria",
3
3
  "description": "Svelte components used for the FloWMS Web Frontend",
4
- "version": "0.37.41",
4
+ "version": "0.37.43",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },