@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.
Files changed (61) hide show
  1. package/dist/actions/file-dropzone.svelte.d.ts +8 -0
  2. package/dist/actions/file-dropzone.svelte.js +43 -0
  3. package/dist/actions/highlight-dragover.svelte.js +16 -3
  4. package/dist/actions/index.d.ts +2 -0
  5. package/dist/actions/index.js +2 -0
  6. package/dist/actions/resizable-width.svelte.d.ts +21 -0
  7. package/dist/actions/resizable-width.svelte.js +162 -0
  8. package/dist/actions/validate.svelte.js +13 -13
  9. package/dist/components/Backdrop/Backdrop.svelte +1 -1
  10. package/dist/components/Button/Button.svelte +2 -2
  11. package/dist/components/Button/Button.svelte.d.ts +1 -1
  12. package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +170 -0
  13. package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte.d.ts +26 -0
  14. package/dist/components/ButtonGroupRadio/index.css +23 -0
  15. package/dist/components/ButtonGroupRadio/index.d.ts +1 -0
  16. package/dist/components/ButtonGroupRadio/index.js +1 -0
  17. package/dist/components/CommandMenu/CommandMenu.svelte +365 -0
  18. package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +25 -0
  19. package/dist/components/CommandMenu/index.d.ts +1 -0
  20. package/dist/components/CommandMenu/index.js +1 -0
  21. package/dist/components/Input/FieldInput.svelte +1 -0
  22. package/dist/components/Input/FieldLikeButton.svelte +16 -7
  23. package/dist/components/Input/FieldLikeButton.svelte.d.ts +1 -1
  24. package/dist/components/Input/FieldOptions.svelte +308 -136
  25. package/dist/components/Input/FieldOptions.svelte.d.ts +15 -8
  26. package/dist/components/Input/_internal/InputWrap.svelte +7 -6
  27. package/dist/components/Modal/Modal.svelte +10 -5
  28. package/dist/components/ModalDialog/ModalDialog.svelte +25 -0
  29. package/dist/components/Notifications/Notifications.svelte +1 -1
  30. package/dist/components/Progress/_internal/Bar.svelte +1 -1
  31. package/dist/components/Spinner/SpinnerUnicode.svelte +130 -0
  32. package/dist/components/Spinner/SpinnerUnicode.svelte.d.ts +12 -0
  33. package/dist/components/Spinner/index.d.ts +1 -0
  34. package/dist/components/Spinner/index.js +1 -0
  35. package/dist/components/TypeaheadInput/TypeaheadInput.svelte +261 -0
  36. package/dist/components/TypeaheadInput/TypeaheadInput.svelte.d.ts +40 -0
  37. package/dist/components/TypeaheadInput/index.d.ts +1 -0
  38. package/dist/components/TypeaheadInput/index.js +1 -0
  39. package/dist/index.css +1 -0
  40. package/dist/index.d.ts +3 -0
  41. package/dist/index.js +3 -0
  42. package/dist/types.d.ts +1 -0
  43. package/dist/utils/escape-regex.d.ts +1 -0
  44. package/dist/utils/escape-regex.js +1 -0
  45. package/dist/utils/event-emitter.d.ts +18 -0
  46. package/dist/utils/event-emitter.js +40 -0
  47. package/dist/utils/index.d.ts +5 -0
  48. package/dist/utils/index.js +5 -0
  49. package/dist/utils/is-plain-object.d.ts +2 -0
  50. package/dist/utils/is-plain-object.js +4 -0
  51. package/dist/utils/replace-map.d.ts +5 -0
  52. package/dist/utils/replace-map.js +22 -0
  53. package/dist/utils/seconds.d.ts +7 -0
  54. package/dist/utils/seconds.js +35 -0
  55. package/dist/utils/tw-merge.d.ts +2 -0
  56. package/dist/utils/tw-merge.js +4 -0
  57. package/dist/utils/unaccent.d.ts +6 -0
  58. package/dist/utils/unaccent.js +8 -0
  59. package/package.json +70 -66
  60. package/dist/components/ColResize/ColResize.svelte +0 -0
  61. 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, false));
29
- UNHIGH.forEach((name) => el.addEventListener(name, unhighlight, false));
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));
@@ -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";
@@ -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
- 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 anything generically reasonable here...
69
- "This field contains invalid data. Please review and try again."),
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-4
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-1.5
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-4\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";
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";