@marianmeres/stuic 2.0.0-next.4 → 2.0.0-next.5
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/Button/Button.svelte +1 -1
- package/dist/components/Button/Button.svelte.d.ts +1 -1
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +135 -0
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte.d.ts +25 -0
- package/dist/components/ButtonGroupRadio/index.css +14 -0
- package/dist/components/Input/FieldOptions.svelte +49 -35
- package/package.json +10 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const BUTTON_STUIC_BASE_CLASSES = "\n\t\tbg-button-bg text-button-text \n\t\tdark:bg-button-bg-dark dark:text-button-text-dark\n\t\tfont-mono text-sm text-center \n\t\tleading-
|
|
1
|
+
export declare const BUTTON_STUIC_BASE_CLASSES = "\n\t\tbg-button-bg text-button-text \n\t\tdark:bg-button-bg-dark dark:text-button-text-dark\n\t\tfont-mono text-sm text-center \n\t\tleading-none\n\t\tborder-1\n\t\tborder-button-border dark:border-button-border-dark\n\t\trounded-md\n\t\tinline-flex items-center justify-center gap-x-2\n\t\tpx-3 py-1.5\n\n\t\thover:brightness-[1.05]\n\t\tactive:brightness-[0.95]\n\t\tdisabled:hover:brightness-100\n\n\t\tfocus:brightness-[1.05] \n\t\tfocus:border-button-border-focus focus:dark:border-button-border-focus-dark\n\n\t\t focus:outline-4 focus:outline-black/10 focus:dark:outline-white/20\n\t\tfocus-visible:outline-4 focus-visible:outline-black/10 focus-visible:dark:outline-white/20\n\t";
|
|
2
2
|
export declare const BUTTON_STUIC_PRESET_CLASSES: any;
|
|
3
3
|
import type { Snippet } from "svelte";
|
|
4
4
|
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ItemCollection, type Item } from "@marianmeres/item-collection";
|
|
3
|
+
import { getId } from "../../utils/get-id.js";
|
|
4
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
5
|
+
import type { FieldRadiosOption } from "../Input/types.js";
|
|
6
|
+
import Button from "../Button/Button.svelte";
|
|
7
|
+
//
|
|
8
|
+
import "./index.css";
|
|
9
|
+
|
|
10
|
+
interface ItemColl extends ItemCollection<{ id: string; option: FieldRadiosOption }> {}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
value?: string;
|
|
14
|
+
tabindex?: number; // tooShort
|
|
15
|
+
size?: "sm" | "md" | "lg" | string;
|
|
16
|
+
//
|
|
17
|
+
options: (string | FieldRadiosOption)[];
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
activeIndex?: number | undefined;
|
|
20
|
+
//
|
|
21
|
+
class?: string;
|
|
22
|
+
classButton?: string;
|
|
23
|
+
classButtonActive?: string;
|
|
24
|
+
style?: string;
|
|
25
|
+
// for side-effects, or validation... if would return explicit false, will not activate
|
|
26
|
+
onButtonClick?: (
|
|
27
|
+
index: number,
|
|
28
|
+
coll: ItemColl
|
|
29
|
+
) => Promise<boolean | undefined | void> | boolean | undefined | void;
|
|
30
|
+
buttonProps?: (index: number, coll: ItemColl) => undefined | Record<string, any>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
options,
|
|
35
|
+
value = $bindable(),
|
|
36
|
+
tabindex = 0,
|
|
37
|
+
disabled,
|
|
38
|
+
size = "md",
|
|
39
|
+
//
|
|
40
|
+
activeIndex = $bindable(undefined),
|
|
41
|
+
//
|
|
42
|
+
class: classProp,
|
|
43
|
+
classButton,
|
|
44
|
+
classButtonActive,
|
|
45
|
+
style,
|
|
46
|
+
onButtonClick,
|
|
47
|
+
buttonProps,
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
const coll: ItemColl = $derived.by(() => {
|
|
51
|
+
const out = new ItemCollection(
|
|
52
|
+
options.map((o, i) => {
|
|
53
|
+
// normalize string to FieldRadiosOption
|
|
54
|
+
if (typeof o === "string") o = { label: o };
|
|
55
|
+
// normalize FieldRadiosOption to ItemCollection's Item
|
|
56
|
+
return { id: `opt-${i}-${Math.random().toString(36).slice(2, 8)}`, option: o };
|
|
57
|
+
}),
|
|
58
|
+
{}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (activeIndex !== undefined) out.setActiveIndex(activeIndex);
|
|
62
|
+
|
|
63
|
+
return out;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
$effect(() => {
|
|
67
|
+
return coll.subscribe((c) => {
|
|
68
|
+
value = c.active?.option.value ?? c.active?.option.label;
|
|
69
|
+
activeIndex = c.activeIndex;
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
//
|
|
74
|
+
const CLS = `
|
|
75
|
+
p-1.5 rounded-lg inline-block space-x-2
|
|
76
|
+
bg-button-group-bg text-button-group-text
|
|
77
|
+
dark:bg-button-group-bg-dark dark:text-button-group-text-dark
|
|
78
|
+
border-1
|
|
79
|
+
border-button-group-border dark:border-button-group-border-dark
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
// we need some active indication by default... use just something subtle here, in the wild
|
|
83
|
+
// this will be styled with classButtonActive
|
|
84
|
+
const CLS_ACTIVE = `
|
|
85
|
+
shadow-[0px_0px_1px_1px_rgba(0_0_0_/_.6)]
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
let els = $state<Record<number, HTMLButtonElement>>({});
|
|
89
|
+
|
|
90
|
+
async function maybe_activate(index: number, coll: ItemColl) {
|
|
91
|
+
if ((await onButtonClick?.(index, coll)) !== false) {
|
|
92
|
+
coll.setActiveIndex(index);
|
|
93
|
+
els[index].focus();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
{#if coll.size}
|
|
99
|
+
<div
|
|
100
|
+
class={twMerge(CLS, classProp)}
|
|
101
|
+
{style}
|
|
102
|
+
role="radiogroup"
|
|
103
|
+
aria-labelledby={$coll?.active?.id || ""}
|
|
104
|
+
>
|
|
105
|
+
{#each coll.items as item, i}
|
|
106
|
+
<Button
|
|
107
|
+
tabindex={i === 0 ? tabindex : undefined}
|
|
108
|
+
class={twMerge(
|
|
109
|
+
"border-none shadow-none",
|
|
110
|
+
classButton,
|
|
111
|
+
$coll.activeIndex === i && [CLS_ACTIVE, classButtonActive].join(" ")
|
|
112
|
+
)}
|
|
113
|
+
{size}
|
|
114
|
+
role="radio"
|
|
115
|
+
aria-checked={$coll.activeIndex === i}
|
|
116
|
+
onclick={async () => {
|
|
117
|
+
await maybe_activate(i, coll);
|
|
118
|
+
}}
|
|
119
|
+
bind:el={els[i]}
|
|
120
|
+
onkeydown={async (e) => {
|
|
121
|
+
if (e.key === "ArrowRight") {
|
|
122
|
+
await maybe_activate(Math.min(i + 1, coll.size - 1), coll);
|
|
123
|
+
}
|
|
124
|
+
if (e.key === "ArrowLeft") {
|
|
125
|
+
await maybe_activate(Math.max(0, i - 1), coll);
|
|
126
|
+
}
|
|
127
|
+
}}
|
|
128
|
+
id={item.id}
|
|
129
|
+
{...buttonProps?.(i, coll) || {}}
|
|
130
|
+
>
|
|
131
|
+
{item.option.label}
|
|
132
|
+
</Button>
|
|
133
|
+
{/each}
|
|
134
|
+
</div>
|
|
135
|
+
{/if}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ItemCollection } from "@marianmeres/item-collection";
|
|
2
|
+
import type { FieldRadiosOption } from "../Input/types.js";
|
|
3
|
+
import "./index.css";
|
|
4
|
+
interface ItemColl extends ItemCollection<{
|
|
5
|
+
id: string;
|
|
6
|
+
option: FieldRadiosOption;
|
|
7
|
+
}> {
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
value?: string;
|
|
11
|
+
tabindex?: number;
|
|
12
|
+
size?: "sm" | "md" | "lg" | string;
|
|
13
|
+
options: (string | FieldRadiosOption)[];
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
activeIndex?: number | undefined;
|
|
16
|
+
class?: string;
|
|
17
|
+
classButton?: string;
|
|
18
|
+
classButtonActive?: string;
|
|
19
|
+
style?: string;
|
|
20
|
+
onButtonClick?: (index: number, coll: ItemColl) => Promise<boolean | undefined | void> | boolean | undefined | void;
|
|
21
|
+
buttonProps?: (index: number, coll: ItemColl) => undefined | Record<string, any>;
|
|
22
|
+
}
|
|
23
|
+
declare const ButtonGroupRadio: import("svelte").Component<Props, {}, "value" | "activeIndex">;
|
|
24
|
+
type ButtonGroupRadio = ReturnType<typeof ButtonGroupRadio>;
|
|
25
|
+
export default ButtonGroupRadio;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@import "../../_shared.css";
|
|
2
|
+
@plugin '@tailwindcss/forms';
|
|
3
|
+
|
|
4
|
+
/* prettier-ignore */
|
|
5
|
+
@theme inline {
|
|
6
|
+
--color-button-group-bg: var(--color-button-group-bg, var(--color-neutral-100));
|
|
7
|
+
--color-button-group-bg-dark: var(--color-button-group-bg-dark, var(--color-neutral-600));
|
|
8
|
+
|
|
9
|
+
--color-button-group-text: var(--color-button-group-text, var(--color-black));
|
|
10
|
+
--color-button-group-text-dark: var(--color-button-group-text-dark, var(--color-white));
|
|
11
|
+
|
|
12
|
+
--color-button-group-border: var(--color-button-group-border, var(--color-neutral-300));
|
|
13
|
+
--color-button-group-border-dark: var(--color-button-group-border-dark, var(--color-neutral-800));
|
|
14
|
+
}
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
submit: "Submit",
|
|
14
14
|
select_all: "Select all",
|
|
15
15
|
clear_all: "Clear all",
|
|
16
|
+
clear: "Clear",
|
|
16
17
|
search_placeholder: "Type to search...",
|
|
18
|
+
search_submit_placeholder: "Type to search and/or submit...",
|
|
17
19
|
cardinality_full: "Max selection reached",
|
|
18
20
|
select_from_list: "Please select from the list",
|
|
19
21
|
x_close: "Clear input or close [esc]",
|
|
@@ -222,11 +224,17 @@
|
|
|
222
224
|
|
|
223
225
|
// second, the selected ones
|
|
224
226
|
const _selectedColl = new ItemCollection([], {
|
|
227
|
+
// svelte-ignore state_referenced_locally
|
|
225
228
|
cardinality,
|
|
226
229
|
sortFn,
|
|
227
230
|
idPropName: itemIdPropName,
|
|
228
231
|
});
|
|
229
232
|
|
|
233
|
+
// reconfigure if the prop ever changes during runtime (most likely will NOT)
|
|
234
|
+
$effect(() => {
|
|
235
|
+
_selectedColl.configure({ cardinality });
|
|
236
|
+
});
|
|
237
|
+
|
|
230
238
|
// now, create the reactive, subscribed variants
|
|
231
239
|
let options = $derived($_optionsColl);
|
|
232
240
|
let selected = $derived($_selectedColl);
|
|
@@ -292,7 +300,41 @@
|
|
|
292
300
|
return prefix + strHash(`${id}`.repeat(3));
|
|
293
301
|
}
|
|
294
302
|
|
|
295
|
-
//
|
|
303
|
+
// "inner" submit
|
|
304
|
+
function try_submit(force = false) {
|
|
305
|
+
if (innerValue) {
|
|
306
|
+
// doing label search, taking first result
|
|
307
|
+
let found = _optionsColl.search(innerValue)?.[0];
|
|
308
|
+
if (!found) {
|
|
309
|
+
if (!allowUnknown) {
|
|
310
|
+
return notifications?.error(t("select_from_list"), { ttl: 1000 });
|
|
311
|
+
}
|
|
312
|
+
found = { [itemIdPropName]: innerValue };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!isMultiple) _selectedColl.clear();
|
|
316
|
+
|
|
317
|
+
// actual selection addon
|
|
318
|
+
_selectedColl.add(found);
|
|
319
|
+
|
|
320
|
+
// we might have added a new one, so add it to options as well
|
|
321
|
+
// (will be noop if already exists)...
|
|
322
|
+
if (allowUnknown) {
|
|
323
|
+
_optionsColl.add(found);
|
|
324
|
+
_optionsColl.setActive(found);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// maybe submit
|
|
328
|
+
if (_selectedColl.isFull || force) submit();
|
|
329
|
+
}
|
|
330
|
+
// enter on empty input always submits
|
|
331
|
+
else {
|
|
332
|
+
submit();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// "outer" submit - will set the outer bound value (always string) and close modal...
|
|
337
|
+
// further process is left on the consumer
|
|
296
338
|
function submit() {
|
|
297
339
|
// clog("modal submit", $state.snapshot(selected.items));
|
|
298
340
|
value = JSON.stringify(selected.items);
|
|
@@ -413,40 +455,12 @@
|
|
|
413
455
|
tabindex={1}
|
|
414
456
|
{required}
|
|
415
457
|
{disabled}
|
|
416
|
-
placeholder={searchPlaceholder ??
|
|
458
|
+
placeholder={searchPlaceholder ??
|
|
459
|
+
t(allowUnknown ? "search_submit_placeholder" : "search_placeholder")}
|
|
417
460
|
onkeydown={(e) => {
|
|
418
461
|
if (e.key === "Enter") {
|
|
419
462
|
e.preventDefault();
|
|
420
|
-
|
|
421
|
-
if (innerValue) {
|
|
422
|
-
// doing label search, taking first result
|
|
423
|
-
let found = _optionsColl.search(innerValue)?.[0];
|
|
424
|
-
if (!found) {
|
|
425
|
-
if (!allowUnknown) {
|
|
426
|
-
return notifications?.error(t("select_from_list"), { ttl: 1000 });
|
|
427
|
-
}
|
|
428
|
-
found = { [itemIdPropName]: innerValue };
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (!isMultiple) _selectedColl.clear();
|
|
432
|
-
|
|
433
|
-
// actual selection addon
|
|
434
|
-
_selectedColl.add(found);
|
|
435
|
-
|
|
436
|
-
// we might have added a new one, so add it to options as well
|
|
437
|
-
// (will be noop if already exists)...
|
|
438
|
-
if (allowUnknown) {
|
|
439
|
-
_optionsColl.add(found);
|
|
440
|
-
_optionsColl.setActive(found);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// maybe submit
|
|
444
|
-
if (_selectedColl.isFull) submit();
|
|
445
|
-
}
|
|
446
|
-
// enter on empty input always submits
|
|
447
|
-
else {
|
|
448
|
-
submit();
|
|
449
|
-
}
|
|
463
|
+
try_submit();
|
|
450
464
|
}
|
|
451
465
|
}}
|
|
452
466
|
autocomplete="off"
|
|
@@ -484,7 +498,7 @@
|
|
|
484
498
|
tabindex={5}
|
|
485
499
|
disabled={!selected.items.length}
|
|
486
500
|
>
|
|
487
|
-
{@html t("clear_all")}
|
|
501
|
+
{@html t(cardinality === 1 ? "clear" : "clear_all")}
|
|
488
502
|
</button>
|
|
489
503
|
|
|
490
504
|
<span class="p-1 m-1 text-xs"> </span>
|
|
@@ -585,9 +599,9 @@
|
|
|
585
599
|
class="control"
|
|
586
600
|
type="button"
|
|
587
601
|
variant="primary"
|
|
588
|
-
onclick={(e) => {
|
|
602
|
+
onclick={async (e) => {
|
|
589
603
|
e.preventDefault();
|
|
590
|
-
|
|
604
|
+
try_submit(true);
|
|
591
605
|
}}
|
|
592
606
|
tabindex={3}
|
|
593
607
|
>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/stuic",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.5",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist",
|
|
6
6
|
"!dist/**/*.test.*",
|
|
@@ -25,32 +25,32 @@
|
|
|
25
25
|
"@marianmeres/icons-fns": "^4.3.1",
|
|
26
26
|
"@marianmeres/random-human-readable": "^1.6.1",
|
|
27
27
|
"@sveltejs/adapter-auto": "^4.0.0",
|
|
28
|
-
"@sveltejs/kit": "^2.21.
|
|
28
|
+
"@sveltejs/kit": "^2.21.5",
|
|
29
29
|
"@sveltejs/package": "^2.3.11",
|
|
30
|
-
"@sveltejs/vite-plugin-svelte": "^5.0
|
|
31
|
-
"@tailwindcss/cli": "^4.1.
|
|
30
|
+
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
|
31
|
+
"@tailwindcss/cli": "^4.1.10",
|
|
32
32
|
"@tailwindcss/forms": "^0.5.10",
|
|
33
33
|
"@tailwindcss/typography": "^0.5.16",
|
|
34
|
-
"@tailwindcss/vite": "^4.1.
|
|
34
|
+
"@tailwindcss/vite": "^4.1.10",
|
|
35
35
|
"dotenv": "^16.5.0",
|
|
36
36
|
"prettier": "^3.5.3",
|
|
37
37
|
"prettier-plugin-svelte": "^3.4.0",
|
|
38
38
|
"publint": "^0.3.12",
|
|
39
|
-
"svelte": "^5.
|
|
39
|
+
"svelte": "^5.34.3",
|
|
40
40
|
"svelte-check": "^4.2.1",
|
|
41
|
-
"tailwindcss": "^4.1.
|
|
41
|
+
"tailwindcss": "^4.1.10",
|
|
42
42
|
"typescript": "^5.8.3",
|
|
43
43
|
"vite": "^6.3.5",
|
|
44
|
-
"vitest": "^3.
|
|
44
|
+
"vitest": "^3.2.3"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@marianmeres/clog": "^2.2.3",
|
|
48
|
-
"@marianmeres/item-collection": "^1.2.
|
|
48
|
+
"@marianmeres/item-collection": "^1.2.15",
|
|
49
49
|
"@marianmeres/parse-boolean": "^1.1.7",
|
|
50
50
|
"@marianmeres/ticker": "^1.15.0",
|
|
51
51
|
"esm-env": "^1.2.2",
|
|
52
52
|
"runed": "^0.23.4",
|
|
53
|
-
"tailwind-merge": "^3.3.
|
|
53
|
+
"tailwind-merge": "^3.3.1"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"dev": "vite dev",
|