@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/ui",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
@@ -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
- const blacklistedAttrs = ['id', 'class', 'style', 'slot', 'header', 'items', 'selectedItemId', 'mode', 'hideHeader', 'w', 'bwr'];
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
- // Add the parent item if it's not just a group label
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
- title: item.title,
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
- title: subItem.title,
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 == "groupLabel":
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.title}
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.title}
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.title}
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.title.charAt(0).toUpperCase()}
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
@@ -10,6 +10,9 @@ propsSchema:
10
10
  type: string
11
11
  place:
12
12
  type: string
13
+ s:
14
+ type: string
15
+ enum: [sm, md, lg]
13
16
  content:
14
17
  type: string
15
18
  events: []
@@ -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 p=sm ah=c av=c:
4
- - rtgl-text ta=c s=sm c=fg: ${content}
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: 1000;
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
  }