@rettangoli/ui 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/rettangoli-iife-layout.min.js +1 -1
- package/dist/rettangoli-iife-ui.min.js +56 -56
- package/package.json +2 -2
- package/src/components/dropdownMenu/dropdownMenu.view.yaml +5 -5
- package/src/components/globalUi/globalUi.handlers.js +92 -1
- package/src/components/globalUi/globalUi.store.js +66 -0
- package/src/components/globalUi/globalUi.view.yaml +37 -0
- package/src/components/select/select.schema.yaml +6 -0
- package/src/components/select/select.store.js +49 -9
- package/src/components/select/select.view.yaml +24 -5
- package/src/deps/createGlobalUI.js +21 -2
- package/src/primitives/popover.js +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A UI component library for building web interfaces.",
|
|
5
5
|
"main": "dist/rettangoli-esm.min.js",
|
|
6
6
|
"type": "module",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"homepage": "https://github.com/yuusoft-org/rettangoli#readme",
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@rettangoli/fe": "1.1.
|
|
65
|
+
"@rettangoli/fe": "1.1.3",
|
|
66
66
|
"jempl": "1.0.1"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -22,7 +22,7 @@ template:
|
|
|
22
22
|
- $for item, i in items:
|
|
23
23
|
- $if item.isLabel:
|
|
24
24
|
- rtgl-view w=f p=md:
|
|
25
|
-
- rtgl-text s=
|
|
25
|
+
- rtgl-text s=xs c=mu-fg: ${item.label}
|
|
26
26
|
$elif item.isItem && item.isDisabled:
|
|
27
27
|
- rtgl-view w=f p=md bgc=${item.bgc}:
|
|
28
28
|
- rtgl-view d=h av=c w=f g=md:
|
|
@@ -34,7 +34,7 @@ template:
|
|
|
34
34
|
- div class=icon-placeholder aria-hidden="true": null
|
|
35
35
|
- rtgl-text s=sm c=${item.c} w=1fg ellipsis: ${item.label}
|
|
36
36
|
- $if item.hasSuffixText:
|
|
37
|
-
- rtgl-text s=
|
|
37
|
+
- rtgl-text s=xs c=${item.suffixTextColor} ta=e: ${item.suffixText}
|
|
38
38
|
$elif item.isItem && item.hasHref && item.linkExtraAttrs:
|
|
39
39
|
- a#option${i} href=${item.href} ${item.linkExtraAttrs} data-index=${item.index} data-testid=${item.testId}:
|
|
40
40
|
- rtgl-view w=f h-bgc=${item.hoverBgc} p=md cur=pointer bgc=${item.bgc}:
|
|
@@ -47,7 +47,7 @@ template:
|
|
|
47
47
|
- div class=icon-placeholder aria-hidden="true": null
|
|
48
48
|
- rtgl-text s=sm c=${item.c} w=1fg ellipsis: ${item.label}
|
|
49
49
|
- $if item.hasSuffixText:
|
|
50
|
-
- rtgl-text s=
|
|
50
|
+
- rtgl-text s=xs c=${item.suffixTextColor} ta=e: ${item.suffixText}
|
|
51
51
|
$elif item.isItem && item.hasHref:
|
|
52
52
|
- a#option${i} href=${item.href} data-index=${item.index} data-testid=${item.testId}:
|
|
53
53
|
- rtgl-view w=f h-bgc=${item.hoverBgc} p=md cur=pointer bgc=${item.bgc}:
|
|
@@ -60,7 +60,7 @@ template:
|
|
|
60
60
|
- div class=icon-placeholder aria-hidden="true": null
|
|
61
61
|
- rtgl-text s=sm c=${item.c} w=1fg ellipsis: ${item.label}
|
|
62
62
|
- $if item.hasSuffixText:
|
|
63
|
-
- rtgl-text s=
|
|
63
|
+
- rtgl-text s=xs c=${item.suffixTextColor} ta=e: ${item.suffixText}
|
|
64
64
|
$elif item.isItem:
|
|
65
65
|
- rtgl-view#option${i} w=f h-bgc=${item.hoverBgc} p=md cur=pointer bgc=${item.bgc} data-index=${item.index} data-testid=${item.testId}:
|
|
66
66
|
- rtgl-view d=h av=c w=f g=md:
|
|
@@ -72,6 +72,6 @@ template:
|
|
|
72
72
|
- div class=icon-placeholder aria-hidden="true": null
|
|
73
73
|
- rtgl-text s=sm c=${item.c} w=1fg ellipsis: ${item.label}
|
|
74
74
|
- $if item.hasSuffixText:
|
|
75
|
-
- rtgl-text s=
|
|
75
|
+
- rtgl-text s=xs c=${item.suffixTextColor} ta=e: ${item.suffixText}
|
|
76
76
|
$elif item.isSeparator:
|
|
77
77
|
- rtgl-view w=f h=1 bgc=mu mv=sm: null
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
const TOAST_DURATION_MS = 3000;
|
|
2
|
+
const TOAST_EXIT_DURATION_MS = 180;
|
|
3
|
+
const toastTimeoutsByGlobalUI = new WeakMap();
|
|
4
|
+
|
|
1
5
|
const clearComponentDialogBody = (refs) => {
|
|
2
6
|
if (!refs) {
|
|
3
7
|
return;
|
|
@@ -74,6 +78,64 @@ const createUiPromise = (globalUI, { rejectOnError = false } = {}) => {
|
|
|
74
78
|
});
|
|
75
79
|
};
|
|
76
80
|
|
|
81
|
+
const getToastTimeouts = (globalUI) => {
|
|
82
|
+
let timeouts = toastTimeoutsByGlobalUI.get(globalUI);
|
|
83
|
+
|
|
84
|
+
if (!timeouts) {
|
|
85
|
+
timeouts = new Map();
|
|
86
|
+
toastTimeoutsByGlobalUI.set(globalUI, timeouts);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return timeouts;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const clearToastTimeout = (globalUI, toastId) => {
|
|
93
|
+
const timeouts = toastTimeoutsByGlobalUI.get(globalUI);
|
|
94
|
+
const timeoutIds = timeouts?.get(toastId);
|
|
95
|
+
|
|
96
|
+
if (timeoutIds) {
|
|
97
|
+
clearTimeout(timeoutIds.exitTimerId);
|
|
98
|
+
clearTimeout(timeoutIds.removeTimerId);
|
|
99
|
+
timeouts.delete(toastId);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const clearAllToastTimeouts = (globalUI) => {
|
|
104
|
+
const timeouts = toastTimeoutsByGlobalUI.get(globalUI);
|
|
105
|
+
|
|
106
|
+
if (!timeouts) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const timeoutIds of timeouts.values()) {
|
|
111
|
+
clearTimeout(timeoutIds.exitTimerId);
|
|
112
|
+
clearTimeout(timeoutIds.removeTimerId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
timeouts.clear();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const scheduleToastRemoval = ({ store, render, globalUI }, toastId) => {
|
|
119
|
+
clearToastTimeout(globalUI, toastId);
|
|
120
|
+
|
|
121
|
+
const timeouts = getToastTimeouts(globalUI);
|
|
122
|
+
const exitTimerId = setTimeout(() => {
|
|
123
|
+
store.setToastPhase?.({ id: toastId, phase: "exiting" });
|
|
124
|
+
render();
|
|
125
|
+
}, Math.max(0, TOAST_DURATION_MS - TOAST_EXIT_DURATION_MS));
|
|
126
|
+
|
|
127
|
+
const removeTimerId = setTimeout(() => {
|
|
128
|
+
timeouts.delete(toastId);
|
|
129
|
+
store.removeToast({ id: toastId });
|
|
130
|
+
render();
|
|
131
|
+
}, TOAST_DURATION_MS);
|
|
132
|
+
|
|
133
|
+
timeouts.set(toastId, {
|
|
134
|
+
exitTimerId,
|
|
135
|
+
removeTimerId,
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
77
139
|
const scheduleFormDialogMount = (deps, expectedKey) => {
|
|
78
140
|
setTimeout(() => {
|
|
79
141
|
const { store, refs } = deps;
|
|
@@ -218,6 +280,21 @@ export const handleShowConfirm = async (deps, payload) => {
|
|
|
218
280
|
return createUiPromise(globalUI);
|
|
219
281
|
};
|
|
220
282
|
|
|
283
|
+
export const handleShowToast = (deps, payload) => {
|
|
284
|
+
const { store, render, globalUI } = deps;
|
|
285
|
+
store.addToast(payload);
|
|
286
|
+
render();
|
|
287
|
+
|
|
288
|
+
const toasts = store.selectToasts?.() ?? [];
|
|
289
|
+
const toastId = toasts[toasts.length - 1]?.id;
|
|
290
|
+
|
|
291
|
+
if (!toastId) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
scheduleToastRemoval({ store, render, globalUI }, toastId);
|
|
296
|
+
};
|
|
297
|
+
|
|
221
298
|
/**
|
|
222
299
|
* Shows a dropdown menu at the specified position with the given items.
|
|
223
300
|
* Closes any existing dialog or dropdown menu before showing the dropdown menu.
|
|
@@ -322,7 +399,21 @@ export const handleFormFieldEvent = (deps, payload) => {
|
|
|
322
399
|
*/
|
|
323
400
|
export const handleCloseAll = (deps) => {
|
|
324
401
|
const { store, render, globalUI, refs } = deps;
|
|
325
|
-
|
|
402
|
+
clearAllToastTimeouts(globalUI);
|
|
403
|
+
const hasToasts = (store.selectToasts?.().length ?? 0) > 0;
|
|
404
|
+
|
|
405
|
+
if (hasToasts) {
|
|
406
|
+
store.clearToasts();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (store.selectIsOpen()) {
|
|
410
|
+
closeCurrentUi({ store, render, globalUI, refs, emitResult: true });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (hasToasts) {
|
|
415
|
+
render();
|
|
416
|
+
}
|
|
326
417
|
};
|
|
327
418
|
|
|
328
419
|
export const handleComponentDialogAction = async (deps, payload) => {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const VALID_DIALOG_SIZES = new Set(["sm", "md", "lg", "f"]);
|
|
2
|
+
const VALID_TOAST_SIZES = new Set(["sm", "md", "lg"]);
|
|
3
|
+
const VALID_TOAST_PHASES = new Set(["active", "exiting"]);
|
|
2
4
|
const VALID_COMPONENT_DIALOG_ROLES = new Set(["confirm", "cancel"]);
|
|
3
5
|
|
|
4
6
|
const DEFAULT_COMPONENT_DIALOG_BUTTONS = Object.freeze([
|
|
@@ -29,6 +31,14 @@ const normalizeDialogSize = (value, fallback = "md") => {
|
|
|
29
31
|
return VALID_DIALOG_SIZES.has(value) ? value : fallback;
|
|
30
32
|
};
|
|
31
33
|
|
|
34
|
+
const normalizeToastSize = (value, fallback = "sm") => {
|
|
35
|
+
return VALID_TOAST_SIZES.has(value) ? value : fallback;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const normalizeToastPhase = (value, fallback = "active") => {
|
|
39
|
+
return VALID_TOAST_PHASES.has(value) ? value : fallback;
|
|
40
|
+
};
|
|
41
|
+
|
|
32
42
|
const normalizeComponentDialogActions = (value) => {
|
|
33
43
|
const sourceButtons = Array.isArray(value?.buttons) && value.buttons.length > 0
|
|
34
44
|
? value.buttons
|
|
@@ -84,6 +94,8 @@ const createDefaultComponentDialogConfig = () => ({
|
|
|
84
94
|
export const createInitialState = () => Object.freeze({
|
|
85
95
|
isOpen: false,
|
|
86
96
|
uiType: "dialog", // "dialog" | "dropdown" | "formDialog" | "componentDialog"
|
|
97
|
+
nextToastId: 0,
|
|
98
|
+
toasts: [],
|
|
87
99
|
config: {
|
|
88
100
|
status: undefined, // undefined | info | warning | error
|
|
89
101
|
title: "",
|
|
@@ -201,6 +213,52 @@ export const setComponentDialogConfig = ({ state }, options = {}) => {
|
|
|
201
213
|
state.isOpen = true;
|
|
202
214
|
};
|
|
203
215
|
|
|
216
|
+
export const addToast = ({ state }, options = {}) => {
|
|
217
|
+
if (typeof options.message !== "string" || options.message.length === 0) {
|
|
218
|
+
throw new Error("message is required for showToast");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const nextToastId = (state.nextToastId ?? 0) + 1;
|
|
222
|
+
const toast = {
|
|
223
|
+
id: `toast-${nextToastId}`,
|
|
224
|
+
message: options.message,
|
|
225
|
+
size: normalizeToastSize(options.size ?? options.s, "sm"),
|
|
226
|
+
phase: "active",
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
state.nextToastId = nextToastId;
|
|
230
|
+
state.toasts = [...(state.toasts ?? []), toast];
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const removeToast = ({ state }, options = {}) => {
|
|
234
|
+
if (typeof options.id !== "string" || options.id.length === 0) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
state.toasts = (state.toasts ?? []).filter((toast) => toast.id !== options.id);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export const setToastPhase = ({ state }, options = {}) => {
|
|
242
|
+
if (typeof options.id !== "string" || options.id.length === 0) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
state.toasts = (state.toasts ?? []).map((toast) => {
|
|
247
|
+
if (toast.id !== options.id) {
|
|
248
|
+
return toast;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
...toast,
|
|
253
|
+
phase: normalizeToastPhase(options.phase, "active"),
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const clearToasts = ({ state }) => {
|
|
259
|
+
state.toasts = [];
|
|
260
|
+
};
|
|
261
|
+
|
|
204
262
|
export const closeAll = ({ state }) => {
|
|
205
263
|
state.isOpen = false;
|
|
206
264
|
state.uiType = "dialog"; // Reset to default type
|
|
@@ -212,6 +270,7 @@ export const selectFormDialogConfig = ({ state }) => state.formDialogConfig;
|
|
|
212
270
|
export const selectComponentDialogConfig = ({ state }) => state.componentDialogConfig;
|
|
213
271
|
export const selectUiType = ({ state }) => state.uiType;
|
|
214
272
|
export const selectIsOpen = ({ state }) => state.isOpen;
|
|
273
|
+
export const selectToasts = ({ state }) => state.toasts ?? [];
|
|
215
274
|
|
|
216
275
|
export const selectViewData = ({ state }) => {
|
|
217
276
|
const isDialogOpen = state.isOpen && state.uiType === "dialog";
|
|
@@ -246,6 +305,13 @@ export const selectViewData = ({ state }) => {
|
|
|
246
305
|
actions: componentDialogConfig.actions ?? normalizeComponentDialogActions(),
|
|
247
306
|
key: componentDialogConfig.key ?? 0,
|
|
248
307
|
},
|
|
308
|
+
toasts: Array.isArray(state.toasts)
|
|
309
|
+
? state.toasts.map((toast) => ({
|
|
310
|
+
...toast,
|
|
311
|
+
size: normalizeToastSize(toast.size, "sm"),
|
|
312
|
+
phase: normalizeToastPhase(toast.phase, "active"),
|
|
313
|
+
}))
|
|
314
|
+
: [],
|
|
249
315
|
isDialogOpen,
|
|
250
316
|
isFormDialogOpen,
|
|
251
317
|
isComponentDialogOpen,
|
|
@@ -28,7 +28,44 @@ refs:
|
|
|
28
28
|
handler: handleDropdownClose
|
|
29
29
|
item-click:
|
|
30
30
|
handler: handleDropdownItemClick
|
|
31
|
+
styles:
|
|
32
|
+
.toast-layer:
|
|
33
|
+
pointer-events: none
|
|
34
|
+
.toast-card:
|
|
35
|
+
pointer-events: auto
|
|
36
|
+
box-sizing: border-box
|
|
37
|
+
width: 33vw
|
|
38
|
+
max-width: calc(100vw - 2 * var(--spacing-lg))
|
|
39
|
+
opacity: 1
|
|
40
|
+
transform: translateY(0) scale(1)
|
|
41
|
+
animation: toast-in 220ms cubic-bezier(0.16, 1, 0.3, 1)
|
|
42
|
+
transition: opacity 180ms cubic-bezier(0.16, 1, 0.3, 1), transform 180ms cubic-bezier(0.16, 1, 0.3, 1)
|
|
43
|
+
will-change: opacity, transform
|
|
44
|
+
.toast-card-md:
|
|
45
|
+
width: 50vw
|
|
46
|
+
.toast-card-lg:
|
|
47
|
+
width: 80vw
|
|
48
|
+
.toast-card-exiting:
|
|
49
|
+
opacity: 0
|
|
50
|
+
transform: translateY(calc(var(--spacing-sm) * -0.5)) scale(0.98)
|
|
51
|
+
.toast-message:
|
|
52
|
+
overflow-wrap: anywhere
|
|
53
|
+
'@keyframes toast-in':
|
|
54
|
+
from:
|
|
55
|
+
opacity: 0
|
|
56
|
+
transform: translateY(calc(var(--spacing-sm) * -1)) scale(0.96)
|
|
57
|
+
to:
|
|
58
|
+
opacity: 1
|
|
59
|
+
transform: translateY(0) scale(1)
|
|
60
|
+
'@media (prefers-reduced-motion: reduce)':
|
|
61
|
+
.toast-card:
|
|
62
|
+
animation: none
|
|
63
|
+
transition: none
|
|
31
64
|
template:
|
|
65
|
+
- rtgl-view class=toast-layer pos=fix edge=t z=2100 w=f ah=c g=sm p=md:
|
|
66
|
+
- $for toast, i in toasts:
|
|
67
|
+
- 'rtgl-view class="toast-card toast-card-${toast.size} toast-card-${toast.phase}" key=toast-${toast.id} bgc=su bc=bo bw=xs br=md shadow=md ph=lg pv=md':
|
|
68
|
+
- rtgl-text class=toast-message ta=c w=f: ${toast.message}
|
|
32
69
|
- rtgl-dialog#dialog ?open=${isDialogContainerOpen} s=${dialogSize}:
|
|
33
70
|
- $if isFormDialogOpen:
|
|
34
71
|
- rtgl-form#formDialog slot=content :form=${formDialogConfig.form} :defaultValues=${formDialogConfig.defaultValues} :context=${formDialogConfig.context} ?disabled=${formDialogConfig.disabled} key=form-dialog-${formDialogConfig.key}: null
|
|
@@ -24,6 +24,45 @@ const stringifyProps = (props = {}) => {
|
|
|
24
24
|
.join(" ");
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
const hasOwnProp = (object, key) => Object.prototype.hasOwnProperty.call(object || {}, key);
|
|
28
|
+
|
|
29
|
+
const getOptionIcon = (option = {}) => {
|
|
30
|
+
return typeof option.icon === 'string' && option.icon.length > 0 ? option.icon : '';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getOptionSuffixText = (option = {}) => {
|
|
34
|
+
if (typeof option.shortcut === 'string' && option.shortcut.length > 0) {
|
|
35
|
+
return option.shortcut;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof option.suffixText === 'string' && option.suffixText.length > 0) {
|
|
39
|
+
return option.suffixText;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return '';
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const normalizeOption = (option = {}, index, currentValue, hoveredOptionId, hasIconColumn) => {
|
|
46
|
+
const isSelected = deepEqual(option.value, currentValue);
|
|
47
|
+
const isHovered = hoveredOptionId === index;
|
|
48
|
+
const icon = getOptionIcon(option);
|
|
49
|
+
const suffixText = getOptionSuffixText(option);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...option,
|
|
53
|
+
isSelected,
|
|
54
|
+
bgc: isHovered ? 'ac' : (isSelected ? 'mu' : ''),
|
|
55
|
+
hasIconSlot: hasIconColumn,
|
|
56
|
+
icon,
|
|
57
|
+
hasIcon: icon.length > 0,
|
|
58
|
+
iconColor: 'fg',
|
|
59
|
+
c: 'fg',
|
|
60
|
+
suffixText,
|
|
61
|
+
hasSuffixText: suffixText.length > 0,
|
|
62
|
+
suffixTextColor: 'mu-fg',
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
27
66
|
export const createInitialState = () => Object.freeze({
|
|
28
67
|
isOpen: false,
|
|
29
68
|
position: {
|
|
@@ -49,22 +88,17 @@ export const selectViewData = ({ state, props }) => {
|
|
|
49
88
|
let isPlaceholderLabel = true;
|
|
50
89
|
|
|
51
90
|
const options = props.options || [];
|
|
52
|
-
const selectedOption = options.find(opt => deepEqual(opt.value, currentValue));
|
|
91
|
+
const selectedOption = options.find((opt) => deepEqual(opt.value, currentValue));
|
|
53
92
|
if (selectedOption) {
|
|
54
93
|
displayLabel = selectedOption.label;
|
|
55
94
|
isPlaceholderLabel = false;
|
|
56
95
|
}
|
|
57
96
|
|
|
58
|
-
|
|
97
|
+
const hasIconColumn = options.some((option) => hasOwnProp(option, 'icon'));
|
|
59
98
|
const optionsWithSelection = options.map((option, index) => {
|
|
60
|
-
|
|
61
|
-
const isHovered = state.hoveredOptionId === index;
|
|
62
|
-
return {
|
|
63
|
-
...option,
|
|
64
|
-
isSelected,
|
|
65
|
-
bgc: isHovered ? 'ac' : (isSelected ? 'mu' : '')
|
|
66
|
-
};
|
|
99
|
+
return normalizeOption(option, index, currentValue, state.hoveredOptionId, hasIconColumn);
|
|
67
100
|
});
|
|
101
|
+
const selectedOptionView = optionsWithSelection.find((option) => option.isSelected);
|
|
68
102
|
|
|
69
103
|
return {
|
|
70
104
|
containerAttrString,
|
|
@@ -75,6 +109,12 @@ export const selectViewData = ({ state, props }) => {
|
|
|
75
109
|
selectedValue: currentValue,
|
|
76
110
|
selectedLabel: displayLabel,
|
|
77
111
|
selectedLabelColor: isPlaceholderLabel ? "mu-fg" : "fg",
|
|
112
|
+
selectedIcon: selectedOptionView?.icon || "",
|
|
113
|
+
hasSelectedIcon: !!selectedOptionView?.hasIcon,
|
|
114
|
+
selectedIconColor: isPlaceholderLabel ? "mu-fg" : "fg",
|
|
115
|
+
selectedSuffixText: selectedOptionView?.suffixText || "",
|
|
116
|
+
hasSelectedSuffixText: !!selectedOptionView?.hasSuffixText,
|
|
117
|
+
selectedSuffixTextColor: "mu-fg",
|
|
78
118
|
selectButtonCursor: isDisabled ? "not-allowed" : "pointer",
|
|
79
119
|
selectButtonHoverBorderColor: isDisabled ? "bo" : "ac",
|
|
80
120
|
selectButtonTabIndex: isDisabled ? -1 : 0,
|
|
@@ -29,17 +29,36 @@ refs:
|
|
|
29
29
|
handler: handleAddOptionMouseEnter
|
|
30
30
|
mouseleave:
|
|
31
31
|
handler: handleAddOptionMouseLeave
|
|
32
|
+
styles:
|
|
33
|
+
.icon-placeholder:
|
|
34
|
+
width: 16px
|
|
35
|
+
height: 16px
|
|
36
|
+
flex-shrink: 0
|
|
32
37
|
template:
|
|
33
38
|
- 'rtgl-view#selectButton style="display: inline-flex;" d=h av=c h=32 ph=md bw=xs bc=bo h-bc=${selectButtonHoverBorderColor} br=md bgc=su cur=${selectButtonCursor} ${containerAttrString} data-testid="select-button" role="button" tabindex=${selectButtonTabIndex} aria-disabled=${isDisabled}':
|
|
34
|
-
- rtgl-view d=h av=c ah=s w=f:
|
|
35
|
-
- rtgl-
|
|
39
|
+
- rtgl-view d=h av=c ah=s w=f g=md:
|
|
40
|
+
- rtgl-view d=h av=c g=md w=1fg:
|
|
41
|
+
- $if hasSelectedIcon:
|
|
42
|
+
- rtgl-svg svg=${selectedIcon} wh=16 c=${selectedIconColor}: null
|
|
43
|
+
- rtgl-text w=1fg ta=s c=${selectedLabelColor} ellipsis: ${selectedLabel}
|
|
44
|
+
- $if hasSelectedSuffixText:
|
|
45
|
+
- rtgl-text s=xs c=${selectedSuffixTextColor} ta=e: ${selectedSuffixText}
|
|
36
46
|
- $if showClear:
|
|
37
|
-
- rtgl-svg#clearButton
|
|
38
|
-
- rtgl-svg
|
|
47
|
+
- rtgl-svg#clearButton svg=x wh=16 c=mu-fg cur=pointer data-testid="select-clear-button": null
|
|
48
|
+
- rtgl-svg svg=chevronDown wh=16 c=mu-fg: null
|
|
39
49
|
- rtgl-popover#popover ?open=${isOpen} x=${position.x} y=${position.y} place=rs content-wh=300 content-g=xs content-sv=true content-pv=sm:
|
|
40
50
|
- $for option, i in options:
|
|
41
51
|
- rtgl-view#option${i} w=f ph=lg pv=md cur=pointer bgc=${option.bgc} data-testid=${option.testId}:
|
|
42
|
-
- rtgl-
|
|
52
|
+
- rtgl-view d=h av=c w=f g=md:
|
|
53
|
+
- rtgl-view d=h av=c g=md w=1fg:
|
|
54
|
+
- $if option.hasIconSlot:
|
|
55
|
+
- $if option.hasIcon:
|
|
56
|
+
- rtgl-svg wh=16 svg=${option.icon} c=${option.iconColor}: null
|
|
57
|
+
$else:
|
|
58
|
+
- div class=icon-placeholder aria-hidden="true": null
|
|
59
|
+
- rtgl-text s=sm c=${option.c} w=1fg ellipsis: ${option.label}
|
|
60
|
+
- $if option.hasSuffixText:
|
|
61
|
+
- rtgl-text s=xs c=${option.suffixTextColor} ta=e: ${option.suffixText}
|
|
43
62
|
- $if showAddOption:
|
|
44
63
|
- rtgl-view w=f bw=xs bc=mu bwt=sm: null
|
|
45
64
|
- rtgl-view#optionAdd w=f ph=lg pv=md cur=pointer bgc=${addOptionBgc} data-testid="select-add-option":
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Creates a GlobalUI manager instance for controlling global UI components.
|
|
3
3
|
* Provides methods for showing alerts, confirm dialogs, form dialogs,
|
|
4
|
-
* dropdown menus, and closing all UI components.
|
|
4
|
+
* dropdown menus, toasts, and closing all UI components.
|
|
5
5
|
*
|
|
6
6
|
* @param {HTMLElement} globalUIElement - The globalUI component element
|
|
7
7
|
* @returns {Object} GlobalUI manager instance
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @returns {Function} returns.showFormDialog - Show a form dialog
|
|
13
13
|
* @returns {Function} returns.showComponentDialog - Show a component dialog
|
|
14
14
|
* @returns {Function} returns.showDropdownMenu - Show a dropdown menu
|
|
15
|
+
* @returns {Function} returns.showToast - Show a temporary toast message
|
|
15
16
|
* @returns {Function} returns.closeAll - General-purpose function to close all currently open UI components
|
|
16
17
|
*/
|
|
17
18
|
const createGlobalUI = (globalUIElement) => {
|
|
@@ -161,9 +162,27 @@ const createGlobalUI = (globalUIElement) => {
|
|
|
161
162
|
return globalUIElement.transformedHandlers.handleShowDropdownMenu(options);
|
|
162
163
|
},
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Shows a top-centered toast message that auto-dismisses after 3 seconds.
|
|
167
|
+
*
|
|
168
|
+
* @param {Object} options - Toast configuration options
|
|
169
|
+
* @param {string} options.message - The toast message (required)
|
|
170
|
+
* @param {('sm'|'md'|'lg')} [options.size] - Toast width preset matching dialog sizing (default: "sm")
|
|
171
|
+
* @returns {void}
|
|
172
|
+
* @throws {Error} If globalUIElement is not initialized
|
|
173
|
+
*/
|
|
174
|
+
showToast: (options) => {
|
|
175
|
+
if(!globalUIElement)
|
|
176
|
+
{
|
|
177
|
+
throw new Error("globalUIElement is not set. Make sure to initialize the global UI component and pass it to createGlobalUIManager.");
|
|
178
|
+
}
|
|
179
|
+
globalUIElement.transformedHandlers.handleShowToast(options);
|
|
180
|
+
},
|
|
181
|
+
|
|
164
182
|
/**
|
|
165
183
|
* General-purpose function to close all currently open UI components.
|
|
166
|
-
* This includes dialogs,
|
|
184
|
+
* This includes dialogs, dropdown menus, toasts, and any other floating UI elements
|
|
185
|
+
* managed by rtgl-global-ui.
|
|
167
186
|
* Useful for programmatically cleaning up the entire UI surface.
|
|
168
187
|
*
|
|
169
188
|
* @returns {Promise<void>} Promise that resolves when all UI components are closed
|
|
@@ -255,7 +255,9 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
255
255
|
|
|
256
256
|
const hasContent = Array.from(wrapper.childNodes).some((node) => !this._isIgnorableTextNode(node));
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
// Keep the content wrapper slotted while open so callers can intentionally
|
|
259
|
+
// show an empty popover shell, such as an empty select menu.
|
|
260
|
+
if (hasContent || this.hasAttribute("open")) {
|
|
259
261
|
wrapper.setAttribute("slot", "content");
|
|
260
262
|
} else {
|
|
261
263
|
wrapper.removeAttribute("slot");
|