@marianmeres/stuic 2.0.0-next.5 → 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 +1 -1
- package/dist/components/Button/Button.svelte.d.ts +1 -1
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +47 -12
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte.d.ts +3 -2
- package/dist/components/ButtonGroupRadio/index.css +11 -2
- 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 +278 -120
- 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
|
@@ -76,7 +76,9 @@
|
|
|
76
76
|
bind:el={elBackdrop}
|
|
77
77
|
bind:visible
|
|
78
78
|
class={twMerge(
|
|
79
|
-
"justify-center items-center bg-black/25 p-2 sm:p-4 md:p-[10vh] transition-all",
|
|
79
|
+
// "justify-center items-center bg-black/25 p-2 sm:p-4 md:p-[10vh] transition-all",
|
|
80
|
+
"justify-center items-center bg-black/25 transition-all",
|
|
81
|
+
"md:p-[10vh]",
|
|
80
82
|
classBackdrop
|
|
81
83
|
)}
|
|
82
84
|
{focusTrap}
|
|
@@ -91,15 +93,18 @@
|
|
|
91
93
|
aria-labelledby={labelledby}
|
|
92
94
|
aria-describedby={describedby}
|
|
93
95
|
class={twMerge(
|
|
94
|
-
"overflow-x-hidden overflow-y-hidden
|
|
95
|
-
|
|
96
|
+
"overflow-x-hidden overflow-y-hidden flex flex-col",
|
|
97
|
+
"w-full max-w-3xl",
|
|
98
|
+
"h-dvh md:h-full",
|
|
96
99
|
classInner
|
|
97
100
|
)}
|
|
98
101
|
>
|
|
99
102
|
<div
|
|
100
103
|
class={twMerge(
|
|
101
|
-
"bg-white dark:bg-neutral-800
|
|
102
|
-
"
|
|
104
|
+
"bg-white dark:bg-neutral-800",
|
|
105
|
+
"flex flex-col overflow-hidden",
|
|
106
|
+
"rounded-none md:rounded-md",
|
|
107
|
+
"w-full flex-1 md:max-h-2/3",
|
|
103
108
|
classProp
|
|
104
109
|
)}
|
|
105
110
|
>
|
|
@@ -76,6 +76,31 @@
|
|
|
76
76
|
() => !noClickOutsideClose && close()
|
|
77
77
|
);
|
|
78
78
|
|
|
79
|
+
let _original: any = {};
|
|
80
|
+
$effect(() => {
|
|
81
|
+
// if (noScrollLock) return;
|
|
82
|
+
if (visible) {
|
|
83
|
+
_original = window.getComputedStyle(document.body);
|
|
84
|
+
const scrollY = window.scrollY;
|
|
85
|
+
|
|
86
|
+
document.body.style.position = "fixed";
|
|
87
|
+
document.body.style.top = `-${scrollY}px`;
|
|
88
|
+
document.body.style.width = "100%";
|
|
89
|
+
document.body.style.overflow = "hidden";
|
|
90
|
+
} else {
|
|
91
|
+
const scrollY = document.body.style.top;
|
|
92
|
+
|
|
93
|
+
document.body.style.position = _original.position;
|
|
94
|
+
document.body.style.position = "";
|
|
95
|
+
document.body.style.top = "";
|
|
96
|
+
document.body.style.width = "";
|
|
97
|
+
document.body.style.overflow = "";
|
|
98
|
+
|
|
99
|
+
// Restore scroll position
|
|
100
|
+
window.scrollTo(0, parseInt(scrollY || "0") * -1);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
79
104
|
// $inspect("Modal dialog mounted, is visible:", visible).with(clog);
|
|
80
105
|
</script>
|
|
81
106
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<div
|
|
24
24
|
class={twMerge(
|
|
25
25
|
"bar",
|
|
26
|
-
"w-full h-full bg-progress-accent transition-[width] ease-linear duration-
|
|
26
|
+
"w-full h-full bg-progress-accent transition-[width] ease-linear duration-100",
|
|
27
27
|
classBar
|
|
28
28
|
)}
|
|
29
29
|
style="width:{Math.min(100, Math.max(0, progress))}%; {styleBar}"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type SpinnerUnicodeVariant =
|
|
3
|
+
| "braille_bar"
|
|
4
|
+
| "braille_bar_dot"
|
|
5
|
+
| "braille_dot_circle"
|
|
6
|
+
| "braille_dot_bounce"
|
|
7
|
+
| "half_circle"
|
|
8
|
+
| "quarter_circle"
|
|
9
|
+
| "ascii"
|
|
10
|
+
| "bar_v"
|
|
11
|
+
| "bar_h"
|
|
12
|
+
| "shade"
|
|
13
|
+
| "arrows"
|
|
14
|
+
| "arrows2";
|
|
15
|
+
|
|
16
|
+
//
|
|
17
|
+
export function spinnerCreateBackAndForthCharFrames(
|
|
18
|
+
width: number,
|
|
19
|
+
hiChar: string,
|
|
20
|
+
loChar: string,
|
|
21
|
+
glue: string = "\u200A"
|
|
22
|
+
): string[] {
|
|
23
|
+
if (width < 0) throw new TypeError("Expecting positive non-zero width");
|
|
24
|
+
if (width === 1) return [hiChar];
|
|
25
|
+
|
|
26
|
+
let frames: string[] = [];
|
|
27
|
+
let hiIdx = 0;
|
|
28
|
+
let inc = true;
|
|
29
|
+
|
|
30
|
+
const framesCount = 2 * width - 2;
|
|
31
|
+
|
|
32
|
+
for (let y = 0; y < framesCount; y++) {
|
|
33
|
+
let rowChars = [];
|
|
34
|
+
for (let x = 0; x < width; x++) {
|
|
35
|
+
rowChars.push(x == hiIdx ? hiChar : loChar);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (inc) {
|
|
39
|
+
hiIdx++;
|
|
40
|
+
if (hiIdx === Math.ceil(framesCount / 2)) inc = false;
|
|
41
|
+
} else {
|
|
42
|
+
hiIdx--;
|
|
43
|
+
if (hiIdx === 0) inc = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
frames.push(rowChars.join(glue));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return frames;
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<script lang="ts">
|
|
54
|
+
import { onMount } from "svelte";
|
|
55
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
56
|
+
|
|
57
|
+
let {
|
|
58
|
+
class: _class = "",
|
|
59
|
+
speed = 70,
|
|
60
|
+
variant = "braille_bar_dot",
|
|
61
|
+
reversed = false,
|
|
62
|
+
frames,
|
|
63
|
+
}: {
|
|
64
|
+
class?: string;
|
|
65
|
+
speed?: number;
|
|
66
|
+
variant?: SpinnerUnicodeVariant;
|
|
67
|
+
reversed?: boolean;
|
|
68
|
+
frames?: string[];
|
|
69
|
+
} = $props();
|
|
70
|
+
|
|
71
|
+
const braille_bar_dot = ["⣾", "⣷", "⣯", "⣟", "⡿", "⢿", "⣻", "⣽"];
|
|
72
|
+
const braille_bar = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
73
|
+
const braille_dot_circle = ["⠁", "⠈", "⠐", "⠠", "⢀", "⡀", "⠄", "⠂"];
|
|
74
|
+
const braille_dot_bounce = ["⠈", "⠐", "⠠", "⢀", "⠠", "⠐"];
|
|
75
|
+
const half_circle = ["◐", "◓", "◑", "◒"];
|
|
76
|
+
const quarter_circle = ["◴", "◷", "◶", "◵"];
|
|
77
|
+
const ascii = ["|", "/", "-", "\\"];
|
|
78
|
+
const bar_v = ["▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃"];
|
|
79
|
+
const bar_h = ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█", "▉", "▋", "▌", "▍", "▎"];
|
|
80
|
+
const shade = ["░", "▒", "▓", "█", "▓", "▒"];
|
|
81
|
+
const arrows = ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"];
|
|
82
|
+
const arrows2 = ["⇠", "⇡", "⇢", "⇣"];
|
|
83
|
+
|
|
84
|
+
let _frames = $derived.by(() => {
|
|
85
|
+
if (frames) return frames;
|
|
86
|
+
const map: Record<SpinnerUnicodeVariant, string[]> = {
|
|
87
|
+
braille_bar,
|
|
88
|
+
braille_bar_dot,
|
|
89
|
+
braille_dot_circle,
|
|
90
|
+
braille_dot_bounce,
|
|
91
|
+
half_circle,
|
|
92
|
+
quarter_circle,
|
|
93
|
+
ascii,
|
|
94
|
+
bar_v,
|
|
95
|
+
bar_h,
|
|
96
|
+
shade,
|
|
97
|
+
arrows,
|
|
98
|
+
arrows2,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const _frames = map[variant] ?? braille_bar;
|
|
102
|
+
return reversed ? _frames.toReversed() : _frames;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let currentFrame = $state(0);
|
|
106
|
+
let timer: any;
|
|
107
|
+
let lastTime = 0;
|
|
108
|
+
|
|
109
|
+
onMount(() => {
|
|
110
|
+
const animate = (currentTime: number) => {
|
|
111
|
+
if (currentTime - lastTime >= speed) {
|
|
112
|
+
currentFrame = (currentFrame + 1) % _frames.length;
|
|
113
|
+
lastTime = currentTime;
|
|
114
|
+
}
|
|
115
|
+
timer = requestAnimationFrame(animate);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
timer = requestAnimationFrame(animate);
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
if (timer) {
|
|
122
|
+
cancelAnimationFrame(timer);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<span class={twMerge(`inline-block font-mono leading-none text-current text-xl`, _class)}>
|
|
129
|
+
{_frames[currentFrame]}
|
|
130
|
+
</span>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type SpinnerUnicodeVariant = "braille_bar" | "braille_bar_dot" | "braille_dot_circle" | "braille_dot_bounce" | "half_circle" | "quarter_circle" | "ascii" | "bar_v" | "bar_h" | "shade" | "arrows" | "arrows2";
|
|
2
|
+
export declare function spinnerCreateBackAndForthCharFrames(width: number, hiChar: string, loChar: string, glue?: string): string[];
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
class?: string;
|
|
5
|
+
speed?: number;
|
|
6
|
+
variant?: SpinnerUnicodeVariant;
|
|
7
|
+
reversed?: boolean;
|
|
8
|
+
frames?: string[];
|
|
9
|
+
};
|
|
10
|
+
declare const SpinnerUnicode: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type SpinnerUnicode = ReturnType<typeof SpinnerUnicode>;
|
|
12
|
+
export default SpinnerUnicode;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends Item">
|
|
2
|
+
import { createClog } from "@marianmeres/clog";
|
|
3
|
+
import { ItemCollection, type Item } from "@marianmeres/item-collection";
|
|
4
|
+
import { Debounced, watch } from "runed";
|
|
5
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
6
|
+
import { unaccent } from "../../utils/unaccent.js";
|
|
7
|
+
import Spinner from "../Spinner/Spinner.svelte";
|
|
8
|
+
|
|
9
|
+
const clog = createClog("TypeaheadInput");
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
input?: HTMLInputElement;
|
|
13
|
+
value: any;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
getOptions: (s: string, current: T[]) => Promise<T[]>;
|
|
16
|
+
renderOptionLabel?: (item: T) => string;
|
|
17
|
+
itemIdPropName?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
// consumer might need to differentiate between "value" and a "true submitted value"
|
|
20
|
+
// (eg. when hitting Enter)... so we have `onSubmit`. Note, that this is NOT a "form.onSubmit"
|
|
21
|
+
onSubmit?: (s: string) => void;
|
|
22
|
+
// when we hit backspace, and we are at cursor position 0... this is useful for
|
|
23
|
+
// consumer...
|
|
24
|
+
onDeleteRequest?: () => void;
|
|
25
|
+
//
|
|
26
|
+
class?: string;
|
|
27
|
+
classInput?: string;
|
|
28
|
+
noSpinner?: boolean;
|
|
29
|
+
// master disable flag not allowing to list all options on empty query
|
|
30
|
+
noListAllOnEmptyQ?: boolean;
|
|
31
|
+
// use empty string to disable hint
|
|
32
|
+
appendHint?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
input = $bindable(),
|
|
37
|
+
value = $bindable(),
|
|
38
|
+
placeholder,
|
|
39
|
+
getOptions,
|
|
40
|
+
renderOptionLabel,
|
|
41
|
+
itemIdPropName = "id",
|
|
42
|
+
name = "text_input",
|
|
43
|
+
//
|
|
44
|
+
onSubmit,
|
|
45
|
+
onDeleteRequest,
|
|
46
|
+
//
|
|
47
|
+
class: classProp,
|
|
48
|
+
classInput = "",
|
|
49
|
+
noSpinner,
|
|
50
|
+
noListAllOnEmptyQ,
|
|
51
|
+
appendHint = " [tab]",
|
|
52
|
+
}: Props = $props();
|
|
53
|
+
|
|
54
|
+
let inputEl: HTMLInputElement = $state()!;
|
|
55
|
+
const randName = "name-" + Math.random().toString(36).slice(2);
|
|
56
|
+
let isFetching = $state(false); // not used currently
|
|
57
|
+
let previousKey = $state();
|
|
58
|
+
|
|
59
|
+
// special case flag to allow listing all available options when navigating with arrows
|
|
60
|
+
// even on empty query
|
|
61
|
+
let allowListAll = $state(false);
|
|
62
|
+
|
|
63
|
+
// ItemCollection of all possible candidates based on the current query (value)
|
|
64
|
+
const options = new ItemCollection<T>([], {
|
|
65
|
+
idPropName: itemIdPropName,
|
|
66
|
+
searchable: { getContent: (item) => _renderOptionLabel(item) },
|
|
67
|
+
allowNextPrevCycle: true,
|
|
68
|
+
sortFn: (a, b) => {
|
|
69
|
+
const _a = _renderOptionLabel(a).toLowerCase();
|
|
70
|
+
const _b = _renderOptionLabel(b).toLowerCase();
|
|
71
|
+
return _a.localeCompare(_b);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// deriving the actual suggestion from available candidates...
|
|
76
|
+
let available = $derived($options);
|
|
77
|
+
let suggestion: string = $derived.by(() => {
|
|
78
|
+
if (!available.active) return "";
|
|
79
|
+
// a little dance, since we need to be case insensitive
|
|
80
|
+
// otherwise we would just: `value = _renderOptionLabel(available.active)`
|
|
81
|
+
const suggestion = _renderOptionLabel(available.active);
|
|
82
|
+
return (value || "") + suggestion.slice(value?.length || 0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
let visibleSuggestion: string = $derived.by(() => {
|
|
86
|
+
if (
|
|
87
|
+
!suggestion ||
|
|
88
|
+
unaccent(suggestion.toLowerCase()) === unaccent(value?.toLowerCase())
|
|
89
|
+
) {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return suggestion ? suggestion + appendHint : suggestion;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// helper
|
|
97
|
+
function _renderOptionLabel(item: T): string {
|
|
98
|
+
return renderOptionLabel?.(item) || `${item[itemIdPropName]}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// reset suggestion asap, even before the debounced search finishes (it feels better)
|
|
102
|
+
// the debounce will take over short after
|
|
103
|
+
watch([() => value], ([currQ], [oldQ]) => {
|
|
104
|
+
if (value === undefined) return;
|
|
105
|
+
|
|
106
|
+
// if we don't have a query or nothing is active, reset asap
|
|
107
|
+
if ((!allowListAll && !currQ) || !available.active) {
|
|
108
|
+
options.clear();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// we need to be case insensitive
|
|
113
|
+
const suggestion = _renderOptionLabel(available.active);
|
|
114
|
+
if (!suggestion.toLowerCase().startsWith(currQ.toLowerCase())) {
|
|
115
|
+
options.clear();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// do the query search
|
|
121
|
+
const debounced = new Debounced(() => value, 150);
|
|
122
|
+
watch([() => debounced.current, () => allowListAll], ([currQ, ala], [oldQ, _]) => {
|
|
123
|
+
// always start fresh
|
|
124
|
+
options.clear();
|
|
125
|
+
|
|
126
|
+
// no suggestion on empty input
|
|
127
|
+
if (!ala && !currQ) return;
|
|
128
|
+
|
|
129
|
+
isFetching = true;
|
|
130
|
+
|
|
131
|
+
getOptions(currQ, [])
|
|
132
|
+
.then((res) => {
|
|
133
|
+
// no options?
|
|
134
|
+
if (!res.length) return;
|
|
135
|
+
|
|
136
|
+
let found = res;
|
|
137
|
+
if (currQ) {
|
|
138
|
+
// so, here we have some candidate items... but, this is not a typical
|
|
139
|
+
// "word search", this is an exact, case-insensitive "string begins with",
|
|
140
|
+
// so we need to filter further...
|
|
141
|
+
found = res.filter((item) => {
|
|
142
|
+
const label = unaccent(_renderOptionLabel(item).toLowerCase());
|
|
143
|
+
return label.startsWith(unaccent(currQ.toLowerCase()));
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// no exact "starts with" found?
|
|
148
|
+
if (!found.length) return;
|
|
149
|
+
|
|
150
|
+
// finally, this is where we pick the actual suggestion (by setting it as active)
|
|
151
|
+
options.addMany(found);
|
|
152
|
+
options.setActiveFirst();
|
|
153
|
+
})
|
|
154
|
+
.catch(clog.error)
|
|
155
|
+
.finally(() => (isFetching = false));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
//
|
|
159
|
+
let _fixedInputClasses = $derived(
|
|
160
|
+
twMerge(
|
|
161
|
+
"form-input z-10 relative",
|
|
162
|
+
classInput,
|
|
163
|
+
"text-black",
|
|
164
|
+
"border-0 p-0 bg-transparent block w-full",
|
|
165
|
+
"focus:outline-0 focus:border-0 focus:ring-0",
|
|
166
|
+
"focus-visible:outline-0 focus-visible:border-0 focus-visible:ring-0"
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
function _on_submit(v: string) {
|
|
171
|
+
v = `${v || ""}`.trim();
|
|
172
|
+
if (v) onSubmit?.(v);
|
|
173
|
+
// reset this flag, next up/down arrow will switch it on again
|
|
174
|
+
allowListAll = false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// $inspect({ isFetching, value, suggestion }).with(clog);
|
|
178
|
+
// $inspect({ previousKey }).with(clog);
|
|
179
|
+
</script>
|
|
180
|
+
|
|
181
|
+
<div class={twMerge("", classProp)}>
|
|
182
|
+
<div class="flex items-center">
|
|
183
|
+
<div class="relative inline-block flex-1">
|
|
184
|
+
<input
|
|
185
|
+
type="text"
|
|
186
|
+
bind:value
|
|
187
|
+
bind:this={inputEl}
|
|
188
|
+
{name}
|
|
189
|
+
class={twMerge(_fixedInputClasses)}
|
|
190
|
+
placeholder={suggestion ? "" : placeholder}
|
|
191
|
+
autocomplete="off"
|
|
192
|
+
onkeydown={(e) => {
|
|
193
|
+
// ignore on any modifier key
|
|
194
|
+
if (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const cursorPos = inputEl.selectionStart ?? 0;
|
|
199
|
+
|
|
200
|
+
// also ignore ArrowRight if cursor is not at the end
|
|
201
|
+
if (value?.length) {
|
|
202
|
+
const maxPos = value.length;
|
|
203
|
+
// clog({ cursorPosition: pos, maxPos, value });
|
|
204
|
+
if (e.key === "ArrowRight" && cursorPos < maxPos) return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// special case Tab handling - if we hit Enter just before, we want Tab
|
|
208
|
+
// to behave normally (so we are able to set value which HAS a suggestion
|
|
209
|
+
// but is NOT a suggestion - eg "New" vs "New York")
|
|
210
|
+
if (previousKey === "Enter" && e.key === "Tab") {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// this acts as trigger for getOptions if empty value (`allowListAll` is watched)
|
|
215
|
+
allowListAll =
|
|
216
|
+
!value?.length &&
|
|
217
|
+
!noListAllOnEmptyQ &&
|
|
218
|
+
["ArrowDown", "ArrowUp"].includes(e.key);
|
|
219
|
+
|
|
220
|
+
//
|
|
221
|
+
const suggestion = options.active ? _renderOptionLabel(options.active) : null;
|
|
222
|
+
if (e.key === "ArrowDown") {
|
|
223
|
+
options.setActiveNext();
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
} else if (e.key === "ArrowUp") {
|
|
226
|
+
options.setActivePrevious();
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
} else if (["ArrowRight", "Tab"].includes(e.key) && suggestion) {
|
|
229
|
+
if (e.key === "Tab" && value !== suggestion) {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
}
|
|
232
|
+
value = suggestion;
|
|
233
|
+
if (e.key === "Tab") _on_submit(value);
|
|
234
|
+
} else if (e.key === "Enter") {
|
|
235
|
+
options.clear();
|
|
236
|
+
_on_submit(value);
|
|
237
|
+
} else if (e.key === "Backspace" && cursorPos === 0) {
|
|
238
|
+
onDeleteRequest?.();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
previousKey = e.key;
|
|
242
|
+
}}
|
|
243
|
+
onblur={() => _on_submit(value)}
|
|
244
|
+
/>
|
|
245
|
+
<input
|
|
246
|
+
type="text"
|
|
247
|
+
bind:value={visibleSuggestion}
|
|
248
|
+
class={twMerge(
|
|
249
|
+
_fixedInputClasses,
|
|
250
|
+
"absolute inset-0 pointer-events-none opacity-40 z-0"
|
|
251
|
+
)}
|
|
252
|
+
name={randName}
|
|
253
|
+
tabindex="-1"
|
|
254
|
+
readonly
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
{#if !noSpinner && isFetching}
|
|
258
|
+
<Spinner class="size-5 opacity-50" />
|
|
259
|
+
{/if}
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Item } from "@marianmeres/item-collection";
|
|
2
|
+
declare function $$render<T extends Item>(): {
|
|
3
|
+
props: {
|
|
4
|
+
input?: HTMLInputElement;
|
|
5
|
+
value: any;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
getOptions: (s: string, current: T[]) => Promise<T[]>;
|
|
8
|
+
renderOptionLabel?: (item: T) => string;
|
|
9
|
+
itemIdPropName?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
onSubmit?: (s: string) => void;
|
|
12
|
+
onDeleteRequest?: () => void;
|
|
13
|
+
class?: string;
|
|
14
|
+
classInput?: string;
|
|
15
|
+
noSpinner?: boolean;
|
|
16
|
+
noListAllOnEmptyQ?: boolean;
|
|
17
|
+
appendHint?: string;
|
|
18
|
+
};
|
|
19
|
+
exports: {};
|
|
20
|
+
bindings: "value" | "input";
|
|
21
|
+
slots: {};
|
|
22
|
+
events: {};
|
|
23
|
+
};
|
|
24
|
+
declare class __sveltets_Render<T extends Item> {
|
|
25
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
26
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
27
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
28
|
+
bindings(): "value" | "input";
|
|
29
|
+
exports(): {};
|
|
30
|
+
}
|
|
31
|
+
interface $$IsomorphicComponent {
|
|
32
|
+
new <T extends Item>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
33
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
34
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
35
|
+
<T extends Item>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
36
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
37
|
+
}
|
|
38
|
+
declare const TypeaheadInput: $$IsomorphicComponent;
|
|
39
|
+
type TypeaheadInput<T extends Item> = InstanceType<typeof TypeaheadInput<T>>;
|
|
40
|
+
export default TypeaheadInput;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as TypeaheadInput } from "./TypeaheadInput.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as TypeaheadInput } from "./TypeaheadInput.svelte";
|
package/dist/index.css
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
@import "./actions/tooltip/index.css";
|
|
6
6
|
@import "./components/Button/index.css";
|
|
7
|
+
@import "./components/ButtonGroupRadio/index.css";
|
|
7
8
|
@import "./components/DismissibleMessage/index.css";
|
|
8
9
|
@import "./components/Input/index.css";
|
|
9
10
|
@import "./components/Notifications/index.css";
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ export * from "./components/AnimatedElipsis/index.js";
|
|
|
3
3
|
export * from "./components/AppShell/index.js";
|
|
4
4
|
export * from "./components/Backdrop/index.js";
|
|
5
5
|
export * from "./components/Button/index.js";
|
|
6
|
+
export * from "./components/ButtonGroupRadio/index.js";
|
|
6
7
|
export * from "./components/ColorScheme/index.js";
|
|
8
|
+
export * from "./components/CommandMenu/index.js";
|
|
7
9
|
export * from "./components/DismissibleMessage/index.js";
|
|
8
10
|
export * from "./components/Drawer/index.js";
|
|
9
11
|
export * from "./components/HoverExpandableWidth/index.js";
|
|
@@ -16,6 +18,7 @@ export * from "./components/Progress/index.js";
|
|
|
16
18
|
export * from "./components/Spinner/index.js";
|
|
17
19
|
export * from "./components/Switch/index.js";
|
|
18
20
|
export * from "./components/TwCheck/index.js";
|
|
21
|
+
export * from "./components/TypeaheadInput/index.js";
|
|
19
22
|
export * from "./components/X/index.js";
|
|
20
23
|
export * from "./utils/index.js";
|
|
21
24
|
export * from "./actions/index.js";
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,9 @@ export * from "./components/AnimatedElipsis/index.js";
|
|
|
4
4
|
export * from "./components/AppShell/index.js";
|
|
5
5
|
export * from "./components/Backdrop/index.js";
|
|
6
6
|
export * from "./components/Button/index.js";
|
|
7
|
+
export * from "./components/ButtonGroupRadio/index.js";
|
|
7
8
|
export * from "./components/ColorScheme/index.js";
|
|
9
|
+
export * from "./components/CommandMenu/index.js";
|
|
8
10
|
export * from "./components/DismissibleMessage/index.js";
|
|
9
11
|
export * from "./components/Drawer/index.js";
|
|
10
12
|
export * from "./components/HoverExpandableWidth/index.js";
|
|
@@ -17,6 +19,7 @@ export * from "./components/Progress/index.js";
|
|
|
17
19
|
export * from "./components/Spinner/index.js";
|
|
18
20
|
export * from "./components/Switch/index.js";
|
|
19
21
|
export * from "./components/TwCheck/index.js";
|
|
22
|
+
export * from "./components/TypeaheadInput/index.js";
|
|
20
23
|
export * from "./components/X/index.js";
|
|
21
24
|
// utils
|
|
22
25
|
export * from "./utils/index.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type DataAttributes = {
|
|
2
2
|
[key: `data-${string}`]: string | number | boolean;
|
|
3
3
|
};
|
|
4
|
+
export type TranslateFn = (k: string, values?: false | null | undefined | Record<string, string | number>, fallback?: string | boolean, i18nSpanWrap?: boolean) => string;
|
|
4
5
|
export type TW_COLORS = "amber" | "blue" | "cyan" | "emerald" | "fuchsia" | "gray" | "green" | "indigo" | "lime" | "neutral" | "orange" | "pink" | "purple" | "red" | "rose" | "sky" | "slate" | "stone" | "teal" | "violet" | "yellow" | "zinc";
|
|
5
6
|
/**
|
|
6
7
|
DO NOTE REMOVE. FOOD FOR TW COMPILER...
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const escapeRegex: (str: string) => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const escapeRegex = (str) => `${str}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Sugar, Ah, Honey, Honey... */
|
|
2
|
+
export declare class EventEmitter extends EventTarget {
|
|
3
|
+
#private;
|
|
4
|
+
constructor();
|
|
5
|
+
/** Overriding so we can use removeAll (via the abortController) later... */
|
|
6
|
+
addEventListener(type: string, listener: any, // fuck it (we would need 3 signatures)
|
|
7
|
+
options?: boolean | AddEventListenerOptions): void;
|
|
8
|
+
/** Yeah! */
|
|
9
|
+
removeAllListeners(): void;
|
|
10
|
+
/** Alias for dispatchEvent(CustomEvent) */
|
|
11
|
+
emit(eventName: string, detail?: any): void;
|
|
12
|
+
/** Alias for addEventListener */
|
|
13
|
+
on(eventName: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): () => void;
|
|
14
|
+
/** Alias for removeEventListener */
|
|
15
|
+
off(eventName: string, listener: EventListenerOrEventListenerObject): this;
|
|
16
|
+
/** Alias for addEventListener with once flag */
|
|
17
|
+
once(eventName: string, listener: EventListenerOrEventListenerObject): () => void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Sugar, Ah, Honey, Honey... */
|
|
2
|
+
export class EventEmitter extends EventTarget {
|
|
3
|
+
#abortController = new AbortController();
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
}
|
|
7
|
+
/** Overriding so we can use removeAll (via the abortController) later... */
|
|
8
|
+
addEventListener(type, listener, // fuck it (we would need 3 signatures)
|
|
9
|
+
options = {}) {
|
|
10
|
+
// normalize opts (for the `once` shorthand)
|
|
11
|
+
options = typeof options === "boolean" ? { capture: options } : options;
|
|
12
|
+
// make sure to always add abort signal
|
|
13
|
+
options = { ...options, signal: this.#abortController.signal };
|
|
14
|
+
super.addEventListener(type, listener, options);
|
|
15
|
+
}
|
|
16
|
+
/** Yeah! */
|
|
17
|
+
removeAllListeners() {
|
|
18
|
+
this.#abortController.abort();
|
|
19
|
+
this.#abortController = new AbortController(); // reset for future listeners
|
|
20
|
+
}
|
|
21
|
+
/** Alias for dispatchEvent(CustomEvent) */
|
|
22
|
+
emit(eventName, detail = null) {
|
|
23
|
+
this.dispatchEvent(new CustomEvent(eventName, { detail }));
|
|
24
|
+
}
|
|
25
|
+
/** Alias for addEventListener */
|
|
26
|
+
on(eventName, listener, options) {
|
|
27
|
+
this.addEventListener(eventName, listener, options);
|
|
28
|
+
return () => this.removeEventListener(eventName, listener);
|
|
29
|
+
}
|
|
30
|
+
/** Alias for removeEventListener */
|
|
31
|
+
off(eventName, listener) {
|
|
32
|
+
this.removeEventListener(eventName, listener);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
/** Alias for addEventListener with once flag */
|
|
36
|
+
once(eventName, listener) {
|
|
37
|
+
this.addEventListener(eventName, listener, { once: true });
|
|
38
|
+
return () => this.removeEventListener(eventName, listener);
|
|
39
|
+
}
|
|
40
|
+
}
|