svseeds 0.0.6 → 0.1.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/README.md +6 -2
- package/_svseeds/Accordion.svelte +91 -0
- package/_svseeds/Accordion.svelte.d.ts +19 -0
- package/_svseeds/DarkToggle.svelte +58 -0
- package/_svseeds/DarkToggle.svelte.d.ts +15 -0
- package/_svseeds/TagsInput.svelte +136 -0
- package/_svseeds/TagsInput.svelte.d.ts +31 -0
- package/_svseeds/_Badge.svelte +58 -0
- package/_svseeds/_Badge.svelte.d.ts +18 -0
- package/_svseeds/_Button.svelte +64 -0
- package/_svseeds/_Button.svelte.d.ts +22 -0
- package/_svseeds/_CheckField.svelte +142 -0
- package/_svseeds/_CheckField.svelte.d.ts +26 -0
- package/_svseeds/_ColorPicker.svelte +74 -0
- package/_svseeds/_ColorPicker.svelte.d.ts +19 -0
- package/_svseeds/_ComboBox.svelte +115 -0
- package/_svseeds/_ComboBox.svelte.d.ts +19 -0
- package/_svseeds/_ContextMenu.svelte +52 -0
- package/_svseeds/_ContextMenu.svelte.d.ts +15 -0
- package/_svseeds/_Disclosure.svelte +119 -0
- package/_svseeds/_Disclosure.svelte.d.ts +20 -0
- package/_svseeds/_HotkeyCapture.svelte +94 -0
- package/_svseeds/_HotkeyCapture.svelte.d.ts +15 -0
- package/_svseeds/_Modal.svelte +67 -0
- package/_svseeds/_Modal.svelte.d.ts +17 -0
- package/_svseeds/_ProgressTracker.svelte +71 -0
- package/_svseeds/_ProgressTracker.svelte.d.ts +17 -0
- package/_svseeds/_SelectField.svelte +147 -0
- package/_svseeds/_SelectField.svelte.d.ts +27 -0
- package/_svseeds/_Slider.svelte +69 -0
- package/_svseeds/_Slider.svelte.d.ts +28 -0
- package/_svseeds/_Sortable.svelte +746 -0
- package/_svseeds/_Sortable.svelte.d.ts +62 -0
- package/_svseeds/_Tabs.svelte +91 -0
- package/_svseeds/_Tabs.svelte.d.ts +15 -0
- package/_svseeds/_TextField.svelte +84 -69
- package/_svseeds/_TextField.svelte.d.ts +12 -8
- package/_svseeds/_Toggle.svelte +104 -0
- package/_svseeds/_Toggle.svelte.d.ts +22 -0
- package/_svseeds/_TogglesField.svelte +148 -0
- package/_svseeds/_TogglesField.svelte.d.ts +26 -0
- package/_svseeds/_Tooltip.svelte +267 -0
- package/_svseeds/_Tooltip.svelte.d.ts +31 -0
- package/_svseeds/core.d.ts +7 -10
- package/_svseeds/core.js +1 -172
- package/index.d.ts +22 -1
- package/index.js +21 -0
- package/package.json +4 -2
- package/_svseeds/core.ts +0 -194
- package/_svseeds/dep.json +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SvSeeds - Simple Headless Components for Svelte
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/svseeds)
|
|
4
|
+
[](https://jsr.io/@svseeds/ui)
|
|
5
|
+
[](LICENSE.md)
|
|
6
|
+
|
|
7
|
+
SvSeeds is a collection of essential functional components for Svelte development. These components are intentionally designed with minimal styling, giving you complete freedom to apply your own styles based on your project requirements. For specialized needs, you can download the components as .svelte files through our CLI tool and customize their functionality directly.
|
|
4
8
|
|
|
5
9
|
**WIP**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type AccordionProps = {
|
|
3
|
+
labels?: string[],
|
|
4
|
+
current?: number, // bindable <-1>
|
|
5
|
+
status?: string, // bindable <STATE.DEFAULT>
|
|
6
|
+
style?: ClassRuleSet | string,
|
|
7
|
+
deps?: AccordionDeps,
|
|
8
|
+
[key: string]: unknown | Snippet,
|
|
9
|
+
};
|
|
10
|
+
export type AccordionDeps = {
|
|
11
|
+
svsDisclosure?: Omit<DisclosureProps, DisclosureReqdProps | DisclosureBindProps>,
|
|
12
|
+
};
|
|
13
|
+
export type AccordionReqdProps = never;
|
|
14
|
+
export type AccordionBindProps = "current" | "status";
|
|
15
|
+
|
|
16
|
+
type NamedId = { id: string, name: string };
|
|
17
|
+
const svs = "svs-accordion";
|
|
18
|
+
const preset: ClassRuleSet = {};
|
|
19
|
+
const roleLabel = "label";
|
|
20
|
+
const rolePanel = "panel";
|
|
21
|
+
|
|
22
|
+
function getSnippetNames(role: string, rest: Record<string, unknown>): string[] {
|
|
23
|
+
return Object.keys(rest)
|
|
24
|
+
.filter((x) => x.startsWith(role) && typeof rest[x] === "function")
|
|
25
|
+
.toSorted((x,y) => x.localeCompare(y));
|
|
26
|
+
}
|
|
27
|
+
function toNamedId(names: string[]): NamedId[] {
|
|
28
|
+
return names.map((x) => ({ id: elemId.get(true)!, name: x }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
import { type Snippet, untrack } from "svelte";
|
|
32
|
+
import { type ClassRuleSet, STATE, AREA, elemId, fnClass, omit } from "./core";
|
|
33
|
+
import Disclosure, { type DisclosureProps, type DisclosureReqdProps, type DisclosureBindProps } from "./_Disclosure.svelte";
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script lang="ts">
|
|
37
|
+
let { labels = [], current = $bindable(-1), status = $bindable(""), style, deps, ...rest }: AccordionProps = $props();
|
|
38
|
+
|
|
39
|
+
// *** Initialize *** //
|
|
40
|
+
if (!status) status = STATE.DEFAULT;
|
|
41
|
+
const cls = fnClass(svs, preset, style);
|
|
42
|
+
const propsDisclosure = omit(deps?.svsDisclosure, "attributes");
|
|
43
|
+
const isStrLabel = labels.length > 0;
|
|
44
|
+
const lbls = toNamedId(isStrLabel ? labels : getSnippetNames(roleLabel, rest));
|
|
45
|
+
const panels = getSnippetNames(rolePanel, rest);
|
|
46
|
+
const isValidAccordion = lbls.length && panels.length && lbls.length === panels.length;
|
|
47
|
+
let guard = current >= 0 && current < lbls.length;
|
|
48
|
+
let event = false;
|
|
49
|
+
let opens = $state(Array(lbls.length).fill(false));
|
|
50
|
+
let elems: HTMLDetailsElement[] = $state([]);
|
|
51
|
+
|
|
52
|
+
// *** Bind Handlers *** //
|
|
53
|
+
$effect.pre(() => {
|
|
54
|
+
current;
|
|
55
|
+
untrack(() => toggle());
|
|
56
|
+
});
|
|
57
|
+
function toggle() {
|
|
58
|
+
if (!event) opens = opens.fill(false).map((_, i) => i === current);
|
|
59
|
+
event = false;
|
|
60
|
+
}
|
|
61
|
+
function setCurrent(index: number) {
|
|
62
|
+
current = index;
|
|
63
|
+
if (guard) return guard = false;
|
|
64
|
+
event = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// *** Event Handlers *** //
|
|
68
|
+
function exclusiveToggle(index: number): (ev: Event) => void {
|
|
69
|
+
return (ev) => {
|
|
70
|
+
deps?.svsDisclosure?.attributes?.ontoggle?.(ev as any);
|
|
71
|
+
if (opens.every((x) => !x)) return setCurrent(-1);
|
|
72
|
+
if (elems[index].open) {
|
|
73
|
+
opens = opens.map((_, i) => i === index);
|
|
74
|
+
setCurrent(index);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<!---------------------------------------->
|
|
81
|
+
|
|
82
|
+
{#if isValidAccordion}
|
|
83
|
+
<div class={cls(AREA.WHOLE, status)} role="group">
|
|
84
|
+
{#each lbls as { id, name }, i (id)}
|
|
85
|
+
{@const ontoggle = exclusiveToggle(i)}
|
|
86
|
+
<Disclosure bind:open={opens[i]} bind:element={elems[i]} label={isStrLabel ? name : (rest[name] as Snippet)} attributes={{...deps?.svsDisclosure?.attributes, ontoggle}} {...propsDisclosure}>
|
|
87
|
+
{@render (rest[panels[i]] as Snippet)()}
|
|
88
|
+
</Disclosure>
|
|
89
|
+
{/each}
|
|
90
|
+
</div>
|
|
91
|
+
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type AccordionProps = {
|
|
2
|
+
labels?: string[];
|
|
3
|
+
current?: number;
|
|
4
|
+
status?: string;
|
|
5
|
+
style?: ClassRuleSet | string;
|
|
6
|
+
deps?: AccordionDeps;
|
|
7
|
+
[key: string]: unknown | Snippet;
|
|
8
|
+
};
|
|
9
|
+
export type AccordionDeps = {
|
|
10
|
+
svsDisclosure?: Omit<DisclosureProps, DisclosureReqdProps | DisclosureBindProps>;
|
|
11
|
+
};
|
|
12
|
+
export type AccordionReqdProps = never;
|
|
13
|
+
export type AccordionBindProps = "current" | "status";
|
|
14
|
+
import { type Snippet } from "svelte";
|
|
15
|
+
import { type ClassRuleSet } from "./core";
|
|
16
|
+
import { type DisclosureProps, type DisclosureReqdProps, type DisclosureBindProps } from "./_Disclosure.svelte";
|
|
17
|
+
declare const Accordion: import("svelte").Component<AccordionProps, {}, "status" | "current">;
|
|
18
|
+
type Accordion = ReturnType<typeof Accordion>;
|
|
19
|
+
export default Accordion;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type DarkToggleProps = {
|
|
3
|
+
dark?: boolean, // bindable <true>
|
|
4
|
+
status?: string, // bindable <STATE.DEFAULT>
|
|
5
|
+
element?: HTMLButtonElement, // bindable
|
|
6
|
+
deps?: DarkToggleDeps,
|
|
7
|
+
};
|
|
8
|
+
export type DarkToggleDeps = {
|
|
9
|
+
svsToggle?: Omit<ToggleProps, ToggleReqdProps | ToggleBindProps>,
|
|
10
|
+
};
|
|
11
|
+
export type DarkToggleReqdProps = never;
|
|
12
|
+
export type DarkToggleBindProps = "dark" | "status" | "element";
|
|
13
|
+
|
|
14
|
+
const ariaLabel = "Toggle theme color";
|
|
15
|
+
|
|
16
|
+
import { untrack } from "svelte";
|
|
17
|
+
import { STATE, omit, theme } from "./core";
|
|
18
|
+
import Toggle, { type ToggleProps, type ToggleReqdProps, type ToggleBindProps } from "./_Toggle.svelte";
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<script lang="ts">
|
|
22
|
+
let { dark = $bindable(false), status = $bindable(""), element = $bindable(), deps }: DarkToggleProps = $props();
|
|
23
|
+
|
|
24
|
+
// *** Initialize *** //
|
|
25
|
+
if (!status) status = STATE.DEFAULT;
|
|
26
|
+
const propsToggle = omit(deps?.svsToggle, "main");
|
|
27
|
+
|
|
28
|
+
// *** Bind Handlers *** //
|
|
29
|
+
$effect.pre(() => { dark;
|
|
30
|
+
untrack(() => theme.switch(dark ? "dark" : "light"));
|
|
31
|
+
});
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<!---------------------------------------->
|
|
35
|
+
|
|
36
|
+
<Toggle bind:value={dark} bind:status bind:element label={ariaLabel} {main} {...propsToggle} />
|
|
37
|
+
{#snippet svgDark()}
|
|
38
|
+
<svg style="width: 100%; height: 100%;" viewBox="0 0 24 24" aria-hidden="true">
|
|
39
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.2256 2.00253C9.59172 1.94346 6.93894 2.9189 4.92893 4.92891C1.02369 8.83415 1.02369 15.1658 4.92893 19.071C8.83418 22.9763 15.1658 22.9763 19.0711 19.071C21.0811 17.061 22.0565 14.4082 21.9975 11.7743C21.9796 10.9772 21.8669 10.1818 21.6595 9.40643C21.0933 9.9488 20.5078 10.4276 19.9163 10.8425C18.5649 11.7906 17.1826 12.4053 15.9301 12.6837C14.0241 13.1072 12.7156 12.7156 12 12C11.2844 11.2844 10.8928 9.97588 11.3163 8.0699C11.5947 6.81738 12.2094 5.43511 13.1575 4.08368C13.5724 3.49221 14.0512 2.90664 14.5935 2.34046C13.8182 2.13305 13.0228 2.02041 12.2256 2.00253ZM17.6569 17.6568C18.9081 16.4056 19.6582 14.8431 19.9072 13.2186C16.3611 15.2643 12.638 15.4664 10.5858 13.4142C8.53361 11.362 8.73568 7.63895 10.7814 4.09281C9.1569 4.34184 7.59434 5.09193 6.34315 6.34313C3.21895 9.46732 3.21895 14.5326 6.34315 17.6568C9.46734 20.781 14.5327 20.781 17.6569 17.6568Z" />
|
|
40
|
+
</svg>
|
|
41
|
+
{/snippet}
|
|
42
|
+
{#snippet svgLight()}
|
|
43
|
+
<svg style="width: 100%; height: 100%;" viewBox="0 0 24 24" aria-hidden="true">
|
|
44
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM12 18C15.3137 18 18 15.3137 18 12C18 8.68629 15.3137 6 12 6C8.68629 6 6 8.68629 6 12C6 15.3137 8.68629 18 12 18Z" />
|
|
45
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 0H13V4.06189C12.6724 4.02104 12.3387 4 12 4C11.6613 4 11.3276 4.02104 11 4.06189V0ZM7.0943 5.68018L4.22173 2.80761L2.80752 4.22183L5.6801 7.09441C6.09071 6.56618 6.56608 6.0908 7.0943 5.68018ZM4.06189 11H0V13H4.06189C4.02104 12.6724 4 12.3387 4 12C4 11.6613 4.02104 11.3276 4.06189 11ZM5.6801 16.9056L2.80751 19.7782L4.22173 21.1924L7.0943 18.3198C6.56608 17.9092 6.09071 17.4338 5.6801 16.9056ZM11 19.9381V24H13V19.9381C12.6724 19.979 12.3387 20 12 20C11.6613 20 11.3276 19.979 11 19.9381ZM16.9056 18.3199L19.7781 21.1924L21.1923 19.7782L18.3198 16.9057C17.9092 17.4339 17.4338 17.9093 16.9056 18.3199ZM19.9381 13H24V11H19.9381C19.979 11.3276 20 11.6613 20 12C20 12.3387 19.979 12.6724 19.9381 13ZM18.3198 7.0943L21.1923 4.22183L19.7781 2.80762L16.9056 5.6801C17.4338 6.09071 17.9092 6.56608 18.3198 7.0943Z" />
|
|
46
|
+
</svg>
|
|
47
|
+
{/snippet}
|
|
48
|
+
{#snippet main(status: string, value: boolean, element: HTMLButtonElement | undefined)}
|
|
49
|
+
{#if deps?.svsToggle?.main}
|
|
50
|
+
{@render deps.svsToggle.main(status, value, element)}
|
|
51
|
+
{:else}
|
|
52
|
+
{#if value}
|
|
53
|
+
{@render svgDark()}
|
|
54
|
+
{:else}
|
|
55
|
+
{@render svgLight()}
|
|
56
|
+
{/if}
|
|
57
|
+
{/if}
|
|
58
|
+
{/snippet}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type DarkToggleProps = {
|
|
2
|
+
dark?: boolean;
|
|
3
|
+
status?: string;
|
|
4
|
+
element?: HTMLButtonElement;
|
|
5
|
+
deps?: DarkToggleDeps;
|
|
6
|
+
};
|
|
7
|
+
export type DarkToggleDeps = {
|
|
8
|
+
svsToggle?: Omit<ToggleProps, ToggleReqdProps | ToggleBindProps>;
|
|
9
|
+
};
|
|
10
|
+
export type DarkToggleReqdProps = never;
|
|
11
|
+
export type DarkToggleBindProps = "dark" | "status" | "element";
|
|
12
|
+
import { type ToggleProps, type ToggleReqdProps, type ToggleBindProps } from "./_Toggle.svelte";
|
|
13
|
+
declare const DarkToggle: import("svelte").Component<DarkToggleProps, {}, "status" | "element" | "dark">;
|
|
14
|
+
type DarkToggle = ReturnType<typeof DarkToggle>;
|
|
15
|
+
export default DarkToggle;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type TagsInputProps = {
|
|
3
|
+
bottom?: string, // bindable
|
|
4
|
+
values?: string[], // bindable
|
|
5
|
+
type?: "left" | "right" | "bottom", // <"left">
|
|
6
|
+
confirm?: string[],
|
|
7
|
+
trim?: boolean, // <false>
|
|
8
|
+
unique?: boolean, // <true>
|
|
9
|
+
min?: TagCountValidation,
|
|
10
|
+
max?: TagCountValidation,
|
|
11
|
+
validations?: ((values: string[]) => string)[],
|
|
12
|
+
status?: string, // bindable <STATE.DEFAULT>
|
|
13
|
+
style?: ClassRuleSet | string,
|
|
14
|
+
element?: HTMLInputElement, // bindable
|
|
15
|
+
deps?: TagsInputDeps,
|
|
16
|
+
};
|
|
17
|
+
export type TagsInputDeps = {
|
|
18
|
+
svsTextField?: Omit<TextFieldProps, TextFieldReqdProps | TextFieldBindProps>,
|
|
19
|
+
svsBadge?: Omit<BadgeProps, BadgeReqdProps | BadgeBindProps | "type" | "href">,
|
|
20
|
+
};
|
|
21
|
+
export type TagsInputReqdProps = never;
|
|
22
|
+
export type TagsInputBindProps = "dark" | "status" | "element";
|
|
23
|
+
export type TagCountValidation = { value: number, message: string };
|
|
24
|
+
|
|
25
|
+
const svs = "svs-tags-input";
|
|
26
|
+
const preset: ClassRuleSet = {};
|
|
27
|
+
const CONFIRM_KEY = "Enter";
|
|
28
|
+
|
|
29
|
+
import { untrack } from "svelte";
|
|
30
|
+
import { type ClassRuleSet, STATE, AREA, fnClass, omit } from "./core";
|
|
31
|
+
import TextField, { type TextFieldProps, type TextFieldReqdProps, type TextFieldBindProps } from "./_TextField.svelte";
|
|
32
|
+
import Badge, { type BadgeProps, type BadgeReqdProps, type BadgeBindProps } from "./_Badge.svelte";
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<script lang="ts">
|
|
36
|
+
let { bottom = $bindable(), values = $bindable([]), type = "left", confirm = [], trim = false, unique = true, min, max, validations = [], status = $bindable(""), style, element = $bindable(), deps }: TagsInputProps = $props();
|
|
37
|
+
|
|
38
|
+
// *** Initialize *** //
|
|
39
|
+
if (!status) status = STATE.DEFAULT;
|
|
40
|
+
const cls = fnClass(svs, preset, style);
|
|
41
|
+
initDeps();
|
|
42
|
+
const confirmKeys = new Set([CONFIRM_KEY, ...confirm]);
|
|
43
|
+
const onkeydown = deps!.svsTextField!.attributes!.onkeydown;
|
|
44
|
+
deps!.svsTextField!.attributes!.onkeydown = confirmTag;
|
|
45
|
+
const onclick = deps!.svsBadge!.onclick;
|
|
46
|
+
const svsBadge = omit(deps!.svsBadge, "onclick", "right");
|
|
47
|
+
if (min) validations.push((values) => values.length < min.value ? min.message : "");
|
|
48
|
+
if (max) deps!.svsTextField!.validations!.push((_) => values.length >= max.value ? max.message : "");
|
|
49
|
+
let value = $state("");
|
|
50
|
+
|
|
51
|
+
function initDeps() {
|
|
52
|
+
if (!deps) deps = {};
|
|
53
|
+
if (!deps.svsTextField) deps.svsTextField = {};
|
|
54
|
+
if (!deps.svsTextField.attributes) deps.svsTextField.attributes = {};
|
|
55
|
+
if (!deps.svsTextField.validations) deps.svsTextField.validations = [];
|
|
56
|
+
if (!deps.svsBadge) deps.svsBadge = {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// *** Bind Handlers *** //
|
|
60
|
+
$effect.pre(() => {
|
|
61
|
+
values;
|
|
62
|
+
untrack(() => validate());
|
|
63
|
+
});
|
|
64
|
+
function validate() {
|
|
65
|
+
for (const v of validations) {
|
|
66
|
+
const msg = v(values);
|
|
67
|
+
if (msg) return element?.setCustomValidity(msg);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// *** Event Handlers *** //
|
|
72
|
+
const change = new Event("change", { bubbles: true, cancelable: true });
|
|
73
|
+
function confirmTag(ev: KeyboardEvent) {
|
|
74
|
+
onkeydown?.(ev as any);
|
|
75
|
+
if (!confirmKeys.has(ev.key) || ev.isComposing) return;
|
|
76
|
+
ev.preventDefault();
|
|
77
|
+
element?.dispatchEvent(change);
|
|
78
|
+
if (status === STATE.INACTIVE) return;
|
|
79
|
+
addTag();
|
|
80
|
+
}
|
|
81
|
+
function addTag() {
|
|
82
|
+
if (unique && values.includes(value)) value = "";
|
|
83
|
+
if (trim) value = value.trim();
|
|
84
|
+
if (!value) return;
|
|
85
|
+
values.push(value);
|
|
86
|
+
value = "";
|
|
87
|
+
}
|
|
88
|
+
function removeTag(index: number): (ev: Event) => void {
|
|
89
|
+
return (ev) => {
|
|
90
|
+
onclick?.(ev as any);
|
|
91
|
+
values.splice(index, 1);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<!---------------------------------------->
|
|
97
|
+
|
|
98
|
+
<TextField bind:value bind:status bind:bottom bind:element type="text" {...deps?.svsTextField} {left} {right} />
|
|
99
|
+
{#if type === "bottom"}
|
|
100
|
+
<div class={cls(AREA.BOTTOM, status)}>
|
|
101
|
+
{@render tags()}
|
|
102
|
+
</div>
|
|
103
|
+
{/if}
|
|
104
|
+
|
|
105
|
+
{#snippet left(status: string)}
|
|
106
|
+
{#if type === "left"}
|
|
107
|
+
{@render tags()}
|
|
108
|
+
{:else}
|
|
109
|
+
{#if deps?.svsTextField?.left}
|
|
110
|
+
{@render deps.svsTextField.left(status, "", element)}
|
|
111
|
+
{/if}
|
|
112
|
+
{/if}
|
|
113
|
+
{/snippet}
|
|
114
|
+
{#snippet right(status: string)}
|
|
115
|
+
{#if type === "right"}
|
|
116
|
+
{@render tags()}
|
|
117
|
+
{:else}
|
|
118
|
+
{#if deps?.svsTextField?.right}
|
|
119
|
+
{@render deps.svsTextField.right(status, "", element)}
|
|
120
|
+
{/if}
|
|
121
|
+
{/if}
|
|
122
|
+
{/snippet}
|
|
123
|
+
{#snippet tags()}
|
|
124
|
+
{#each values as value, i}
|
|
125
|
+
<Badge type="right" onclick={removeTag(i)} {...svsBadge}>
|
|
126
|
+
{#snippet right(status)}
|
|
127
|
+
{#if deps?.svsBadge?.right}
|
|
128
|
+
{@render deps.svsBadge.right(status)}
|
|
129
|
+
{:else}
|
|
130
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="min-width: 5px; min-height: 5px;"><path d="M511.998 70.682 441.315 0 256.002 185.313 70.685 0 .002 70.692l185.314 185.314L.002 441.318 70.69 512l185.312-185.312L441.315 512l70.683-70.682-185.314-185.312z" /></svg>
|
|
131
|
+
{/if}
|
|
132
|
+
{/snippet}
|
|
133
|
+
{value}
|
|
134
|
+
</Badge>
|
|
135
|
+
{/each}
|
|
136
|
+
{/snippet}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type TagsInputProps = {
|
|
2
|
+
bottom?: string;
|
|
3
|
+
values?: string[];
|
|
4
|
+
type?: "left" | "right" | "bottom";
|
|
5
|
+
confirm?: string[];
|
|
6
|
+
trim?: boolean;
|
|
7
|
+
unique?: boolean;
|
|
8
|
+
min?: TagCountValidation;
|
|
9
|
+
max?: TagCountValidation;
|
|
10
|
+
validations?: ((values: string[]) => string)[];
|
|
11
|
+
status?: string;
|
|
12
|
+
style?: ClassRuleSet | string;
|
|
13
|
+
element?: HTMLInputElement;
|
|
14
|
+
deps?: TagsInputDeps;
|
|
15
|
+
};
|
|
16
|
+
export type TagsInputDeps = {
|
|
17
|
+
svsTextField?: Omit<TextFieldProps, TextFieldReqdProps | TextFieldBindProps>;
|
|
18
|
+
svsBadge?: Omit<BadgeProps, BadgeReqdProps | BadgeBindProps | "type" | "href">;
|
|
19
|
+
};
|
|
20
|
+
export type TagsInputReqdProps = never;
|
|
21
|
+
export type TagsInputBindProps = "dark" | "status" | "element";
|
|
22
|
+
export type TagCountValidation = {
|
|
23
|
+
value: number;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
import { type ClassRuleSet } from "./core";
|
|
27
|
+
import { type TextFieldProps, type TextFieldReqdProps, type TextFieldBindProps } from "./_TextField.svelte";
|
|
28
|
+
import { type BadgeProps, type BadgeReqdProps, type BadgeBindProps } from "./_Badge.svelte";
|
|
29
|
+
declare const TagsInput: import("svelte").Component<TagsInputProps, {}, "bottom" | "status" | "element" | "values">;
|
|
30
|
+
type TagsInput = ReturnType<typeof TagsInput>;
|
|
31
|
+
export default TagsInput;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type BadgeProps = {
|
|
3
|
+
children: Snippet,
|
|
4
|
+
left?: Snippet<[string]>, // Snippet<[status]>
|
|
5
|
+
right?: Snippet<[string]>, // Snippet<[status]>
|
|
6
|
+
type?: "plain" | "link" | "left" | "right", // <"plain">
|
|
7
|
+
href?: string,
|
|
8
|
+
onclick?: MouseEventHandler<HTMLButtonElement> | null,
|
|
9
|
+
status?: string, // bindable <STATE.DEFAULT>
|
|
10
|
+
style?: ClassRuleSet | string,
|
|
11
|
+
};
|
|
12
|
+
export type BadgeReqdProps = "children";
|
|
13
|
+
export type BadgeBindProps = "status";
|
|
14
|
+
|
|
15
|
+
const svs = "svs-badge";
|
|
16
|
+
const preset: ClassRuleSet = {};
|
|
17
|
+
|
|
18
|
+
import { type Snippet } from "svelte";
|
|
19
|
+
import { type MouseEventHandler } from "svelte/elements";
|
|
20
|
+
import { type ClassRuleSet, STATE, AREA, fnClass } from "./core";
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<script lang="ts">
|
|
24
|
+
let { children, left, right, type = "plain", href = "", onclick, status = $bindable(""), style }: BadgeProps = $props();
|
|
25
|
+
|
|
26
|
+
// *** Initialize *** //
|
|
27
|
+
if (!status) status = STATE.DEFAULT;
|
|
28
|
+
const cls = fnClass(svs, preset, style);
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<!---------------------------------------->
|
|
32
|
+
|
|
33
|
+
{#if type === "link"}
|
|
34
|
+
<a class={cls(AREA.WHOLE, status)} {href}>
|
|
35
|
+
{@render whole()}
|
|
36
|
+
</a>
|
|
37
|
+
{:else}
|
|
38
|
+
<span class={cls(AREA.WHOLE, status)}>
|
|
39
|
+
{@render whole()}
|
|
40
|
+
</span>
|
|
41
|
+
{/if}
|
|
42
|
+
|
|
43
|
+
{#snippet side(area: string, btn: boolean, body?: Snippet<[string]>)}
|
|
44
|
+
{#if body}
|
|
45
|
+
{#if btn}
|
|
46
|
+
<button class={cls(area, status)} {onclick}>{@render body(status)}</button>
|
|
47
|
+
{:else}
|
|
48
|
+
<span class={cls(area, status)}>{@render body(status)}</span>
|
|
49
|
+
{/if}
|
|
50
|
+
{/if}
|
|
51
|
+
{/snippet}
|
|
52
|
+
{#snippet whole()}
|
|
53
|
+
{@render side(AREA.LEFT, type === "left", left)}
|
|
54
|
+
<span class={cls(AREA.MAIN, status)}>
|
|
55
|
+
{@render children()}
|
|
56
|
+
</span>
|
|
57
|
+
{@render side(AREA.RIGHT, type === "right", right)}
|
|
58
|
+
{/snippet}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type BadgeProps = {
|
|
2
|
+
children: Snippet;
|
|
3
|
+
left?: Snippet<[string]>;
|
|
4
|
+
right?: Snippet<[string]>;
|
|
5
|
+
type?: "plain" | "link" | "left" | "right";
|
|
6
|
+
href?: string;
|
|
7
|
+
onclick?: MouseEventHandler<HTMLButtonElement> | null;
|
|
8
|
+
status?: string;
|
|
9
|
+
style?: ClassRuleSet | string;
|
|
10
|
+
};
|
|
11
|
+
export type BadgeReqdProps = "children";
|
|
12
|
+
export type BadgeBindProps = "status";
|
|
13
|
+
import { type Snippet } from "svelte";
|
|
14
|
+
import { type MouseEventHandler } from "svelte/elements";
|
|
15
|
+
import { type ClassRuleSet } from "./core";
|
|
16
|
+
declare const Badge: import("svelte").Component<BadgeProps, {}, "status">;
|
|
17
|
+
type Badge = ReturnType<typeof Badge>;
|
|
18
|
+
export default Badge;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type ButtonProps = {
|
|
3
|
+
children: Snippet,
|
|
4
|
+
left?: Snippet<[string, HTMLButtonElement | undefined]>, // Snippet<[status,element]>
|
|
5
|
+
right?: Snippet<[string, HTMLButtonElement | undefined]>, // Snippet<[status,element]>
|
|
6
|
+
type?: "submit" | "reset" | "button", // <"button">
|
|
7
|
+
onclick?: MouseEventHandler<HTMLButtonElement> | null,
|
|
8
|
+
form?: HTMLFormElement, // bindable
|
|
9
|
+
status?: string, // bindable <STATE.DEFAULT>
|
|
10
|
+
style?: ClassRuleSet | string,
|
|
11
|
+
attributes?: HTMLButtonAttributes,
|
|
12
|
+
action?: Action,
|
|
13
|
+
element?: HTMLButtonElement, // bindable
|
|
14
|
+
};
|
|
15
|
+
export type ButtonReqdProps = "children";
|
|
16
|
+
export type ButtonBindProps = "form" | "status" | "element";
|
|
17
|
+
|
|
18
|
+
const svs = "svs-button";
|
|
19
|
+
const preset: ClassRuleSet = {};
|
|
20
|
+
|
|
21
|
+
import { type Snippet } from "svelte";
|
|
22
|
+
import { type Action } from "svelte/action";
|
|
23
|
+
import { type HTMLButtonAttributes, type MouseEventHandler } from "svelte/elements";
|
|
24
|
+
import { type ClassRuleSet, STATE, AREA, fnClass, omit } from "./core";
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<script lang="ts">
|
|
28
|
+
let { children, left, right, type = "button", onclick, form = $bindable(), status = $bindable(STATE.DEFAULT), style, attributes, action, element = $bindable()}: ButtonProps = $props();
|
|
29
|
+
|
|
30
|
+
// *** Initialize *** //
|
|
31
|
+
const cls = fnClass(svs, preset, style);
|
|
32
|
+
const click = onclick ?? attributes?.["onclick"];
|
|
33
|
+
const attrs = omit(attributes, "class", "type", "onclick");
|
|
34
|
+
|
|
35
|
+
// *** Event Handlers *** //
|
|
36
|
+
onclick = (ev) => {
|
|
37
|
+
if (form?.checkValidity() ?? true) click?.(ev);
|
|
38
|
+
};
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<!---------------------------------------->
|
|
42
|
+
|
|
43
|
+
{#if action}
|
|
44
|
+
<button bind:this={element} class={cls(AREA.WHOLE, status)} {type} {onclick} {...attrs} use:action>
|
|
45
|
+
{@render whole()}
|
|
46
|
+
</button>
|
|
47
|
+
{:else}
|
|
48
|
+
<button bind:this={element} class={cls(AREA.WHOLE, status)} {type} {onclick} {...attrs}>
|
|
49
|
+
{@render whole()}
|
|
50
|
+
</button>
|
|
51
|
+
{/if}
|
|
52
|
+
|
|
53
|
+
{#snippet side(area: string, body?: Snippet<[string, HTMLButtonElement | undefined]>)}
|
|
54
|
+
{#if body}
|
|
55
|
+
<span class={cls(area, status)}>{@render body(status, element)}</span>
|
|
56
|
+
{/if}
|
|
57
|
+
{/snippet}
|
|
58
|
+
{#snippet whole()}
|
|
59
|
+
{@render side(AREA.LEFT, left)}
|
|
60
|
+
<span class={cls(AREA.MAIN, status)}>
|
|
61
|
+
{@render children()}
|
|
62
|
+
</span>
|
|
63
|
+
{@render side(AREA.RIGHT, right)}
|
|
64
|
+
{/snippet}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type ButtonProps = {
|
|
2
|
+
children: Snippet;
|
|
3
|
+
left?: Snippet<[string, HTMLButtonElement | undefined]>;
|
|
4
|
+
right?: Snippet<[string, HTMLButtonElement | undefined]>;
|
|
5
|
+
type?: "submit" | "reset" | "button";
|
|
6
|
+
onclick?: MouseEventHandler<HTMLButtonElement> | null;
|
|
7
|
+
form?: HTMLFormElement;
|
|
8
|
+
status?: string;
|
|
9
|
+
style?: ClassRuleSet | string;
|
|
10
|
+
attributes?: HTMLButtonAttributes;
|
|
11
|
+
action?: Action;
|
|
12
|
+
element?: HTMLButtonElement;
|
|
13
|
+
};
|
|
14
|
+
export type ButtonReqdProps = "children";
|
|
15
|
+
export type ButtonBindProps = "form" | "status" | "element";
|
|
16
|
+
import { type Snippet } from "svelte";
|
|
17
|
+
import { type Action } from "svelte/action";
|
|
18
|
+
import { type HTMLButtonAttributes, type MouseEventHandler } from "svelte/elements";
|
|
19
|
+
import { type ClassRuleSet } from "./core";
|
|
20
|
+
declare const Button: import("svelte").Component<ButtonProps, {}, "form" | "status" | "element">;
|
|
21
|
+
type Button = ReturnType<typeof Button>;
|
|
22
|
+
export default Button;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type CheckFieldProps = {
|
|
3
|
+
options: SvelteMap<string, string> | Map<string, string>,
|
|
4
|
+
label?: string,
|
|
5
|
+
extra?: string,
|
|
6
|
+
aux?: Snippet<[string, string[], HTMLInputElement[] | undefined]>, // Snippet<[status,values,elements]>
|
|
7
|
+
bottom?: string, // bindable
|
|
8
|
+
values?: string[], // bindable
|
|
9
|
+
multiple?: boolean, // <true>
|
|
10
|
+
descFirst?: boolean, // <false>
|
|
11
|
+
validations?: ((values: string[], validities?: ValidityState[]) => string)[],
|
|
12
|
+
status?: string, // bindable <STATE.DEFAULT>
|
|
13
|
+
style?: ClassRuleSet | string,
|
|
14
|
+
attributes?: HTMLInputAttributes,
|
|
15
|
+
action?: Action,
|
|
16
|
+
elements?: HTMLInputElement[], // bindable
|
|
17
|
+
};
|
|
18
|
+
export type CheckFieldReqdProps = "options";
|
|
19
|
+
export type CheckFieldBindProps = "bottom" | "values" | "status" | "elements";
|
|
20
|
+
|
|
21
|
+
type CheckFieldTarget = { currentTarget: EventTarget & HTMLInputElement };
|
|
22
|
+
const svs = "svs-check-field";
|
|
23
|
+
const preset: ClassRuleSet = {};
|
|
24
|
+
|
|
25
|
+
import { type Snippet, untrack } from "svelte";
|
|
26
|
+
import { type Action } from "svelte/action";
|
|
27
|
+
import { type SvelteMap } from "svelte/reactivity";
|
|
28
|
+
import { type HTMLInputAttributes } from "svelte/elements";
|
|
29
|
+
import { type ClassRuleSet, STATE, AREA, elemId, fnClass, isNeutral, omit } from "./core";
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<script lang="ts">
|
|
33
|
+
let { options, label, extra, aux, bottom = $bindable(), values = $bindable([]), multiple = true, descFirst = false, validations = [], status = $bindable(""), style, attributes, action, elements = $bindable([])}: CheckFieldProps = $props();
|
|
34
|
+
|
|
35
|
+
// *** Initialize *** //
|
|
36
|
+
if (!status) status = STATE.DEFAULT;
|
|
37
|
+
const cls = fnClass(svs, preset, style);
|
|
38
|
+
const type = multiple ? "checkbox" : "radio";
|
|
39
|
+
const name = attributes?.["name"] ? attributes?.["name"] : elemId.get(true);
|
|
40
|
+
const idLabel = elemId.get(label?.trim());
|
|
41
|
+
const idDesc = elemId.get(bottom?.trim());
|
|
42
|
+
const idErr = idDesc ?? elemId.get(true);
|
|
43
|
+
const attrs = omit(attributes, "class", "id", "type", "name", "value", "onchange", "oninvalid");
|
|
44
|
+
const roleGroup = multiple ? "group" : "radiogroup";
|
|
45
|
+
const description = bottom;
|
|
46
|
+
|
|
47
|
+
// *** Status *** //
|
|
48
|
+
const phase = { change: false, submit: false };
|
|
49
|
+
let neutral = isNeutral(status) ? status : STATE.DEFAULT;
|
|
50
|
+
$effect(() => { neutral = isNeutral(status) ? status : neutral });
|
|
51
|
+
let alert = $derived(status === STATE.INACTIVE ? "alert" : undefined);
|
|
52
|
+
let invalid = $derived(status === STATE.INACTIVE ? true : undefined);
|
|
53
|
+
let errMsg = $derived(status === STATE.INACTIVE ? idErr : undefined);
|
|
54
|
+
const toInvalid = (msg?: string) => shiftStatus(phase.submit ? STATE.INACTIVE : neutral, msg);
|
|
55
|
+
const toNonInvalid = () => shiftStatus(phase.change && values.length ? STATE.ACTIVE : neutral);
|
|
56
|
+
function shiftStatus(stat: string, msg?: string) {
|
|
57
|
+
status = stat;
|
|
58
|
+
bottom = stat === STATE.INACTIVE ? msg ?? description : description;
|
|
59
|
+
elements[0]?.setCustomValidity(msg ?? "");
|
|
60
|
+
}
|
|
61
|
+
function validate() {
|
|
62
|
+
if (!values.length) return toNonInvalid();
|
|
63
|
+
const validities = elements.map((x) => x.validity);
|
|
64
|
+
for (const v of validations) {
|
|
65
|
+
const msg = v(values, validities);
|
|
66
|
+
if (msg) return toInvalid(msg);
|
|
67
|
+
}
|
|
68
|
+
toNonInvalid();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// *** Bind Handlers *** //
|
|
72
|
+
let opts = $derived([...options.entries().map(([value, text]) => ({ value, text, checked: values.includes(value) }))]);
|
|
73
|
+
$effect.pre(() => {
|
|
74
|
+
values;
|
|
75
|
+
untrack(() => validate());
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// *** Event Handlers *** //
|
|
79
|
+
function onchange(ev: Event & CheckFieldTarget) {
|
|
80
|
+
attributes?.onchange?.(ev);
|
|
81
|
+
values = elements.filter((elem) => elem.checked).map((elem) => elem.value);
|
|
82
|
+
phase.change = true;
|
|
83
|
+
}
|
|
84
|
+
function oninvalid(ev: Event & CheckFieldTarget) {
|
|
85
|
+
attributes?.oninvalid?.(ev);
|
|
86
|
+
ev.preventDefault();
|
|
87
|
+
phase.submit = true;
|
|
88
|
+
if (status !== STATE.INACTIVE) toInvalid(elements[0]?.validationMessage);
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<!---------------------------------------->
|
|
93
|
+
|
|
94
|
+
{#if opts.length}
|
|
95
|
+
{#if label?.trim() || aux || bottom?.trim()}
|
|
96
|
+
<div class={cls(AREA.WHOLE, status)} role="group" aria-labelledby={idLabel}>
|
|
97
|
+
{#if aux}
|
|
98
|
+
<div class={cls(AREA.TOP, status)}>
|
|
99
|
+
{@render lbl()}
|
|
100
|
+
<span class={cls(AREA.AUX, status)}>{@render aux(status, values, elements)}</span>
|
|
101
|
+
</div>
|
|
102
|
+
{:else}
|
|
103
|
+
{@render lbl()}
|
|
104
|
+
{/if}
|
|
105
|
+
{@render desc(descFirst)}
|
|
106
|
+
{@render main()}
|
|
107
|
+
{@render desc(!descFirst)}
|
|
108
|
+
</div>
|
|
109
|
+
{:else}
|
|
110
|
+
{@render main()}
|
|
111
|
+
{/if}
|
|
112
|
+
{/if}
|
|
113
|
+
|
|
114
|
+
{#snippet lbl()}
|
|
115
|
+
{#if label?.trim()}
|
|
116
|
+
<span class={cls(AREA.LABEL, status)} id={idLabel}>
|
|
117
|
+
{label}
|
|
118
|
+
{#if extra?.trim()}
|
|
119
|
+
<span class={cls(AREA.EXTRA, status)}>{extra}</span>
|
|
120
|
+
{/if}
|
|
121
|
+
</span>
|
|
122
|
+
{/if}
|
|
123
|
+
{/snippet}
|
|
124
|
+
{#snippet main()}
|
|
125
|
+
<div class={cls(AREA.MIDDLE, status)} role={roleGroup} aria-describedby={idDesc} aria-invalid={!multiple ? invalid : undefined} aria-errormessage={!multiple && bottom?.trim() ? errMsg : undefined}>
|
|
126
|
+
{#each opts as {value, text, checked}, i (value)}
|
|
127
|
+
<label class={cls(AREA.MAIN, status)}>
|
|
128
|
+
{#if action}
|
|
129
|
+
<input bind:this={elements[i]} class={cls(AREA.LEFT, status)} aria-invalid={multiple ? invalid : undefined} {value} {name} {type} {checked} {onchange} {oninvalid} {...attrs} use:action />
|
|
130
|
+
{:else}
|
|
131
|
+
<input bind:this={elements[i]} class={cls(AREA.LEFT, status)} aria-invalid={multiple ? invalid : undefined} {value} {name} {type} {checked} {onchange} {oninvalid} {...attrs} />
|
|
132
|
+
{/if}
|
|
133
|
+
<span class={cls(AREA.RIGHT, status)}>{text}</span>
|
|
134
|
+
</label>
|
|
135
|
+
{/each}
|
|
136
|
+
</div>
|
|
137
|
+
{/snippet}
|
|
138
|
+
{#snippet desc(show: boolean)}
|
|
139
|
+
{#if show && bottom?.trim()}
|
|
140
|
+
<div class={cls(AREA.BOTTOM, status)} id={idDesc ?? idErr} role={alert}>{bottom}</div>
|
|
141
|
+
{/if}
|
|
142
|
+
{/snippet}
|