@rettangoli/ui 1.0.21 → 1.0.23
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 +4 -3
- package/dist/rettangoli-iife-ui.min.js +43 -42
- package/package.json +1 -1
- package/src/components/sidebar/sidebar.handlers.js +63 -1
- package/src/components/sidebar/sidebar.schema.yaml +13 -0
- package/src/components/sidebar/sidebar.store.js +74 -10
- package/src/components/sidebar/sidebar.view.yaml +17 -6
- package/src/components/tooltip/tooltip.schema.yaml +3 -0
- package/src/components/tooltip/tooltip.store.js +29 -1
- package/src/components/tooltip/tooltip.view.yaml +3 -3
- package/src/primitives/popover.js +4 -3
package/package.json
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
const parseBooleanProp = (value) => {
|
|
2
|
+
if (value === true) {
|
|
3
|
+
return true;
|
|
4
|
+
}
|
|
5
|
+
if (value === false || value === undefined || value === null) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === 'string') {
|
|
9
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
10
|
+
return normalizedValue === '' || normalizedValue === 'true';
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const resolveCompactTooltipEnabled = (props = {}) => {
|
|
16
|
+
if (props.tooltip !== undefined && props.tooltip !== null) {
|
|
17
|
+
return parseBooleanProp(props.tooltip);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return parseBooleanProp(props.showCompactTooltip);
|
|
21
|
+
};
|
|
22
|
+
|
|
1
23
|
export const handleHeaderClick = (deps, payload) => {
|
|
2
24
|
const { store, dispatchEvent } = deps;
|
|
3
25
|
const event = payload._event;
|
|
@@ -24,10 +46,12 @@ export const handleHeaderClick = (deps, payload) => {
|
|
|
24
46
|
}
|
|
25
47
|
|
|
26
48
|
export const handleItemClick = (deps, payload) => {
|
|
27
|
-
const { store, dispatchEvent } = deps;
|
|
49
|
+
const { store, dispatchEvent, render } = deps;
|
|
28
50
|
const event = payload._event;
|
|
29
51
|
const id = event.currentTarget.dataset.itemId || event.currentTarget.id.slice('item'.length);
|
|
30
52
|
const item = store.selectItem(id);
|
|
53
|
+
store.hideTooltip({});
|
|
54
|
+
render();
|
|
31
55
|
dispatchEvent(new CustomEvent('item-click', {
|
|
32
56
|
detail: {
|
|
33
57
|
item,
|
|
@@ -36,3 +60,41 @@ export const handleItemClick = (deps, payload) => {
|
|
|
36
60
|
composed: true
|
|
37
61
|
}));
|
|
38
62
|
}
|
|
63
|
+
|
|
64
|
+
export const handleItemMouseEnter = (deps, payload) => {
|
|
65
|
+
const { props, store, render } = deps;
|
|
66
|
+
const showCompactTooltip = resolveCompactTooltipEnabled(props);
|
|
67
|
+
|
|
68
|
+
if (!showCompactTooltip || (props.mode || 'full') === 'full') {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const event = payload._event;
|
|
73
|
+
const id = event.currentTarget.dataset.itemId || event.currentTarget.id.slice('item'.length);
|
|
74
|
+
const item = store.selectItem(id);
|
|
75
|
+
|
|
76
|
+
if (!item || item.type === 'groupLabel') {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
81
|
+
const content = item.tooltip || item.label || item.title;
|
|
82
|
+
|
|
83
|
+
if (!content) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
store.showTooltip({
|
|
88
|
+
x: rect.right,
|
|
89
|
+
y: rect.top + rect.height / 2,
|
|
90
|
+
place: 'r',
|
|
91
|
+
content,
|
|
92
|
+
});
|
|
93
|
+
render();
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const handleItemMouseLeave = (deps) => {
|
|
97
|
+
const { store, render } = deps;
|
|
98
|
+
store.hideTooltip({});
|
|
99
|
+
render();
|
|
100
|
+
};
|
|
@@ -7,6 +7,13 @@ propsSchema:
|
|
|
7
7
|
hideHeader:
|
|
8
8
|
type: boolean
|
|
9
9
|
default: false
|
|
10
|
+
tooltip:
|
|
11
|
+
type: boolean
|
|
12
|
+
default: false
|
|
13
|
+
showCompactTooltip:
|
|
14
|
+
type: boolean
|
|
15
|
+
default: false
|
|
16
|
+
description: Deprecated alias for tooltip. Use tooltip instead.
|
|
10
17
|
w:
|
|
11
18
|
type: string
|
|
12
19
|
bwr:
|
|
@@ -38,12 +45,18 @@ propsSchema:
|
|
|
38
45
|
items:
|
|
39
46
|
type: object
|
|
40
47
|
properties:
|
|
48
|
+
label:
|
|
49
|
+
type: string
|
|
41
50
|
title:
|
|
42
51
|
type: string
|
|
52
|
+
description: Deprecated item text field. Use label instead.
|
|
43
53
|
slug:
|
|
44
54
|
type: string
|
|
45
55
|
type:
|
|
46
56
|
type: string
|
|
57
|
+
description: Item row type. Supports item, groupLabel, divider, and spacer.
|
|
58
|
+
tooltip:
|
|
59
|
+
type: string
|
|
47
60
|
items:
|
|
48
61
|
type: array
|
|
49
62
|
testId:
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
export const createInitialState = () => Object.freeze({
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export const createInitialState = () => Object.freeze({
|
|
2
|
+
tooltipState: {
|
|
3
|
+
open: false,
|
|
4
|
+
x: 0,
|
|
5
|
+
y: 0,
|
|
6
|
+
place: 'r',
|
|
7
|
+
content: '',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const blacklistedAttrs = ['id', 'class', 'style', 'slot', 'header', 'items', 'selectedItemId', 'mode', 'hideHeader', 'tooltip', 'showCompactTooltip', 'w', 'bwr'];
|
|
4
12
|
|
|
5
13
|
const stringifyAttrs = (props = {}) => {
|
|
6
14
|
return Object.entries(props).filter(([key]) => !blacklistedAttrs.includes(key)).map(([key, value]) => `${key}=${value}`).join(' ');
|
|
@@ -42,6 +50,14 @@ const parseBooleanProp = (value) => {
|
|
|
42
50
|
return false;
|
|
43
51
|
};
|
|
44
52
|
|
|
53
|
+
const resolveCompactTooltipEnabled = (props = {}) => {
|
|
54
|
+
if (props.tooltip !== undefined && props.tooltip !== null) {
|
|
55
|
+
return parseBooleanProp(props.tooltip);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return parseBooleanProp(props.showCompactTooltip);
|
|
59
|
+
};
|
|
60
|
+
|
|
45
61
|
const resolveSidebarWidth = (value, mode) => {
|
|
46
62
|
if (value !== undefined && value !== null && value !== '') {
|
|
47
63
|
return value;
|
|
@@ -49,20 +65,35 @@ const resolveSidebarWidth = (value, mode) => {
|
|
|
49
65
|
return mode === 'full' ? 272 : 64;
|
|
50
66
|
};
|
|
51
67
|
|
|
68
|
+
const resolveItemLabel = (item = {}) => {
|
|
69
|
+
if (item.label !== undefined && item.label !== null) {
|
|
70
|
+
return item.label;
|
|
71
|
+
}
|
|
72
|
+
if (item.title !== undefined && item.title !== null) {
|
|
73
|
+
return item.title;
|
|
74
|
+
}
|
|
75
|
+
return '';
|
|
76
|
+
};
|
|
77
|
+
|
|
52
78
|
function flattenItems(items, selectedItemId = null) {
|
|
53
79
|
let result = [];
|
|
54
80
|
|
|
55
81
|
for (const item of items) {
|
|
56
82
|
const itemId = item.id || item.href || item.path;
|
|
57
|
-
const isSelected = selectedItemId === itemId;
|
|
83
|
+
const isSelected = itemId !== undefined && itemId !== null && selectedItemId === itemId;
|
|
84
|
+
const label = resolveItemLabel(item);
|
|
58
85
|
|
|
59
|
-
//
|
|
86
|
+
// Normalize all sidebar rows to a single shape so the view can branch on type.
|
|
60
87
|
result.push({
|
|
61
88
|
id: itemId,
|
|
62
|
-
|
|
89
|
+
label,
|
|
90
|
+
title: item.title ?? label,
|
|
63
91
|
href: item.href,
|
|
64
92
|
type: item.type || 'item',
|
|
65
93
|
icon: item.icon,
|
|
94
|
+
testId: item.testId,
|
|
95
|
+
tooltip: item.tooltip,
|
|
96
|
+
path: item.path,
|
|
66
97
|
hrefAttr: item.href ? `href=${item.href}` : '',
|
|
67
98
|
isSelected,
|
|
68
99
|
itemBgc: isSelected ? 'ac' : 'bg',
|
|
@@ -73,14 +104,19 @@ function flattenItems(items, selectedItemId = null) {
|
|
|
73
104
|
if (item.items && Array.isArray(item.items)) {
|
|
74
105
|
for (const subItem of item.items) {
|
|
75
106
|
const subItemId = subItem.id || subItem.href || subItem.path;
|
|
76
|
-
const isSubSelected = selectedItemId === subItemId;
|
|
107
|
+
const isSubSelected = subItemId !== undefined && subItemId !== null && selectedItemId === subItemId;
|
|
108
|
+
const label = resolveItemLabel(subItem);
|
|
77
109
|
|
|
78
110
|
result.push({
|
|
79
111
|
id: subItemId,
|
|
80
|
-
|
|
112
|
+
label,
|
|
113
|
+
title: subItem.title ?? label,
|
|
81
114
|
href: subItem.href,
|
|
82
115
|
type: subItem.type || 'item',
|
|
83
116
|
icon: subItem.icon,
|
|
117
|
+
testId: subItem.testId,
|
|
118
|
+
tooltip: subItem.tooltip,
|
|
119
|
+
path: subItem.path,
|
|
84
120
|
hrefAttr: subItem.href ? `href=${subItem.href}` : '',
|
|
85
121
|
isSelected: isSubSelected,
|
|
86
122
|
itemBgc: isSubSelected ? 'ac' : 'bg',
|
|
@@ -93,7 +129,7 @@ function flattenItems(items, selectedItemId = null) {
|
|
|
93
129
|
return result;
|
|
94
130
|
}
|
|
95
131
|
|
|
96
|
-
export const selectViewData = ({ props }) => {
|
|
132
|
+
export const selectViewData = ({ state, props }) => {
|
|
97
133
|
const resolvedHeader = parseMaybeEncodedJson(props.header) || props.header;
|
|
98
134
|
const resolvedItems = parseMaybeEncodedJson(props.items) || props.items;
|
|
99
135
|
const selectedItemId = props.selectedItemId;
|
|
@@ -114,6 +150,7 @@ export const selectViewData = ({ props }) => {
|
|
|
114
150
|
const items = resolvedItems ? flattenItems(resolvedItems, selectedItemId) : [];
|
|
115
151
|
|
|
116
152
|
const showHeader = !parseBooleanProp(props.hideHeader);
|
|
153
|
+
const showCompactTooltip = resolveCompactTooltipEnabled(props);
|
|
117
154
|
const rightBorderWidth = props.bwr || 'xs';
|
|
118
155
|
// Computed values based on mode
|
|
119
156
|
const sidebarWidth = resolveSidebarWidth(props.w, mode);
|
|
@@ -126,6 +163,7 @@ export const selectViewData = ({ props }) => {
|
|
|
126
163
|
const firstLetterSize = mode === 'shrunk-lg' ? 'md' : 'sm';
|
|
127
164
|
const showLabels = mode === 'full';
|
|
128
165
|
const showGroupLabels = mode === 'full';
|
|
166
|
+
const enableCompactTooltip = showCompactTooltip && !showLabels;
|
|
129
167
|
|
|
130
168
|
// For items with icons in full mode, we need left alignment within the container
|
|
131
169
|
// but the container itself should use flex-start alignment
|
|
@@ -141,7 +179,7 @@ export const selectViewData = ({ props }) => {
|
|
|
141
179
|
const headerWidth = itemWidth;
|
|
142
180
|
|
|
143
181
|
const ah = mode === 'shrunk-lg' || mode === 'shrunk-md' ? 'c' : '';
|
|
144
|
-
const listAttrString = mode === 'full' ? 'sv' : 'sv hsb';
|
|
182
|
+
const listAttrString = mode === 'full' ? 'd=v sv' : 'd=v sv hsb';
|
|
145
183
|
|
|
146
184
|
return {
|
|
147
185
|
containerAttrString,
|
|
@@ -166,7 +204,15 @@ export const selectViewData = ({ props }) => {
|
|
|
166
204
|
ah,
|
|
167
205
|
listAttrString,
|
|
168
206
|
showHeader,
|
|
207
|
+
enableCompactTooltip,
|
|
169
208
|
rightBorderWidth,
|
|
209
|
+
tooltipState: state.tooltipState || {
|
|
210
|
+
open: false,
|
|
211
|
+
x: 0,
|
|
212
|
+
y: 0,
|
|
213
|
+
place: 'r',
|
|
214
|
+
content: '',
|
|
215
|
+
},
|
|
170
216
|
};
|
|
171
217
|
}
|
|
172
218
|
|
|
@@ -189,3 +235,21 @@ export const selectItem = ({ props }, id) => {
|
|
|
189
235
|
export const setState = ({ state }) => {
|
|
190
236
|
// State management if needed
|
|
191
237
|
};
|
|
238
|
+
|
|
239
|
+
export const showTooltip = ({ state }, payload = {}) => {
|
|
240
|
+
const { x, y, place = 'r', content = '' } = payload;
|
|
241
|
+
state.tooltipState = {
|
|
242
|
+
open: true,
|
|
243
|
+
x,
|
|
244
|
+
y,
|
|
245
|
+
place,
|
|
246
|
+
content,
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const hideTooltip = ({ state }) => {
|
|
251
|
+
state.tooltipState = {
|
|
252
|
+
...state.tooltipState,
|
|
253
|
+
open: false,
|
|
254
|
+
};
|
|
255
|
+
};
|
|
@@ -15,6 +15,10 @@ refs:
|
|
|
15
15
|
eventListeners:
|
|
16
16
|
click:
|
|
17
17
|
handler: handleItemClick
|
|
18
|
+
mouseenter:
|
|
19
|
+
handler: handleItemMouseEnter
|
|
20
|
+
mouseleave:
|
|
21
|
+
handler: handleItemMouseLeave
|
|
18
22
|
anchors:
|
|
19
23
|
- - $if header.image && header.image.src:
|
|
20
24
|
- $if header.image.href:
|
|
@@ -77,24 +81,31 @@ template:
|
|
|
77
81
|
- rtgl-text s=lg: ${header.label}
|
|
78
82
|
- rtgl-view w=f h=1fg ph=${headerPadding} pb=lg g=xs ah=${ah} ${listAttrString}:
|
|
79
83
|
- $for item, i in items:
|
|
80
|
-
- $if item.type == "
|
|
84
|
+
- $if item.type == "divider":
|
|
85
|
+
- rtgl-view w=f pv=md:
|
|
86
|
+
- rtgl-view h=1 w=f bgc=mu: null
|
|
87
|
+
$elif item.type == "spacer":
|
|
88
|
+
- rtgl-view h=1fg w=f: null
|
|
89
|
+
$elif item.type == "groupLabel":
|
|
81
90
|
- $if showGroupLabels:
|
|
82
91
|
- rtgl-view mt=md h=32 av=c ph=md:
|
|
83
|
-
- rtgl-text s=xs c=mu-fg: ${item.
|
|
92
|
+
- rtgl-text s=xs c=mu-fg: ${item.label}
|
|
84
93
|
$else:
|
|
85
94
|
- rtgl-view mt=md h=1 bgc=mu: null
|
|
86
95
|
$else:
|
|
87
|
-
- rtgl-view#item${i} data-item-id=${item.id} ${item.hrefAttr} h=${itemHeight} av=c ${itemAlignAttr} ph=${itemPadding} w=${itemWidth} h-bgc=${item.itemHoverBgc} br=lg bgc=${item.itemBgc} cur=pointer data-testid=${item.testId}:
|
|
96
|
+
- rtgl-view#item${i} data-item-id=${item.id} ${item.hrefAttr} h=${itemHeight} av=c ${itemAlignAttr} ph=${itemPadding} w=${itemWidth} h-bgc=${item.itemHoverBgc} br=lg bgc=${item.itemBgc} cur=pointer data-testid=${item.testId} aria-label="${item.label}":
|
|
88
97
|
- $if item.icon:
|
|
89
98
|
- $if showLabels:
|
|
90
99
|
- rtgl-view d=h ah=${itemContentAlign} g=sm:
|
|
91
100
|
- rtgl-svg wh=16 svg=${item.icon} c=fg: null
|
|
92
|
-
- rtgl-text s=sm: ${item.
|
|
101
|
+
- rtgl-text s=sm: ${item.label}
|
|
93
102
|
$else:
|
|
94
103
|
- rtgl-svg wh=${iconSize} svg=${item.icon} c=fg: null
|
|
95
104
|
$else:
|
|
96
105
|
- $if showLabels:
|
|
97
|
-
- rtgl-text s=sm: ${item.
|
|
106
|
+
- rtgl-text s=sm: ${item.label}
|
|
98
107
|
$else:
|
|
99
108
|
- rtgl-view wh=${iconSize} br=f bgc=mu av=c ah=c:
|
|
100
|
-
- rtgl-text s=${firstLetterSize} c=fg: ${item.
|
|
109
|
+
- rtgl-text s=${firstLetterSize} c=fg: ${item.label.charAt(0).toUpperCase()}
|
|
110
|
+
- $if enableCompactTooltip:
|
|
111
|
+
- rtgl-tooltip#tooltip s="md" ?open=${tooltipState.open} x=${tooltipState.x} y=${tooltipState.y} place=${tooltipState.place} content="${tooltipState.content}": null
|
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
export const createInitialState = () => Object.freeze({
|
|
2
2
|
});
|
|
3
3
|
|
|
4
|
+
const sizePresets = {
|
|
5
|
+
sm: {
|
|
6
|
+
textSize: 'sm',
|
|
7
|
+
paddingX: 'md',
|
|
8
|
+
paddingY: 'sm',
|
|
9
|
+
maxWidth: 'min(320px, calc(100vw - 16px))',
|
|
10
|
+
},
|
|
11
|
+
md: {
|
|
12
|
+
textSize: 'sm',
|
|
13
|
+
paddingX: 'lg',
|
|
14
|
+
paddingY: 'md',
|
|
15
|
+
maxWidth: 'min(360px, calc(100vw - 16px))',
|
|
16
|
+
},
|
|
17
|
+
lg: {
|
|
18
|
+
textSize: 'md',
|
|
19
|
+
paddingX: 'lg',
|
|
20
|
+
paddingY: 'md',
|
|
21
|
+
maxWidth: 'min(420px, calc(100vw - 16px))',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
4
25
|
export const selectViewData = ({ props }) => {
|
|
26
|
+
const size = sizePresets[props.s] ? props.s : 'sm';
|
|
27
|
+
const preset = sizePresets[size];
|
|
28
|
+
|
|
5
29
|
return {
|
|
6
30
|
open: !!props.open,
|
|
7
31
|
x: props.x || 0,
|
|
8
32
|
y: props.y || 0,
|
|
9
33
|
place: props.place || 't',
|
|
10
|
-
content: props.content || ''
|
|
34
|
+
content: props.content || '',
|
|
35
|
+
textSize: preset.textSize,
|
|
36
|
+
paddingX: preset.paddingX,
|
|
37
|
+
paddingY: preset.paddingY,
|
|
38
|
+
popoverStyle: `--rtgl-popover-content-padding: 0; --rtgl-popover-content-min-width: 0; --rtgl-popover-content-max-width: ${preset.maxWidth}`,
|
|
11
39
|
};
|
|
12
40
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
template:
|
|
2
|
-
- rtgl-popover#popover ?open=${open} x=${x} y=${y} place=${place} no-overlay:
|
|
3
|
-
- rtgl-view slot=content bc=bo br=md
|
|
4
|
-
- rtgl-text ta=
|
|
2
|
+
- 'rtgl-popover#popover ?open=${open} x=${x} y=${y} place=${place} no-overlay style="${popoverStyle}"':
|
|
3
|
+
- rtgl-view slot=content bc=bo br=md ph=${paddingX} pv=${paddingY} ah=s av=c:
|
|
4
|
+
- rtgl-text ta=s s=${textSize} c=fg: ${content}
|
|
5
5
|
refs: {}
|
|
@@ -25,6 +25,7 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
25
25
|
left: 0;
|
|
26
26
|
width: 100vw;
|
|
27
27
|
height: 100vh;
|
|
28
|
+
z-index: 2000;
|
|
28
29
|
/* Prevent dialog from being focused */
|
|
29
30
|
pointer-events: none;
|
|
30
31
|
}
|
|
@@ -37,7 +38,7 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
37
38
|
|
|
38
39
|
.popover-container {
|
|
39
40
|
position: fixed;
|
|
40
|
-
z-index:
|
|
41
|
+
z-index: inherit;
|
|
41
42
|
outline: none;
|
|
42
43
|
pointer-events: auto;
|
|
43
44
|
}
|
|
@@ -57,8 +58,8 @@ class RettangoliPopoverElement extends HTMLElement {
|
|
|
57
58
|
border: 1px solid var(--border);
|
|
58
59
|
border-radius: var(--rtgl-popover-content-border-radius, var(--border-radius-md));
|
|
59
60
|
padding: var(--rtgl-popover-content-padding, var(--spacing-md));
|
|
60
|
-
min-width: 200px;
|
|
61
|
-
max-width: 400px;
|
|
61
|
+
min-width: var(--rtgl-popover-content-min-width, 200px);
|
|
62
|
+
max-width: var(--rtgl-popover-content-max-width, 400px);
|
|
62
63
|
}
|
|
63
64
|
`);
|
|
64
65
|
}
|