@lavalogic/scoria 0.37.42 → 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.
- package/dist/Components/HorizontalTabGroup.svelte +5 -0
- package/dist/Components/HorizontalTabGroupProps.d.ts +12 -0
- package/dist/Components/Table/Misc/FilterInput.svelte +60 -5
- package/dist/Components/VerticalTabGroup.svelte +11 -0
- package/dist/Components/VerticalTabGroupProps.d.ts +12 -0
- package/package.json +1 -1
|
@@ -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. */
|
|
@@ -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={
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|