@rettangoli/ui 0.1.2-rc26 → 0.1.2-rc28

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": "0.1.2-rc26",
3
+ "version": "0.1.2-rc28",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
@@ -186,3 +186,17 @@ export const handleWaveformClick = (e, deps) => {
186
186
  }),
187
187
  );
188
188
  };
189
+
190
+ export const handleSelectAddOption = (e, deps) => {
191
+ const { store, dispatchEvent } = deps;
192
+ const name = e.currentTarget.id.replace("select-", "");
193
+ dispatchEvent(
194
+ new CustomEvent("action-click", {
195
+ detail: {
196
+ actionId: 'select-options-add',
197
+ name: name,
198
+ formValues: store.selectFormValues(),
199
+ },
200
+ }),
201
+ );
202
+ };
@@ -50,19 +50,23 @@ propsSchema:
50
50
  const: select
51
51
  placeholder:
52
52
  type: string
53
+ noClear:
54
+ type: boolean
55
+ addOption:
56
+ type: object
57
+ properties:
58
+ label:
59
+ type: string
53
60
  options:
54
61
  type: array
55
62
  items:
56
63
  type: object
57
64
  properties:
58
- id:
59
- type: string
60
65
  label:
61
66
  type: string
62
67
  value:
63
68
  type: any
64
69
  required:
65
- - id
66
70
  - label
67
71
  - value
68
72
  required:
@@ -209,6 +213,8 @@ refs:
209
213
  eventListeners:
210
214
  select-change:
211
215
  handler: handleSelectChange
216
+ add-option-selected:
217
+ handler: handleSelectAddOption
212
218
  colorpicker-*:
213
219
  eventListeners:
214
220
  colorpicker-change:
@@ -241,6 +247,7 @@ refs:
241
247
  events:
242
248
  form-change: {}
243
249
  extra-event: {}
250
+ action-click: {}
244
251
 
245
252
  template:
246
253
  - rtgl-view w=f p=md g=lg ${containerAttrString}:
@@ -260,7 +267,7 @@ template:
260
267
  - $if field.inputType == "popover-input":
261
268
  - rtgl-popover-input#popover-input-${field.name} label="${field.label}" .defaultValue=fields[${i}].defaultValue:
262
269
  - $if field.inputType == "select":
263
- - rtgl-select#select-${field.name} key=${key} w=f .options=fields[${i}].options .placeholder=fields[${i}].placeholder .selectedValue=fields[${i}].defaultValue:
270
+ - rtgl-select#select-${field.name} key=${key} w=f .options=fields[${i}].options .placeholder=fields[${i}].placeholder .selectedValue=fields[${i}].defaultValue ?no-clear=fields[${i}].noClear .addOption=fields[${i}].addOption:
264
271
  - $if field.inputType == "colorPicker":
265
272
  - rtgl-color-picker#colorpicker-${field.name} key=${key} value=${field.defaultValue}:
266
273
  - $if field.inputType == "slider":
@@ -2,7 +2,7 @@ import { deepEqual } from '../../common.js';
2
2
 
