@rettangoli/ui 1.0.21 → 1.0.22

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.22",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
@@ -24,10 +24,12 @@ export const handleHeaderClick = (deps, payload) => {
24
24
  }
25
25
 
26
26
  export const handleItemClick = (deps, payload) => {
27
- const { store, dispatchEvent } = deps;
27
+ const { store, dispatchEvent, render } = deps;
28
28
  const event = payload._event;
29
29
  const id = event.currentTarget.dataset.itemId || event.currentTarget.id.slice('item'.length);
30
30
  const item = store.selectItem(id);
31
+ store.hideTooltip({});
32
+ render();
31
33
  dispatchEvent(new CustomEvent('item-click', {
32
34
  detail: {
33
35
  item,
@@ -36,3 +38,43 @@ export const handleItemClick = (deps, payload) => {
36
38
  composed: true
37
39
  }));
38
40
  }
41
+
42
+ export const handleItemMouseEnter = (deps, payload) => {
43
+ const { props, store, render } = deps;
44
+ const showCompactTooltip = props.showCompactTooltip === true ||
45
+ props.showCompactTooltip === '' ||
46
+ props.showCompactTooltip === 'true';
47
+
48
+ if (!showCompactTooltip || (props.mode || 'full') === 'full') {
49
+ return;
50
+ }
51
+
52
+ const event = payload._event;
53
+ const id = event.currentTarget.dataset.itemId || event.currentTarget.id.slice('item'.length);
54
+ const item = store.selectItem(id);
55
+
56
+ if (!item || item.type === 'groupLabel') {
57
+ return;
58
+ }
59
+
60
+ const rect = event.currentTarget.getBoundingClientRect();
61
+ const content = item.tooltip || item.label || item.title;
62
+
63
+ if (!content) {
64
+ return;
65
+ }
66
+
67
+ store.showTooltip({
68
+ x: rect.right,
69
+ y: rect.top + rect.height / 2,
70
+ place: 'r',
71
+ content,
72
+ });
73
+ render();
74
+ };
75
+
76
+ export const handleItemMouseLeave = (deps) => {
77
+ const { store, render } = deps;
78
+ store.hideTooltip({});
79
+ render();
80
+ };
@@ -7,6 +7,9 @@ propsSchema:
7
7
  hideHeader:
8
8
  type: boolean
9
9
  default: false
10
+ showCompactTooltip:
11
+ type: boolean
12
+ default: false
10
13
  w:
11
14
  type: string
12
15
  bwr:
@@ -38,12 +41,18 @@ propsSchema:
38
41
  items:
39
42
  type: object
40
43
  properties:
44
+ label:
45
+ type: string
41
46
  title:
42
47
  type: string
48
+ description: Deprecated item text field. Use label instead.
43
49
  slug:
44
50
  type: string
45
51
  type:
46
52
  type: string
53
+ description: Item row type. Supports item, groupLabel, divider, and spacer.
54
+ tooltip:
55
+ type: string
47
56
  items:
48
57
  type: array
49
58
  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', '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(' ');
@@ -49,20 +57,35 @@ const resolveSidebarWidth = (value, mode) => {
49
57
  return mode === 'full' ? 272 : 64;
50
58
  };
51
59
 
60
+ const resolveItemLabel = (item = {}) => {
61
+ if (item.label !== undefined && item.label !== null) {
62
+ return item.label;
63
+ }
64
+ if (item.title !== undefined && item.title !== null) {
65
+ return item.title;
66
+ }
67
+ return '';
68
+ };
69
+
52
70
  function flattenItems(items, selectedItemId = null) {
53
71
  let result = [];
54
72
 
55
73
  for (const item of items) {
56
74
  const itemId = item.id || item.href || item.path;
57
- const isSelected = selectedItemId === itemId;
75
+ const isSelected = itemId !== undefined && itemId !== null && selectedItemId === itemId;
76
+ const label = resolveItemLabel(item);
58
77
 
59
- // Add the parent item if it's not just a group label
78
+ // Normalize all sidebar rows to a single shape so the view can branch on type.
60
79
  result.push({
61
80
  id: itemId,
62
- title: item.title,
81
+ label,
82
+ title: item.title ?? label,
63
83
  href: item.href,
64
84
  type: item.type || 'item',
65
85
  icon: item.icon,
86
+ testId: item.testId,
87
+ tooltip: item.tooltip,
88
+ path: item.path,
66
89
  hrefAttr: item.href ? `href=${item.href}` : '',
67
90
  isSelected,
68
91
  itemBgc: isSelected ? 'ac' : 'bg',
@@ -73,14 +96,19 @@ function flattenItems(items, selectedItemId = null) {
73
96
  if (item.items && Array.isArray(item.items)) {
74
97
  for (const subItem of item.items) {
75
98
  const subItemId = subItem.id || subItem.href || subItem.path;
76
- const isSubSelected = selectedItemId === subItemId;
99
+ const isSubSelected = subItemId !== undefined && subItemId !== null && selectedItemId === subItemId;
100
+ const label = resolveItemLabel(subItem);
77
101
 
78
102
  result.push({
79
103
  id: subItemId,
80
- title: subItem.title,
104
+ label,
105
+ title: subItem.title ?? label,
81
106
  href: subItem.href,
82
107
  type: subItem.type || 'item',
83
108
  icon: subItem.icon,
109
+ testId: subItem.testId,
110
+ tooltip: subItem.tooltip,
111
+ path: subItem.path,
84
112
  hrefAttr: subItem.href ? `href=${subItem.href}` : '',
85
113
  isSelected: isSubSelected,
86
114
  itemBgc: isSubSelected ? 'ac' : 'bg',
@@ -93,7 +121,7 @@ function flattenItems(items, selectedItemId = null) {
93
121
  return result;
94
122
  }
95
123
 
96
- export const selectViewData = ({ props }) => {
124
+ export const selectViewData = ({ state, props }) => {
97
125
  const resolvedHeader = parseMaybeEncodedJson(props.header) || props.header;
98
126
  const resolvedItems = parseMaybeEncodedJson(props.items) || props.items;
99
127
  const selectedItemId = props.selectedItemId;
@@ -114,6 +142,7 @@ export const selectViewData = ({ props }) => {
114
142
  const items = resolvedItems ? flattenItems(resolvedItems, selectedItemId) : [];
115
143
 
116
144
  const showHeader = !parseBooleanProp(props.hideHeader);
145
+ const showCompactTooltip = parseBooleanProp(props.showCompactTooltip);
117
146
  const rightBorderWidth = props.bwr || 'xs';
118
147
  // Computed values based on mode
119
148
  const sidebarWidth = resolveSidebarWidth(props.w, mode);
@@ -126,6 +155,7 @@ export const selectViewData = ({ props }) => {
126
155
  const firstLetterSize = mode === 'shrunk-lg' ? 'md' : 'sm';
127
156
  const showLabels = mode === 'full';
128
157
  const showGroupLabels = mode === 'full';
158
+ const enableCompactTooltip = showCompactTooltip && !showLabels;
129
159
 
130
160
  // For items with icons in full mode, we need left alignment within the container
131
161
  // but the container itself should use flex-start alignment
@@ -141,7 +171,7 @@ export const selectViewData = ({ props }) => {
141
171
  const headerWidth = itemWidth;
142
172
 
143
173
  const ah = mode === 'shrunk-lg' || mode === 'shrunk-md' ? 'c' : '';
144
- const listAttrString = mode === 'full' ? 'sv' : 'sv hsb';
174
+ const listAttrString = mode === 'full' ? 'd=v sv' : 'd=v sv hsb';
145
175
 
146
176
  return {
147
177
  containerAttrString,
@@ -166,7 +196,15 @@ export const selectViewData = ({ props }) => {
166
196
  ah,
167
197
  listAttrString,
168
198
  showHeader,
199
+ enableCompactTooltip,
169
200
  rightBorderWidth,
201
+ tooltipState: state.tooltipState || {
202
+ open: false,
203
+ x: 0,
204
+ y: 0,
205
+ place: 'r',
206
+ content: '',
207
+ },
170
208
  };
171
209
  }
172
210
 
@@ -189,3 +227,21 @@ export const selectItem = ({ props }, id) => {
189
227
  export const setState = ({ state }) => {
190
228
  // State management if needed
191
229
  };
230
+
231
+ export const showTooltip = ({ state }, payload = {}) => {
232
+ const { x, y, place = 'r', content = '' } = payload;
233
+ state.tooltipState = {
234
+ open: true,
235
+ x,
236
+ y,
237
+ place,
238
+ content,
239
+ };
240
+ };
241
+
242
+ export const hideTooltip = ({ state }) => {
243
+ state.tooltipState = {
244
+ ...state.tooltipState,
245
+ open: false,
246
+ };
247
+ };
@@ -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: {}
@@ -57,8 +57,8 @@ class RettangoliPopoverElement extends HTMLElement {
57
57
  border: 1px solid var(--border);
58
58
  border-radius: var(--rtgl-popover-content-border-radius, var(--border-radius-md));
59
59
  padding: var(--rtgl-popover-content-padding, var(--spacing-md));
60
- min-width: 200px;
61
- max-width: 400px;
60
+ min-width: var(--rtgl-popover-content-min-width, 200px);
61
+ max-width: var(--rtgl-popover-content-max-width, 400px);
62
62
  }
63
63
  `);
64
64
  }