@rettangoli/ui 1.6.0 → 1.7.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.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
@@ -71,6 +71,14 @@ const updateFieldAttributes = ({
71
71
  }
72
72
  }
73
73
 
74
+ if (field.type === "tag-select" && ref.store?.updateSelectedValues) {
75
+ const value = get(formValues, field.name);
76
+ ref.store.updateSelectedValues({ values: Array.isArray(value) ? value : [] });
77
+ if (typeof ref.render === "function") {
78
+ ref.render();
79
+ }
80
+ }
81
+
74
82
  if (field.type === "checkbox") {
75
83
  const value = get(formValues, field.name);
76
84
  if (value) {
@@ -60,8 +60,13 @@ const syncFieldValueAttribute = ({ ref, fieldType, value, forceRefresh = false }
60
60
 
61
61
  const syncChoiceFieldState = ({ ref, value }) => {
62
62
  if (!ref) return;
63
- if (!ref?.store?.updateSelectedValue) return;
64
- ref.store.updateSelectedValue({ value });
63
+ if (ref?.store?.updateSelectedValues) {
64
+ ref.store.updateSelectedValues({ values: Array.isArray(value) ? value : [] });
65
+ } else if (ref?.store?.updateSelectedValue) {
66
+ ref.store.updateSelectedValue({ value });
67
+ } else {
68
+ return;
69
+ }
65
70
  if (typeof ref.render === "function") {
66
71
  ref.render();
67
72
  }
@@ -93,7 +98,7 @@ const syncChoiceRefsFromValues = ({ root, values = {} }) => {
93
98
  if (!root || typeof root.querySelectorAll !== "function") return;
94
99
 
95
100
  const choiceRefs = root.querySelectorAll(
96
- "rtgl-select[data-field-name], rtgl-segmented-control[data-field-name]",
101
+ "rtgl-select[data-field-name], rtgl-tag-select[data-field-name], rtgl-segmented-control[data-field-name]",
97
102
  );
98
103
  choiceRefs.forEach((ref) => {
99
104
  const fieldName = ref.dataset?.fieldName;
@@ -142,7 +147,7 @@ export const setValues = function (payload = {}) {
142
147
  forceRefresh: true,
143
148
  });
144
149
 
145
- if (typeof ref?.tagName === "string" && ref.tagName.toUpperCase() === "RTGL-SELECT") {
150
+ if (["select", "tag-select", "segmented-control"].includes(field.type)) {
146
151
  syncChoiceFieldState({ ref, value });
147
152
  }
148
153
 
@@ -284,6 +284,7 @@ export const validateField = (field, value) => {
284
284
  value === undefined ||
285
285
  value === null ||
286
286
  value === "" ||
287
+ (Array.isArray(value) && value.length === 0) ||
287
288
  (typeof value === "boolean" && value === false);
288
289
  // For numbers, 0 is a valid value
289
290
  const isEmptyNumber = field.type === "input-number" && value === null;
@@ -404,6 +405,8 @@ export const getDefaultValue = (field) => {
404
405
  case "select":
405
406
  case "segmented-control":
406
407
  return null;
408
+ case "tag-select":
409
+ return [];
407
410
  case "checkbox":
408
411
  return false;
409
412
  case "color-picker":
@@ -507,9 +510,10 @@ export const selectViewData = ({ state, props }) => {
507
510
  field._inputType = field.inputType || "text";
508
511
  }
509
512
 
510
- if (field.type === "select" || field.type === "segmented-control") {
513
+ if (field.type === "select" || field.type === "tag-select" || field.type === "segmented-control") {
511
514
  const val = get(state.formValues, field.name);
512
515
  field._selectedValue = val !== undefined ? val : null;
516
+ field._selectedValues = Array.isArray(val) ? val : [];
513
517
  field.placeholder = field.placeholder || "";
514
518
  // clearable defaults to true; noClear is the inverse
515
519
  field.noClear = field.clearable === false;
@@ -68,6 +68,8 @@ template:
68
68
  - rtgl-textarea#field${field._idx} data-field-name=${field.name} w=f rows=${field.rows} placeholder=${field.placeholder} ?disabled=${field._disabled}: null
69
69
  - $if field.type == "select":
70
70
  - rtgl-select#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} ?no-clear=${field.noClear} :selectedValue=${field._selectedValue} :placeholder=${field.placeholder} ?disabled=${field._disabled}: null
71
+ - $if field.type == "tag-select":
72
+ - rtgl-tag-select#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} :selectedValues=${field._selectedValues} :placeholder=${field.placeholder} :addOption=${field.addOption} ?disabled=${field._disabled}: null
71
73
  - $if field.type == "segmented-control":
72
74
  - rtgl-segmented-control#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} ?no-clear=${field.noClear} :selectedValue=${field._selectedValue} :placeholder=${field.placeholder} ?disabled=${field._disabled}: null
73
75
  - $if field.type == "color-picker":
@@ -1,6 +1,7 @@
1
1
  const VALID_DIALOG_SIZES = new Set(["sm", "md", "lg", "f"]);
2
2
  const VALID_TOAST_SIZES = new Set(["sm", "md", "lg"]);
3
3
  const VALID_TOAST_PHASES = new Set(["active", "exiting"]);
4
+ const VALID_TOAST_POSITIONS = new Set(["top", "bottom"]);
4
5
  const VALID_COMPONENT_DIALOG_ROLES = new Set(["confirm", "cancel"]);
5
6
 
6
7
  const DEFAULT_COMPONENT_DIALOG_BUTTONS = Object.freeze([
@@ -39,6 +40,10 @@ const normalizeToastPhase = (value, fallback = "active") => {
39
40
  return VALID_TOAST_PHASES.has(value) ? value : fallback;
40
41
  };
41
42
 
43
+ const normalizeToastPosition = (value, fallback = "top") => {
44
+ return VALID_TOAST_POSITIONS.has(value) ? value : fallback;
45
+ };
46
+
42
47
  const normalizeComponentDialogActions = (value) => {
43
48
  const sourceButtons = Array.isArray(value?.buttons) && value.buttons.length > 0
44
49
  ? value.buttons
@@ -223,6 +228,7 @@ export const addToast = ({ state }, options = {}) => {
223
228
  id: `toast-${nextToastId}`,
224
229
  message: options.message,
225
230
  size: normalizeToastSize(options.size ?? options.s, "sm"),
231
+ position: normalizeToastPosition(options.position, "top"),
226
232
  phase: "active",
227
233
  };
228
234
 
@@ -277,6 +283,14 @@ export const selectViewData = ({ state }) => {
277
283
  const isFormDialogOpen = state.isOpen && state.uiType === "formDialog";
278
284
  const isComponentDialogOpen = state.isOpen && state.uiType === "componentDialog";
279
285
  const componentDialogConfig = state.componentDialogConfig ?? createDefaultComponentDialogConfig();
286
+ const normalizedToasts = Array.isArray(state.toasts)
287
+ ? state.toasts.map((toast) => ({
288
+ ...toast,
289
+ size: normalizeToastSize(toast.size, "sm"),
290
+ position: normalizeToastPosition(toast.position, "top"),
291
+ phase: normalizeToastPhase(toast.phase, "active"),
292
+ }))
293
+ : [];
280
294
 
281
295
  return {
282
296
  isOpen: state.isOpen,
@@ -305,13 +319,9 @@ export const selectViewData = ({ state }) => {
305
319
  actions: componentDialogConfig.actions ?? normalizeComponentDialogActions(),
306
320
  key: componentDialogConfig.key ?? 0,
307
321
  },
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
- : [],
322
+ toasts: normalizedToasts,
323
+ topToasts: normalizedToasts.filter((toast) => toast.position === "top"),
324
+ bottomToasts: normalizedToasts.filter((toast) => toast.position === "bottom"),
315
325
  isDialogOpen,
316
326
  isFormDialogOpen,
317
327
  isComponentDialogOpen,
@@ -38,34 +38,53 @@ styles:
38
38
  max-width: calc(100vw - 2 * var(--spacing-lg))
39
39
  opacity: 1
40
40
  transform: translateY(0) scale(1)
41
- animation: toast-in 220ms cubic-bezier(0.16, 1, 0.3, 1)
42
41
  transition: opacity 180ms cubic-bezier(0.16, 1, 0.3, 1), transform 180ms cubic-bezier(0.16, 1, 0.3, 1)
43
42
  will-change: opacity, transform
43
+ .toast-card-top:
44
+ animation: toast-in-top 220ms cubic-bezier(0.16, 1, 0.3, 1)
45
+ .toast-card-bottom:
46
+ animation: toast-in-bottom 220ms cubic-bezier(0.16, 1, 0.3, 1)
44
47
  .toast-card-md:
45
48
  width: 50vw
46
49
  .toast-card-lg:
47
50
  width: 80vw
48
- .toast-card-exiting:
51
+ .toast-card-top.toast-card-exiting:
49
52
  opacity: 0
50
53
  transform: translateY(calc(var(--spacing-sm) * -0.5)) scale(0.98)
54
+ .toast-card-bottom.toast-card-exiting:
55
+ opacity: 0
56
+ transform: translateY(calc(var(--spacing-sm) * 0.5)) scale(0.98)
51
57
  .toast-message:
52
58
  overflow-wrap: anywhere
53
- '@keyframes toast-in':
59
+ '@keyframes toast-in-top':
54
60
  from:
55
61
  opacity: 0
56
62
  transform: translateY(calc(var(--spacing-sm) * -1)) scale(0.96)
57
63
  to:
58
64
  opacity: 1
59
65
  transform: translateY(0) scale(1)
66
+ '@keyframes toast-in-bottom':
67
+ from:
68
+ opacity: 0
69
+ transform: translateY(var(--spacing-sm)) scale(0.96)
70
+ to:
71
+ opacity: 1
72
+ transform: translateY(0) scale(1)
60
73
  '@media (prefers-reduced-motion: reduce)':
61
74
  .toast-card:
62
75
  animation: none
63
76
  transition: none
64
77
  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}
78
+ - $if topToasts.length > 0:
79
+ - rtgl-view class="toast-layer toast-layer-top" pos=fix edge=t z=2100 w=f ah=c g=sm ph=md pt=xl:
80
+ - $for toast, i in topToasts:
81
+ - 'rtgl-view class="toast-card toast-card-top toast-card-${toast.size} toast-card-${toast.phase}" key=toast-${toast.id} bgc=su bc=bo bw=xs br=md shadow=md ph=xl pv=lg':
82
+ - rtgl-text class=toast-message ta=c w=f: ${toast.message}
83
+ - $if bottomToasts.length > 0:
84
+ - rtgl-view class="toast-layer toast-layer-bottom" pos=fix edge=b z=2100 w=f ah=c g=sm ph=md pb=xl:
85
+ - $for toast, i in bottomToasts:
86
+ - 'rtgl-view class="toast-card toast-card-bottom toast-card-${toast.size} toast-card-${toast.phase}" key=toast-${toast.id} bgc=su bc=bo bw=xs br=md shadow=md ph=xl pv=lg':
87
+ - rtgl-text class=toast-message ta=c w=f: ${toast.message}
69
88
  - rtgl-dialog#dialog ?open=${isDialogContainerOpen} s=${dialogSize}:
70
89
  - $if isFormDialogOpen:
71
90
  - rtgl-form#formDialog slot=content :form=${formDialogConfig.form} :defaultValues=${formDialogConfig.defaultValues} :context=${formDialogConfig.context} ?disabled=${formDialogConfig.disabled} key=form-dialog-${formDialogConfig.key}: null
@@ -0,0 +1,192 @@
1
+ const getOptionType = (option = {}) => {
2
+ if (option.type === "section") {
3
+ return "section";
4
+ }
5
+
6
+ if (option.type === "separator") {
7
+ return "separator";
8
+ }
9
+
10
+ return "item";
11
+ };
12
+
13
+ const normalizeSelectedValues = (selectedValues) => {
14
+ if (!Array.isArray(selectedValues)) {
15
+ return [];
16
+ }
17
+
18
+ return [...selectedValues];
19
+ };
20
+
21
+ const resolveCurrentValues = ({ store, props }) => {
22
+ if (store.selectHasSelectedValues()) {
23
+ return store.selectSelectedValues();
24
+ }
25
+
26
+ return normalizeSelectedValues(props.selectedValues);
27
+ };
28
+
29
+ const emitValueChange = ({
30
+ dispatchEvent,
31
+ value,
32
+ operation,
33
+ changedValue,
34
+ label,
35
+ index,
36
+ item,
37
+ }) => {
38
+ dispatchEvent(
39
+ new CustomEvent("value-change", {
40
+ detail: {
41
+ value,
42
+ operation,
43
+ changedValue,
44
+ label,
45
+ index,
46
+ item,
47
+ },
48
+ bubbles: true,
49
+ }),
50
+ );
51
+ };
52
+
53
+ export const handleBeforeMount = (deps) => {
54
+ const { store, props, render } = deps;
55
+
56
+ if (props.selectedValues !== undefined) {
57
+ store.updateSelectedValues({ values: props.selectedValues });
58
+ render();
59
+ }
60
+ };
61
+
62
+ export const handleOnUpdate = (deps, payload) => {
63
+ const { oldProps, newProps } = payload;
64
+ const { store, render } = deps;
65
+ let shouldRender = false;
66
+
67
+ if (!!newProps?.disabled && !oldProps?.disabled) {
68
+ store.closeOptionsPopover({});
69
+ shouldRender = true;
70
+ }
71
+
72
+ if (oldProps.selectedValues !== newProps.selectedValues) {
73
+ store.updateSelectedValues({ values: newProps.selectedValues, syncDraft: true });
74
+ shouldRender = true;
75
+ }
76
+
77
+ if (oldProps.options !== newProps.options) {
78
+ shouldRender = true;
79
+ }
80
+
81
+ if (shouldRender) {
82
+ render();
83
+ }
84
+ };
85
+
86
+ export const handleTriggerClick = (deps, payload) => {
87
+ const { store, render, refs, props } = deps;
88
+ if (props.disabled) return;
89
+
90
+ const event = payload._event;
91
+ event.stopPropagation();
92
+
93
+ const trigger = refs.trigger;
94
+ const rect = trigger.getBoundingClientRect();
95
+ const currentValues = resolveCurrentValues({ store, props });
96
+
97
+ store.openOptionsPopover({
98
+ position: {
99
+ x: Math.round(rect.left),
100
+ y: Math.round(rect.bottom + 12),
101
+ w: Math.max(Math.round(rect.width), 240),
102
+ },
103
+ values: currentValues,
104
+ });
105
+ render();
106
+ };
107
+
108
+ export const handleTriggerKeyDown = (deps, payload) => {
109
+ const event = payload._event;
110
+ if (event.key !== "Enter" && event.key !== " ") {
111
+ return;
112
+ }
113
+
114
+ event.preventDefault();
115
+ handleTriggerClick(deps, payload);
116
+ };
117
+
118
+ export const handlePopoverClose = (deps) => {
119
+ const { store, render } = deps;
120
+ store.closeOptionsPopover({});
121
+ render();
122
+ };
123
+
124
+ export const handleOptionClick = (deps, payload) => {
125
+ const { render, props, store } = deps;
126
+ if (props.disabled) return;
127
+
128
+ const event = payload._event;
129
+ event.stopPropagation();
130
+
131
+ const id = event.currentTarget.id.slice("option".length);
132
+ const index = Number(id);
133
+ const option = props.options[index];
134
+
135
+ if (!option || getOptionType(option) !== "item") {
136
+ return;
137
+ }
138
+
139
+ store.toggleDraftSelectedValue({ value: option.value });
140
+ render();
141
+ };
142
+
143
+ export const handleOptionKeyDown = (deps, payload) => {
144
+ const event = payload._event;
145
+ if (event.key !== "Enter" && event.key !== " ") {
146
+ return;
147
+ }
148
+
149
+ event.preventDefault();
150
+ handleOptionClick(deps, payload);
151
+ };
152
+
153
+ export const handleSubmitClick = (deps, payload) => {
154
+ const { store, props, render, dispatchEvent } = deps;
155
+ if (props.disabled) return;
156
+
157
+ const event = payload._event;
158
+ event.stopPropagation();
159
+
160
+ const nextValues = store.selectDraftSelectedValues();
161
+
162
+ store.commitDraftSelectedValues({});
163
+
164
+ if (props.onChange && typeof props.onChange === "function") {
165
+ props.onChange(nextValues);
166
+ }
167
+
168
+ emitValueChange({
169
+ dispatchEvent,
170
+ value: nextValues,
171
+ operation: "set",
172
+ changedValue: undefined,
173
+ label: undefined,
174
+ index: null,
175
+ item: undefined,
176
+ });
177
+
178
+ render();
179
+ };
180
+
181
+ export const handleAddOptionClick = (deps, payload) => {
182
+ const { props, dispatchEvent } = deps;
183
+ if (props.disabled) return;
184
+
185
+ const event = payload._event;
186
+ event.stopPropagation();
187
+
188
+ dispatchEvent(new CustomEvent("add-option-click", {
189
+ bubbles: true,
190
+ composed: true,
191
+ }));
192
+ };
@@ -0,0 +1,32 @@
1
+ const resolvePopoverPosition = (trigger) => {
2
+ if (!trigger || typeof trigger.getBoundingClientRect !== "function") {
3
+ return null;
4
+ }
5
+
6
+ const rect = trigger.getBoundingClientRect();
7
+ return {
8
+ x: Math.round(rect.left),
9
+ y: Math.round(rect.bottom + 12),
10
+ w: Math.max(Math.round(rect.width), 240),
11
+ };
12
+ };
13
+
14
+ export const refreshPopover = function () {
15
+ const state = this.store?.getState?.();
16
+
17
+ if (state?.isOpen) {
18
+ const position = resolvePopoverPosition(this.refs?.trigger);
19
+ const draftValues = this.store?.selectDraftSelectedValues?.() || [];
20
+
21
+ if (position) {
22
+ this.store.openOptionsPopover({
23
+ position,
24
+ values: draftValues,
25
+ });
26
+ }
27
+ }
28
+
29
+ if (typeof this.render === "function") {
30
+ this.render();
31
+ }
32
+ };
@@ -0,0 +1,51 @@
1
+ componentName: rtgl-tag-select
2
+ propsSchema:
3
+ type: object
4
+ properties:
5
+ placeholder:
6
+ type: string
7
+ options:
8
+ type: array
9
+ items:
10
+ type: object
11
+ properties:
12
+ type:
13
+ type: string
14
+ enum:
15
+ - section
16
+ - item
17
+ - separator
18
+ label:
19
+ type: string
20
+ value:
21
+ type: any
22
+ icon:
23
+ type: string
24
+ shortcut:
25
+ type: string
26
+ suffixText:
27
+ type: string
28
+ testId:
29
+ type: string
30
+ selectedValues:
31
+ type: array
32
+ items:
33
+ type: any
34
+ onChange:
35
+ type: function
36
+ addOption:
37
+ type: object
38
+ properties:
39
+ label:
40
+ type: string
41
+ disabled:
42
+ type: boolean
43
+ w:
44
+ type: string
45
+ events:
46
+ value-change: {}
47
+ add-option-click: {}
48
+ methods:
49
+ type: object
50
+ properties:
51
+ refreshPopover: {}