@rettangoli/ui 1.0.0-rc9 → 1.0.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.
Files changed (45) hide show
  1. package/dist/rettangoli-iife-layout.min.js +74 -42
  2. package/dist/rettangoli-iife-ui.min.js +161 -60
  3. package/dist/themes/base.css +2 -2
  4. package/dist/themes/theme-catppuccin.css +1 -1
  5. package/dist/themes/theme-rtgl-mono.css +1 -1
  6. package/dist/themes/theme-rtgl-slate.css +1 -1
  7. package/package.json +11 -7
  8. package/src/common/dimensions.js +85 -0
  9. package/src/common/responsive.js +72 -0
  10. package/src/common.js +6 -4
  11. package/src/components/dropdownMenu/dropdownMenu.schema.yaml +1 -1
  12. package/src/components/form/form.handlers.js +328 -152
  13. package/src/components/form/form.methods.js +205 -0
  14. package/src/components/form/form.schema.yaml +16 -271
  15. package/src/components/form/form.store.js +542 -97
  16. package/src/components/form/form.view.yaml +73 -52
  17. package/src/components/globalUi/globalUi.handlers.js +4 -4
  18. package/src/components/popoverInput/popoverInput.handlers.js +64 -50
  19. package/src/components/popoverInput/popoverInput.schema.yaml +3 -1
  20. package/src/components/popoverInput/popoverInput.store.js +9 -3
  21. package/src/components/popoverInput/popoverInput.view.yaml +4 -4
  22. package/src/components/select/select.handlers.js +15 -19
  23. package/src/components/select/select.schema.yaml +2 -0
  24. package/src/components/select/select.store.js +8 -6
  25. package/src/components/select/select.view.yaml +4 -4
  26. package/src/components/sliderInput/sliderInput.handlers.js +15 -1
  27. package/src/components/sliderInput/sliderInput.schema.yaml +3 -0
  28. package/src/components/sliderInput/sliderInput.store.js +2 -1
  29. package/src/components/sliderInput/sliderInput.view.yaml +2 -2
  30. package/src/components/tooltip/tooltip.schema.yaml +1 -1
  31. package/src/deps/createGlobalUI.js +4 -4
  32. package/src/entry-iife-layout.js +6 -0
  33. package/src/entry-iife-ui.js +8 -0
  34. package/src/index.js +8 -0
  35. package/src/primitives/checkbox.js +295 -0
  36. package/src/primitives/input-date.js +31 -0
  37. package/src/primitives/input-datetime.js +31 -0
  38. package/src/primitives/input-time.js +31 -0
  39. package/src/primitives/input.js +43 -1
  40. package/src/primitives/textarea.js +3 -0
  41. package/src/primitives/view.js +8 -2
  42. package/src/themes/base.css +2 -2
  43. package/src/themes/theme-catppuccin.css +1 -1
  44. package/src/themes/theme-rtgl-mono.css +1 -1
  45. package/src/themes/theme-rtgl-slate.css +1 -1
@@ -15,35 +15,74 @@ const encode = (input) => {
15
15
  return ""
16
16
  }
17
17
  return `"${escapeHtml(String(input))}"`;
18
+ };
19
+
20
+ const isObjectLike = (value) => value !== null && typeof value === "object";
21
+ const isPlainObject = (value) => isObjectLike(value) && !Array.isArray(value);
22
+ const isPathLike = (path) => typeof path === "string" && path.includes(".");
23
+ const hasBracketPathToken = (path) => typeof path === "string" && /[\[\]]/.test(path);
24
+
25
+ function pickByPaths(obj, paths) {
26
+ const result = {};
27
+ for (const path of paths) {
28
+ if (typeof path !== "string" || path.length === 0) continue;
29
+ const value = get(obj, path);
30
+ if (value !== undefined) {
31
+ set(result, path, value);
32
+ }
33
+ }
34
+ return result;
18
35
  }
19
36
 
