@marianmeres/stuic 2.66.0 → 3.0.1
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 +292 -4
- package/dist/README.md +41 -18
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/popover/README.md +19 -0
- package/dist/actions/popover/index.css +6 -9
- package/dist/actions/popover/popover.svelte.js +2 -2
- package/dist/actions/tooltip/README.md +18 -0
- package/dist/actions/tooltip/index.css +5 -8
- package/dist/actions/tooltip/tooltip.svelte.js +1 -1
- package/dist/actions/typeahead.svelte.d.ts +53 -0
- package/dist/actions/typeahead.svelte.js +328 -0
- package/dist/base.css +17 -0
- package/dist/components/AlertConfirmPrompt/AlertConfirmPrompt.svelte +10 -10
- package/dist/components/AlertConfirmPrompt/AlertConfirmPrompt.svelte.d.ts +4 -3
- package/dist/components/AlertConfirmPrompt/Current.svelte +15 -18
- package/dist/components/AlertConfirmPrompt/Current.svelte.d.ts +4 -3
- package/dist/components/AlertConfirmPrompt/acp-icons.js +5 -4
- package/dist/components/AlertConfirmPrompt/index.css +66 -0
- package/dist/components/AssetsPreview/AssetsPreview.svelte +91 -73
- package/dist/components/AssetsPreview/index.css +61 -0
- package/dist/components/Avatar/Avatar.svelte +31 -18
- package/dist/components/Avatar/README.md +166 -0
- package/dist/components/Avatar/index.css +130 -0
- package/dist/components/Backdrop/Backdrop.svelte +7 -2
- package/dist/components/Backdrop/README.md +71 -6
- package/dist/components/Backdrop/index.css +31 -0
- package/dist/components/Button/Button.svelte +116 -124
- package/dist/components/Button/Button.svelte.d.ts +35 -24
- package/dist/components/Button/README.md +87 -21
- package/dist/components/Button/index.css +475 -9
- package/dist/components/Button/index.d.ts +1 -1
- package/dist/components/Button/index.js +1 -1
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +7 -39
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte.d.ts +0 -1
- package/dist/components/ButtonGroupRadio/README.md +82 -4
- package/dist/components/ButtonGroupRadio/index.css +158 -14
- package/dist/components/Collapsible/Collapsible.svelte +7 -7
- package/dist/components/Collapsible/Collapsible.svelte.d.ts +2 -2
- package/dist/components/Collapsible/README.md +34 -2
- package/dist/components/Collapsible/index.css +40 -0
- package/dist/components/CommandMenu/CommandMenu.svelte +18 -26
- package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +0 -1
- package/dist/components/CommandMenu/README.md +39 -0
- package/dist/components/CommandMenu/index.css +47 -2
- package/dist/components/DismissibleMessage/DismissibleMessage.svelte +53 -51
- package/dist/components/DismissibleMessage/DismissibleMessage.svelte.d.ts +6 -6
- package/dist/components/DismissibleMessage/README.md +93 -11
- package/dist/components/DismissibleMessage/index.css +128 -8
- package/dist/components/DismissibleMessage/index.d.ts +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.svelte +14 -51
- package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +6 -7
- package/dist/components/DropdownMenu/README.md +132 -0
- package/dist/components/DropdownMenu/index.css +258 -52
- package/dist/components/Input/FieldAssets.svelte +8 -5
- package/dist/components/Input/FieldCheckbox.svelte +7 -44
- package/dist/components/Input/FieldFile.svelte +1 -6
- package/dist/components/Input/FieldInput.svelte +9 -1
- package/dist/components/Input/FieldInput.svelte.d.ts +2 -0
- package/dist/components/Input/FieldOptions.svelte +42 -39
- package/dist/components/Input/FieldRadios.svelte +7 -16
- package/dist/components/Input/FieldSelect.svelte +1 -1
- package/dist/components/Input/FieldSwitch.svelte +1 -5
- package/dist/components/Input/FieldTextarea.svelte +1 -1
- package/dist/components/Input/README.md +194 -0
- package/dist/components/Input/_internal/FieldRadioInternal.svelte +2 -40
- package/dist/components/Input/_internal/InputWrap.svelte +8 -48
- package/dist/components/Input/index.css +524 -116
- package/dist/components/KbdShortcut/KbdShortcut.svelte +4 -12
- package/dist/components/KbdShortcut/README.md +34 -0
- package/dist/components/KbdShortcut/index.css +55 -0
- package/dist/components/ListItemButton/ListItemButton.svelte +37 -74
- package/dist/components/ListItemButton/ListItemButton.svelte.d.ts +1 -10
- package/dist/components/ListItemButton/README.md +100 -45
- package/dist/components/ListItemButton/index.css +173 -52
- package/dist/components/ListItemButton/index.d.ts +1 -1
- package/dist/components/ListItemButton/index.js +1 -1
- package/dist/components/Modal/Modal.svelte +1 -8
- package/dist/components/Modal/README.md +29 -0
- package/dist/components/Modal/index.css +38 -0
- package/dist/components/ModalDialog/ModalDialog.svelte +2 -21
- package/dist/components/ModalDialog/README.md +35 -0
- package/dist/components/ModalDialog/index.css +59 -0
- package/dist/components/Nav/Nav.svelte +732 -0
- package/dist/components/Nav/Nav.svelte.d.ts +110 -0
- package/dist/components/Nav/README.md +334 -0
- package/dist/components/Nav/index.css +318 -0
- package/dist/components/Nav/index.d.ts +1 -0
- package/dist/components/Nav/index.js +1 -0
- package/dist/components/Notifications/Notifications.svelte +44 -129
- package/dist/components/Notifications/Notifications.svelte.d.ts +9 -18
- package/dist/components/Notifications/README.md +186 -70
- package/dist/components/Notifications/index.css +212 -15
- package/dist/components/Notifications/notifications-stack.svelte.d.ts +4 -0
- package/dist/components/Notifications/notifications-stack.svelte.js +8 -0
- package/dist/components/Progress/Progress.svelte +4 -2
- package/dist/components/Progress/Progress.svelte.d.ts +1 -0
- package/dist/components/Progress/README.md +97 -11
- package/dist/components/Progress/_internal/Bar.svelte +4 -15
- package/dist/components/Progress/_internal/Bar.svelte.d.ts +1 -1
- package/dist/components/Progress/_internal/Circle.svelte +30 -2
- package/dist/components/Progress/_internal/Circle.svelte.d.ts +1 -0
- package/dist/components/Progress/index.css +50 -4
- package/dist/components/Skeleton/README.md +152 -0
- package/dist/components/Skeleton/Skeleton.svelte +9 -9
- package/dist/components/Skeleton/Skeleton.svelte.d.ts +0 -1
- package/dist/components/Skeleton/index.css +72 -45
- package/dist/components/Spinner/README.md +149 -37
- package/dist/components/Spinner/Spinner.svelte +14 -38
- package/dist/components/Spinner/Spinner.svelte.d.ts +2 -1
- package/dist/components/Spinner/SpinnerCircle.svelte +6 -34
- package/dist/components/Spinner/SpinnerCircle.svelte.d.ts +1 -0
- package/dist/components/Spinner/SpinnerCircleOscillate.svelte +10 -5
- package/dist/components/Spinner/SpinnerUnicode.svelte +3 -1
- package/dist/components/Spinner/SpinnerUnicode.svelte.d.ts +1 -0
- package/dist/components/Spinner/index.css +104 -0
- package/dist/components/Switch/README.md +45 -14
- package/dist/components/Switch/Switch.svelte +23 -48
- package/dist/components/Switch/Switch.svelte.d.ts +4 -2
- package/dist/components/Switch/index.css +121 -4
- package/dist/components/Switch/index.d.ts +1 -2
- package/dist/components/Switch/index.js +1 -2
- package/dist/components/TabbedMenu/README.md +37 -21
- package/dist/components/TabbedMenu/TabbedMenu.svelte +5 -46
- package/dist/components/TabbedMenu/TabbedMenu.svelte.d.ts +0 -1
- package/dist/components/TabbedMenu/index.css +84 -17
- package/dist/components/ThemePreview/README.md +289 -0
- package/dist/components/ThemePreview/ThemePreview.svelte +394 -0
- package/dist/components/ThemePreview/ThemePreview.svelte.d.ts +35 -0
- package/dist/components/ThemePreview/index.css +509 -0
- package/dist/components/ThemePreview/index.d.ts +1 -0
- package/dist/components/ThemePreview/index.js +1 -0
- package/dist/components/TwCheck/README.md +32 -13
- package/dist/components/TwCheck/TwCheck.svelte +11 -9
- package/dist/components/TwCheck/TwCheck.svelte.d.ts +0 -1
- package/dist/components/TwCheck/index.css +17 -2
- package/dist/components/TypeaheadInput/TypeaheadInput.svelte +20 -188
- package/dist/components/TypeaheadInput/TypeaheadInput.svelte.d.ts +4 -2
- package/dist/components/X/X.svelte +12 -5
- package/dist/components/X/X.svelte.d.ts +1 -0
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.css +46 -26
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/themes/blue-orange.css +217 -0
- package/dist/themes/blue-orange.d.ts +6 -0
- package/dist/themes/blue-orange.js +175 -0
- package/dist/themes/cyan-red.css +217 -0
- package/dist/themes/cyan-red.d.ts +6 -0
- package/dist/themes/cyan-red.js +175 -0
- package/dist/themes/cyan-slate.css +217 -0
- package/dist/themes/cyan-slate.d.ts +6 -0
- package/dist/themes/cyan-slate.js +175 -0
- package/dist/themes/emerald-pink.css +217 -0
- package/dist/themes/emerald-pink.d.ts +6 -0
- package/dist/themes/emerald-pink.js +175 -0
- package/dist/themes/fuchsia-emerald.css +217 -0
- package/dist/themes/fuchsia-emerald.d.ts +6 -0
- package/dist/themes/fuchsia-emerald.js +175 -0
- package/dist/themes/gray.css +217 -0
- package/dist/themes/gray.d.ts +6 -0
- package/dist/themes/gray.js +175 -0
- package/dist/themes/indigo-amber.css +217 -0
- package/dist/themes/indigo-amber.d.ts +6 -0
- package/dist/themes/indigo-amber.js +175 -0
- package/dist/themes/neutral.css +217 -0
- package/dist/themes/neutral.d.ts +6 -0
- package/dist/themes/neutral.js +175 -0
- package/dist/themes/pink-emerald.css +217 -0
- package/dist/themes/pink-emerald.d.ts +6 -0
- package/dist/themes/pink-emerald.js +175 -0
- package/dist/themes/purple-yellow.css +217 -0
- package/dist/themes/purple-yellow.d.ts +6 -0
- package/dist/themes/purple-yellow.js +175 -0
- package/dist/themes/rainbow.css +217 -0
- package/dist/themes/rainbow.d.ts +6 -0
- package/dist/themes/rainbow.js +180 -0
- package/dist/themes/red-blue.css +217 -0
- package/dist/themes/red-blue.d.ts +6 -0
- package/dist/themes/red-blue.js +175 -0
- package/dist/themes/red-cyan.css +217 -0
- package/dist/themes/red-cyan.d.ts +6 -0
- package/dist/themes/red-cyan.js +175 -0
- package/dist/themes/rose-teal.css +217 -0
- package/dist/themes/rose-teal.d.ts +6 -0
- package/dist/themes/rose-teal.js +175 -0
- package/dist/themes/sky-amber.css +217 -0
- package/dist/themes/sky-amber.d.ts +6 -0
- package/dist/themes/sky-amber.js +175 -0
- package/dist/themes/slate-cyan.css +217 -0
- package/dist/themes/slate-cyan.d.ts +6 -0
- package/dist/themes/slate-cyan.js +175 -0
- package/dist/themes/tailwind-color-pairs.md +31 -0
- package/dist/themes/teal-rose.css +217 -0
- package/dist/themes/teal-rose.d.ts +6 -0
- package/dist/themes/teal-rose.js +175 -0
- package/dist/themes/violet-lime.css +217 -0
- package/dist/themes/violet-lime.d.ts +6 -0
- package/dist/themes/violet-lime.js +175 -0
- package/dist/utils/design-tokens.d.ts +43 -0
- package/dist/utils/design-tokens.js +127 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/storage-abstraction.js +1 -1
- package/package.json +14 -11
- package/dist/components/Switch/SwitchButton.svelte +0 -135
- package/dist/components/Switch/SwitchButton.svelte.d.ts +0 -21
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { ItemCollection } from "@marianmeres/item-collection";
|
|
2
|
+
import { unaccent } from "../utils/unaccent.js";
|
|
3
|
+
import { createClog } from "@marianmeres/clog";
|
|
4
|
+
const DEFAULT_DEBOUNCE_MS = 150;
|
|
5
|
+
const DEFAULT_APPEND_HINT = " [tab]";
|
|
6
|
+
const clog = createClog("typeahead", { color: "auto" });
|
|
7
|
+
/**
|
|
8
|
+
* A Svelte action that adds typeahead/autocomplete functionality to an input element.
|
|
9
|
+
*
|
|
10
|
+
* Creates a "ghost input" behind the main input to show suggestions. Supports keyboard
|
|
11
|
+
* navigation with ArrowUp/Down, accepts suggestions with Tab/Enter/ArrowRight, and
|
|
12
|
+
* handles accent-insensitive matching.
|
|
13
|
+
*
|
|
14
|
+
* @param el - The input element to add typeahead to
|
|
15
|
+
* @param fn - Function returning configuration options
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```svelte
|
|
19
|
+
* <input
|
|
20
|
+
* bind:value
|
|
21
|
+
* use:typeahead={() => ({
|
|
22
|
+
* getOptions: async (q) => fetchSuggestions(q),
|
|
23
|
+
* renderOptionLabel: (item) => item.name,
|
|
24
|
+
* onSubmit: (v) => console.log('Selected:', v),
|
|
25
|
+
* })}
|
|
26
|
+
* />
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function typeahead(el, fn) {
|
|
30
|
+
let ghostEl = null;
|
|
31
|
+
let options = null;
|
|
32
|
+
let debounceTimer = null;
|
|
33
|
+
let previousKey = "";
|
|
34
|
+
let allowListAll = false;
|
|
35
|
+
// Current resolved options
|
|
36
|
+
let currentOpts = { getOptions: async () => [] };
|
|
37
|
+
// Helper: render option label
|
|
38
|
+
function renderLabel(item) {
|
|
39
|
+
return (currentOpts.renderOptionLabel?.(item) ??
|
|
40
|
+
`${item[currentOpts.itemIdPropName || "id"]}`);
|
|
41
|
+
}
|
|
42
|
+
// Helper: get current suggestion string
|
|
43
|
+
function getSuggestion() {
|
|
44
|
+
const active = options?.active;
|
|
45
|
+
if (!active)
|
|
46
|
+
return "";
|
|
47
|
+
const value = el.value || "";
|
|
48
|
+
const suggestion = renderLabel(active);
|
|
49
|
+
// Preserve user's case, append rest from suggestion
|
|
50
|
+
return value + suggestion.slice(value.length);
|
|
51
|
+
}
|
|
52
|
+
// Helper: get visible suggestion (with hint, or empty if matches value)
|
|
53
|
+
function getVisibleSuggestion() {
|
|
54
|
+
const suggestion = getSuggestion();
|
|
55
|
+
const value = el.value || "";
|
|
56
|
+
if (!suggestion ||
|
|
57
|
+
unaccent(suggestion.toLowerCase()) === unaccent(value.toLowerCase())) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
const hint = currentOpts.appendHint ?? DEFAULT_APPEND_HINT;
|
|
61
|
+
return suggestion ? suggestion + hint : "";
|
|
62
|
+
}
|
|
63
|
+
// Update ghost input value
|
|
64
|
+
function updateGhost() {
|
|
65
|
+
if (ghostEl) {
|
|
66
|
+
ghostEl.value = getVisibleSuggestion();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Create ghost input element
|
|
70
|
+
function createGhost() {
|
|
71
|
+
if (ghostEl)
|
|
72
|
+
return;
|
|
73
|
+
ghostEl = document.createElement("input");
|
|
74
|
+
ghostEl.type = "text";
|
|
75
|
+
ghostEl.tabIndex = -1;
|
|
76
|
+
ghostEl.readOnly = true;
|
|
77
|
+
ghostEl.setAttribute("aria-hidden", "true");
|
|
78
|
+
ghostEl.setAttribute("autocomplete", "off");
|
|
79
|
+
// Ensure parent has relative positioning
|
|
80
|
+
const parent = el.parentElement;
|
|
81
|
+
if (parent) {
|
|
82
|
+
const parentPos = getComputedStyle(parent).position;
|
|
83
|
+
if (parentPos === "static") {
|
|
84
|
+
parent.style.position = "relative";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Style main input for proper layering
|
|
88
|
+
el.style.position = "relative";
|
|
89
|
+
el.style.zIndex = "10";
|
|
90
|
+
el.style.background = "transparent";
|
|
91
|
+
// Style ghost input
|
|
92
|
+
applyGhostStyles();
|
|
93
|
+
// Insert after main input
|
|
94
|
+
el.insertAdjacentElement("afterend", ghostEl);
|
|
95
|
+
}
|
|
96
|
+
function applyGhostStyles() {
|
|
97
|
+
if (!ghostEl)
|
|
98
|
+
return;
|
|
99
|
+
const computed = getComputedStyle(el);
|
|
100
|
+
// Copy relevant styles from main input
|
|
101
|
+
ghostEl.style.position = "absolute";
|
|
102
|
+
ghostEl.style.inset = "0";
|
|
103
|
+
ghostEl.style.pointerEvents = "none";
|
|
104
|
+
ghostEl.style.opacity = "0.4";
|
|
105
|
+
ghostEl.style.zIndex = "0";
|
|
106
|
+
ghostEl.style.font = computed.font;
|
|
107
|
+
ghostEl.style.padding = computed.padding;
|
|
108
|
+
ghostEl.style.borderColor = "transparent";
|
|
109
|
+
ghostEl.style.background = "transparent";
|
|
110
|
+
ghostEl.style.color = "inherit";
|
|
111
|
+
ghostEl.style.outline = "none";
|
|
112
|
+
ghostEl.style.width = "100%";
|
|
113
|
+
ghostEl.style.boxSizing = "border-box";
|
|
114
|
+
ghostEl.style.transition = "none";
|
|
115
|
+
// Apply custom class if provided
|
|
116
|
+
if (currentOpts.classGhost) {
|
|
117
|
+
ghostEl.className = currentOpts.classGhost;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Debounced search
|
|
121
|
+
function scheduleSearch(query) {
|
|
122
|
+
if (debounceTimer)
|
|
123
|
+
clearTimeout(debounceTimer);
|
|
124
|
+
debounceTimer = setTimeout(async () => {
|
|
125
|
+
await performSearch(query);
|
|
126
|
+
}, currentOpts.debounceMs ?? DEFAULT_DEBOUNCE_MS);
|
|
127
|
+
}
|
|
128
|
+
async function performSearch(query) {
|
|
129
|
+
if (!options || !currentOpts.getOptions)
|
|
130
|
+
return;
|
|
131
|
+
options.clear();
|
|
132
|
+
if (!allowListAll && !query) {
|
|
133
|
+
updateGhost();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
currentOpts.onFetchingChange?.(true);
|
|
137
|
+
try {
|
|
138
|
+
const results = await currentOpts.getOptions(query, []);
|
|
139
|
+
if (!results.length) {
|
|
140
|
+
updateGhost();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// Filter for exact "starts with" match
|
|
144
|
+
let found = results;
|
|
145
|
+
if (query) {
|
|
146
|
+
found = results.filter((item) => {
|
|
147
|
+
const label = unaccent(renderLabel(item).toLowerCase());
|
|
148
|
+
return label.startsWith(unaccent(query.toLowerCase()));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (!found.length) {
|
|
152
|
+
updateGhost();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
options.addMany(found);
|
|
156
|
+
options.setActiveFirst();
|
|
157
|
+
updateGhost();
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
clog.error(err);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
currentOpts.onFetchingChange?.(false);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Submit handler
|
|
167
|
+
function handleSubmit(value) {
|
|
168
|
+
value = (value || "").trim();
|
|
169
|
+
if (value) {
|
|
170
|
+
currentOpts.onSubmit?.(value);
|
|
171
|
+
}
|
|
172
|
+
allowListAll = false;
|
|
173
|
+
}
|
|
174
|
+
// Set input value (triggers Svelte binding update via input event)
|
|
175
|
+
function setInputValue(newValue) {
|
|
176
|
+
el.value = newValue;
|
|
177
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
178
|
+
}
|
|
179
|
+
// Keyboard handler
|
|
180
|
+
function onKeyDown(e) {
|
|
181
|
+
// Ignore with modifier keys
|
|
182
|
+
if (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey)
|
|
183
|
+
return;
|
|
184
|
+
const cursorPos = el.selectionStart ?? 0;
|
|
185
|
+
const value = el.value || "";
|
|
186
|
+
// ArrowRight only applies if cursor at end
|
|
187
|
+
if (value.length && e.key === "ArrowRight" && cursorPos < value.length) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Tab after Enter should behave normally
|
|
191
|
+
if (previousKey === "Enter" && e.key === "Tab") {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Allow listing all on arrow keys with empty value
|
|
195
|
+
allowListAll =
|
|
196
|
+
!value.length &&
|
|
197
|
+
!currentOpts.noListAllOnEmptyQ &&
|
|
198
|
+
["ArrowDown", "ArrowUp"].includes(e.key);
|
|
199
|
+
if (allowListAll) {
|
|
200
|
+
scheduleSearch("");
|
|
201
|
+
}
|
|
202
|
+
const suggestion = options?.active ? renderLabel(options.active) : null;
|
|
203
|
+
if (e.key === "ArrowDown") {
|
|
204
|
+
options?.setActiveNext();
|
|
205
|
+
updateGhost();
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
}
|
|
208
|
+
else if (e.key === "ArrowUp") {
|
|
209
|
+
options?.setActivePrevious();
|
|
210
|
+
updateGhost();
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
}
|
|
213
|
+
else if (["ArrowRight", "Tab"].includes(e.key) && suggestion) {
|
|
214
|
+
if (e.key === "Tab" && value !== suggestion) {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
}
|
|
217
|
+
setInputValue(suggestion);
|
|
218
|
+
// Clear options and ghost immediately after accepting suggestion
|
|
219
|
+
options?.clear();
|
|
220
|
+
updateGhost();
|
|
221
|
+
if (e.key === "Tab")
|
|
222
|
+
handleSubmit(suggestion);
|
|
223
|
+
}
|
|
224
|
+
else if (e.key === "Enter") {
|
|
225
|
+
options?.clear();
|
|
226
|
+
handleSubmit(value);
|
|
227
|
+
}
|
|
228
|
+
else if (e.key === "Backspace" && cursorPos === 0) {
|
|
229
|
+
currentOpts.onDeleteRequest?.();
|
|
230
|
+
}
|
|
231
|
+
previousKey = e.key;
|
|
232
|
+
}
|
|
233
|
+
// Input handler (for value changes)
|
|
234
|
+
function onInput() {
|
|
235
|
+
const value = el.value || "";
|
|
236
|
+
// Quick clear if needed
|
|
237
|
+
if ((!allowListAll && !value) || !options?.active) {
|
|
238
|
+
options?.clear();
|
|
239
|
+
updateGhost();
|
|
240
|
+
}
|
|
241
|
+
else if (options?.active) {
|
|
242
|
+
// Check if current suggestion still matches
|
|
243
|
+
const suggestion = renderLabel(options.active);
|
|
244
|
+
if (!suggestion.toLowerCase().startsWith(value.toLowerCase())) {
|
|
245
|
+
options.clear();
|
|
246
|
+
updateGhost();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Schedule new search
|
|
250
|
+
scheduleSearch(value);
|
|
251
|
+
}
|
|
252
|
+
// Blur handler
|
|
253
|
+
function onBlur() {
|
|
254
|
+
handleSubmit(el.value || "");
|
|
255
|
+
}
|
|
256
|
+
// Remove ghost and cleanup
|
|
257
|
+
function destroyGhost() {
|
|
258
|
+
ghostEl?.remove();
|
|
259
|
+
ghostEl = null;
|
|
260
|
+
// Reset main input styles
|
|
261
|
+
el.style.position = "";
|
|
262
|
+
el.style.zIndex = "";
|
|
263
|
+
el.style.background = "";
|
|
264
|
+
}
|
|
265
|
+
// Main effect for setup/cleanup
|
|
266
|
+
$effect(() => {
|
|
267
|
+
const opts = fn?.() || { getOptions: async () => [] };
|
|
268
|
+
currentOpts = {
|
|
269
|
+
enabled: opts.enabled ?? true,
|
|
270
|
+
getOptions: opts.getOptions,
|
|
271
|
+
renderOptionLabel: opts.renderOptionLabel,
|
|
272
|
+
itemIdPropName: opts.itemIdPropName || "id",
|
|
273
|
+
debounceMs: opts.debounceMs ?? DEFAULT_DEBOUNCE_MS,
|
|
274
|
+
onSubmit: opts.onSubmit,
|
|
275
|
+
onDeleteRequest: opts.onDeleteRequest,
|
|
276
|
+
onFetchingChange: opts.onFetchingChange,
|
|
277
|
+
noListAllOnEmptyQ: opts.noListAllOnEmptyQ ?? false,
|
|
278
|
+
appendHint: opts.appendHint ?? DEFAULT_APPEND_HINT,
|
|
279
|
+
classGhost: opts.classGhost,
|
|
280
|
+
onResetGhost: opts.onResetGhost,
|
|
281
|
+
};
|
|
282
|
+
if (!currentOpts.enabled || !currentOpts.getOptions) {
|
|
283
|
+
// Cleanup if disabled or no getOptions provided
|
|
284
|
+
destroyGhost();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Initialize ItemCollection if needed
|
|
288
|
+
if (!options) {
|
|
289
|
+
options = new ItemCollection([], {
|
|
290
|
+
idPropName: currentOpts.itemIdPropName,
|
|
291
|
+
searchable: { getContent: (item) => renderLabel(item) },
|
|
292
|
+
allowNextPrevCycle: true,
|
|
293
|
+
sortFn: (a, b) => {
|
|
294
|
+
const _a = renderLabel(a).toLowerCase();
|
|
295
|
+
const _b = renderLabel(b).toLowerCase();
|
|
296
|
+
return _a.localeCompare(_b);
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
// Create ghost input
|
|
301
|
+
createGhost();
|
|
302
|
+
// Provide reset function via callback
|
|
303
|
+
currentOpts.onResetGhost?.(() => {
|
|
304
|
+
// Cancel any pending search
|
|
305
|
+
if (debounceTimer) {
|
|
306
|
+
clearTimeout(debounceTimer);
|
|
307
|
+
debounceTimer = null;
|
|
308
|
+
}
|
|
309
|
+
options?.clear();
|
|
310
|
+
updateGhost();
|
|
311
|
+
});
|
|
312
|
+
// Add event listeners
|
|
313
|
+
el.addEventListener("keydown", onKeyDown);
|
|
314
|
+
el.addEventListener("input", onInput);
|
|
315
|
+
el.addEventListener("blur", onBlur);
|
|
316
|
+
// Set autocomplete off
|
|
317
|
+
el.setAttribute("autocomplete", "off");
|
|
318
|
+
return () => {
|
|
319
|
+
// Cleanup
|
|
320
|
+
el.removeEventListener("keydown", onKeyDown);
|
|
321
|
+
el.removeEventListener("input", onInput);
|
|
322
|
+
el.removeEventListener("blur", onBlur);
|
|
323
|
+
destroyGhost();
|
|
324
|
+
if (debounceTimer)
|
|
325
|
+
clearTimeout(debounceTimer);
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
}
|
package/dist/base.css
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* Base styles for STUIC components */
|
|
2
|
+
@layer base {
|
|
3
|
+
button:not(:disabled),
|
|
4
|
+
[role="button"]:not(:disabled) {
|
|
5
|
+
cursor: pointer;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
button:disabled,
|
|
9
|
+
[role="button"]:disabled,
|
|
10
|
+
input:disabled {
|
|
11
|
+
cursor: not-allowed !important;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.scrollbar-thin {
|
|
16
|
+
scrollbar-width: thin;
|
|
17
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
import type { AlertConfirmPromptStack } from "./alert-confirm-prompt-stack.svelte.js";
|
|
3
|
+
import type { IntentColorKey } from "../../utils/design-tokens.js";
|
|
3
4
|
|
|
4
5
|
export interface Props {
|
|
5
6
|
acp?: AlertConfirmPromptStack;
|
|
@@ -21,9 +22,9 @@
|
|
|
21
22
|
classButtonCancel?: string;
|
|
22
23
|
classButtonCustom?: string;
|
|
23
24
|
classButtonPrimary?: string;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
intentButtonCancel?: IntentColorKey;
|
|
26
|
+
intentButtonCustom?: IntentColorKey;
|
|
27
|
+
intentButtonPrimary?: IntentColorKey;
|
|
27
28
|
classSpinnerBox?: string;
|
|
28
29
|
defaultIcons?: Partial<
|
|
29
30
|
Record<"info" | "success" | "warn" | "error" | "spinner", () => string | undefined>
|
|
@@ -34,7 +35,6 @@
|
|
|
34
35
|
<script lang="ts">
|
|
35
36
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
36
37
|
import ModalDialog from "../ModalDialog/ModalDialog.svelte";
|
|
37
|
-
import { isTHCNotEmpty } from "../Thc/Thc.svelte";
|
|
38
38
|
import { AlertConfirmPromptType } from "./alert-confirm-prompt-stack.svelte.js";
|
|
39
39
|
import Current from "./Current.svelte";
|
|
40
40
|
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
classButtonCancel,
|
|
64
64
|
classButtonCustom,
|
|
65
65
|
classButtonPrimary,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
intentButtonCancel,
|
|
67
|
+
intentButtonCustom,
|
|
68
|
+
intentButtonPrimary,
|
|
69
69
|
}: Props = $props();
|
|
70
70
|
|
|
71
71
|
let modal = $state<ModalDialog>();
|
|
@@ -124,9 +124,9 @@
|
|
|
124
124
|
{classButtonCancel}
|
|
125
125
|
{classButtonCustom}
|
|
126
126
|
{classButtonPrimary}
|
|
127
|
-
{
|
|
128
|
-
{
|
|
129
|
-
{
|
|
127
|
+
{intentButtonCancel}
|
|
128
|
+
{intentButtonCustom}
|
|
129
|
+
{intentButtonPrimary}
|
|
130
130
|
/>
|
|
131
131
|
</ModalDialog>
|
|
132
132
|
{/if}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AlertConfirmPromptStack } from "./alert-confirm-prompt-stack.svelte.js";
|
|
2
|
+
import type { IntentColorKey } from "../../utils/design-tokens.js";
|
|
2
3
|
export interface Props {
|
|
3
4
|
acp?: AlertConfirmPromptStack;
|
|
4
5
|
forceAsHtml?: boolean;
|
|
@@ -19,9 +20,9 @@ export interface Props {
|
|
|
19
20
|
classButtonCancel?: string;
|
|
20
21
|
classButtonCustom?: string;
|
|
21
22
|
classButtonPrimary?: string;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
intentButtonCancel?: IntentColorKey;
|
|
24
|
+
intentButtonCustom?: IntentColorKey;
|
|
25
|
+
intentButtonPrimary?: IntentColorKey;
|
|
25
26
|
classSpinnerBox?: string;
|
|
26
27
|
defaultIcons?: Partial<Record<"info" | "success" | "warn" | "error" | "spinner", () => string | undefined>>;
|
|
27
28
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import type { IntentColorKey } from "../../utils/design-tokens.js";
|
|
3
3
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
4
4
|
import Button from "../Button/Button.svelte";
|
|
5
5
|
import FieldInput from "../Input/FieldInput.svelte";
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
classButtonCancel?: string;
|
|
38
38
|
classButtonCustom?: string;
|
|
39
39
|
classButtonPrimary?: string;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
intentButtonCancel?: IntentColorKey;
|
|
41
|
+
intentButtonCustom?: IntentColorKey;
|
|
42
|
+
intentButtonPrimary?: IntentColorKey;
|
|
43
43
|
classSpinnerBox?: string;
|
|
44
44
|
defaultIcons?: Partial<
|
|
45
45
|
Record<"info" | "success" | "warn" | "error" | "spinner", () => string | undefined>
|
|
@@ -66,9 +66,9 @@
|
|
|
66
66
|
classButtonCancel,
|
|
67
67
|
classButtonCustom,
|
|
68
68
|
classButtonPrimary,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
intentButtonCancel,
|
|
70
|
+
intentButtonCustom,
|
|
71
|
+
intentButtonPrimary = "primary",
|
|
72
72
|
classSpinnerBox,
|
|
73
73
|
defaultIcons = acpDefaultIcons,
|
|
74
74
|
}: Props = $props();
|
|
@@ -119,11 +119,8 @@
|
|
|
119
119
|
|
|
120
120
|
const _classIconBox = `size-12 sm:size-10
|
|
121
121
|
mt-1 mb-4 sm:my-0 sm:mr-5
|
|
122
|
-
mx-auto
|
|
123
|
-
flex flex-shrink-0 items-center justify-center
|
|
124
|
-
rounded-full
|
|
125
|
-
bg-neutral-950/10 text-neutral-950/80
|
|
126
|
-
dark:bg-neutral-50/20 dark:text-neutral-50/80`;
|
|
122
|
+
mx-auto
|
|
123
|
+
flex flex-shrink-0 items-center justify-center`;
|
|
127
124
|
|
|
128
125
|
const _classContentBox = `mt-3 sm:mt-0 flex-1 h-full flex flex-col`; // overflow-hidden
|
|
129
126
|
|
|
@@ -137,12 +134,12 @@
|
|
|
137
134
|
|
|
138
135
|
const _classButton = "min-w-24 text-center w-full sm:w-auto";
|
|
139
136
|
|
|
140
|
-
const _classSpinnerBox = `absolute inset-0 flex items-center justify-center
|
|
137
|
+
const _classSpinnerBox = `absolute inset-0 flex items-center justify-center`;
|
|
141
138
|
|
|
142
139
|
let hasCustom = $derived(current.labelCustom && typeof current.onCustom === "function");
|
|
143
140
|
</script>
|
|
144
141
|
|
|
145
|
-
<div class={twMerge("stuic-acp", _class, classProp)}>
|
|
142
|
+
<div class={twMerge("stuic-acp", _class, classProp)} data-variant={current.variant}>
|
|
146
143
|
<div class={twMerge("wrap", _classWrap, classWrap)}>
|
|
147
144
|
{#if iconHtml}
|
|
148
145
|
<div
|
|
@@ -215,7 +212,7 @@
|
|
|
215
212
|
<li class={twMerge(_classMenuLi, classMenuLi, hasCustom && classMenuLiCustom)}>
|
|
216
213
|
<CmpButtonCancel
|
|
217
214
|
class={twMerge("cancel", _classButton, classButton, classButtonCancel)}
|
|
218
|
-
|
|
215
|
+
intent={intentButtonCancel}
|
|
219
216
|
disabled={isPending}
|
|
220
217
|
onclick={createOnClick("cancel", current.onCancel)}
|
|
221
218
|
>
|
|
@@ -227,7 +224,7 @@
|
|
|
227
224
|
<li class={twMerge(_classMenuLi, classMenuLi, classMenuLiCustom)}>
|
|
228
225
|
<CmpButtonCustom
|
|
229
226
|
class={twMerge("custom", _classButton, classButton, classButtonCustom)}
|
|
230
|
-
|
|
227
|
+
intent={intentButtonCustom}
|
|
231
228
|
disabled={isPending}
|
|
232
229
|
onclick={createOnClick("custom", current.onCustom!)}
|
|
233
230
|
>
|
|
@@ -238,7 +235,7 @@
|
|
|
238
235
|
<li class={twMerge(_classMenuLi, classMenuLi, hasCustom && classMenuLiCustom)}>
|
|
239
236
|
<CmpButtonOk
|
|
240
237
|
class={twMerge("ok", _classButton, classButton, classButtonPrimary)}
|
|
241
|
-
|
|
238
|
+
intent={intentButtonPrimary}
|
|
242
239
|
disabled={isPending}
|
|
243
240
|
onclick={createOnClick("ok", current.onOk)}
|
|
244
241
|
bind:el={okButtonEl}
|
|
@@ -249,7 +246,7 @@
|
|
|
249
246
|
</menu>
|
|
250
247
|
{#if isPending}
|
|
251
248
|
<div class={twMerge("spinner-box", _classSpinnerBox, classSpinnerBox)}>
|
|
252
|
-
<Spinner
|
|
249
|
+
<Spinner />
|
|
253
250
|
</div>
|
|
254
251
|
{/if}
|
|
255
252
|
</div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IntentColorKey } from "../../utils/design-tokens.js";
|
|
1
2
|
import { type AlertConfirmPromptStack } from "./alert-confirm-prompt-stack.svelte.js";
|
|
2
3
|
interface Props {
|
|
3
4
|
acp?: AlertConfirmPromptStack;
|
|
@@ -19,9 +20,9 @@ interface Props {
|
|
|
19
20
|
classButtonCancel?: string;
|
|
20
21
|
classButtonCustom?: string;
|
|
21
22
|
classButtonPrimary?: string;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
intentButtonCancel?: IntentColorKey;
|
|
24
|
+
intentButtonCustom?: IntentColorKey;
|
|
25
|
+
intentButtonPrimary?: IntentColorKey;
|
|
25
26
|
classSpinnerBox?: string;
|
|
26
27
|
defaultIcons?: Partial<Record<"info" | "success" | "warn" | "error" | "spinner", () => string | undefined>>;
|
|
27
28
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { iconAlertInfo, iconAlertSuccess, iconAlertWarning, iconAlertError, iconRefresh, } from "../../icons/index.js";
|
|
2
|
+
const size = 23;
|
|
2
3
|
export const acpDefaultIcons = {
|
|
3
|
-
info: () => iconAlertInfo({ size
|
|
4
|
-
success: () => iconAlertSuccess({}),
|
|
5
|
-
warn: () => iconAlertWarning({ class: "-mt-[
|
|
6
|
-
error: () => iconAlertError({}),
|
|
4
|
+
info: () => iconAlertInfo({ size }),
|
|
5
|
+
success: () => iconAlertSuccess({ size }),
|
|
6
|
+
warn: () => iconAlertWarning({ size, class: "-mt-[2px]" }), // move up a little because it looks better with the triangle
|
|
7
|
+
error: () => iconAlertError({ size }),
|
|
7
8
|
spinner: () => iconRefresh({ size: 32, class: "opacity-50" }),
|
|
8
9
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
ALERT CONFIRM PROMPT COMPONENT TOKENS
|
|
3
|
+
Override globally: :root { --stuic-acp-icon-box-radius: 0; }
|
|
4
|
+
Override locally: <AlertConfirmPrompt style="--stuic-acp-icon-box-radius: 4px;">
|
|
5
|
+
============================================================================ */
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--stuic-acp-icon-box-radius: 9999px;
|
|
9
|
+
--stuic-acp-spinner-overlay-opacity: 0.8;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@layer components {
|
|
13
|
+
/* ============================================================================
|
|
14
|
+
ICON BOX - colors set by variant
|
|
15
|
+
============================================================================ */
|
|
16
|
+
|
|
17
|
+
.stuic-acp .icon-box {
|
|
18
|
+
background: var(--_icon-bg);
|
|
19
|
+
color: var(--_icon-text);
|
|
20
|
+
border-radius: var(--stuic-acp-icon-box-radius);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ============================================================================
|
|
24
|
+
SPINNER OVERLAY - uses surface color
|
|
25
|
+
============================================================================ */
|
|
26
|
+
|
|
27
|
+
.stuic-acp .spinner-box {
|
|
28
|
+
background: color-mix(
|
|
29
|
+
in srgb,
|
|
30
|
+
var(--stuic-color-surface) calc(var(--stuic-acp-spinner-overlay-opacity) * 100%),
|
|
31
|
+
transparent
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ============================================================================
|
|
36
|
+
VARIANT COLOR MAPPING
|
|
37
|
+
Each variant sets internal CSS vars for the icon box.
|
|
38
|
+
============================================================================ */
|
|
39
|
+
|
|
40
|
+
/* NOTE: using primary in all except error */
|
|
41
|
+
|
|
42
|
+
/* Variant: info (default) */
|
|
43
|
+
.stuic-acp[data-variant="info"],
|
|
44
|
+
.stuic-acp:not([data-variant]) {
|
|
45
|
+
--_icon-bg: color-mix(in srgb, var(--stuic-color-primary) 15%, transparent);
|
|
46
|
+
--_icon-text: var(--stuic-color-primary);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Variant: success */
|
|
50
|
+
.stuic-acp[data-variant="success"] {
|
|
51
|
+
--_icon-bg: color-mix(in srgb, var(--stuic-color-primary) 15%, transparent);
|
|
52
|
+
--_icon-text: var(--stuic-color-primary);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Variant: warn */
|
|
56
|
+
.stuic-acp[data-variant="warn"] {
|
|
57
|
+
--_icon-bg: color-mix(in srgb, var(--stuic-color-primary) 15%, transparent);
|
|
58
|
+
--_icon-text: var(--stuic-color-primary);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Variant: error */
|
|
62
|
+
.stuic-acp[data-variant="error"] {
|
|
63
|
+
--_icon-bg: color-mix(in srgb, var(--stuic-color-destructive) 15%, transparent);
|
|
64
|
+
--_icon-text: var(--stuic-color-destructive);
|
|
65
|
+
}
|
|
66
|
+
}
|