@marianmeres/stuic 3.130.0 → 3.131.0
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/Input/FieldOptions.svelte +557 -178
- package/dist/components/Input/FieldOptions.svelte.d.ts +7 -0
- package/dist/components/Input/README.md +43 -15
- package/dist/components/Input/_internal/InputWrap.svelte +4 -0
- package/dist/components/Input/_internal/InputWrap.svelte.d.ts +2 -0
- package/dist/icons/index.d.ts +2 -0
- package/dist/icons/index.js +2 -0
- package/package.json +12 -12
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
import { createClog } from "@marianmeres/clog";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
iconSearch,
|
|
5
|
+
iconCheck,
|
|
6
|
+
iconCircle,
|
|
7
|
+
iconSquare,
|
|
8
|
+
iconChevronUp,
|
|
9
|
+
iconChevronDown,
|
|
10
|
+
iconArrowUpToLine,
|
|
11
|
+
iconArrowDownToLine,
|
|
12
|
+
iconTrash,
|
|
13
|
+
} from "../../icons/index.js";
|
|
4
14
|
import { ItemCollection, type Item } from "@marianmeres/item-collection";
|
|
5
15
|
import { Debounced, watch } from "runed";
|
|
6
16
|
import { onDestroy, tick, type Snippet } from "svelte";
|
|
@@ -73,6 +83,13 @@
|
|
|
73
83
|
renderOptionLabel?: (item: Item) => string;
|
|
74
84
|
renderOptionGroup?: (s: string) => string;
|
|
75
85
|
allowUnknown?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Opt-in: when true (and multi-select), exposes a dedicated "Arrange" screen in the
|
|
88
|
+
* modal that lets the user manually reorder the current selection (buttons only, no
|
|
89
|
+
* drag), plus "Sort A–Z" / "Reverse" shortcuts. The chosen order is what gets
|
|
90
|
+
* serialized to `value` on submit. No-op for single-select. Default `false`.
|
|
91
|
+
*/
|
|
92
|
+
ordered?: boolean;
|
|
76
93
|
showIconsCheckbox?: boolean;
|
|
77
94
|
showIconsRadio?: boolean;
|
|
78
95
|
searchPlaceholder?: string;
|
|
@@ -101,11 +118,28 @@
|
|
|
101
118
|
cardinality_full: "Max selection reached",
|
|
102
119
|
select_from_list: "Please select from the list only",
|
|
103
120
|
x_close: "Clear input or close [esc]",
|
|
121
|
+
close: "Close [esc]",
|
|
104
122
|
unknown_allowed: "Select or type and submit",
|
|
105
123
|
unknown_not_allowed: "Select from the list",
|
|
106
124
|
no_results: "No results found.",
|
|
107
125
|
add_new: 'Add "{{value}}"...',
|
|
108
126
|
click_add_new: "You must add the value to continue",
|
|
127
|
+
//
|
|
128
|
+
pick_tab: "Pick",
|
|
129
|
+
arrange_tab: "Arrange ({{value}})",
|
|
130
|
+
arrange_help: "Reorder the selected items. Use the buttons to move them.",
|
|
131
|
+
sort_az: "Sort A–Z",
|
|
132
|
+
reverse: "Reverse",
|
|
133
|
+
move_up: "Move up",
|
|
134
|
+
move_down: "Move down",
|
|
135
|
+
move_to_top: "Move to top",
|
|
136
|
+
move_to_bottom: "Move to bottom",
|
|
137
|
+
remove_item: "Remove",
|
|
138
|
+
moved_up: "Moved {{value}} up",
|
|
139
|
+
moved_down: "Moved {{value}} down",
|
|
140
|
+
removed_item: "Removed {{value}}",
|
|
141
|
+
sorted_az: "Sorted A to Z",
|
|
142
|
+
reversed: "Order reversed",
|
|
109
143
|
};
|
|
110
144
|
let out = m[k] ?? fallback ?? k;
|
|
111
145
|
|
|
@@ -177,6 +211,7 @@
|
|
|
177
211
|
renderOptionLabel,
|
|
178
212
|
renderOptionGroup = (s: string) => `${s}`.replaceAll("_", " "),
|
|
179
213
|
allowUnknown = false,
|
|
214
|
+
ordered = false,
|
|
180
215
|
showIconsCheckbox = true,
|
|
181
216
|
showIconsRadio = false,
|
|
182
217
|
searchPlaceholder,
|
|
@@ -229,6 +264,21 @@
|
|
|
229
264
|
let isMultiple = $derived(cardinality > 1);
|
|
230
265
|
let showIcons = $derived(isMultiple ? showIconsCheckbox : showIconsRadio);
|
|
231
266
|
|
|
267
|
+
// the "Arrange" (manual ordering) feature is opt-in and only meaningful for multi-select
|
|
268
|
+
let canArrange = $derived(ordered && isMultiple);
|
|
269
|
+
// false = "Pick" screen (search + options), true = "Arrange" screen (reorder selection)
|
|
270
|
+
let arrangeMode = $state(false);
|
|
271
|
+
// aria-live announcement text for reorder/remove actions
|
|
272
|
+
let liveAnnouncement = $state("");
|
|
273
|
+
// the arrange list scroller (for focus management)
|
|
274
|
+
let arrangeListBox: HTMLDivElement | undefined = $state();
|
|
275
|
+
// hydrate-once tracker (ordered mode): the `value` we last loaded the selection from
|
|
276
|
+
let hydratedValue: string | undefined = undefined;
|
|
277
|
+
// keep arrange state clean if the feature gets disabled at runtime (e.g. cardinality -> 1)
|
|
278
|
+
$effect(() => {
|
|
279
|
+
if (!canArrange && arrangeMode) arrangeMode = false;
|
|
280
|
+
});
|
|
281
|
+
|
|
232
282
|
//
|
|
233
283
|
let wrappedValidate: Omit<ValidateOptions, "setValidationResult"> = $derived({
|
|
234
284
|
enabled: true,
|
|
@@ -294,7 +344,13 @@
|
|
|
294
344
|
|
|
295
345
|
// reconfigure if the prop ever changes during runtime (most likely will NOT)
|
|
296
346
|
$effect(() => {
|
|
297
|
-
|
|
347
|
+
// In "arrange" mode we UNSET the sortFn (item-collection: `null` unsets, a function
|
|
348
|
+
// keeps it) so the selection keeps its manual/insertion order instead of being
|
|
349
|
+
// auto-sorted alphabetically. Default mode keeps the alpha sortFn => byte-identical.
|
|
350
|
+
_selectedColl.configure({
|
|
351
|
+
cardinality,
|
|
352
|
+
sortFn: canArrange ? (null as any) : sortFn,
|
|
353
|
+
});
|
|
298
354
|
// trim excess selections if cardinality was reduced
|
|
299
355
|
if (_selectedColl.size > cardinality) {
|
|
300
356
|
const trimmed = _selectedColl.items.slice(0, cardinality);
|
|
@@ -356,15 +412,35 @@
|
|
|
356
412
|
}
|
|
357
413
|
);
|
|
358
414
|
|
|
415
|
+
function _hydrateSelectedFromValue(v: string) {
|
|
416
|
+
_selectedColl.clear().addMany(maybeJsonParse(v) as Item[]);
|
|
417
|
+
// IMPORTANT: focus first selected so it scrolls into view on open
|
|
418
|
+
if (_selectedColl.size) {
|
|
419
|
+
waitForNextRepaint().then(() => {
|
|
420
|
+
if (!isUnmounted) _optionsColl.setActive(_selectedColl.items[0]);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
359
425
|
$effect(() => {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
426
|
+
const visible = modalDialog.visibility().visible;
|
|
427
|
+
if (canArrange) {
|
|
428
|
+
// ORDERED MODE: hydrate the selection from `value` ONLY on open / external value
|
|
429
|
+
// change — deliberately NOT on every fetch (`touch`), otherwise a manual reorder
|
|
430
|
+
// (or any not-yet-submitted pick) would be silently reset on the next keystroke.
|
|
431
|
+
const v = value;
|
|
432
|
+
if (!visible) {
|
|
433
|
+
hydratedValue = undefined;
|
|
434
|
+
return;
|
|
367
435
|
}
|
|
436
|
+
if (hydratedValue === v) return;
|
|
437
|
+
hydratedValue = v;
|
|
438
|
+
// ensure the sortFn is unset before addMany, so the saved order survives the load
|
|
439
|
+
_selectedColl.configure({ sortFn: null as any }, false);
|
|
440
|
+
_hydrateSelectedFromValue(v);
|
|
441
|
+
} else {
|
|
442
|
+
// DEFAULT MODE: unchanged behavior (re-sync to `value` whenever visible & touched)
|
|
443
|
+
if (visible && touch) _hydrateSelectedFromValue(value);
|
|
368
444
|
}
|
|
369
445
|
});
|
|
370
446
|
|
|
@@ -384,6 +460,87 @@
|
|
|
384
460
|
return prefix + strHash(`${id}`.repeat(3));
|
|
385
461
|
}
|
|
386
462
|
|
|
463
|
+
// --- Arrange (ordered) screen helpers ---
|
|
464
|
+
|
|
465
|
+
function arrange_row_id(id: string | number) {
|
|
466
|
+
return btn_id(id, "arr-");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function _announce(key: string, item: Item) {
|
|
470
|
+
liveAnnouncement = t(key, { value: _renderOptionLabel(item) }) as string;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// re-focus the same logical button on the row that moved, so repeated presses keep
|
|
474
|
+
// walking the item; if that button is now disabled (a boundary), fall back to an enabled one
|
|
475
|
+
function focusRowButton(itemId: string | number, which: "up" | "down") {
|
|
476
|
+
tick().then(() => {
|
|
477
|
+
const row = arrangeListBox?.querySelector(`#${arrange_row_id(itemId)}`);
|
|
478
|
+
if (!row) return;
|
|
479
|
+
let btn = row.querySelector<HTMLButtonElement>(`[data-arrange-btn="${which}"]`);
|
|
480
|
+
if (!btn || btn.disabled) {
|
|
481
|
+
btn =
|
|
482
|
+
row.querySelector<HTMLButtonElement>(
|
|
483
|
+
`[data-arrange-btn="up"]:not([disabled])`
|
|
484
|
+
) ||
|
|
485
|
+
row.querySelector<HTMLButtonElement>(
|
|
486
|
+
`[data-arrange-btn="down"]:not([disabled])`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
btn?.focus();
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function moveItem(from: number, to: number) {
|
|
494
|
+
if (to < 0 || to >= _selectedColl.size || from === to) return;
|
|
495
|
+
const item = _selectedColl.items[from];
|
|
496
|
+
if (!_selectedColl.move(from, to)) return;
|
|
497
|
+
if (item) {
|
|
498
|
+
_announce(to < from ? "moved_up" : "moved_down", item);
|
|
499
|
+
focusRowButton(item[itemIdPropName], to < from ? "up" : "down");
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function removeItem(i: number) {
|
|
504
|
+
const item = _selectedColl.items[i];
|
|
505
|
+
_selectedColl.removeAt(i);
|
|
506
|
+
if (item) _announce("removed_item", item);
|
|
507
|
+
tick().then(() => {
|
|
508
|
+
if (!_selectedColl.size) return enterPick();
|
|
509
|
+
const next = _selectedColl.items[Math.min(i, _selectedColl.size - 1)];
|
|
510
|
+
if (next) focusRowButton(next[itemIdPropName], "down");
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// one-shot sort by visible label (does NOT re-enable auto-sort; subsequent adds still append)
|
|
515
|
+
function sortAZ() {
|
|
516
|
+
_selectedColl.sort((a, b) =>
|
|
517
|
+
_renderOptionLabel(a).localeCompare(_renderOptionLabel(b), undefined, {
|
|
518
|
+
sensitivity: "base",
|
|
519
|
+
})
|
|
520
|
+
);
|
|
521
|
+
liveAnnouncement = t("sorted_az") as string;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// no reverse() on the collection; snapshot, clear (no publish), re-add in reversed order
|
|
525
|
+
function reverse() {
|
|
526
|
+
const reversed = [...selected.items].reverse();
|
|
527
|
+
_selectedColl.clear(false).addMany(reversed);
|
|
528
|
+
liveAnnouncement = t("reversed") as string;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function enterArrange() {
|
|
532
|
+
if (!selected.items.length) return;
|
|
533
|
+
arrangeMode = true;
|
|
534
|
+
input?.blur(); // dismiss the soft keyboard (no text input on this screen)
|
|
535
|
+
const first = selected.items[0];
|
|
536
|
+
if (first) focusRowButton(first[itemIdPropName], "down");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function enterPick() {
|
|
540
|
+
arrangeMode = false;
|
|
541
|
+
tick().then(() => input?.focus());
|
|
542
|
+
}
|
|
543
|
+
|
|
387
544
|
// "inner" submit
|
|
388
545
|
function try_submit(force = false) {
|
|
389
546
|
// clog("try_submit", innerValue, _selectedColl.dump());
|
|
@@ -440,6 +597,7 @@
|
|
|
440
597
|
// clog("modal submit", $state.snapshot(selected.items));
|
|
441
598
|
value = JSON.stringify(selected.items);
|
|
442
599
|
innerValue = "";
|
|
600
|
+
arrangeMode = false;
|
|
443
601
|
_optionsColl.clear();
|
|
444
602
|
modalDialog.close();
|
|
445
603
|
_dispatch_change_to_owner();
|
|
@@ -449,6 +607,7 @@
|
|
|
449
607
|
// clears state and dispatches change; close is handled by ModalDialog's preEscapeClose
|
|
450
608
|
function escape() {
|
|
451
609
|
innerValue = "";
|
|
610
|
+
arrangeMode = false;
|
|
452
611
|
_optionsColl.clear();
|
|
453
612
|
_dispatch_change_to_owner();
|
|
454
613
|
}
|
|
@@ -506,7 +665,9 @@
|
|
|
506
665
|
<!-- this must be on window as we're catching any typing anywhere -->
|
|
507
666
|
<svelte:window
|
|
508
667
|
onkeydown={(e) => {
|
|
509
|
-
|
|
668
|
+
// on the Arrange screen the reorder buttons own the keyboard; the option-list
|
|
669
|
+
// navigation + "any key focuses search" behavior must NOT run there
|
|
670
|
+
if (modalDialog.visibility().visible && !arrangeMode) {
|
|
510
671
|
// arrow navigation
|
|
511
672
|
if (["ArrowDown", "ArrowUp"].includes(e.key)) {
|
|
512
673
|
e.preventDefault();
|
|
@@ -602,6 +763,11 @@
|
|
|
602
763
|
>
|
|
603
764
|
<div class="pt-0 md:pt-[20vh] w-full">
|
|
604
765
|
<div class="pointer-events-auto">
|
|
766
|
+
<!-- screen-reader announcements for reorder / remove actions -->
|
|
767
|
+
<div class="sr-only" aria-live="polite" aria-atomic="true">
|
|
768
|
+
{liveAnnouncement}
|
|
769
|
+
</div>
|
|
770
|
+
|
|
605
771
|
<InputWrap
|
|
606
772
|
size={renderSize}
|
|
607
773
|
class={twMerge("m-1 sm:m-2 mb-12 shadow-xl", classModalField)}
|
|
@@ -613,12 +779,75 @@
|
|
|
613
779
|
{id}
|
|
614
780
|
{required}
|
|
615
781
|
>
|
|
782
|
+
{#snippet inputAbove()}
|
|
783
|
+
{#if canArrange}
|
|
784
|
+
<!-- Pick | Arrange tab header — lives INSIDE the card (solid bg) -->
|
|
785
|
+
<div
|
|
786
|
+
class="relative flex items-stretch px-2 border-b"
|
|
787
|
+
style={`border-color: var(--stuic-field-options-divider); min-height: var(--stuic-input-min-height-${renderSize}, 3rem);`}
|
|
788
|
+
>
|
|
789
|
+
<!-- full-height tabs => titles are vertically centered in the row -->
|
|
790
|
+
<div role="tablist" class="flex items-stretch gap-1">
|
|
791
|
+
<button
|
|
792
|
+
type="button"
|
|
793
|
+
role="tab"
|
|
794
|
+
aria-selected={!arrangeMode}
|
|
795
|
+
onclick={enterPick}
|
|
796
|
+
class={twMerge(
|
|
797
|
+
// keep font-weight constant across states so the tab doesn't reflow
|
|
798
|
+
"flex items-center px-3 -mb-px text-sm font-semibold border-b-2",
|
|
799
|
+
!arrangeMode
|
|
800
|
+
? "border-(--stuic-input-accent) text-(--stuic-input-accent)"
|
|
801
|
+
: "border-transparent stuic-field-options-muted"
|
|
802
|
+
)}
|
|
803
|
+
>
|
|
804
|
+
{@html t("pick_tab")}
|
|
805
|
+
</button>
|
|
806
|
+
<button
|
|
807
|
+
type="button"
|
|
808
|
+
role="tab"
|
|
809
|
+
aria-selected={arrangeMode}
|
|
810
|
+
disabled={!selected.items.length}
|
|
811
|
+
onclick={enterArrange}
|
|
812
|
+
class={twMerge(
|
|
813
|
+
// keep font-weight constant across states so the tab doesn't reflow
|
|
814
|
+
"flex items-center px-3 -mb-px text-sm font-semibold border-b-2",
|
|
815
|
+
arrangeMode
|
|
816
|
+
? "border-(--stuic-input-accent) text-(--stuic-input-accent)"
|
|
817
|
+
: "border-transparent stuic-field-options-muted"
|
|
818
|
+
)}
|
|
819
|
+
>
|
|
820
|
+
{@html t("arrange_tab", { value: selected.items.length })}
|
|
821
|
+
</button>
|
|
822
|
+
</div>
|
|
823
|
+
|
|
824
|
+
<!-- modal close lives in the tab header whenever tabs exist (both screens).
|
|
825
|
+
Absolutely positioned + height-matched row, so it never causes a jump. -->
|
|
826
|
+
<div class="absolute inset-y-0 right-1 flex items-center">
|
|
827
|
+
<Button
|
|
828
|
+
x
|
|
829
|
+
size="sm"
|
|
830
|
+
variant="ghost"
|
|
831
|
+
roundedFull
|
|
832
|
+
type="button"
|
|
833
|
+
tooltip={t("close")}
|
|
834
|
+
onclick={(e) => {
|
|
835
|
+
e.preventDefault();
|
|
836
|
+
escape();
|
|
837
|
+
modalDialog.close();
|
|
838
|
+
}}
|
|
839
|
+
/>
|
|
840
|
+
</div>
|
|
841
|
+
</div>
|
|
842
|
+
{/if}
|
|
843
|
+
{/snippet}
|
|
844
|
+
|
|
616
845
|
<input
|
|
617
846
|
bind:value={innerValue}
|
|
618
847
|
bind:this={input}
|
|
619
848
|
{type}
|
|
620
849
|
{id}
|
|
621
|
-
class={twMerge(renderSize, classInput)}
|
|
850
|
+
class={twMerge(renderSize, classInput, arrangeMode && "hidden")}
|
|
622
851
|
tabindex={1}
|
|
623
852
|
{required}
|
|
624
853
|
{disabled}
|
|
@@ -638,203 +867,353 @@
|
|
|
638
867
|
|
|
639
868
|
{#snippet inputBelow()}
|
|
640
869
|
<div
|
|
641
|
-
class="h-full
|
|
870
|
+
class={twMerge("h-full p-2", !arrangeMode && "border-t")}
|
|
642
871
|
style="border-color: var(--stuic-field-options-divider);"
|
|
643
872
|
>
|
|
644
|
-
|
|
645
|
-
|
|
873
|
+
{#snippet footerSubmitBtn()}
|
|
874
|
+
<div>
|
|
875
|
+
<Button
|
|
876
|
+
class="control"
|
|
877
|
+
type="button"
|
|
878
|
+
intent="primary"
|
|
879
|
+
onclick={async (e) => {
|
|
880
|
+
e.preventDefault();
|
|
881
|
+
try_submit(true);
|
|
882
|
+
}}
|
|
883
|
+
tabindex={3}
|
|
884
|
+
>
|
|
885
|
+
{@html t("submit")}
|
|
886
|
+
</Button>
|
|
887
|
+
</div>
|
|
888
|
+
{/snippet}
|
|
889
|
+
|
|
890
|
+
{#if canArrange && arrangeMode}
|
|
891
|
+
<!-- ARRANGE screen: reorder the current selection (buttons only, no drag) -->
|
|
892
|
+
<div class="text-sm -mt-1 flex items-center">
|
|
646
893
|
<button
|
|
647
894
|
type="button"
|
|
648
|
-
onclick={
|
|
895
|
+
onclick={sortAZ}
|
|
896
|
+
disabled={selected.items.length < 2}
|
|
649
897
|
class="control flex items-center p-1 m-1 text-sm underline rounded stuic-field-options-control"
|
|
650
898
|
tabindex={4}
|
|
651
|
-
disabled={!options.size}
|
|
652
899
|
>
|
|
653
|
-
{@html t("
|
|
900
|
+
{@html t("sort_az")}
|
|
654
901
|
</button>
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
902
|
+
<button
|
|
903
|
+
type="button"
|
|
904
|
+
onclick={reverse}
|
|
905
|
+
disabled={selected.items.length < 2}
|
|
906
|
+
class="control flex items-center p-1 m-1 text-sm underline rounded stuic-field-options-control"
|
|
907
|
+
tabindex={5}
|
|
908
|
+
>
|
|
909
|
+
{@html t("reverse")}
|
|
910
|
+
</button>
|
|
911
|
+
<span
|
|
912
|
+
class="flex-1 block justify-end text-right text-xs p-1 pr-2 stuic-field-options-muted"
|
|
913
|
+
>
|
|
914
|
+
{selected.items.length}
|
|
915
|
+
{#if cardinality > 0 && cardinality < Infinity}
|
|
916
|
+
{@html t("cardinality_of")} {cardinality}
|
|
917
|
+
{/if}
|
|
918
|
+
{@html t("cardinality_selected")}
|
|
919
|
+
</span>
|
|
920
|
+
</div>
|
|
669
921
|
|
|
670
|
-
<
|
|
671
|
-
|
|
672
|
-
|
|
922
|
+
<div
|
|
923
|
+
class={[
|
|
924
|
+
"options overflow-y-auto overflow-x-hidden space-y-1 scrollbar-thin",
|
|
925
|
+
"h-55 max-h-55",
|
|
926
|
+
]}
|
|
927
|
+
bind:this={arrangeListBox}
|
|
928
|
+
tabindex="-1"
|
|
673
929
|
>
|
|
674
|
-
{selected.items
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
930
|
+
{#each selected.items as item, i (item[itemIdPropName])}
|
|
931
|
+
{@const last = selected.items.length - 1}
|
|
932
|
+
<div
|
|
933
|
+
id={arrange_row_id(item[itemIdPropName])}
|
|
934
|
+
class="flex items-center gap-1 px-1 py-0.5"
|
|
935
|
+
>
|
|
936
|
+
<span class="flex-1 min-w-0 truncate text-sm"
|
|
937
|
+
>{_renderOptionLabel(item)}</span
|
|
938
|
+
>
|
|
939
|
+
<span class="hidden sm:inline-flex">
|
|
940
|
+
<Button
|
|
941
|
+
iconButton
|
|
942
|
+
variant="ghost"
|
|
943
|
+
type="button"
|
|
944
|
+
aria-label={t("move_to_top")}
|
|
945
|
+
tooltip={t("move_to_top")}
|
|
946
|
+
data-arrange-btn="top"
|
|
947
|
+
disabled={i === 0}
|
|
948
|
+
onclick={() => moveItem(i, 0)}
|
|
949
|
+
>
|
|
950
|
+
{@html iconArrowUpToLine({ size: 18 })}
|
|
951
|
+
</Button>
|
|
952
|
+
</span>
|
|
953
|
+
<Button
|
|
954
|
+
iconButton
|
|
955
|
+
variant="ghost"
|
|
956
|
+
type="button"
|
|
957
|
+
aria-label={t("move_up")}
|
|
958
|
+
tooltip={t("move_up")}
|
|
959
|
+
data-arrange-btn="up"
|
|
960
|
+
disabled={i === 0}
|
|
961
|
+
onclick={() => moveItem(i, i - 1)}
|
|
962
|
+
>
|
|
963
|
+
{@html iconChevronUp({ size: 18 })}
|
|
964
|
+
</Button>
|
|
965
|
+
<Button
|
|
966
|
+
iconButton
|
|
967
|
+
variant="ghost"
|
|
968
|
+
type="button"
|
|
969
|
+
aria-label={t("move_down")}
|
|
970
|
+
tooltip={t("move_down")}
|
|
971
|
+
data-arrange-btn="down"
|
|
972
|
+
disabled={i === last}
|
|
973
|
+
onclick={() => moveItem(i, i + 1)}
|
|
974
|
+
>
|
|
975
|
+
{@html iconChevronDown({ size: 18 })}
|
|
976
|
+
</Button>
|
|
977
|
+
<span class="hidden sm:inline-flex">
|
|
978
|
+
<Button
|
|
979
|
+
iconButton
|
|
980
|
+
variant="ghost"
|
|
981
|
+
type="button"
|
|
982
|
+
aria-label={t("move_to_bottom")}
|
|
983
|
+
tooltip={t("move_to_bottom")}
|
|
984
|
+
data-arrange-btn="bottom"
|
|
985
|
+
disabled={i === last}
|
|
986
|
+
onclick={() => moveItem(i, last)}
|
|
987
|
+
>
|
|
988
|
+
{@html iconArrowDownToLine({ size: 18 })}
|
|
989
|
+
</Button>
|
|
990
|
+
</span>
|
|
991
|
+
<Button
|
|
992
|
+
iconButton
|
|
993
|
+
variant="ghost"
|
|
994
|
+
type="button"
|
|
995
|
+
aria-label={t("remove_item")}
|
|
996
|
+
tooltip={t("remove_item")}
|
|
997
|
+
data-arrange-btn="remove"
|
|
998
|
+
onclick={() => removeItem(i)}
|
|
999
|
+
>
|
|
1000
|
+
{@html iconTrash({ size: 18 })}
|
|
1001
|
+
</Button>
|
|
1002
|
+
</div>
|
|
1003
|
+
{/each}
|
|
1004
|
+
</div>
|
|
681
1005
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
class={[
|
|
686
|
-
"options overflow-y-auto overflow-x-hidden space-y-1 scrollbar-thin",
|
|
687
|
-
"h-55 max-h-55",
|
|
688
|
-
]}
|
|
689
|
-
bind:this={optionsBox}
|
|
690
|
-
tabindex="-1"
|
|
691
|
-
>
|
|
692
|
-
{#if isFetching && !options.items.length}
|
|
693
|
-
<div
|
|
694
|
-
class="flex text-sm h-full items-center justify-center stuic-field-options-placeholder"
|
|
695
|
-
>
|
|
696
|
-
<Spinner class="w-4" />
|
|
1006
|
+
<div class="pt-3 pl-1 flex items-end justify-between">
|
|
1007
|
+
<div class="text-xs stuic-field-options-muted">
|
|
1008
|
+
{@html t("arrange_help")}
|
|
697
1009
|
</div>
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1010
|
+
{@render footerSubmitBtn()}
|
|
1011
|
+
</div>
|
|
1012
|
+
{:else}
|
|
1013
|
+
<div class="text-sm -mt-1 flex items-center">
|
|
1014
|
+
{#if isMultiple}
|
|
1015
|
+
<button
|
|
1016
|
+
type="button"
|
|
1017
|
+
onclick={() => _selectedColl.addMany(options.items)}
|
|
1018
|
+
class="control flex items-center p-1 m-1 text-sm underline rounded stuic-field-options-control"
|
|
1019
|
+
tabindex={4}
|
|
1020
|
+
disabled={!options.size}
|
|
1021
|
+
>
|
|
1022
|
+
{@html t("select_all")}
|
|
1023
|
+
</button>
|
|
1024
|
+
{/if}
|
|
1025
|
+
<button
|
|
1026
|
+
type="button"
|
|
1027
|
+
onclick={() => {
|
|
1028
|
+
_selectedColl.clear();
|
|
1029
|
+
input?.focus();
|
|
1030
|
+
}}
|
|
1031
|
+
class="control flex items-center p-1 m-1 text-sm underline rounded stuic-field-options-control"
|
|
1032
|
+
data-disabled={!selected.items.length || undefined}
|
|
1033
|
+
tabindex={5}
|
|
1034
|
+
disabled={!selected.items.length}
|
|
701
1035
|
>
|
|
702
|
-
{@html t("
|
|
703
|
-
</
|
|
704
|
-
{/if}
|
|
1036
|
+
{@html t(cardinality === 1 ? "clear" : "clear_all")}
|
|
1037
|
+
</button>
|
|
705
1038
|
|
|
706
|
-
|
|
707
|
-
<
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
</div>
|
|
718
|
-
{/if}
|
|
1039
|
+
<span class="p-1 m-1 text-sm"> </span>
|
|
1040
|
+
<span
|
|
1041
|
+
class="flex-1 block justify-end text-right text-xs p-1 pr-2 stuic-field-options-muted"
|
|
1042
|
+
>
|
|
1043
|
+
{selected.items.length}
|
|
1044
|
+
{#if cardinality > 0 && cardinality < Infinity}
|
|
1045
|
+
{@html t("cardinality_of")} {cardinality}
|
|
1046
|
+
{/if}
|
|
1047
|
+
{@html t("cardinality_selected")}
|
|
1048
|
+
</span>
|
|
1049
|
+
</div>
|
|
719
1050
|
|
|
720
|
-
{#
|
|
721
|
-
|
|
1051
|
+
<!-- {#if options.items.length} -->
|
|
1052
|
+
<div
|
|
1053
|
+
id={`${id}-options`}
|
|
1054
|
+
class={[
|
|
1055
|
+
"options overflow-y-auto overflow-x-hidden space-y-1 scrollbar-thin",
|
|
1056
|
+
"h-55 max-h-55",
|
|
1057
|
+
]}
|
|
1058
|
+
bind:this={optionsBox}
|
|
1059
|
+
tabindex="-1"
|
|
1060
|
+
>
|
|
1061
|
+
{#if isFetching && !options.items.length}
|
|
722
1062
|
<div
|
|
723
|
-
class=
|
|
724
|
-
"mb-1 p-1 text-xs font-semibold uppercase tracking-wide stuic-field-options-optgroup",
|
|
725
|
-
classOptgroup
|
|
726
|
-
)}
|
|
1063
|
+
class="flex text-sm h-full items-center justify-center stuic-field-options-placeholder"
|
|
727
1064
|
>
|
|
728
|
-
|
|
1065
|
+
<Spinner class="w-4" />
|
|
1066
|
+
</div>
|
|
1067
|
+
{:else if !options.items.length && !allowUnknown}
|
|
1068
|
+
<div
|
|
1069
|
+
class="flex text-sm h-full items-center justify-center stuic-field-options-placeholder"
|
|
1070
|
+
>
|
|
1071
|
+
{@html t("no_results")}
|
|
729
1072
|
</div>
|
|
730
1073
|
{/if}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
return notifications?.error(t("cardinality_full"), {
|
|
745
|
-
ttl: 1000,
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
_selectedColl.toggleAdd(item);
|
|
749
|
-
} else {
|
|
750
|
-
_selectedColl.clear();
|
|
751
|
-
_selectedColl.add(item);
|
|
752
|
-
submit();
|
|
753
|
-
}
|
|
754
|
-
}}
|
|
755
|
-
active={isSelected}
|
|
756
|
-
focused={active}
|
|
757
|
-
contentBefore={showIcons ? getIconThc(isSelected) : undefined}
|
|
758
|
-
classContentBefore={isSelected
|
|
759
|
-
? "stuic-field-options-icon stuic-field-options-icon--selected"
|
|
760
|
-
: "stuic-field-options-icon"}
|
|
761
|
-
class={classOption}
|
|
762
|
-
classActive={classOptionActive}
|
|
763
|
-
classFocused={classOptionActive}
|
|
764
|
-
tabindex={-1}
|
|
765
|
-
role={isMultiple ? "checkbox" : "radio"}
|
|
766
|
-
aria-checked={isSelected}
|
|
767
|
-
>
|
|
768
|
-
{_renderOptionLabel(item)}
|
|
769
|
-
</ListItemButton>
|
|
770
|
-
</li>
|
|
771
|
-
{/each}
|
|
772
|
-
</ul>
|
|
773
|
-
{/each}
|
|
774
|
-
</div>
|
|
775
|
-
<!-- {/if} -->
|
|
776
|
-
<div class="pt-3 pl-1 flex items-end justify-between">
|
|
777
|
-
<div class="text-xs stuic-field-options-muted">
|
|
778
|
-
<!-- Use arrows to navigate. Spacebar and Enter to select and/or submit. -->
|
|
779
|
-
{#if allowUnknown}
|
|
780
|
-
{@html t("unknown_allowed")}
|
|
781
|
-
{:else}
|
|
782
|
-
{@html t("unknown_not_allowed")}
|
|
1074
|
+
|
|
1075
|
+
{#if !isFetching && allowUnknown && innerValue && !have_option_label_like(options.items, innerValue)}
|
|
1076
|
+
<div class="px-1">
|
|
1077
|
+
<ListItemButton
|
|
1078
|
+
bind:el={addNewBtn}
|
|
1079
|
+
onclick={add_new}
|
|
1080
|
+
focused={isAddNewBtnActive}
|
|
1081
|
+
class={classOption}
|
|
1082
|
+
classFocused={classOptionActive}
|
|
1083
|
+
>
|
|
1084
|
+
{t("add_new", { value: innerValue })}
|
|
1085
|
+
</ListItemButton>
|
|
1086
|
+
</div>
|
|
783
1087
|
{/if}
|
|
1088
|
+
|
|
1089
|
+
{#each groupedOptions as [_optgroup, _opts]}
|
|
1090
|
+
{#if _optgroup}
|
|
1091
|
+
<div
|
|
1092
|
+
class={twMerge(
|
|
1093
|
+
"mb-1 p-1 text-xs font-semibold uppercase tracking-wide stuic-field-options-optgroup",
|
|
1094
|
+
classOptgroup
|
|
1095
|
+
)}
|
|
1096
|
+
>
|
|
1097
|
+
{_optgroup}
|
|
1098
|
+
</div>
|
|
1099
|
+
{/if}
|
|
1100
|
+
<ul role="presentation" class="space-y-1">
|
|
1101
|
+
<!-- {#each options.items as item} -->
|
|
1102
|
+
{#each _opts as item (item[itemIdPropName])}
|
|
1103
|
+
{@const active =
|
|
1104
|
+
item[itemIdPropName] === options.active?.[itemIdPropName]}
|
|
1105
|
+
{@const isSelected =
|
|
1106
|
+
selected.items && _selectedColl.exists(item[itemIdPropName])}
|
|
1107
|
+
<li class:active role="presentation" class="px-1">
|
|
1108
|
+
<ListItemButton
|
|
1109
|
+
id={btn_id(item[itemIdPropName])}
|
|
1110
|
+
onclick={() => {
|
|
1111
|
+
if (isMultiple) {
|
|
1112
|
+
if (selected.isFull && !_selectedColl.exists(item)) {
|
|
1113
|
+
return notifications?.error(t("cardinality_full"), {
|
|
1114
|
+
ttl: 1000,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
_selectedColl.toggleAdd(item);
|
|
1118
|
+
} else {
|
|
1119
|
+
_selectedColl.clear();
|
|
1120
|
+
_selectedColl.add(item);
|
|
1121
|
+
submit();
|
|
1122
|
+
}
|
|
1123
|
+
}}
|
|
1124
|
+
active={isSelected}
|
|
1125
|
+
focused={active}
|
|
1126
|
+
contentBefore={showIcons ? getIconThc(isSelected) : undefined}
|
|
1127
|
+
classContentBefore={isSelected
|
|
1128
|
+
? "stuic-field-options-icon stuic-field-options-icon--selected"
|
|
1129
|
+
: "stuic-field-options-icon"}
|
|
1130
|
+
class={classOption}
|
|
1131
|
+
classActive={classOptionActive}
|
|
1132
|
+
classFocused={classOptionActive}
|
|
1133
|
+
tabindex={-1}
|
|
1134
|
+
role={isMultiple ? "checkbox" : "radio"}
|
|
1135
|
+
aria-checked={isSelected}
|
|
1136
|
+
>
|
|
1137
|
+
{_renderOptionLabel(item)}
|
|
1138
|
+
</ListItemButton>
|
|
1139
|
+
</li>
|
|
1140
|
+
{/each}
|
|
1141
|
+
</ul>
|
|
1142
|
+
{/each}
|
|
784
1143
|
</div>
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
>
|
|
796
|
-
|
|
797
|
-
|
|
1144
|
+
<!-- {/if} -->
|
|
1145
|
+
<div class="pt-3 pl-1 flex items-end justify-between">
|
|
1146
|
+
<div class="text-xs stuic-field-options-muted">
|
|
1147
|
+
<!-- Use arrows to navigate. Spacebar and Enter to select and/or submit. -->
|
|
1148
|
+
{#if allowUnknown}
|
|
1149
|
+
{@html t("unknown_allowed")}
|
|
1150
|
+
{:else}
|
|
1151
|
+
{@html t("unknown_not_allowed")}
|
|
1152
|
+
{/if}
|
|
1153
|
+
</div>
|
|
1154
|
+
<div>
|
|
1155
|
+
<Button
|
|
1156
|
+
class="control"
|
|
1157
|
+
type="button"
|
|
1158
|
+
intent="primary"
|
|
1159
|
+
onclick={async (e) => {
|
|
1160
|
+
e.preventDefault();
|
|
1161
|
+
try_submit(true);
|
|
1162
|
+
}}
|
|
1163
|
+
tabindex={3}
|
|
1164
|
+
>
|
|
1165
|
+
{@html t("submit")}
|
|
1166
|
+
</Button>
|
|
1167
|
+
</div>
|
|
798
1168
|
</div>
|
|
799
|
-
|
|
1169
|
+
{/if}
|
|
800
1170
|
</div>
|
|
801
1171
|
{/snippet}
|
|
802
1172
|
|
|
803
1173
|
{#snippet inputAfter()}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1174
|
+
<!-- the whole search row is hidden on the Arrange screen (close moves to the tabs) -->
|
|
1175
|
+
{#if !arrangeMode}
|
|
1176
|
+
<div
|
|
1177
|
+
class="flex pl-2 items-center justify-center stuic-field-options-placeholder"
|
|
1178
|
+
>
|
|
1179
|
+
{#if isFetching}
|
|
1180
|
+
<Spinner class="w-4" />
|
|
1181
|
+
{/if}
|
|
1182
|
+
</div>
|
|
1183
|
+
<!-- ordered fields have a single close ✕ in the tab header, so the
|
|
1184
|
+
search row drops its own ✕ (avoids a confusing double-✕) -->
|
|
1185
|
+
{#if !canArrange}
|
|
1186
|
+
<div class="flex pl-2 pr-1 items-center justify-center">
|
|
1187
|
+
<Button
|
|
1188
|
+
x
|
|
1189
|
+
variant="ghost"
|
|
1190
|
+
roundedFull
|
|
1191
|
+
type="button"
|
|
1192
|
+
tooltip={t("x_close")}
|
|
1193
|
+
onclick={(e) => {
|
|
1194
|
+
e.preventDefault();
|
|
1195
|
+
if (innerValue.trim() == "") {
|
|
1196
|
+
escape();
|
|
1197
|
+
return modalDialog.close();
|
|
1198
|
+
}
|
|
1199
|
+
innerValue = "";
|
|
1200
|
+
input?.focus();
|
|
1201
|
+
}}
|
|
1202
|
+
tabindex={2}
|
|
1203
|
+
/>
|
|
1204
|
+
</div>
|
|
809
1205
|
{/if}
|
|
810
|
-
|
|
811
|
-
<div class="flex pl-2 pr-1 items-center justify-center">
|
|
812
|
-
<Button
|
|
813
|
-
x
|
|
814
|
-
variant="ghost"
|
|
815
|
-
roundedFull
|
|
816
|
-
type="button"
|
|
817
|
-
tooltip={t("x_close")}
|
|
818
|
-
onclick={(e) => {
|
|
819
|
-
e.preventDefault();
|
|
820
|
-
if (innerValue.trim() == "") {
|
|
821
|
-
escape();
|
|
822
|
-
return modalDialog.close();
|
|
823
|
-
}
|
|
824
|
-
innerValue = "";
|
|
825
|
-
input?.focus();
|
|
826
|
-
}}
|
|
827
|
-
tabindex={2}
|
|
828
|
-
/>
|
|
829
|
-
</div>
|
|
1206
|
+
{/if}
|
|
830
1207
|
{/snippet}
|
|
831
1208
|
|
|
832
1209
|
{#snippet inputBefore()}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1210
|
+
{#if !arrangeMode}
|
|
1211
|
+
<div
|
|
1212
|
+
class="flex flex-col items-center justify-center pl-3 stuic-field-options-muted"
|
|
1213
|
+
>
|
|
1214
|
+
{@html iconSearch({ size: 19 })}
|
|
1215
|
+
</div>
|
|
1216
|
+
{/if}
|
|
838
1217
|
{/snippet}
|
|
839
1218
|
</InputWrap>
|
|
840
1219
|
</div>
|
|
@@ -55,6 +55,13 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
|
55
55
|
renderOptionLabel?: (item: Item) => string;
|
|
56
56
|
renderOptionGroup?: (s: string) => string;
|
|
57
57
|
allowUnknown?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Opt-in: when true (and multi-select), exposes a dedicated "Arrange" screen in the
|
|
60
|
+
* modal that lets the user manually reorder the current selection (buttons only, no
|
|
61
|
+
* drag), plus "Sort A–Z" / "Reverse" shortcuts. The chosen order is what gets
|
|
62
|
+
* serialized to `value` on submit. No-op for single-select. Default `false`.
|
|
63
|
+
*/
|
|
64
|
+
ordered?: boolean;
|
|
58
65
|
showIconsCheckbox?: boolean;
|
|
59
66
|
showIconsRadio?: boolean;
|
|
60
67
|
searchPlaceholder?: string;
|
|
@@ -359,21 +359,22 @@ A modal-based multi-select/single-select component with search functionality, ty
|
|
|
359
359
|
|
|
360
360
|
### Props
|
|
361
361
|
|
|
362
|
-
| Prop | Type | Default | Description
|
|
363
|
-
| ------------------- | ---------------------------------------------------------- | ---------- |
|
|
364
|
-
| `value` | `string` | `"[]"` | JSON array of selected items (bindable)
|
|
365
|
-
| `name` | `string` | - | Form field name
|
|
366
|
-
| `getOptions` | `(q: string, current: Item[]) => Promise<{found: Item[]}>` | - | Async function to fetch options
|
|
367
|
-
| `cardinality` | `number` | `Infinity` | Max selections (-1 for unlimited)
|
|
368
|
-
| `allowUnknown` | `boolean` | `false` | Allow typing custom values
|
|
369
|
-
| `
|
|
370
|
-
| `
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
362
|
+
| Prop | Type | Default | Description |
|
|
363
|
+
| ------------------- | ---------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------- |
|
|
364
|
+
| `value` | `string` | `"[]"` | JSON array of selected items (bindable) |
|
|
365
|
+
| `name` | `string` | - | Form field name |
|
|
366
|
+
| `getOptions` | `(q: string, current: Item[]) => Promise<{found: Item[]}>` | - | Async function to fetch options |
|
|
367
|
+
| `cardinality` | `number` | `Infinity` | Max selections (-1 for unlimited) |
|
|
368
|
+
| `allowUnknown` | `boolean` | `false` | Allow typing custom values |
|
|
369
|
+
| `ordered` | `boolean` | `false` | Opt-in: add an "Arrange" screen to manually order the selection (multi-select only) |
|
|
370
|
+
| `renderOptionLabel` | `(item: Item) => string` | - | Custom option label renderer |
|
|
371
|
+
| `renderOptionGroup` | `(s: string) => string` | - | Custom optgroup label renderer |
|
|
372
|
+
| `renderValue` | `(stringifiedItems: string) => string` | - | Custom value display renderer |
|
|
373
|
+
| `showIconsCheckbox` | `boolean` | `true` | Show checkbox icons in multi-select |
|
|
374
|
+
| `showIconsRadio` | `boolean` | `false` | Show radio icons in single-select |
|
|
375
|
+
| `searchPlaceholder` | `string` | - | Custom search placeholder |
|
|
376
|
+
| `itemIdPropName` | `string` | `"id"` | Property name for item ID |
|
|
377
|
+
| `notifications` | `NotificationsStack` | - | Notification handler for errors |
|
|
377
378
|
|
|
378
379
|
### Class Props
|
|
379
380
|
|
|
@@ -422,6 +423,33 @@ A modal-based multi-select/single-select component with search functionality, ty
|
|
|
422
423
|
/>
|
|
423
424
|
```
|
|
424
425
|
|
|
426
|
+
### Ordering the selection (`ordered`)
|
|
427
|
+
|
|
428
|
+
By default the selected items are serialized to `value` in alphabetical order. For
|
|
429
|
+
relations where the order matters, opt in with `ordered` (multi-select only). This adds a
|
|
430
|
+
`Pick | Arrange` tab header inside the modal. The **Arrange** screen shows the current
|
|
431
|
+
selection as a flat list where each row has **Move up / down** (and, on wider screens,
|
|
432
|
+
**Move to top / bottom**) plus **Remove** buttons — buttons only, no drag — and offers
|
|
433
|
+
**Sort A–Z** / **Reverse** shortcuts. The order you set is what gets serialized to `value`
|
|
434
|
+
on submit (and round-trips on reopen). Single-select fields ignore the prop.
|
|
435
|
+
|
|
436
|
+
```svelte
|
|
437
|
+
<FieldOptions
|
|
438
|
+
label="Steps (in order)"
|
|
439
|
+
name="steps"
|
|
440
|
+
bind:value
|
|
441
|
+
{getOptions}
|
|
442
|
+
cardinality={-1}
|
|
443
|
+
ordered
|
|
444
|
+
/>
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
> Note: with `ordered`, **Select all** appends in the options' (alphabetical) order as a
|
|
448
|
+
> starting point — use the per-row buttons or the Sort/Reverse shortcuts to arrange from
|
|
449
|
+
> there. The `value` must hold full item objects (with their label), which is already the
|
|
450
|
+
> default contract, so the Arrange list can render selected items even when they aren't in
|
|
451
|
+
> the current search results.
|
|
452
|
+
|
|
425
453
|
### Customization Examples
|
|
426
454
|
|
|
427
455
|
```css
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
label?: SnippetWithId | THC;
|
|
17
17
|
labelAfter?: SnippetWithId | THC;
|
|
18
18
|
inputBefore?: SnippetWithId | THC;
|
|
19
|
+
/** Rendered inside the input card, above the input row (e.g. a tab header) */
|
|
20
|
+
inputAbove?: SnippetWithId | THC;
|
|
19
21
|
children: Snippet;
|
|
20
22
|
inputAfter?: SnippetWithId | THC;
|
|
21
23
|
inputBelow?: SnippetWithId | THC;
|
|
@@ -40,6 +42,7 @@
|
|
|
40
42
|
label,
|
|
41
43
|
labelAfter,
|
|
42
44
|
inputBefore,
|
|
45
|
+
inputAbove,
|
|
43
46
|
children,
|
|
44
47
|
inputAfter,
|
|
45
48
|
inputBelow,
|
|
@@ -144,6 +147,7 @@
|
|
|
144
147
|
invalid && classInputBoxWrapInvalid
|
|
145
148
|
)}
|
|
146
149
|
>
|
|
150
|
+
{@render snippetOrThc({ id, value: inputAbove })}
|
|
147
151
|
<div class="flex">
|
|
148
152
|
{@render snippetOrThc({ id, value: inputBefore })}
|
|
149
153
|
{@render children()}
|
|
@@ -12,6 +12,8 @@ interface Props extends InputWrapClassProps {
|
|
|
12
12
|
label?: SnippetWithId | THC;
|
|
13
13
|
labelAfter?: SnippetWithId | THC;
|
|
14
14
|
inputBefore?: SnippetWithId | THC;
|
|
15
|
+
/** Rendered inside the input card, above the input row (e.g. a tab header) */
|
|
16
|
+
inputAbove?: SnippetWithId | THC;
|
|
15
17
|
children: Snippet;
|
|
16
18
|
inputAfter?: SnippetWithId | THC;
|
|
17
19
|
inputBelow?: SnippetWithId | THC;
|
package/dist/icons/index.d.ts
CHANGED
|
@@ -17,9 +17,11 @@ export { iconLucideOctagonX as iconAlertError } from "@marianmeres/icons-fns/luc
|
|
|
17
17
|
export { iconLucideRefreshCw as iconRefresh } from "@marianmeres/icons-fns/lucide/iconLucideRefreshCw.js";
|
|
18
18
|
export { iconLucideTriangleAlert as iconAlertWarning } from "@marianmeres/icons-fns/lucide/iconLucideTriangleAlert.js";
|
|
19
19
|
export { iconLucideArrowDown as iconArrowDown } from "@marianmeres/icons-fns/lucide/iconLucideArrowDown.js";
|
|
20
|
+
export { iconLucideArrowDownToLine as iconArrowDownToLine } from "@marianmeres/icons-fns/lucide/iconLucideArrowDownToLine.js";
|
|
20
21
|
export { iconLucideArrowLeft as iconArrowLeft } from "@marianmeres/icons-fns/lucide/iconLucideArrowLeft.js";
|
|
21
22
|
export { iconLucideArrowRight as iconArrowRight } from "@marianmeres/icons-fns/lucide/iconLucideArrowRight.js";
|
|
22
23
|
export { iconLucideArrowUp as iconArrowUp } from "@marianmeres/icons-fns/lucide/iconLucideArrowUp.js";
|
|
24
|
+
export { iconLucideArrowUpToLine as iconArrowUpToLine } from "@marianmeres/icons-fns/lucide/iconLucideArrowUpToLine.js";
|
|
23
25
|
export { iconLucideDownload as iconDownload } from "@marianmeres/icons-fns/lucide/iconLucideDownload.js";
|
|
24
26
|
export { iconLucideMinus as iconMinus } from "@marianmeres/icons-fns/lucide/iconLucideMinus.js";
|
|
25
27
|
export { iconLucidePlus as iconPlus } from "@marianmeres/icons-fns/lucide/iconLucidePlus.js";
|
package/dist/icons/index.js
CHANGED
|
@@ -21,9 +21,11 @@ export { iconLucideRefreshCw as iconRefresh } from "@marianmeres/icons-fns/lucid
|
|
|
21
21
|
export { iconLucideTriangleAlert as iconAlertWarning } from "@marianmeres/icons-fns/lucide/iconLucideTriangleAlert.js";
|
|
22
22
|
// Action Icons (Lucide)
|
|
23
23
|
export { iconLucideArrowDown as iconArrowDown } from "@marianmeres/icons-fns/lucide/iconLucideArrowDown.js";
|
|
24
|
+
export { iconLucideArrowDownToLine as iconArrowDownToLine } from "@marianmeres/icons-fns/lucide/iconLucideArrowDownToLine.js";
|
|
24
25
|
export { iconLucideArrowLeft as iconArrowLeft } from "@marianmeres/icons-fns/lucide/iconLucideArrowLeft.js";
|
|
25
26
|
export { iconLucideArrowRight as iconArrowRight } from "@marianmeres/icons-fns/lucide/iconLucideArrowRight.js";
|
|
26
27
|
export { iconLucideArrowUp as iconArrowUp } from "@marianmeres/icons-fns/lucide/iconLucideArrowUp.js";
|
|
28
|
+
export { iconLucideArrowUpToLine as iconArrowUpToLine } from "@marianmeres/icons-fns/lucide/iconLucideArrowUpToLine.js";
|
|
27
29
|
export { iconLucideDownload as iconDownload } from "@marianmeres/icons-fns/lucide/iconLucideDownload.js";
|
|
28
30
|
export { iconLucideMinus as iconMinus } from "@marianmeres/icons-fns/lucide/iconLucideMinus.js";
|
|
29
31
|
export { iconLucidePlus as iconPlus } from "@marianmeres/icons-fns/lucide/iconLucidePlus.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/stuic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.131.0",
|
|
4
4
|
"packageManager": "pnpm@11.5.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -122,12 +122,12 @@
|
|
|
122
122
|
}
|
|
123
123
|
},
|
|
124
124
|
"devDependencies": {
|
|
125
|
-
"@codemirror/commands": "^6.10.
|
|
125
|
+
"@codemirror/commands": "^6.10.4",
|
|
126
126
|
"@codemirror/lang-markdown": "^6.5.0",
|
|
127
127
|
"@codemirror/language": "^6.12.3",
|
|
128
128
|
"@codemirror/language-data": "^6.5.2",
|
|
129
|
-
"@codemirror/state": "^6.
|
|
130
|
-
"@codemirror/view": "^6.43.
|
|
129
|
+
"@codemirror/state": "^6.7.0",
|
|
130
|
+
"@codemirror/view": "^6.43.2",
|
|
131
131
|
"@eslint/js": "^9.39.4",
|
|
132
132
|
"@marianmeres/random-human-readable": "^1.10.2",
|
|
133
133
|
"@milkdown/core": "^7.21.2",
|
|
@@ -140,29 +140,29 @@
|
|
|
140
140
|
"@milkdown/transformer": "^7.21.2",
|
|
141
141
|
"@milkdown/utils": "^7.21.2",
|
|
142
142
|
"@sveltejs/adapter-auto": "^4.0.0",
|
|
143
|
-
"@sveltejs/kit": "^2.
|
|
143
|
+
"@sveltejs/kit": "^2.68.0",
|
|
144
144
|
"@sveltejs/package": "^2.5.8",
|
|
145
145
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
146
146
|
"@tailwindcss/cli": "^4.3.1",
|
|
147
147
|
"@tailwindcss/forms": "^0.5.11",
|
|
148
148
|
"@tailwindcss/typography": "^0.5.20",
|
|
149
149
|
"@tailwindcss/vite": "^4.3.1",
|
|
150
|
-
"@types/node": "^25.9.
|
|
150
|
+
"@types/node": "^25.9.4",
|
|
151
151
|
"@vitest/browser-playwright": "^4.1.9",
|
|
152
152
|
"dotenv": "^16.6.1",
|
|
153
153
|
"eslint": "^9.39.4",
|
|
154
154
|
"globals": "^16.5.0",
|
|
155
|
-
"playwright": "^1.61.
|
|
155
|
+
"playwright": "^1.61.1",
|
|
156
156
|
"prettier": "^3.8.4",
|
|
157
157
|
"prettier-plugin-svelte": "^3.5.2",
|
|
158
158
|
"publint": "^0.3.21",
|
|
159
|
-
"svelte": "^5.56.
|
|
160
|
-
"svelte-check": "^4.
|
|
159
|
+
"svelte": "^5.56.4",
|
|
160
|
+
"svelte-check": "^4.7.1",
|
|
161
161
|
"tailwindcss": "^4.3.1",
|
|
162
162
|
"tsx": "^4.22.4",
|
|
163
163
|
"typescript": "^5.9.3",
|
|
164
|
-
"typescript-eslint": "^8.
|
|
165
|
-
"vite": "^7.3.
|
|
164
|
+
"typescript-eslint": "^8.62.0",
|
|
165
|
+
"vite": "^7.3.6",
|
|
166
166
|
"vitest": "^4.1.9",
|
|
167
167
|
"vitest-browser-svelte": "^2.1.1"
|
|
168
168
|
},
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
"@marianmeres/parse-boolean": "^2.1.0",
|
|
178
178
|
"@marianmeres/ticker": "^1.17.1",
|
|
179
179
|
"@marianmeres/tree": "^2.3.0",
|
|
180
|
-
"libphonenumber-js": "^1.13.
|
|
180
|
+
"libphonenumber-js": "^1.13.7",
|
|
181
181
|
"runed": "^0.23.4",
|
|
182
182
|
"tailwind-merge": "^3.6.0"
|
|
183
183
|
}
|