3
3
  export const handleBeforeMount = (deps) => {
4
4
  const { store, props, render } = deps;
5
-
5
+
6
6
  if (props.selectedValue !== null && props.selectedValue !== undefined && props.options) {
7
7
  const selectedOption = props.options.find(opt => deepEqual(opt.value, props.selectedValue));
8
8
  if (selectedOption) {
@@ -15,16 +15,16 @@ export const handleBeforeMount = (deps) => {
15
15
  export const handleOnUpdate = (changes, deps) => {
16
16
  const { oldAttrs, newAttrs, oldProps, newProps } = changes;
17
17
  const { store, props, render } = deps;
18
-
18
+
19
19
  // Check if key changed
20
20
  if (oldAttrs?.key !== newAttrs?.key && newAttrs?.key) {
21
21
  // Clear current state using store action
22
22
  store.resetSelection();
23
-
23
+
24
24
  // Re-apply the prop value if available
25
25
  const selectedValue = newProps?.selectedValue || props?.selectedValue;
26
26
  const options = newProps?.options || props?.options;
27
-
27
+
28
28
  if (selectedValue !== null && selectedValue !== undefined && options) {
29
29
  const selectedOption = options.find(opt => deepEqual(opt.value, selectedValue));
30
30
  if (selectedOption) {
@@ -36,12 +36,29 @@ export const handleOnUpdate = (changes, deps) => {
36
36
  }
37
37
 
38
38
  export const handleButtonClick = (e, deps) => {
39
- const { store, render, getRefIds } = deps;
39
+ const { store, render, getRefIds, props } = deps;
40
+
41
+ const button = getRefIds()['select-button'].elm;
42
+
43
+ // Get first child's bounding rectangle (since button has display: contents)
44
+ const firstChild = button.firstElementChild;
45
+ const rect = firstChild ? firstChild.getBoundingClientRect() : button.getBoundingClientRect();
46
+
47
+ // Find the index of the currently selected option
48
+ const storeSelectedValue = store.selectSelectedValue();
49
+ const currentValue = storeSelectedValue !== null ? storeSelectedValue : props.selectedValue;
50
+ let selectedIndex = null;
51
+ if (currentValue !== null && currentValue !== undefined && props.options) {
52
+ selectedIndex = props.options.findIndex(opt => deepEqual(opt.value, currentValue));
53
+ if (selectedIndex === -1) selectedIndex = null;
54
+ }
55
+
40
56
  store.openOptionsPopover({
41
57
  position: {
42
- y: e.clientY,
43
- x: e.clientX,
44
- }
58
+ y: rect.bottom + 12, // Bottom edge of button
59
+ x: rect.left - 24, // Left edge of button
60
+ },
61
+ selectedIndex
45
62
  })
46
63
  render();
47
64
  }
@@ -77,6 +94,73 @@ export const handleOptionClick = (e, deps) => {
77
94
  detail: { selectedValue: option.value },
78
95
  bubbles: true
79
96
  }));
97
+
98
+ render();
99
+ }
100
+
101
+ export const handleOptionMouseEnter = (e, deps) => {
102
+ const { store, render } = deps;
103
+ const id = parseInt(e.currentTarget.id.replace('option-', ''));
104
+ store.setHoveredOption(id);
105
+ render();
106
+ }
107
+
108
+ export const handleOptionMouseLeave = (e, deps) => {
109
+ const { store, render } = deps;
110
+ store.clearHoveredOption();
111
+ render();
112
+ }
113
+
114
+ export const handleClearClick = (e, deps) => {
115
+ const { store, render, dispatchEvent, props } = deps;
116
+
117
+ e.stopPropagation();
118
+
119
+ // Clear the internal state
120
+ store.clearSelectedValue();
121
+
122
+ // Call onChange if provided
123
+ if (props.onChange && typeof props.onChange === 'function') {
124
+ props.onChange(undefined);
125
+ }
126
+
127
+ // Dispatch custom event for backward compatibility
128
+ dispatchEvent(new CustomEvent('option-selected', {
129
+ detail: { value: undefined, label: undefined },
130
+ bubbles: true
131
+ }));
132
+
133
+ // Also dispatch select-change event to match form's event listener pattern
134
+ dispatchEvent(new CustomEvent('select-change', {
135
+ detail: { selectedValue: undefined },
136
+ bubbles: true
137
+ }));
138
+
139
+ render();
140
+ }
141
+
142
+ export const handleAddOptionClick = (e, deps) => {
143
+ const { store, render, dispatchEvent } = deps;
80
144
 
145
+ // Close the popover
146
+ store.closeOptionsPopover();
147
+
148
+ // Dispatch custom event for add option (no detail)
149
+ dispatchEvent(new CustomEvent('add-option-selected', {
150
+ bubbles: true
151
+ }));
152
+
153
+ render();
154
+ }
155
+
156
+ export const handleAddOptionMouseEnter = (e, deps) => {
157
+ const { store, render } = deps;
158
+ store.setHoveredAddOption(true);
159
+ render();
160
+ }
161
+
162
+ export const handleAddOptionMouseLeave = (e, deps) => {
163
+ const { store, render } = deps;
164
+ store.setHoveredAddOption(false);
81
165
  render();
82
166
  }
@@ -1,5 +1,28 @@
1
1
  import { deepEqual } from '../../common.js';
2
2
 
3
+ // Attributes that should not be passed through to the container
4
+ // These are either handled internally or have special meaning
5
+ const blacklistedAttrs = [
6
+ "id",
7
+ "class",
8
+ "style",
9
+ "slot",
10
+ // Select-specific props that are handled separately
11
+ "placeholder",
12
+ "selectedValue",
13
+ "selected-value",
14
+ "onChange",
15
+ "on-change",
16
+ "options"
17
+ ];
18
+
19
+ const stringifyAttrs = (attrs) => {
20
+ return Object.entries(attrs || {})
21
+ .filter(([key]) => !blacklistedAttrs.includes(key))
22
+ .map(([key, value]) => `${key}=${value}`)
23
+ .join(" ");
24
+ };
25
+
3
26
  export const INITIAL_STATE = Object.freeze({
4
27
  isOpen: false,
5
28
  position: {
@@ -7,38 +30,53 @@ export const INITIAL_STATE = Object.freeze({
7
30
  y: 0,
8
31
  },
9
32
  selectedValue: null,
33
+ hoveredOptionId: null,
34
+ hoveredAddOption: false,
10
35
  });
11
36
 
12
- export const toViewData = ({ state, props }) => {
37
+ export const toViewData = ({ state, props, attrs }) => {
38
+ // Generate container attribute string
39
+ const containerAttrString = stringifyAttrs(attrs);
40
+
13
41
  // Use state's selected value if available, otherwise use props.selectedValue
14
42
  const currentValue = state.selectedValue !== null ? state.selectedValue : props.selectedValue;
15
-
43
+
16
44
  // Calculate display label from value
17
45
  let displayLabel = props.placeholder || 'Select an option';
46
+ let isPlaceholderLabel = true;
18
47
  if (currentValue !== null && currentValue !== undefined && props.options) {
19
48
  const selectedOption = props.options.find(opt => deepEqual(opt.value, currentValue));
20
49
  if (selectedOption) {
21
50
  displayLabel = selectedOption.label;
51
+ isPlaceholderLabel = false;
22
52
  }
23
53
  }
24
-
54
+
25
55
  // Map options to include isSelected flag and computed background color
26
- const optionsWithSelection = (props.options || []).map(option => {
56
+ const optionsWithSelection = (props.options || []).map((option, index) => {
27
57
  const isSelected = deepEqual(option.value, currentValue);
58
+ const isHovered = state.hoveredOptionId === index;
28
59
  return {
29
60
  ...option,
30
61
  isSelected,
31
- bgc: isSelected ? 'mu' : ''
62
+ bgc: isHovered ? 'ac' : (isSelected ? 'mu' : '')
32
63
  };
33
64
  });
34
-
65
+
35
66
  return {
67
+ containerAttrString,
36
68
  isOpen: state.isOpen,
37
69
  position: state.position,
38
70
  options: optionsWithSelection,
39
71
  selectedValue: currentValue,
40
72
  selectedLabel: displayLabel,
41
- placeholder: props.placeholder || 'Select an option'
73
+ selectedLabelColor: isPlaceholderLabel ? "mu-fg" : "fg",
74
+ placeholder: props.placeholder || 'Select an option',
75
+ hasValue: currentValue !== null && currentValue !== undefined,
76
+ showClear: !attrs['no-clear'] && !props['no-clear'] && (currentValue !== null && currentValue !== undefined),
77
+ showAddOption: !!props.addOption,
78
+ addOptionLabel: props.addOption?.label ? `+ ${props.addOption.label}` : '+ Add',
79
+ addOptionBgc: state.hoveredAddOption ? 'ac' : ''
42
80
  };
43
81
  }
44
82
 
@@ -46,10 +84,18 @@ export const selectState = ({ state }) => {
46
84
  return state;
47
85
  }
48
86
 
87
+ export const selectSelectedValue = ({ state }) => {
88
+ return state.selectedValue;
89
+ }
90
+
49
91
  export const openOptionsPopover = (state, payload) => {
50
- const { position } = payload;
92
+ const { position, selectedIndex } = payload;
51
93
  state.position = position;
52
94
  state.isOpen = true;
95
+ // Set hoveredOptionId to the selected option's index if available
96
+ if (selectedIndex !== undefined && selectedIndex !== null) {
97
+ state.hoveredOptionId = selectedIndex;
98
+ }
53
99
  }
54
100
 
55
101
  export const closeOptionsPopover = (state) => {
@@ -65,5 +111,21 @@ export const resetSelection = (state) => {
65
111
  state.selectedValue = undefined;
66
112
  }
67
113
 
114
+ export const setHoveredOption = (state, optionId) => {
115
+ state.hoveredOptionId = optionId;
116
+ }
117
+
118
+ export const clearHoveredOption = (state) => {
119
+ state.hoveredOptionId = null;
120
+ }
121
+
122
+ export const clearSelectedValue = (state) => {
123
+ state.selectedValue = undefined;
124
+ }
125
+
126
+ export const setHoveredAddOption = (state, isHovered) => {
127
+ state.hoveredAddOption = isHovered;
128
+ }
129
+
68
130
 
69
131
 
@@ -11,8 +11,6 @@ propsSchema:
11
11
  items:
12
12
  type: object
13
13
  properties:
14
- id:
15
- type: string
16
14
  label:
17
15
  type: string
18
16
  value:
@@ -23,12 +21,23 @@ propsSchema:
23
21
  type: string
24
22
  onChange:
25
23
  type: function
24
+ no-clear:
25
+ type: boolean
26
+ addOption:
27
+ type: object
28
+ properties:
29
+ label:
30
+ type: string
26
31
 
27
32
  refs:
28
33
  select-button:
29
34
  eventListeners:
30
35
  click:
31
36
  handler: handleButtonClick
37
+ clear-button:
38
+ eventListeners:
39
+ click:
40
+ handler: handleClearClick
32
41
  popover:
33
42
  eventListeners:
34
43
  close:
@@ -37,14 +46,35 @@ refs:
37
46
  eventListeners:
38
47
  click:
39
48
  handler: handleOptionClick
49
+ mouseenter:
50
+ handler: handleOptionMouseEnter
51
+ mouseleave:
52
+ handler: handleOptionMouseLeave
53
+ option-add:
54
+ eventListeners:
55
+ click:
56
+ handler: handleAddOptionClick
57
+ mouseenter:
58
+ handler: handleAddOptionMouseEnter
59
+ mouseleave:
60
+ handler: handleAddOptionMouseLeave
40
61
 
41
62
  events: {}
42
63
 
43
64
  template:
44
- - rtgl-button#select-button v=ol:
45
- - ${selectedLabel}
46
- - rtgl-popover#popover ?open=${isOpen} x=${position.x} y=${position.y}:
47
- - rtgl-view wh=300 g=xs slot=content bgc=background br=md sv=true:
48
- - $for option, i in options:
49
- - rtgl-view#option-${i} w=f h-bgc=ac ph=lg pv=md cur=p br=md bgc=${option.bgc}:
50
- - rtgl-text: ${option.label}
65
+ - rtgl-button#select-button v=ol ${containerAttrString}:
66
+ - rtgl-view d=h av=c w=f:
67
+ - rtgl-text c=${selectedLabelColor}: ${selectedLabel}
68
+ - rtgl-view mh=md flex=1:
69
+ - $if showClear:
70
+ - rtgl-svg#clear-button mr=md svg=x wh=16 c=mu-fg cur=p:
71
+ - rtgl-svg svg=chevronDown wh=16 c=mu-fg:
72
+ - rtgl-popover#popover ?open=${isOpen} x=${position.x} y=${position.y} placement=right-start:
73
+ - rtgl-view wh=300 g=xs slot=content bgc=background br=md sv=true:
74
+ - $for option, i in options:
75
+ - rtgl-view#option-${i} w=f ph=lg pv=md cur=p br=md bgc=${option.bgc}:
76
+ - rtgl-text: ${option.label}
77
+ - $if showAddOption:
78
+ - rtgl-view w=f bw=xs bc=mu-bg bt=sm:
79
+ - rtgl-view#option-add w=f ph=lg pv=md cur=p br=md bgc=${addOptionBgc}:
80
+ - rtgl-text c=ac: ${addOptionLabel}
@@ -1,6 +1,7 @@
1
1
  import { css, dimensionWithUnit } from "../common.js";
2
2
  import flexChildStyles from "../styles/flexChildStyles.js";
3
3
  import paddingSvgStyles from "../styles/paddingSvgStyles.js";
4
+ import marginStyles from "../styles/marginStyles.js";
4
5
  import cursorStyles from "../styles/cursorStyles.js";
5
6
  import textColorStyles from "../styles/textColorStyles.js";
6
7
 
@@ -18,6 +19,7 @@ class RettangoliSvgElement extends HTMLElement {
18
19
  }
19
20
  ${textColorStyles}
20
21
  ${paddingSvgStyles}
22
+ ${marginStyles}
21
23
  ${flexChildStyles}
22
24
  ${cursorStyles}
23
25
  `);