@rettangoli/ui 1.5.0 → 1.6.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/dist/rettangoli-iife-ui.min.js +50 -50
- package/package.json +1 -1
- package/src/components/dropdownMenu/dropdownMenu.handlers.js +13 -1
- package/src/components/dropdownMenu/dropdownMenu.schema.yaml +2 -1
- package/src/components/dropdownMenu/dropdownMenu.store.js +14 -3
- package/src/components/dropdownMenu/dropdownMenu.view.yaml +1 -1
- package/src/components/globalUi/globalUi.store.js +17 -7
- package/src/components/globalUi/globalUi.view.yaml +26 -7
- package/src/components/select/select.handlers.js +17 -2
- package/src/components/select/select.schema.yaml +6 -0
- package/src/components/select/select.store.js +47 -3
- package/src/components/select/select.view.yaml +17 -11
- package/src/deps/createGlobalUI.js +2 -1
package/package.json
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
|
|
2
|
+
const getItemType = (item = {}) => {
|
|
3
|
+
if (item.type === 'section' || item.type === 'label') {
|
|
4
|
+
return 'section';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (item.type === 'separator') {
|
|
8
|
+
return 'separator';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return 'item';
|
|
12
|
+
};
|
|
13
|
+
|
|
2
14
|
export const handleClosePopover = (deps, payload) => {
|
|
3
15
|
const { dispatchEvent } = deps;
|
|
4
16
|
dispatchEvent(new CustomEvent('close'));
|
|
@@ -9,7 +21,7 @@ export const handleClickMenuItem = (deps, payload) => {
|
|
|
9
21
|
const event = payload._event;
|
|
10
22
|
const index = Number(event.currentTarget.dataset.index ?? event.currentTarget.id.slice('option'.length));
|
|
11
23
|
const item = props.items[index];
|
|
12
|
-
const itemType = item
|
|
24
|
+
const itemType = getItemType(item);
|
|
13
25
|
|
|
14
26
|
if (!item || itemType !== 'item' || item.disabled) {
|
|
15
27
|
event.preventDefault();
|
|
@@ -3,12 +3,23 @@ export const createInitialState = () => Object.freeze({
|
|
|
3
3
|
});
|
|
4
4
|
|
|
5
5
|
const escapeAttrValue = (value) => `${value}`.replace(/"/g, '"');
|
|
6
|
+
const getItemType = (item = {}) => {
|
|
7
|
+
if (item.type === 'section' || item.type === 'label') {
|
|
8
|
+
return 'section';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (item.type === 'separator') {
|
|
12
|
+
return 'separator';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return 'item';
|
|
16
|
+
};
|
|
6
17
|
|
|
7
18
|
const normalizeItems = (items) => {
|
|
8
19
|
return items.map((item, index) => {
|
|
9
|
-
const type = item
|
|
20
|
+
const type = getItemType(item);
|
|
10
21
|
const isSeparator = type === 'separator';
|
|
11
|
-
const
|
|
22
|
+
const isSection = type === 'section';
|
|
12
23
|
const isItem = type === 'item';
|
|
13
24
|
const isDisabled = !!item.disabled;
|
|
14
25
|
const isInteractive = isItem && !isDisabled;
|
|
@@ -34,7 +45,7 @@ const normalizeItems = (items) => {
|
|
|
34
45
|
index,
|
|
35
46
|
type,
|
|
36
47
|
isSeparator,
|
|
37
|
-
|
|
48
|
+
isSection,
|
|
38
49
|
isItem,
|
|
39
50
|
isDisabled,
|
|
40
51
|
isInteractive,
|
|
@@ -20,7 +20,7 @@ styles:
|
|
|
20
20
|
template:
|
|
21
21
|
- rtgl-popover#popover ?open=${open} x=${x} y=${y} place=${place} content-w=${w} content-h=${h} content-sv=true content-g=xs content-pv=sm:
|
|
22
22
|
- $for item, i in items:
|
|
23
|
-
- $if item.
|
|
23
|
+
- $if item.isSection:
|
|
24
24
|
- rtgl-view w=f p=md:
|
|
25
25
|
- rtgl-text s=xs c=mu-fg: ${item.label}
|
|
26
26
|
$elif item.isItem && item.isDisabled:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const VALID_DIALOG_SIZES = new Set(["sm", "md", "lg", "f"]);
|
|
2
2
|
const VALID_TOAST_SIZES = new Set(["sm", "md", "lg"]);
|
|
3
3
|
const VALID_TOAST_PHASES = new Set(["active", "exiting"]);
|
|
4
|
+
const VALID_TOAST_POSITIONS = new Set(["top", "bottom"]);
|
|
4
5
|
const VALID_COMPONENT_DIALOG_ROLES = new Set(["confirm", "cancel"]);
|
|
5
6
|
|
|
6
7
|
const DEFAULT_COMPONENT_DIALOG_BUTTONS = Object.freeze([
|
|
@@ -39,6 +40,10 @@ const normalizeToastPhase = (value, fallback = "active") => {
|
|
|
39
40
|
return VALID_TOAST_PHASES.has(value) ? value : fallback;
|
|
40
41
|
};
|
|
41
42
|
|
|
43
|
+
const normalizeToastPosition = (value, fallback = "top") => {
|
|
44
|
+
return VALID_TOAST_POSITIONS.has(value) ? value : fallback;
|
|
45
|
+
};
|
|
46
|
+
|
|
42
47
|
const normalizeComponentDialogActions = (value) => {
|
|
43
48
|
const sourceButtons = Array.isArray(value?.buttons) && value.buttons.length > 0
|
|
44
49
|
? value.buttons
|
|
@@ -223,6 +228,7 @@ export const addToast = ({ state }, options = {}) => {
|
|
|
223
228
|
id: `toast-${nextToastId}`,
|
|
224
229
|
message: options.message,
|
|
225
230
|
size: normalizeToastSize(options.size ?? options.s, "sm"),
|
|
231
|
+
position: normalizeToastPosition(options.position, "top"),
|
|
226
232
|
phase: "active",
|
|
227
233
|
};
|
|
228
234
|
|
|
@@ -277,6 +283,14 @@ export const selectViewData = ({ state }) => {
|
|
|
277
283
|
const isFormDialogOpen = state.isOpen && state.uiType === "formDialog";
|
|
278
284
|
const isComponentDialogOpen = state.isOpen && state.uiType === "componentDialog";
|
|
279
285
|
const componentDialogConfig = state.componentDialogConfig ?? createDefaultComponentDialogConfig();
|
|
286
|
+
const normalizedToasts = Array.isArray(state.toasts)
|
|
287
|
+
? state.toasts.map((toast) => ({
|
|
288
|
+
...toast,
|
|
289
|
+
size: normalizeToastSize(toast.size, "sm"),
|
|
290
|
+
position: normalizeToastPosition(toast.position, "top"),
|
|
291
|
+
phase: normalizeToastPhase(toast.phase, "active"),
|
|
292
|
+
}))
|
|
293
|
+
: [];
|
|
280
294
|
|
|
281
295
|
return {
|
|
282
296
|
isOpen: state.isOpen,
|
|
@@ -305,13 +319,9 @@ export const selectViewData = ({ state }) => {
|
|
|
305
319
|
actions: componentDialogConfig.actions ?? normalizeComponentDialogActions(),
|
|
306
320
|
key: componentDialogConfig.key ?? 0,
|
|
307
321
|
},
|
|
308
|
-
toasts:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
size: normalizeToastSize(toast.size, "sm"),
|
|
312
|
-
phase: normalizeToastPhase(toast.phase, "active"),
|
|
313
|
-
}))
|
|
314
|
-
: [],
|
|
322
|
+
toasts: normalizedToasts,
|
|
323
|
+
topToasts: normalizedToasts.filter((toast) => toast.position === "top"),
|
|
324
|
+
bottomToasts: normalizedToasts.filter((toast) => toast.position === "bottom"),
|
|
315
325
|
isDialogOpen,
|
|
316
326
|
isFormDialogOpen,
|
|
317
327
|
isComponentDialogOpen,
|
|
@@ -38,34 +38,53 @@ styles:
|
|
|
38
38
|
max-width: calc(100vw - 2 * var(--spacing-lg))
|
|
39
39
|
opacity: 1
|
|
40
40
|
transform: translateY(0) scale(1)
|
|
41
|
-
animation: toast-in 220ms cubic-bezier(0.16, 1, 0.3, 1)
|
|
42
41
|
transition: opacity 180ms cubic-bezier(0.16, 1, 0.3, 1), transform 180ms cubic-bezier(0.16, 1, 0.3, 1)
|
|
43
42
|
will-change: opacity, transform
|
|
43
|
+
.toast-card-top:
|
|
44
|
+
animation: toast-in-top 220ms cubic-bezier(0.16, 1, 0.3, 1)
|
|
45
|
+
.toast-card-bottom:
|
|
46
|
+
animation: toast-in-bottom 220ms cubic-bezier(0.16, 1, 0.3, 1)
|
|
44
47
|
.toast-card-md:
|
|
45
48
|
width: 50vw
|
|
46
49
|
.toast-card-lg:
|
|
47
50
|
width: 80vw
|
|
48
|
-
.toast-card-exiting:
|
|
51
|
+
.toast-card-top.toast-card-exiting:
|
|
49
52
|
opacity: 0
|
|
50
53
|
transform: translateY(calc(var(--spacing-sm) * -0.5)) scale(0.98)
|
|
54
|
+
.toast-card-bottom.toast-card-exiting:
|
|
55
|
+
opacity: 0
|
|
56
|
+
transform: translateY(calc(var(--spacing-sm) * 0.5)) scale(0.98)
|
|
51
57
|
.toast-message:
|
|
52
58
|
overflow-wrap: anywhere
|
|
53
|
-
'@keyframes toast-in':
|
|
59
|
+
'@keyframes toast-in-top':
|
|
54
60
|
from:
|
|
55
61
|
opacity: 0
|
|
56
62
|
transform: translateY(calc(var(--spacing-sm) * -1)) scale(0.96)
|
|
57
63
|
to:
|
|
58
64
|
opacity: 1
|
|
59
65
|
transform: translateY(0) scale(1)
|
|
66
|
+
'@keyframes toast-in-bottom':
|
|
67
|
+
from:
|
|
68
|
+
opacity: 0
|
|
69
|
+
transform: translateY(var(--spacing-sm)) scale(0.96)
|
|
70
|
+
to:
|
|
71
|
+
opacity: 1
|
|
72
|
+
transform: translateY(0) scale(1)
|
|
60
73
|
'@media (prefers-reduced-motion: reduce)':
|
|
61
74
|
.toast-card:
|
|
62
75
|
animation: none
|
|
63
76
|
transition: none
|
|
64
77
|
template:
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
- rtgl-
|
|
78
|
+
- $if topToasts.length > 0:
|
|
79
|
+
- rtgl-view class="toast-layer toast-layer-top" pos=fix edge=t z=2100 w=f ah=c g=sm ph=md pt=xl:
|
|
80
|
+
- $for toast, i in topToasts:
|
|
81
|
+
- 'rtgl-view class="toast-card toast-card-top toast-card-${toast.size} toast-card-${toast.phase}" key=toast-${toast.id} bgc=su bc=bo bw=xs br=md shadow=md ph=xl pv=lg':
|
|
82
|
+
- rtgl-text class=toast-message ta=c w=f: ${toast.message}
|
|
83
|
+
- $if bottomToasts.length > 0:
|
|
84
|
+
- rtgl-view class="toast-layer toast-layer-bottom" pos=fix edge=b z=2100 w=f ah=c g=sm ph=md pb=xl:
|
|
85
|
+
- $for toast, i in bottomToasts:
|
|
86
|
+
- 'rtgl-view class="toast-card toast-card-bottom toast-card-${toast.size} toast-card-${toast.phase}" key=toast-${toast.id} bgc=su bc=bo bw=xs br=md shadow=md ph=xl pv=lg':
|
|
87
|
+
- rtgl-text class=toast-message ta=c w=f: ${toast.message}
|
|
69
88
|
- rtgl-dialog#dialog ?open=${isDialogContainerOpen} s=${dialogSize}:
|
|
70
89
|
- $if isFormDialogOpen:
|
|
71
90
|
- rtgl-form#formDialog slot=content :form=${formDialogConfig.form} :defaultValues=${formDialogConfig.defaultValues} :context=${formDialogConfig.context} ?disabled=${formDialogConfig.disabled} key=form-dialog-${formDialogConfig.key}: null
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { deepEqual } from '../../common.js';
|
|
2
2
|
|
|
3
|
+
const getOptionType = (option = {}) => {
|
|
4
|
+
if (option.type === 'section') {
|
|
5
|
+
return 'section';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (option.type === 'separator') {
|
|
9
|
+
return 'separator';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return 'item';
|
|
13
|
+
};
|
|
14
|
+
|
|
3
15
|
export const handleBeforeMount = (deps) => {
|
|
4
16
|
const { store, props, render } = deps;
|
|
5
17
|
|
|
6
18
|
if (props.selectedValue !== null && props.selectedValue !== undefined && props.options) {
|
|
7
|
-
const selectedOption = props.options.find(opt => deepEqual(opt.value, props.selectedValue));
|
|
19
|
+
const selectedOption = props.options.find((opt) => getOptionType(opt) === 'item' && deepEqual(opt.value, props.selectedValue));
|
|
8
20
|
if (selectedOption) {
|
|
9
21
|
store.updateSelectedValue({
|
|
10
22
|
value: selectedOption?.value
|
|
@@ -48,7 +60,7 @@ export const handleButtonClick = (deps, payload) => {
|
|
|
48
60
|
const currentValue = storeSelectedValue !== null ? storeSelectedValue : props.selectedValue;
|
|
49
61
|
let selectedIndex = null;
|
|
50
62
|
if (currentValue !== null && currentValue !== undefined && props.options) {
|
|
51
|
-
selectedIndex = props.options.findIndex(opt => deepEqual(opt.value, currentValue));
|
|
63
|
+
selectedIndex = props.options.findIndex((opt) => getOptionType(opt) === 'item' && deepEqual(opt.value, currentValue));
|
|
52
64
|
if (selectedIndex === -1) selectedIndex = null;
|
|
53
65
|
}
|
|
54
66
|
|
|
@@ -87,6 +99,9 @@ export const handleOptionClick = (deps, payload) => {
|
|
|
87
99
|
const index = Number(id);
|
|
88
100
|
|
|
89
101
|
const option = props.options[id];
|
|
102
|
+
if (getOptionType(option) !== 'item') {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
90
105
|
|
|
91
106
|
// Update internal state
|
|
92
107
|
store.updateSelectedValue({ value: option?.value });
|
|
@@ -25,6 +25,18 @@ const stringifyProps = (props = {}) => {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const hasOwnProp = (object, key) => Object.prototype.hasOwnProperty.call(object || {}, key);
|
|
28
|
+
const getOptionType = (option = {}) => {
|
|
29
|
+
if (option.type === 'section') {
|
|
30
|
+
return 'section';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (option.type === 'separator') {
|
|
34
|
+
return 'separator';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return 'item';
|
|
38
|
+
};
|
|
39
|
+
const isSelectableOption = (option = {}) => getOptionType(option) === 'item';
|
|
28
40
|
|
|
29
41
|
const getOptionIcon = (option = {}) => {
|
|
30
42
|
return typeof option.icon === 'string' && option.icon.length > 0 ? option.icon : '';
|
|
@@ -43,6 +55,33 @@ const getOptionSuffixText = (option = {}) => {
|
|
|
43
55
|
};
|
|
44
56
|
|
|
45
57
|
const normalizeOption = (option = {}, index, currentValue, hoveredOptionId, hasIconColumn) => {
|
|
58
|
+
const type = getOptionType(option);
|
|
59
|
+
const isSection = type === 'section';
|
|
60
|
+
const isSeparator = type === 'separator';
|
|
61
|
+
const isItem = type === 'item';
|
|
62
|
+
|
|
63
|
+
if (isSection) {
|
|
64
|
+
return {
|
|
65
|
+
...option,
|
|
66
|
+
index,
|
|
67
|
+
type,
|
|
68
|
+
isSection,
|
|
69
|
+
isSeparator,
|
|
70
|
+
isItem,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isSeparator) {
|
|
75
|
+
return {
|
|
76
|
+
...option,
|
|
77
|
+
index,
|
|
78
|
+
type,
|
|
79
|
+
isSection,
|
|
80
|
+
isSeparator,
|
|
81
|
+
isItem,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
46
85
|
const isSelected = deepEqual(option.value, currentValue);
|
|
47
86
|
const isHovered = hoveredOptionId === index;
|
|
48
87
|
const icon = getOptionIcon(option);
|
|
@@ -50,6 +89,11 @@ const normalizeOption = (option = {}, index, currentValue, hoveredOptionId, hasI
|
|
|
50
89
|
|
|
51
90
|
return {
|
|
52
91
|
...option,
|
|
92
|
+
index,
|
|
93
|
+
type,
|
|
94
|
+
isSection,
|
|
95
|
+
isSeparator,
|
|
96
|
+
isItem,
|
|
53
97
|
isSelected,
|
|
54
98
|
bgc: isHovered ? 'ac' : (isSelected ? 'mu' : ''),
|
|
55
99
|
hasIconSlot: hasIconColumn,
|
|
@@ -88,17 +132,17 @@ export const selectViewData = ({ state, props }) => {
|
|
|
88
132
|
let isPlaceholderLabel = true;
|
|
89
133
|
|
|
90
134
|
const options = props.options || [];
|
|
91
|
-
const selectedOption = options.find((opt) => deepEqual(opt.value, currentValue));
|
|
135
|
+
const selectedOption = options.find((opt) => isSelectableOption(opt) && deepEqual(opt.value, currentValue));
|
|
92
136
|
if (selectedOption) {
|
|
93
137
|
displayLabel = selectedOption.label;
|
|
94
138
|
isPlaceholderLabel = false;
|
|
95
139
|
}
|
|
96
140
|
|
|
97
|
-
const hasIconColumn = options.some((option) => hasOwnProp(option, 'icon'));
|
|
141
|
+
const hasIconColumn = options.some((option) => isSelectableOption(option) && hasOwnProp(option, 'icon'));
|
|
98
142
|
const optionsWithSelection = options.map((option, index) => {
|
|
99
143
|
return normalizeOption(option, index, currentValue, state.hoveredOptionId, hasIconColumn);
|
|
100
144
|
});
|
|
101
|
-
const selectedOptionView = optionsWithSelection.find((option) => option.isSelected);
|
|
145
|
+
const selectedOptionView = optionsWithSelection.find((option) => option.isItem && option.isSelected);
|
|
102
146
|
|
|
103
147
|
return {
|
|
104
148
|
containerAttrString,
|
|
@@ -48,17 +48,23 @@ template:
|
|
|
48
48
|
- rtgl-svg svg=chevronDown wh=16 c=mu-fg: null
|
|
49
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:
|
|
50
50
|
- $for option, i in options:
|
|
51
|
-
-
|
|
52
|
-
- rtgl-view
|
|
53
|
-
- rtgl-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
- $if option.isSection:
|
|
52
|
+
- rtgl-view w=f p=md:
|
|
53
|
+
- rtgl-text s=xs c=mu-fg: ${option.label}
|
|
54
|
+
$elif option.isItem:
|
|
55
|
+
- rtgl-view#option${option.index} w=f ph=lg pv=md cur=pointer bgc=${option.bgc} data-testid=${option.testId}:
|
|
56
|
+
- rtgl-view d=h av=c w=f g=md:
|
|
57
|
+
- rtgl-view d=h av=c g=md w=1fg:
|
|
58
|
+
- $if option.hasIconSlot:
|
|
59
|
+
- $if option.hasIcon:
|
|
60
|
+
- rtgl-svg wh=16 svg=${option.icon} c=${option.iconColor}: null
|
|
61
|
+
$else:
|
|
62
|
+
- div class=icon-placeholder aria-hidden="true": null
|
|
63
|
+
- rtgl-text s=sm c=${option.c} w=1fg ellipsis: ${option.label}
|
|
64
|
+
- $if option.hasSuffixText:
|
|
65
|
+
- rtgl-text s=xs c=${option.suffixTextColor} ta=e: ${option.suffixText}
|
|
66
|
+
$elif option.isSeparator:
|
|
67
|
+
- rtgl-view w=f h=1 bgc=mu mv=sm: null
|
|
62
68
|
- $if showAddOption:
|
|
63
69
|
- rtgl-view w=f bw=xs bc=mu bwt=sm: null
|
|
64
70
|
- rtgl-view#optionAdd w=f ph=lg pv=md cur=pointer bgc=${addOptionBgc} data-testid="select-add-option":
|
|
@@ -163,11 +163,12 @@ const createGlobalUI = (globalUIElement) => {
|
|
|
163
163
|
},
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
* Shows a
|
|
166
|
+
* Shows a toast message that auto-dismisses after 3 seconds.
|
|
167
167
|
*
|
|
168
168
|
* @param {Object} options - Toast configuration options
|
|
169
169
|
* @param {string} options.message - The toast message (required)
|
|
170
170
|
* @param {('sm'|'md'|'lg')} [options.size] - Toast width preset matching dialog sizing (default: "sm")
|
|
171
|
+
* @param {('top'|'bottom')} [options.position] - Vertical viewport placement (default: "top")
|
|
171
172
|
* @returns {void}
|
|
172
173
|
* @throws {Error} If globalUIElement is not initialized
|
|
173
174
|
*/
|