20
- function pick(obj, keys) {
21
- return keys.reduce((acc, key) => {
22
- if (key in obj) acc[key] = obj[key];
23
- return acc;
24
- }, {});
37
+ function normalizeWhenDirectives(form) {
38
+ if (!isPlainObject(form) || !Array.isArray(form.fields)) {
39
+ return form;
40
+ }
41
+
42
+ const normalizeFields = (fields = []) =>
43
+ fields.map((field) => {
44
+ if (!isPlainObject(field)) {
45
+ return field;
46
+ }
47
+
48
+ if (typeof field.$when === "string" && field.$when.trim().length > 0) {
49
+ const { $when, ...rest } = field;
50
+ const normalizedField = Array.isArray(rest.fields)
51
+ ? { ...rest, fields: normalizeFields(rest.fields) }
52
+ : rest;
53
+ return {
54
+ [`$if ${$when}`]: normalizedField,
55
+ };
56
+ }
57
+
58
+ if (Array.isArray(field.fields)) {
59
+ return {
60
+ ...field,
61
+ fields: normalizeFields(field.fields),
62
+ };
63
+ }
64
+
65
+ return field;
66
+ });
67
+
68
+ return {
69
+ ...form,
70
+ fields: normalizeFields(form.fields),
71
+ };
25
72
  }
26
73
 
27
- export const createInitialState = () => Object.freeze({
28
- formValues: {},
29
- tooltipState: {
30
- open: false,
31
- x: 0,
32
- y: 0,
33
- content: ''
34
- },
35
- });
36
-
37
- // Lodash-like utility functions for nested property access
38
- const get = (obj, path, defaultValue = undefined) => {
39
- if (!path) {
40
- return;
41
- }
42
- const keys = path.split(/[\[\].]/).filter((key) => key !== "");
74
+ // Nested property access utilities
75
+ export const get = (obj, path, defaultValue = undefined) => {
76
+ if (!path) return defaultValue;
77
+ if (!isObjectLike(obj)) return defaultValue;
78
+ if (hasBracketPathToken(path)) return defaultValue;
79
+ const keys = path.split(".").filter((key) => key !== "");
43
80
  let current = obj;
44
-
45
81
  for (const key of keys) {
46
82
  if (current === null || current === undefined || !(key in current)) {
83
+ if (Object.prototype.hasOwnProperty.call(obj, path)) {
84
+ return obj[path];
85
+ }
47
86
  return defaultValue;
48
87
  }
49
88
  current = current[key];
@@ -51,16 +90,21 @@ const get = (obj, path, defaultValue = undefined) => {
51
90
  return current;
52
91
  };
53
92
 
54
- const set = (obj, path, value) => {
55
- const keys = path.split(/[\[\].]/).filter((key) => key !== "");
56
-
57
- // If path contains array notation, delete the original flat key
58
- if (path.includes("[") && path in obj) {
93
+ export const set = (obj, path, value) => {
94
+ if (!isObjectLike(obj) || typeof path !== "string" || path.length === 0) {
95
+ return obj;
96
+ }
97
+ if (hasBracketPathToken(path)) {
98
+ return obj;
99
+ }
100
+ const keys = path.split(".").filter((key) => key !== "");
101
+ if (keys.length === 0) {
102
+ return obj;
103
+ }
104
+ if (isPathLike(path) && Object.prototype.hasOwnProperty.call(obj, path)) {
59
105
  delete obj[path];
60
106
  }
61
-
62
107
  let current = obj;
63
-
64
108
  for (let i = 0; i < keys.length - 1; i++) {
65
109
  const key = keys[i];
66
110
  if (
@@ -68,121 +112,522 @@ const set = (obj, path, value) => {
68
112
  typeof current[key] !== "object" ||
69
113
  current[key] === null
70
114
  ) {
71
- // Check if next key is a number to create array
72
- const nextKey = keys[i + 1];
73
- const isArrayIndex = /^\d+$/.test(nextKey);
74
- current[key] = isArrayIndex ? [] : {};
115
+ current[key] = {};
75
116
  }
76
117
  current = current[key];
77
118
  }
78
-
79
119
  current[keys[keys.length - 1]] = value;
80
120
  return obj;
81
121
  };
82
122
 
83
- const blacklistedAttrs = ["id", "class", "style", "slot", "form", "defaultValues", "context", "autofocus", "key"];
123
+ const blacklistedAttrs = ["id", "class", "style", "slot", "form", "defaultValues", "disabled", "context"];
84
124
 
85
125
  const stringifyAttrs = (props = {}) => {
86
126
  return Object.entries(props)
87
- .filter(([key]) => !blacklistedAttrs.includes(key))
88
- .map(([key, value]) => `${key}=${value}`)
127
+ .filter(([key, value]) => {
128
+ if (blacklistedAttrs.includes(key)) return false;
129
+ if (value === undefined || value === null) return false;
130
+ if (typeof value === "object" || typeof value === "function") return false;
131
+ return true;
132
+ })
133
+ .map(([key, value]) => `${key}=${String(value)}`)
89
134
  .join(" ");
90
135
  };
91
136
 
92
- export const selectForm = ({ props }) => {
93
- const { form = {} } = props;
94
- const { context } = props;
137
+ // --- Validation ---
138
+
139
+ const PATTERN_PRESETS = {
140
+ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
141
+ url: /^https?:\/\/.+/,
142
+ };
143
+
144
+ const DEFAULT_MESSAGES = {
145
+ required: "This field is required",
146
+ minLength: (val) => `Must be at least ${val} characters`,
147
+ maxLength: (val) => `Must be at most ${val} characters`,
148
+ pattern: "Invalid format",
149
+ invalidDate: "Invalid date format",
150
+ invalidTime: "Invalid time format",
151
+ invalidDateTime: "Invalid date and time format",
152
+ minTemporal: (val) => `Must be on or after ${val}`,
153
+ maxTemporal: (val) => `Must be on or before ${val}`,
154
+ };
155
+
156
+ const DATE_FIELD_TYPE = "input-date";
157
+ const TIME_FIELD_TYPE = "input-time";
158
+ const DATETIME_FIELD_TYPE = "input-datetime";
159
+
160
+ const DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
161
+ const TIME_REGEX = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
162
+ const DATETIME_REGEX = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}(?::\d{2})?)$/;
163
+
164
+ const parseDateParts = (value) => {
165
+ if (typeof value !== "string") return null;
166
+ const match = DATE_REGEX.exec(value);
167
+ if (!match) return null;
168
+
169
+ const year = Number(match[1]);
170
+ const month = Number(match[2]);
171
+ const day = Number(match[3]);
172
+ if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
173
+ return null;
174
+ }
175
+ if (month < 1 || month > 12 || day < 1 || day > 31) {
176
+ return null;
177
+ }
178
+
179
+ const date = new Date(Date.UTC(year, month - 1, day));
180
+ const valid = date.getUTCFullYear() === year
181
+ && date.getUTCMonth() === month - 1
182
+ && date.getUTCDate() === day;
183
+ if (!valid) return null;
184
+
185
+ return { year, month, day };
186
+ };
187
+
188
+ const parseTimeParts = (value) => {
189
+ if (typeof value !== "string") return null;
190
+ const match = TIME_REGEX.exec(value);
191
+ if (!match) return null;
192
+
193
+ const hour = Number(match[1]);
194
+ const minute = Number(match[2]);
195
+ const second = match[3] === undefined ? 0 : Number(match[3]);
196
+ if (!Number.isInteger(hour) || !Number.isInteger(minute) || !Number.isInteger(second)) {
197
+ return null;
198
+ }
199
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
200
+ return null;
201
+ }
202
+
203
+ return { hour, minute, second };
204
+ };
205
+
206
+ const normalizeTimeComparable = (value) => {
207
+ const parts = parseTimeParts(value);
208
+ if (!parts) return null;
209
+ return `${String(parts.hour).padStart(2, "0")}:${String(parts.minute).padStart(2, "0")}:${String(parts.second).padStart(2, "0")}`;
210
+ };
211
+
212
+ const normalizeDateComparable = (value) => {
213
+ return parseDateParts(value) ? value : null;
214
+ };
215
+
216
+ const normalizeDateTimeComparable = (value) => {
217
+ if (typeof value !== "string") return null;
218
+ const match = DATETIME_REGEX.exec(value);
219
+ if (!match) return null;
220
+
221
+ const date = normalizeDateComparable(match[1]);
222
+ const time = normalizeTimeComparable(match[2]);
223
+ if (!date || !time) return null;
224
+
225
+ return `${date}T${time}`;
226
+ };
227
+
228
+ const getTemporalNormalization = (fieldType) => {
229
+ if (fieldType === DATE_FIELD_TYPE) {
230
+ return {
231
+ normalize: normalizeDateComparable,
232
+ invalidMessage: DEFAULT_MESSAGES.invalidDate,
233
+ };
234
+ }
235
+ if (fieldType === TIME_FIELD_TYPE) {
236
+ return {
237
+ normalize: normalizeTimeComparable,
238
+ invalidMessage: DEFAULT_MESSAGES.invalidTime,
239
+ };
240
+ }
241
+ if (fieldType === DATETIME_FIELD_TYPE) {
242
+ return {
243
+ normalize: normalizeDateTimeComparable,
244
+ invalidMessage: DEFAULT_MESSAGES.invalidDateTime,
245
+ };
246
+ }
247
+ return null;
248
+ };
249
+
250
+ const validateTemporalField = (field, value) => {
251
+ const temporal = getTemporalNormalization(field.type);
252
+ if (!temporal) return null;
253
+
254
+ if (value === undefined || value === null || value === "") {
255
+ return null;
256
+ }
257
+
258
+ const comparableValue = temporal.normalize(String(value));
259
+ if (!comparableValue) {
260
+ return temporal.invalidMessage;
261
+ }
262
+
263
+ if (field.min !== undefined && field.min !== null && String(field.min) !== "") {
264
+ const minComparable = temporal.normalize(String(field.min));
265
+ if (minComparable && comparableValue < minComparable) {
266
+ return DEFAULT_MESSAGES.minTemporal(String(field.min));
267
+ }
268
+ }
269
+
270
+ if (field.max !== undefined && field.max !== null && String(field.max) !== "") {
271
+ const maxComparable = temporal.normalize(String(field.max));
272
+ if (maxComparable && comparableValue > maxComparable) {
273
+ return DEFAULT_MESSAGES.maxTemporal(String(field.max));
274
+ }
275
+ }
276
+
277
+ return null;
278
+ };
279
+
280
+ export const validateField = (field, value) => {
281
+ // Check required
282
+ if (field.required) {
283
+ const isEmpty =
284
+ value === undefined ||
285
+ value === null ||
286
+ value === "" ||
287
+ (typeof value === "boolean" && value === false);
288
+ // For numbers, 0 is a valid value
289
+ const isEmptyNumber = field.type === "input-number" && value === null;
290
+ const shouldFail = field.type === "input-number" ? isEmptyNumber : isEmpty;
291
+
292
+ if (shouldFail) {
293
+ if (typeof field.required === "object" && field.required.message) {
294
+ return field.required.message;
295
+ }
296
+ return DEFAULT_MESSAGES.required;
297
+ }
298
+ }
299
+
300
+ const temporalError = validateTemporalField(field, value);
301
+ if (temporalError) {
302
+ return temporalError;
303
+ }
304
+
305
+ // Check rules
306
+ if (Array.isArray(field.rules)) {
307
+ for (const rule of field.rules) {
308
+ const error = validateRule(rule, value);
309
+ if (error) return error;
310
+ }
311
+ }
312
+
313
+ return null;
314
+ };
315
+
316
+ const validateRule = (rule, value) => {
317
+ // Skip validation on empty values (required handles that)
318
+ if (value === undefined || value === null || value === "") return null;
95
319
 
96
- if (context) {
97
- const result = parseAndRender(form, context);
98
- return result
320
+ const strValue = String(value);
321
+
322
+ switch (rule.rule) {
323
+ case "minLength": {
324
+ if (strValue.length < rule.value) {
325
+ return rule.message || DEFAULT_MESSAGES.minLength(rule.value);
326
+ }
327
+ return null;
328
+ }
329
+ case "maxLength": {
330
+ if (strValue.length > rule.value) {
331
+ return rule.message || DEFAULT_MESSAGES.maxLength(rule.value);
332
+ }
333
+ return null;
334
+ }
335
+ case "pattern": {
336
+ const preset = PATTERN_PRESETS[rule.value];
337
+ let regex = preset;
338
+ if (!regex) {
339
+ try {
340
+ regex = new RegExp(rule.value);
341
+ } catch {
342
+ return rule.message || DEFAULT_MESSAGES.pattern;
343
+ }
344
+ }
345
+ if (!regex.test(strValue)) {
346
+ return rule.message || DEFAULT_MESSAGES.pattern;
347
+ }
348
+ return null;
349
+ }
350
+ default:
351
+ return null;
352
+ }
353
+ };
354
+
355
+ export const validateForm = (fields, formValues) => {
356
+ const errors = {};
357
+ const dataFields = collectAllDataFields(fields);
358
+
359
+ for (const field of dataFields) {
360
+ const value = get(formValues, field.name);
361
+ const error = validateField(field, value);
362
+ if (error) {
363
+ errors[field.name] = error;
364
+ }
365
+ }
366
+
367
+ return {
368
+ valid: Object.keys(errors).length === 0,
369
+ errors,
370
+ };
371
+ };
372
+
373
+ // --- Field helpers ---
374
+
375
+ const DISPLAY_TYPES = ["section", "read-only-text", "slot"];
376
+
377
+ export const isDataField = (field) => {
378
+ return !DISPLAY_TYPES.includes(field.type);
379
+ };
380
+
381
+ export const collectAllDataFields = (fields) => {
382
+ const result = [];
383
+ for (const field of fields) {
384
+ if (field.type === "section" && Array.isArray(field.fields)) {
385
+ result.push(...collectAllDataFields(field.fields));
386
+ } else if (isDataField(field)) {
387
+ result.push(field);
388
+ }
389
+ }
390
+ return result;
391
+ };
392
+
393
+ export const getDefaultValue = (field) => {
394
+ switch (field.type) {
395
+ case "input-text":
396
+ case "input-date":
397
+ case "input-time":
398
+ case "input-datetime":
399
+ case "input-textarea":
400
+ case "popover-input":
401
+ return "";
402
+ case "input-number":
403
+ return null;
404
+ case "select":
405
+ return null;
406
+ case "checkbox":
407
+ return false;
408
+ case "color-picker":
409
+ return "#000000";
410
+ case "slider":
411
+ case "slider-with-input":
412
+ return field.min !== undefined ? field.min : 0;
413
+ case "image":
414
+ return null;
415
+ default:
416
+ return null;
417
+ }
418
+ };
419
+
420
+ export const flattenFields = (fields, startIdx = 0) => {
421
+ const result = [];
422
+ let idx = startIdx;
423
+
424
+ for (const field of fields) {
425
+ if (field.type === "section") {
426
+ result.push({
427
+ ...field,
428
+ _isSection: true,
429
+ _idx: idx,
430
+ });
431
+ idx++;
432
+ if (Array.isArray(field.fields)) {
433
+ const nested = flattenFields(field.fields, idx);
434
+ result.push(...nested);
435
+ idx += nested.length;
436
+ }
437
+ } else {
438
+ result.push({
439
+ ...field,
440
+ _isSection: false,
441
+ _idx: idx,
442
+ });
443
+ idx++;
444
+ }
99
445
  }
100
446
 
101
- return form;
447
+ return result;
102
448
  };
103
449
 
450
+ // --- Store ---
451
+
452
+ export const createInitialState = () =>
453
+ Object.freeze({
454
+ formValues: {},
455
+ errors: {},
456
+ reactiveMode: false,
457
+ tooltipState: {
458
+ open: false,
459
+ x: 0,
460
+ y: 0,
461
+ content: "",
462
+ },
463
+ });
464
+
465
+ export const selectForm = ({ state, props }) => {
466
+ const { form = {} } = props || {};
467
+ const normalizedForm = normalizeWhenDirectives(form);
468
+ const context = isPlainObject(props?.context) ? props.context : {};
469
+ const stateFormValues = isPlainObject(state?.formValues)
470
+ ? state.formValues
471
+ : {};
472
+ const mergedContext = {
473
+ ...context,
474
+ ...stateFormValues,
475
+ formValues: stateFormValues,
476
+ };
477
+
478
+ if (Object.keys(mergedContext).length > 0) {
479
+ return parseAndRender(normalizedForm, mergedContext);
480
+ }
481
+ return normalizedForm;
482
+ };
104
483
 
105
484
  export const selectViewData = ({ state, props }) => {
106
485
  const containerAttrString = stringifyAttrs(props);
107
-
108
486
  const form = selectForm({ state, props });
109
- const fields = structuredClone(form.fields || []);
110
- fields.forEach((field) => {
111
- // Use formValues from state if available, otherwise fall back to defaultValues from props
112
- const defaultValue = get(state.formValues, field.name)
113
- if (["read-only-text"].includes(field.inputType)) {
114
- field.defaultValue = defaultValue
115
- }
116
- if (["select"].includes(field.inputType)) {
117
- const defaultValues = props?.defaultValues;
118
- if (defaultValues && defaultValues[field.name] !== undefined) {
119
- field.selectedValue = defaultValues[field.name];
120
- }
487
+ const fields = form.fields || [];
488
+ const formDisabled = !!props?.disabled;
489
+
490
+ // Flatten fields for template iteration
491
+ const flatFields = flattenFields(fields);
492
+
493
+ // Enrich each field with computed properties
494
+ flatFields.forEach((field, arrIdx) => {
495
+ field._arrIdx = arrIdx;
496
+
497
+ if (field._isSection) return;
498
+
499
+ const isData = isDataField(field);
500
+ field._disabled = formDisabled || !!field.disabled;
501
+
502
+ if (isData && field.name) {
503
+ field._error = state.errors[field.name] || null;
504
+ }
505
+
506
+ // Type-specific computed props
507
+ if (field.type === "input-text") {
508
+ field._inputType = field.inputType || "text";
121
509
  }
122
- if (field.inputType === "image") {
123
- const src = field.src;
124
- // Only set imageSrc if src exists and is not empty
125
- field.imageSrc = src && src.trim() ? src : null;
126
- // Set placeholder text
127
- field.placeholderText = field.placeholder || "No Image";
510
+
511
+ if (field.type === "select") {
512
+ const val = get(state.formValues, field.name);
513
+ field._selectedValue = val !== undefined ? val : null;
514
+ field.placeholder = field.placeholder || "";
515
+ // clearable defaults to true; noClear is the inverse
516
+ field.noClear = field.clearable === false;
517
+ }
518
+
519
+ if (field.type === "image") {
520
+ const src = get(state.formValues, field.name);
521
+ field._imageSrc = src && String(src).trim() ? src : null;
522
+ field.placeholderText = field.placeholderText || "No Image";
523
+ }
524
+
525
+ if (field.type === "read-only-text") {
526
+ field.content = field.content || "";
128
527
  }
129
- if (field.inputType === "waveform") {
130
- const waveformData = field.waveformData;
131
- // Only set waveformData if it exists
132
- field.waveformData = waveformData || null;
133
- // Set placeholder text
134
- field.placeholderText = field.placeholder || "No Waveform";
528
+
529
+ if (field.type === "checkbox") {
530
+ const inlineText = typeof field.content === "string"
531
+ ? field.content
532
+ : (typeof field.checkboxLabel === "string" ? field.checkboxLabel : "");
533
+ field._checkboxText = inlineText;
135
534
  }
136
535
  });
137
536
 
537
+ // Actions
538
+ const actions = form.actions || { buttons: [] };
539
+ const layout = actions.layout || "split";
540
+ const buttons = (actions.buttons || []).map((btn, i) => ({
541
+ ...btn,
542
+ _globalIdx: i,
543
+ variant: btn.variant || "se",
544
+ _disabled: formDisabled || !!btn.disabled,
545
+ pre: btn.pre || "",
546
+ suf: btn.suf || "",
547
+ }));
548
+
549
+ let actionsData;
550
+ if (layout === "split") {
551
+ actionsData = {
552
+ _layout: "split",
553
+ buttons,
554
+ _leftButtons: buttons.filter((b) => b.align === "left"),
555
+ _rightButtons: buttons.filter((b) => b.align !== "left"),
556
+ };
557
+ } else {
558
+ actionsData = {
559
+ _layout: layout,
560
+ buttons,
561
+ _allButtons: buttons,
562
+ };
563
+ }
564
+
138
565
  return {
139
- key: props?.key,
140
566
  containerAttrString,
141
567
  title: form?.title || "",
142
568
  description: form?.description || "",
143
- fields: fields,
144
- actions: props?.form?.actions || {
145
- buttons: [],
146
- },
569
+ flatFields,
570
+ actions: actionsData,
147
571
  formValues: state.formValues,
148
572
  tooltipState: state.tooltipState,
149
573
  };
150
574
  };
151
575
 
152
- export const selectState = ({ state }) => {
153
- return state;
154
- };
155
-
156
576
  export const selectFormValues = ({ state, props }) => {
157
577
  const form = selectForm({ state, props });
158
-
159
- return pick(
578
+ const dataFields = collectAllDataFields(form.fields || []);
579
+ return pickByPaths(
160
580
  state.formValues,
161
- form.fields.map((field) => field.name),
581
+ dataFields.map((f) => f.name).filter((name) => typeof name === "string" && name.length > 0),
162
582
  );
163
583
  };
164
584
 
165
- export const getFormFieldValue = ({ state }, name) => {
166
- return get(state.formValues, name);
167
- };
168
-
169
- export const setFormValues = ({ state }, payload = {}) => {
170
- state.formValues = payload.formValues || {};
171
- };
172
-
173
585
  export const setFormFieldValue = ({ state, props }, payload = {}) => {
174
586
  const { name, value } = payload;
175
- if (!name) {
176
- return;
177
- }
587
+ if (!name) return;
178
588
  set(state.formValues, name, value);
179
- // remove non visible values
589
+ pruneHiddenValues({ state, props });
590
+ };
591
+
592
+ export const pruneHiddenValues = ({ state, props }) => {
593
+ if (!props) return;
594
+ // Prune to only visible field names
180
595
  const form = selectForm({ state, props });
181
- const formValues = pick(
596
+ const dataFields = collectAllDataFields(form.fields || []);
597
+ state.formValues = pickByPaths(
182
598
  state.formValues,
183
- form.fields.map((field) => field.name),
599
+ dataFields.map((f) => f.name).filter((name) => typeof name === "string" && name.length > 0),
184
600
  );
185
- state.formValues = formValues;
601
+ };
602
+
603
+ export const setFormValues = ({ state }, payload = {}) => {
604
+ const { values } = payload;
605
+ if (!values || typeof values !== "object") return;
606
+ Object.keys(values).forEach((key) => {
607
+ set(state.formValues, key, values[key]);
608
+ });
609
+ };
610
+
611
+ export const resetFormValues = ({ state }, payload = {}) => {
612
+ const { defaultValues = {} } = payload;
613
+ state.formValues = defaultValues ? structuredClone(defaultValues) : {};
614
+ state.errors = {};
615
+ state.reactiveMode = false;
616
+ };
617
+
618
+ export const setErrors = ({ state }, payload = {}) => {
619
+ state.errors = payload.errors || {};
620
+ };
621
+
622
+ export const clearFieldError = ({ state }, payload = {}) => {
623
+ const { name } = payload;
624
+ if (name && state.errors[name]) {
625
+ delete state.errors[name];
626
+ }
627
+ };
628
+
629
+ export const setReactiveMode = ({ state }) => {
630
+ state.reactiveMode = true;
186
631
  };
187
632
 
188
633
  export const showTooltip = ({ state }, payload = {}) => {
@@ -198,6 +643,6 @@ export const showTooltip = ({ state }, payload = {}) => {
198
643
  export const hideTooltip = ({ state }) => {
199
644
  state.tooltipState = {
200
645
  ...state.tooltipState,
201
- open: false
646
+ open: false,
202
647
  };
203
648
  };