@marianmeres/stuic 2.0.0-next.4 → 2.0.2
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/actions/file-dropzone.svelte.d.ts +8 -0
- package/dist/actions/file-dropzone.svelte.js +43 -0
- package/dist/actions/highlight-dragover.svelte.js +16 -3
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/resizable-width.svelte.d.ts +21 -0
- package/dist/actions/resizable-width.svelte.js +162 -0
- package/dist/actions/validate.svelte.js +13 -13
- package/dist/components/Backdrop/Backdrop.svelte +1 -1
- package/dist/components/Button/Button.svelte +2 -2
- package/dist/components/Button/Button.svelte.d.ts +1 -1
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +170 -0
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte.d.ts +26 -0
- package/dist/components/ButtonGroupRadio/index.css +23 -0
- package/dist/components/ButtonGroupRadio/index.d.ts +1 -0
- package/dist/components/ButtonGroupRadio/index.js +1 -0
- package/dist/components/CommandMenu/CommandMenu.svelte +365 -0
- package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +25 -0
- package/dist/components/CommandMenu/index.d.ts +1 -0
- package/dist/components/CommandMenu/index.js +1 -0
- package/dist/components/Input/FieldInput.svelte +1 -0
- package/dist/components/Input/FieldLikeButton.svelte +16 -7
- package/dist/components/Input/FieldLikeButton.svelte.d.ts +1 -1
- package/dist/components/Input/FieldOptions.svelte +308 -136
- package/dist/components/Input/FieldOptions.svelte.d.ts +15 -8
- package/dist/components/Input/_internal/InputWrap.svelte +7 -6
- package/dist/components/Modal/Modal.svelte +10 -5
- package/dist/components/ModalDialog/ModalDialog.svelte +25 -0
- package/dist/components/Notifications/Notifications.svelte +1 -1
- package/dist/components/Progress/_internal/Bar.svelte +1 -1
- package/dist/components/Spinner/SpinnerUnicode.svelte +130 -0
- package/dist/components/Spinner/SpinnerUnicode.svelte.d.ts +12 -0
- package/dist/components/Spinner/index.d.ts +1 -0
- package/dist/components/Spinner/index.js +1 -0
- package/dist/components/TypeaheadInput/TypeaheadInput.svelte +261 -0
- package/dist/components/TypeaheadInput/TypeaheadInput.svelte.d.ts +40 -0
- package/dist/components/TypeaheadInput/index.d.ts +1 -0
- package/dist/components/TypeaheadInput/index.js +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/escape-regex.d.ts +1 -0
- package/dist/utils/escape-regex.js +1 -0
- package/dist/utils/event-emitter.d.ts +18 -0
- package/dist/utils/event-emitter.js +40 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/is-plain-object.d.ts +2 -0
- package/dist/utils/is-plain-object.js +4 -0
- package/dist/utils/replace-map.d.ts +5 -0
- package/dist/utils/replace-map.js +22 -0
- package/dist/utils/seconds.d.ts +7 -0
- package/dist/utils/seconds.js +35 -0
- package/dist/utils/tw-merge.d.ts +2 -0
- package/dist/utils/tw-merge.js +4 -0
- package/dist/utils/unaccent.d.ts +6 -0
- package/dist/utils/unaccent.js +8 -0
- package/package.json +70 -66
- package/dist/components/ColResize/ColResize.svelte +0 -0
- package/dist/components/ColResize/ColResize.svelte.d.ts +0 -26
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface FileDropzoneOptions {
|
|
2
|
+
enabled?: boolean;
|
|
3
|
+
inputEl: HTMLInputElement;
|
|
4
|
+
allowClick?: boolean;
|
|
5
|
+
processFiles?: (files: FileList | null) => any | Promise<any>;
|
|
6
|
+
}
|
|
7
|
+
export declare function fileDropzone(el: HTMLElement, fn?: () => FileDropzoneOptions): void;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function fileDropzone(el, fn) {
|
|
2
|
+
$effect(() => {
|
|
3
|
+
let { enabled = true, allowClick = true, inputEl, processFiles, } = fn?.() || {};
|
|
4
|
+
if (!enabled)
|
|
5
|
+
return;
|
|
6
|
+
if (!inputEl) {
|
|
7
|
+
console.warn("Missing inputEl instance, can't continue...");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
function preventDefault(e) {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
// e.stopPropagation();
|
|
13
|
+
}
|
|
14
|
+
function handle_drop(e) {
|
|
15
|
+
handle_files(e?.dataTransfer?.files ?? null);
|
|
16
|
+
}
|
|
17
|
+
function handle_change(e) {
|
|
18
|
+
if (e.target instanceof HTMLInputElement) {
|
|
19
|
+
handle_files(e.target.files);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function handle_files(files) {
|
|
23
|
+
processFiles?.(files);
|
|
24
|
+
}
|
|
25
|
+
function handle_click() {
|
|
26
|
+
allowClick && inputEl.click();
|
|
27
|
+
}
|
|
28
|
+
// over/drop are critical, enter/leave I'm not sure
|
|
29
|
+
const PREVENT = ["dragenter", "dragover", "dragleave", "drop"];
|
|
30
|
+
PREVENT.forEach((name) => el.addEventListener(name, preventDefault));
|
|
31
|
+
//
|
|
32
|
+
el.addEventListener("drop", handle_drop);
|
|
33
|
+
el.addEventListener("click", handle_click);
|
|
34
|
+
inputEl.addEventListener("change", handle_change);
|
|
35
|
+
//
|
|
36
|
+
return () => {
|
|
37
|
+
PREVENT.forEach((name) => el.removeEventListener(name, preventDefault));
|
|
38
|
+
el.removeEventListener("drop", handle_drop);
|
|
39
|
+
el.removeEventListener("click", handle_click);
|
|
40
|
+
inputEl.removeEventListener("change", handle_change);
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -11,7 +11,7 @@ export function highlightDragover(el, fn) {
|
|
|
11
11
|
// }
|
|
12
12
|
function prevent(e) {
|
|
13
13
|
e.preventDefault();
|
|
14
|
-
e.stopPropagation();
|
|
14
|
+
// e.stopPropagation();
|
|
15
15
|
}
|
|
16
16
|
const HIGH = ["dragenter", "dragover"];
|
|
17
17
|
const UNHIGH = ["dragleave", "drop"];
|
|
@@ -22,13 +22,26 @@ export function highlightDragover(el, fn) {
|
|
|
22
22
|
return;
|
|
23
23
|
if (!Array.isArray(classes))
|
|
24
24
|
classes = [classes];
|
|
25
|
+
// allow strings
|
|
26
|
+
classes = classes.reduce((m, c) => {
|
|
27
|
+
m = [
|
|
28
|
+
...m,
|
|
29
|
+
...c
|
|
30
|
+
.split(/\s/)
|
|
31
|
+
.map((v) => v.trim())
|
|
32
|
+
.filter(Boolean),
|
|
33
|
+
];
|
|
34
|
+
return m;
|
|
35
|
+
}, []);
|
|
36
|
+
// el.addEventListener("drop", prevent);
|
|
25
37
|
const highlight = () => el.classList.add(...classes);
|
|
26
38
|
const unhighlight = () => el.classList.remove(...classes);
|
|
27
39
|
// ALL.forEach((name: any) => el.addEventListener(name, prevent, false));
|
|
28
|
-
HIGH.forEach((name) => el.addEventListener(name, highlight
|
|
29
|
-
UNHIGH.forEach((name) => el.addEventListener(name, unhighlight
|
|
40
|
+
HIGH.forEach((name) => el.addEventListener(name, highlight));
|
|
41
|
+
UNHIGH.forEach((name) => el.addEventListener(name, unhighlight));
|
|
30
42
|
// el.addEventListener("drop", handle_drop, false);
|
|
31
43
|
return () => {
|
|
44
|
+
// el.removeEventListener("drop", prevent);
|
|
32
45
|
// ALL.forEach((name: any) => el.removeEventListener(name, prevent));
|
|
33
46
|
HIGH.forEach((name) => el.removeEventListener(name, highlight));
|
|
34
47
|
UNHIGH.forEach((name) => el.removeEventListener(name, unhighlight));
|
package/dist/actions/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export * from "./autogrow.svelte.js";
|
|
2
2
|
export * from "./autoscroll.js";
|
|
3
|
+
export * from "./file-dropzone.svelte.js";
|
|
3
4
|
export * from "./focus-trap.js";
|
|
4
5
|
export * from "./highlight-dragover.svelte.js";
|
|
5
6
|
export * from "./on-submit-validity-check.svelte.js";
|
|
7
|
+
export * from "./resizable-width.svelte.js";
|
|
6
8
|
export * from "./tooltip/tooltip.svelte.js";
|
|
7
9
|
export * from "./trim.svelte.js";
|
|
8
10
|
export * from "./validate.svelte.js";
|
package/dist/actions/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export * from "./autogrow.svelte.js";
|
|
2
2
|
export * from "./autoscroll.js";
|
|
3
|
+
export * from "./file-dropzone.svelte.js";
|
|
3
4
|
export * from "./focus-trap.js";
|
|
4
5
|
export * from "./highlight-dragover.svelte.js";
|
|
5
6
|
export * from "./on-submit-validity-check.svelte.js";
|
|
7
|
+
export * from "./resizable-width.svelte.js";
|
|
6
8
|
export * from "./tooltip/tooltip.svelte.js";
|
|
7
9
|
export * from "./trim.svelte.js";
|
|
8
10
|
export * from "./validate.svelte.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ResizableWidthOptions {
|
|
2
|
+
enabled?: boolean;
|
|
3
|
+
initial?: number;
|
|
4
|
+
min?: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
units?: "px" | "%";
|
|
7
|
+
key?: string | number | null | undefined;
|
|
8
|
+
storage?: "local" | "session";
|
|
9
|
+
handleClass?: string;
|
|
10
|
+
handleDragClass?: string;
|
|
11
|
+
onResize?: (info: {
|
|
12
|
+
width: number;
|
|
13
|
+
units: "px" | "%";
|
|
14
|
+
container: number;
|
|
15
|
+
}) => void;
|
|
16
|
+
debug?: (...args: any[]) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Note: units should not be changed on the fly...
|
|
20
|
+
*/
|
|
21
|
+
export declare function resizableWidth(el: HTMLDivElement, fn?: () => ResizableWidthOptions): void;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { localStorageState, sessionStorageState, } from "../utils/persistent-state.svelte.js";
|
|
2
|
+
import { twMerge } from "../utils/tw-merge.js";
|
|
3
|
+
/**
|
|
4
|
+
* Note: units should not be changed on the fly...
|
|
5
|
+
*/
|
|
6
|
+
export function resizableWidth(el, fn) {
|
|
7
|
+
const DEFAULT_HANDLE_CLS = [
|
|
8
|
+
"group",
|
|
9
|
+
"absolute top-0 right-0 bottom-0",
|
|
10
|
+
"w-[1px]",
|
|
11
|
+
"bg-black/20 hover:bg-black/30",
|
|
12
|
+
"dark:bg-white/10 dark:hover:bg-white/20",
|
|
13
|
+
"transition-colors duration-200",
|
|
14
|
+
"touch-none cursor-ew-resize",
|
|
15
|
+
].join(" ");
|
|
16
|
+
const DEFAULT_DRAG_HANDLE_CLS = [
|
|
17
|
+
"absolute h-[20px] w-[9px]",
|
|
18
|
+
"-translate-x-[4px] top-1/2 -translate-y-1/2",
|
|
19
|
+
"rounded border border-black/20 dark:border-white/20",
|
|
20
|
+
"bg-gray-300 group-hover:bg-gray-400",
|
|
21
|
+
"dark:bg-gray-600 dark:group-hover:bg-gray-500",
|
|
22
|
+
"transition-colors duration-200",
|
|
23
|
+
"touch-none cursor-ew-resize",
|
|
24
|
+
].join(" ");
|
|
25
|
+
function create_handle(el, handleClass, handleDragClass) {
|
|
26
|
+
const handle = document.createElement("div");
|
|
27
|
+
handle.setAttribute("data-handle", "true");
|
|
28
|
+
const dragHandle = document.createElement("div");
|
|
29
|
+
dragHandle.classList.add(...twMerge(DEFAULT_DRAG_HANDLE_CLS, handleDragClass).split(" "));
|
|
30
|
+
handle.appendChild(dragHandle);
|
|
31
|
+
el.appendChild(handle);
|
|
32
|
+
//
|
|
33
|
+
handle.classList.add(...twMerge(DEFAULT_HANDLE_CLS, handleClass).split(" "));
|
|
34
|
+
return handle;
|
|
35
|
+
}
|
|
36
|
+
$effect(() => {
|
|
37
|
+
let { enabled = true, initial = 0, min = 0, max = 0, units = "px", key, storage = "session", handleClass = "", handleDragClass = "", onResize, debug, } = fn?.() || {};
|
|
38
|
+
const _debug = (...args) => debug?.("[resizable-width]", ...args);
|
|
39
|
+
_debug("$effect");
|
|
40
|
+
if (!enabled)
|
|
41
|
+
return;
|
|
42
|
+
// initialize ////////////////////////////////////////////////////////////////////
|
|
43
|
+
//
|
|
44
|
+
let isResizing = false;
|
|
45
|
+
let startX = 0;
|
|
46
|
+
let startWidth = 0;
|
|
47
|
+
let containerW = undefined;
|
|
48
|
+
//
|
|
49
|
+
const handle = create_handle(el, handleClass, handleDragClass);
|
|
50
|
+
const container = el.parentElement;
|
|
51
|
+
// do we have a storage? if so, adjust the initial value...
|
|
52
|
+
const initialBackup = initial;
|
|
53
|
+
const _storage = get_storage(storage, key, initial);
|
|
54
|
+
if (_storage)
|
|
55
|
+
initial = _storage.current ?? initial;
|
|
56
|
+
// handlers/workers/helpers //////////////////////////////////////////////////////
|
|
57
|
+
function set_width(pxOrPercent) {
|
|
58
|
+
if (pxOrPercent) {
|
|
59
|
+
_debug(`set_width(${pxOrPercent})`);
|
|
60
|
+
set_width_px(units === "%" ? container.offsetWidth * (pxOrPercent / 100) : pxOrPercent);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function set_width_px(widthPx) {
|
|
64
|
+
_debug(`set_width_px(${widthPx})`);
|
|
65
|
+
containerW ??= container.offsetWidth;
|
|
66
|
+
const clamp = (value) => {
|
|
67
|
+
const _initial = value;
|
|
68
|
+
if (min)
|
|
69
|
+
value = Math.max(min, value);
|
|
70
|
+
if (max)
|
|
71
|
+
value = Math.min(max, value);
|
|
72
|
+
_initial !== value && _debug("clamped", value, units);
|
|
73
|
+
return value;
|
|
74
|
+
};
|
|
75
|
+
let width;
|
|
76
|
+
if (units === "%") {
|
|
77
|
+
const widthPercent = Math.min(100, (widthPx / containerW) * 100); // convert to % (with 100 max)
|
|
78
|
+
width = clamp(widthPercent);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
width = clamp(widthPx);
|
|
82
|
+
}
|
|
83
|
+
el.style.width = `${width}${units}`;
|
|
84
|
+
_debug("new width", width, units);
|
|
85
|
+
const info = { width, units, container: containerW };
|
|
86
|
+
onResize?.(info);
|
|
87
|
+
// maybe save to storage
|
|
88
|
+
if (_storage)
|
|
89
|
+
_storage.current = width;
|
|
90
|
+
return info;
|
|
91
|
+
}
|
|
92
|
+
function resize_start(e) {
|
|
93
|
+
e.preventDefault(); // prevent scrolling on touch devices
|
|
94
|
+
isResizing = true;
|
|
95
|
+
//
|
|
96
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
97
|
+
startX = clientX;
|
|
98
|
+
startWidth = parseInt(getComputedStyle(el).width, 10);
|
|
99
|
+
containerW = container.offsetWidth;
|
|
100
|
+
//
|
|
101
|
+
document.body.style.cursor = "ew-resize";
|
|
102
|
+
document.body.style.userSelect = "none";
|
|
103
|
+
}
|
|
104
|
+
function resize(e) {
|
|
105
|
+
if (!isResizing)
|
|
106
|
+
return;
|
|
107
|
+
e.preventDefault(); // prevent scrolling on touch devices
|
|
108
|
+
//
|
|
109
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
110
|
+
const deltaX = clientX - startX;
|
|
111
|
+
let width = startWidth + deltaX;
|
|
112
|
+
set_width_px(width);
|
|
113
|
+
}
|
|
114
|
+
function resize_stop() {
|
|
115
|
+
if (isResizing) {
|
|
116
|
+
isResizing = false;
|
|
117
|
+
containerW = undefined;
|
|
118
|
+
//
|
|
119
|
+
document.body.style.cursor = "";
|
|
120
|
+
document.body.style.userSelect = "";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function on_dblclick() {
|
|
124
|
+
set_width(initialBackup);
|
|
125
|
+
}
|
|
126
|
+
// initial styles ////////////////////////////////////////////////////////////////
|
|
127
|
+
el.style.position = "relative"; // so the handle will work
|
|
128
|
+
set_width(initial);
|
|
129
|
+
// listeners /////////////////////////////////////////////////////////////////////
|
|
130
|
+
// handle
|
|
131
|
+
handle.addEventListener("dblclick", on_dblclick);
|
|
132
|
+
handle.addEventListener("selectstart", (e) => e.preventDefault()); // prevent text selection during resize
|
|
133
|
+
// mouse
|
|
134
|
+
handle.addEventListener("mousedown", resize_start);
|
|
135
|
+
document.addEventListener("mousemove", resize);
|
|
136
|
+
document.addEventListener("mouseup", resize_stop);
|
|
137
|
+
// touch
|
|
138
|
+
handle.addEventListener("touchstart", resize_start, { passive: false });
|
|
139
|
+
document.addEventListener("touchmove", resize, { passive: false });
|
|
140
|
+
document.addEventListener("touchend", resize_stop);
|
|
141
|
+
document.addEventListener("touchcancel", resize_stop);
|
|
142
|
+
// cleanup ///////////////////////////////////////////////////////////////////////
|
|
143
|
+
return () => {
|
|
144
|
+
// mouse
|
|
145
|
+
document.removeEventListener("mousemove", resize);
|
|
146
|
+
document.removeEventListener("mouseup", resize_stop);
|
|
147
|
+
// touch
|
|
148
|
+
document.removeEventListener("touchmove", resize);
|
|
149
|
+
document.removeEventListener("touchend", resize_stop);
|
|
150
|
+
document.removeEventListener("touchcancel", resize_stop);
|
|
151
|
+
// will also remove it's own event listeners
|
|
152
|
+
handle.remove();
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// helpers ///////////////////////////////////////////////////////////////////////////////
|
|
157
|
+
function get_storage(type, key, initialValue) {
|
|
158
|
+
if (key) {
|
|
159
|
+
return (type === "session" ? sessionStorageState : localStorageState)(`resizable-width-${key}`, initialValue);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
@@ -55,20 +55,20 @@ export function validate(el, fn) {
|
|
|
55
55
|
// console.log(1111, validityState, el);
|
|
56
56
|
// hm... Uncaught Svelte error: state_unsafe_mutation...
|
|
57
57
|
// the `tick` await helps, but I'm not really sure I understand the internals...
|
|
58
|
-
tick().then(() => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
58
|
+
// tick().then(() => {
|
|
59
|
+
setValidationResult?.({
|
|
60
|
+
validity: validityState,
|
|
61
|
+
reasons,
|
|
62
|
+
valid: validityState?.valid,
|
|
63
|
+
// use translate fn for first reason (if fn provided and allowed),
|
|
64
|
+
// otherwise fallback to native msg
|
|
65
|
+
message: _t(reasons?.[0], el.value, el.validationMessage ||
|
|
66
|
+
// PROBLEM: hidden inputs do not report validationMessage-s even
|
|
67
|
+
// if correctly reported as invalid. So all we can do, is
|
|
68
|
+
// put only something generic here...
|
|
69
|
+
"This field is invalid. Please review and try again."),
|
|
71
70
|
});
|
|
71
|
+
// });
|
|
72
72
|
};
|
|
73
73
|
el.addEventListener(on, _doValidate);
|
|
74
74
|
//
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
bind:this={el}
|
|
140
140
|
role="presentation"
|
|
141
141
|
tabindex="-1"
|
|
142
|
-
class={twMerge("fixed inset-0 flex z-10", classProp)}
|
|
142
|
+
class={twMerge("fixed inset-0 flex z-10 h-dvh", classProp)}
|
|
143
143
|
in:fade={{ duration: fadeInDuration }}
|
|
144
144
|
out:fade={{ duration: fadeOutDuration }}
|
|
145
145
|
use:focusTrapAction={{
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
bg-button-bg text-button-text
|
|
4
4
|
dark:bg-button-bg-dark dark:text-button-text-dark
|
|
5
5
|
font-mono text-sm text-center
|
|
6
|
-
leading-
|
|
6
|
+
leading-none
|
|
7
7
|
border-1
|
|
8
8
|
border-button-border dark:border-button-border-dark
|
|
9
9
|
rounded-md
|
|
10
10
|
inline-flex items-center justify-center gap-x-2
|
|
11
|
-
px-3 py-
|
|
11
|
+
px-3 py-2
|
|
12
12
|
|
|
13
13
|
hover:brightness-[1.05]
|
|
14
14
|
active:brightness-[0.95]
|
|
@@ -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-2\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,170 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ItemCollection } from "@marianmeres/item-collection";
|
|
3
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
4
|
+
import Button from "../Button/Button.svelte";
|
|
5
|
+
import type { FieldRadiosOption } from "../Input/types.js";
|
|
6
|
+
//
|
|
7
|
+
import "./index.css";
|
|
8
|
+
|
|
9
|
+
interface ItemCollectionType {
|
|
10
|
+
id: string;
|
|
11
|
+
option: FieldRadiosOption;
|
|
12
|
+
}
|
|
13
|
+
interface ItemColl extends ItemCollection<ItemCollectionType> {}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
value?: string;
|
|
17
|
+
tabindex?: number; // tooShort
|
|
18
|
+
size?: "sm" | "md" | "lg" | string;
|
|
19
|
+
//
|
|
20
|
+
options: (string | FieldRadiosOption)[];
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
activeIndex?: number | undefined;
|
|
23
|
+
//
|
|
24
|
+
class?: string;
|
|
25
|
+
classButton?: string;
|
|
26
|
+
classButtonActive?: string;
|
|
27
|
+
style?: string;
|
|
28
|
+
// for side-effects, or validation... if would return explicit false, will not activate
|
|
29
|
+
onButtonClick?: (
|
|
30
|
+
index: number,
|
|
31
|
+
coll: ItemColl
|
|
32
|
+
) => Promise<boolean | undefined | void> | boolean | undefined | void;
|
|
33
|
+
buttonProps?: (index: number, coll: ItemColl) => undefined | Record<string, any>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
options,
|
|
38
|
+
value = $bindable(),
|
|
39
|
+
tabindex = 0,
|
|
40
|
+
disabled,
|
|
41
|
+
size = "md",
|
|
42
|
+
//
|
|
43
|
+
activeIndex = $bindable(undefined),
|
|
44
|
+
//
|
|
45
|
+
class: classProp,
|
|
46
|
+
classButton,
|
|
47
|
+
classButtonActive,
|
|
48
|
+
style,
|
|
49
|
+
onButtonClick,
|
|
50
|
+
buttonProps,
|
|
51
|
+
}: Props = $props();
|
|
52
|
+
|
|
53
|
+
const coll: ItemColl = $derived.by(() => {
|
|
54
|
+
const out = new ItemCollection(
|
|
55
|
+
options.map((o, i) => {
|
|
56
|
+
// normalize string to FieldRadiosOption
|
|
57
|
+
if (typeof o === "string") o = { label: o };
|
|
58
|
+
// normalize FieldRadiosOption to ItemCollection's Item
|
|
59
|
+
return { id: `opt-${i}-${Math.random().toString(36).slice(2, 8)}`, option: o };
|
|
60
|
+
}),
|
|
61
|
+
{}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (value !== undefined) {
|
|
65
|
+
const index = out.items.findIndex((item: ItemCollectionType) => {
|
|
66
|
+
return value === (item?.option.value ?? item.option.label);
|
|
67
|
+
});
|
|
68
|
+
if (index > -1) out.setActiveIndex(index);
|
|
69
|
+
} else if (activeIndex !== undefined) out.setActiveIndex(activeIndex);
|
|
70
|
+
|
|
71
|
+
return out;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
$effect(() => {
|
|
75
|
+
return coll.subscribe((c) => {
|
|
76
|
+
value = c.active?.option.value ?? c.active?.option.label;
|
|
77
|
+
activeIndex = c.activeIndex;
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const rounded = "rounded-md";
|
|
82
|
+
const roundedBtn = "rounded-md";
|
|
83
|
+
//
|
|
84
|
+
const CLS = `
|
|
85
|
+
stuic-button-group
|
|
86
|
+
${rounded}
|
|
87
|
+
w-full
|
|
88
|
+
py-1.5 px-1.5 inline-block space-x-1
|
|
89
|
+
bg-button-group-bg text-button-group-text
|
|
90
|
+
dark:bg-button-group-bg-dark dark:text-button-group-text-dark
|
|
91
|
+
border-1
|
|
92
|
+
border-button-group-border dark:border-button-group-border-dark
|
|
93
|
+
flex justify-between
|
|
94
|
+
|
|
95
|
+
focus-within:border-button-group-accent focus-within:dark:border-button-group-accent-dark
|
|
96
|
+
focus-within:ring-button-group-accent/20 focus-within:dark:ring-button-group-accent-dark/20
|
|
97
|
+
focus-within:ring-4
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
const CLS_BUTTON = `
|
|
101
|
+
${rounded}
|
|
102
|
+
w-full inline-block
|
|
103
|
+
bg-transparent text-button-group-text dark:text-button-group-text-dark
|
|
104
|
+
hover:bg-transparent hover:text-button-group-text hover:dark:text-button-group-text-dark
|
|
105
|
+
outline-none focus:outline-none
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
// we need some active indication by default... use just something subtle here, in the wild
|
|
109
|
+
// this will be styled with classButtonActive
|
|
110
|
+
const CLS_BUTTON_ACTIVE = `
|
|
111
|
+
shadow-none
|
|
112
|
+
bg-button-group-bg-active dark:bg-button-group-bg-active-dark
|
|
113
|
+
text-button-group-text-active dark:text-button-group-text-active-dark
|
|
114
|
+
hover:bg-button-group-bg-active hover:dark:bg-button-group-bg-active
|
|
115
|
+
hover:text-button-group-text-active hover:dark:text-button-group-text-active-dark
|
|
116
|
+
${roundedBtn}
|
|
117
|
+
`;
|
|
118
|
+
// shadow-[0px_0px_1px_1px_rgba(0_0_0_/_.6)]
|
|
119
|
+
|
|
120
|
+
let els = $state<Record<number, HTMLButtonElement>>({});
|
|
121
|
+
|
|
122
|
+
async function maybe_activate(index: number, coll: ItemColl) {
|
|
123
|
+
if ((await onButtonClick?.(index, coll)) !== false) {
|
|
124
|
+
coll.setActiveIndex(index);
|
|
125
|
+
els[index].focus();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
{#if coll.size}
|
|
131
|
+
<div
|
|
132
|
+
class={twMerge(CLS, classProp)}
|
|
133
|
+
{style}
|
|
134
|
+
role="radiogroup"
|
|
135
|
+
aria-labelledby={$coll?.active?.id || ""}
|
|
136
|
+
>
|
|
137
|
+
{#each coll.items as item, i}
|
|
138
|
+
<Button
|
|
139
|
+
tabindex={$coll.activeIndex === i ? tabindex : -1}
|
|
140
|
+
class={twMerge(
|
|
141
|
+
"border-none shadow-none",
|
|
142
|
+
CLS_BUTTON,
|
|
143
|
+
classButton,
|
|
144
|
+
$coll.activeIndex === i && [CLS_BUTTON_ACTIVE, classButtonActive].join(" ")
|
|
145
|
+
)}
|
|
146
|
+
{disabled}
|
|
147
|
+
{size}
|
|
148
|
+
type="button"
|
|
149
|
+
role="radio"
|
|
150
|
+
aria-checked={$coll.activeIndex === i}
|
|
151
|
+
onclick={async () => {
|
|
152
|
+
await maybe_activate(i, coll);
|
|
153
|
+
}}
|
|
154
|
+
bind:el={els[i]}
|
|
155
|
+
onkeydown={async (e) => {
|
|
156
|
+
if (["ArrowRight", "ArrowDown"].includes(e.key)) {
|
|
157
|
+
await maybe_activate(Math.min(i + 1, coll.size - 1), coll);
|
|
158
|
+
}
|
|
159
|
+
if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
|
|
160
|
+
await maybe_activate(Math.max(0, i - 1), coll);
|
|
161
|
+
}
|
|
162
|
+
}}
|
|
163
|
+
id={item.id}
|
|
164
|
+
{...buttonProps?.(i, coll) || {}}
|
|
165
|
+
>
|
|
166
|
+
{item.option.label}
|
|
167
|
+
</Button>
|
|
168
|
+
{/each}
|
|
169
|
+
</div>
|
|
170
|
+
{/if}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ItemCollection } from "@marianmeres/item-collection";
|
|
2
|
+
import type { FieldRadiosOption } from "../Input/types.js";
|
|
3
|
+
import "./index.css";
|
|
4
|
+
interface ItemCollectionType {
|
|
5
|
+
id: string;
|
|
6
|
+
option: FieldRadiosOption;
|
|
7
|
+
}
|
|
8
|
+
interface ItemColl extends ItemCollection<ItemCollectionType> {
|
|
9
|
+
}
|
|
10
|
+
interface Props {
|
|
11
|
+
value?: string;
|
|
12
|
+
tabindex?: number;
|
|
13
|
+
size?: "sm" | "md" | "lg" | string;
|
|
14
|
+
options: (string | FieldRadiosOption)[];
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
activeIndex?: number | undefined;
|
|
17
|
+
class?: string;
|
|
18
|
+
classButton?: string;
|
|
19
|
+
classButtonActive?: string;
|
|
20
|
+
style?: string;
|
|
21
|
+
onButtonClick?: (index: number, coll: ItemColl) => Promise<boolean | undefined | void> | boolean | undefined | void;
|
|
22
|
+
buttonProps?: (index: number, coll: ItemColl) => undefined | Record<string, any>;
|
|
23
|
+
}
|
|
24
|
+
declare const ButtonGroupRadio: import("svelte").Component<Props, {}, "value" | "activeIndex">;
|
|
25
|
+
type ButtonGroupRadio = ReturnType<typeof ButtonGroupRadio>;
|
|
26
|
+
export default ButtonGroupRadio;
|
|
@@ -0,0 +1,23 @@
|
|
|
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-white));
|
|
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-neutral-300));
|
|
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
|
+
|
|
15
|
+
--color-button-group-accent: var(--color-button-group-accent, var(--color-red-600));
|
|
16
|
+
--color-button-group-accent-dark: var(--color-button-group-accent-dark, var(--color-red-400));
|
|
17
|
+
|
|
18
|
+
--color-button-group-bg-active: var(--color-button-group-bg-active, var(--color-neutral-500));
|
|
19
|
+
--color-button-group-bg-active-dark: var(--color-button-group-bg-active-dark, var(--color-neutral-500));
|
|
20
|
+
|
|
21
|
+
--color-button-group-text-active: var(--color-button-group-text-active, var(--color-white));
|
|
22
|
+
--color-button-group-text-active-dark: var(--color-button-group-text-active-dark, var(--color-white));
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ButtonGroupRadio } from "./ButtonGroupRadio.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ButtonGroupRadio } from "./ButtonGroupRadio.svelte";
|