@rettangoli/ui 1.4.2 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/ui",
3
- "version": "1.4.2",
3
+ "version": "1.6.0",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
@@ -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?.type || 'item';
24
+ const itemType = getItemType(item);
13
25
 
14
26
  if (!item || itemType !== 'item' || item.disabled) {
15
27
  event.preventDefault();
@@ -12,9 +12,10 @@ propsSchema:
12
12
  type:
13
13
  type: string
14
14
  enum:
15
- - label
15
+ - section
16
16
  - item
17
17
  - separator
18
+ - label
18
19
  id:
19
20
  type: string
20
21
  icon:
@@ -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.type || 'item';
20
+ const type = getItemType(item);
10
21
  const isSeparator = type === 'separator';
11
- const isLabel = type === 'label';
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
- isLabel,
48
+ isSection,
38
49
  isItem,
39
50
  isDisabled,
40
51
  isInteractive,
@@ -20,9 +20,9 @@ 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.isLabel:
23
+ - $if item.isSection:
24
24
  - rtgl-view w=f p=md:
25
- - rtgl-text s=sm c=mu-fg: ${item.label}
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=sm c=${item.suffixTextColor} ta=e: ${item.suffixText}
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=sm c=${item.suffixTextColor} ta=e: ${item.suffixText}
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=sm c=${item.suffixTextColor} ta=e: ${item.suffixText}
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=sm c=${item.suffixTextColor} ta=e: ${item.suffixText}
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
- closeCurrentUi({ store, render, globalUI, refs, emitResult: true });
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
@@ -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 });
@@ -9,10 +9,22 @@ propsSchema:
9
9
  items:
10
10
  type: object
11
11
  properties:
12
+ type:
13
+ type: string
14
+ enum:
15
+ - section
16
+ - item
17
+ - separator
12
18
  label:
13
19
  type: string
14
20
  value:
15
21
  type: any
22
+ icon:
23
+ type: string
24
+ shortcut:
25
+ type: string
26
+ suffixText:
27
+ type: string
16
28
  testId:
17
29
  type: string
18
30
  selectedValue:
@@ -24,6 +24,89 @@ const stringifyProps = (props = {}) => {
24
24
  .join(" ");
25
25
  };
26
26
 
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';
40
+
41
+ const getOptionIcon = (option = {}) => {
42
+ return typeof option.icon === 'string' && option.icon.length > 0 ? option.icon : '';
43
+ };
44
+
45
+ const getOptionSuffixText = (option = {}) => {
46
+ if (typeof option.shortcut === 'string' && option.shortcut.length > 0) {
47
+ return option.shortcut;
48
+ }
49
+
50
+ if (typeof option.suffixText === 'string' && option.suffixText.length > 0) {
51
+ return option.suffixText;
52
+ }
53
+
54
+ return '';
55
+ };
56
+
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
+
85
+ const isSelected = deepEqual(option.value, currentValue);
86
+ const isHovered = hoveredOptionId === index;
87
+ const icon = getOptionIcon(option);
88
+ const suffixText = getOptionSuffixText(option);
89
+
90
+ return {
91
+ ...option,
92
+ index,
93
+ type,
94
+ isSection,
95
+ isSeparator,
96
+ isItem,
97
+ isSelected,
98
+ bgc: isHovered ? 'ac' : (isSelected ? 'mu' : ''),
99
+ hasIconSlot: hasIconColumn,
100
+ icon,
101
+ hasIcon: icon.length > 0,
102
+ iconColor: 'fg',
103
+ c: 'fg',
104
+ suffixText,
105
+ hasSuffixText: suffixText.length > 0,
106
+ suffixTextColor: 'mu-fg',
107
+ };
108
+ };
109
+
27
110
  export const createInitialState = () => Object.freeze({
28
111
  isOpen: false,
29
112
  position: {
@@ -49,22 +132,17 @@ export const selectViewData = ({ state, props }) => {
49
132
  let isPlaceholderLabel = true;
50
133
 
51
134
  const options = props.options || [];
52
- const selectedOption = options.find(opt => deepEqual(opt.value, currentValue));
135
+ const selectedOption = options.find((opt) => isSelectableOption(opt) && deepEqual(opt.value, currentValue));
53
136
  if (selectedOption) {
54
137
  displayLabel = selectedOption.label;
55
138
  isPlaceholderLabel = false;
56
139
  }
57
140
 
58
- // Map options to include isSelected flag and computed background color
141
+ const hasIconColumn = options.some((option) => isSelectableOption(option) && hasOwnProp(option, 'icon'));
59
142
  const optionsWithSelection = options.map((option, index) => {
60
- const isSelected = deepEqual(option.value, currentValue);
61
- const isHovered = state.hoveredOptionId === index;
62
- return {
63
- ...option,
64
- isSelected,
65
- bgc: isHovered ? 'ac' : (isSelected ? 'mu' : '')
66
- };
143
+ return normalizeOption(option, index, currentValue, state.hoveredOptionId, hasIconColumn);
67
144
  });
145
+ const selectedOptionView = optionsWithSelection.find((option) => option.isItem && option.isSelected);
68
146
 
69
147
  return {
70
148
  containerAttrString,
@@ -75,6 +153,12 @@ export const selectViewData = ({ state, props }) => {
75
153
  selectedValue: currentValue,
76
154
  selectedLabel: displayLabel,
77
155
  selectedLabelColor: isPlaceholderLabel ? "mu-fg" : "fg",
156
+ selectedIcon: selectedOptionView?.icon || "",
157
+ hasSelectedIcon: !!selectedOptionView?.hasIcon,
158
+ selectedIconColor: isPlaceholderLabel ? "mu-fg" : "fg",
159
+ selectedSuffixText: selectedOptionView?.suffixText || "",
160
+ hasSelectedSuffixText: !!selectedOptionView?.hasSuffixText,
161
+ selectedSuffixTextColor: "mu-fg",
78
162
  selectButtonCursor: isDisabled ? "not-allowed" : "pointer",
79
163
  selectButtonHoverBorderColor: isDisabled ? "bo" : "ac",
80
164
  selectButtonTabIndex: isDisabled ? -1 : 0,
@@ -29,17 +29,42 @@ 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-text w=1fg ta=s c=${selectedLabelColor} ellipsis: ${selectedLabel}
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 ml=md svg=x wh=16 c=mu-fg cur=pointer data-testid="select-clear-button": null
38
- - rtgl-svg ml=md svg=chevronDown wh=16 c=mu-fg: null
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
- - rtgl-view#option${i} w=f ph=lg pv=md cur=pointer bgc=${option.bgc} data-testid=${option.testId}:
42
- - rtgl-text s=sm: ${option.label}
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
43
68
  - $if showAddOption:
44
69
  - rtgl-view w=f bw=xs bc=mu bwt=sm: null
45
70
  - rtgl-view#optionAdd w=f ph=lg pv=md cur=pointer bgc=${addOptionBgc} data-testid="select-add-option":