@omnitend/dashboard-for-laravel 0.33.1 → 0.35.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 (57) hide show
  1. package/dist/charts.js +82 -90
  2. package/dist/charts.js.map +1 -1
  3. package/dist/charts.umd.cjs +1 -1
  4. package/dist/charts.umd.cjs.map +1 -1
  5. package/dist/components/base/DButton.vue.d.ts +1 -1
  6. package/dist/components/charts/DXBarChart.vue.d.ts +3 -18
  7. package/dist/components/charts/DXLineChart.vue.d.ts +3 -18
  8. package/dist/components/charts/useThemedChart.d.ts +35 -0
  9. package/dist/components/extended/DXChoiceField.vue.d.ts +26 -0
  10. package/dist/components/extended/DXDashboardSidebar.vue.d.ts +1 -1
  11. package/dist/components/extended/DXField.vue.d.ts +10 -113
  12. package/dist/components/extended/DXFieldShell.vue.d.ts +73 -0
  13. package/dist/components/extended/DXForm.vue.d.ts +28 -6
  14. package/dist/components/extended/DXNumericField.vue.d.ts +23 -0
  15. package/dist/components/extended/DXRepeater.vue.d.ts +4 -4
  16. package/dist/components/extended/DXRepeaterCards.vue.d.ts +45 -0
  17. package/dist/components/extended/DXTable.vue.d.ts +107 -2
  18. package/dist/components/extended/DXTableEditorModal.vue.d.ts +22 -2
  19. package/dist/components/extended/DXUserAvatar.vue.d.ts +1 -1
  20. package/dist/composables/useAsyncOptions.d.ts +20 -0
  21. package/dist/composables/useContainerWidth.d.ts +96 -0
  22. package/dist/composables/useResourceEditor.d.ts +2 -1
  23. package/dist/dashboard-for-laravel.js +10513 -10443
  24. package/dist/dashboard-for-laravel.js.map +1 -1
  25. package/dist/dashboard-for-laravel.umd.cjs +7 -7
  26. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  27. package/dist/index.d.ts +5 -2
  28. package/dist/style.css +1 -1
  29. package/dist/types/index.d.ts +146 -0
  30. package/dist/utils/formSchema.d.ts +59 -0
  31. package/docs/public/api-reference.json +494 -33
  32. package/docs/public/docs-map.md +1 -1
  33. package/docs/public/llms.txt +5 -2
  34. package/package.json +1 -1
  35. package/resources/css/theme.scss +80 -2
  36. package/resources/js/components/charts/DXBarChart.vue +4 -43
  37. package/resources/js/components/charts/DXLineChart.vue +4 -43
  38. package/resources/js/components/charts/useThemedChart.ts +69 -0
  39. package/resources/js/components/extended/DXChoiceField.vue +114 -0
  40. package/resources/js/components/extended/DXDashboardSidebar.vue +1 -1
  41. package/resources/js/components/extended/DXField.vue +328 -615
  42. package/resources/js/components/extended/DXFieldShell.vue +124 -0
  43. package/resources/js/components/extended/DXForm.vue +92 -14
  44. package/resources/js/components/extended/DXNumericField.vue +144 -0
  45. package/resources/js/components/extended/DXRepeater.vue +53 -175
  46. package/resources/js/components/extended/DXRepeaterCards.vue +133 -0
  47. package/resources/js/components/extended/DXTable.vue +418 -43
  48. package/resources/js/components/extended/DXTableEditorModal.vue +39 -2
  49. package/resources/js/components/extended/DXTablePagination.vue +19 -3
  50. package/resources/js/components/extended/DXTableShell.vue +70 -1
  51. package/resources/js/composables/defineForm.ts +8 -44
  52. package/resources/js/composables/useAsyncOptions.ts +61 -0
  53. package/resources/js/composables/useContainerWidth.ts +303 -0
  54. package/resources/js/composables/useResourceEditor.ts +24 -38
  55. package/resources/js/index.ts +40 -0
  56. package/resources/js/types/index.ts +231 -0
  57. package/resources/js/utils/formSchema.ts +126 -0
@@ -2,6 +2,12 @@ import { computed, ref, type Ref } from "vue";
2
2
  import { api } from "../utils/api";
3
3
  import { useForm } from "./useForm";
4
4
  import { useToast } from "./useToast";
5
+ import type { FieldDefinition } from "../types";
6
+ import {
7
+ resolveFieldDefault,
8
+ isFieldVisible as isFieldVisibleFor,
9
+ isSubmittableField,
10
+ } from "../utils/formSchema";
5
11
 
6
12
  /**
7
13
  * The subset of `DXTable`'s props the resource editor reads. Declared
@@ -11,7 +17,7 @@ import { useToast } from "./useToast";
11
17
  */
12
18
  export interface ResourceEditorProps<T = any> {
13
19
  /** Field definitions for the edit/create modal (enables edit-on-row-click). */
14
- editFields?: any[];
20
+ editFields?: FieldDefinition[];
15
21
  /** Tab definitions — only their `key` is read here (DXForm consumes the rest). */
16
22
  editTabs?: Array<{ key: string }>;
17
23
  /** Modal title, or a function of the edited row. */
@@ -108,58 +114,38 @@ export function useResourceEditor<T = any>(
108
114
  // and auto-switching to the first tab with a validation error.
109
115
 
110
116
  /*
111
- * A field's own starting value, used to seed a CREATE form and as the fallback
112
- * when a row lacks the key (#122).
113
- *
114
- * `field.default ?? ''` can't express a NULL default: a select whose "none"
115
- * option is `value: null` — which is what the column stores — seeds '' instead,
116
- * matches no option, and renders blank. Presence, not nullishness, decides here
117
- * too, so the create and edit paths agree about the same field.
118
- */
119
- const defaultValueFor = (field: any): any =>
120
- Object.prototype.hasOwnProperty.call(field, 'default') ? field.default : '';
121
-
122
- /*
123
- * Seeding an EDIT form from a row (#117).
124
- *
125
- * `row[key] ?? field.default` is wrong: it can't tell "the row has no such key"
126
- * from "the row's value IS null", so an explicitly-null column gets overwritten
127
- * by the field's default — a value the user never saw, on a field they may not
128
- * even be able to see. Presence, not nullishness, decides.
117
+ * Seeding an EDIT form from a row (#117): use the row's own value when it
118
+ * HAS the key (presence, not nullishness — an explicitly-null column must
119
+ * not be overwritten by the field's default), else the field's own default.
120
+ * The default comes from the shared `resolveFieldDefault` rule, so create
121
+ * and edit seeding agree with defineForm/DXRepeater (#134) — this path is
122
+ * now type-aware (an array field falls back to `[]`, not `''`).
129
123
  */
130
124
  const seedValueFor = (field: any, row: T): any => {
131
125
  if (Object.prototype.hasOwnProperty.call(row as any, field.key)) {
132
126
  return (row as any)[field.key];
133
127
  }
134
- return defaultValueFor(field);
128
+ return resolveFieldDefault(field);
135
129
  };
136
130
 
137
131
  /*
138
- * Whether a field is currently VISIBLE, by the same rule DXForm renders by
139
- * (`when` + the legacy `show`), against the same model (context + live data).
140
- * Kept in step with DXForm.isFieldVisible.
132
+ * The model field/tab predicates evaluate against: the edited row widened
133
+ * with the live form data (context + data), same as DXForm's `model`.
141
134
  */
142
135
  const editPredicateModel = computed(() => ({
143
136
  ...((selectedItem.value as any) ?? {}),
144
137
  ...(editForm.value?.data ?? {}),
145
138
  }));
146
139
 
147
- const isEditFieldVisible = (field: any): boolean => {
148
- const when = field.when;
149
- const whenOk =
150
- when === undefined
151
- ? true
152
- : typeof when === 'function'
153
- ? when(editPredicateModel.value)
154
- : when;
155
- const showOk = field.show ? field.show() : true;
156
- return whenOk && showOk;
157
- };
140
+ // Visibility by the one shared rule (`when` + legacy `show`), against the
141
+ // edit model — see formSchema.isFieldVisible (#134).
142
+ const isEditFieldVisible = (field: any): boolean =>
143
+ isFieldVisibleFor(field, editPredicateModel.value);
158
144
 
159
145
  // Fields whose value actually belongs in the payload. A presentational field
160
146
  // (submit: false) renders but holds no data — see FieldDefinition.submit.
161
147
  const submittableEditFields = computed(() =>
162
- (props.editFields ?? []).filter((field) => field.submit !== false),
148
+ (props.editFields ?? []).filter(isSubmittableField),
163
149
  );
164
150
 
165
151
  // Enforced at SUBMIT, not just at seeding: the modal still renders every field,
@@ -172,7 +158,7 @@ export function useResourceEditor<T = any>(
172
158
  .filter(
173
159
  (field) =>
174
160
  // Presentational: lays the form out, holds no data (#110).
175
- field.submit === false ||
161
+ !isSubmittableField(field) ||
176
162
  // Hidden by `when` at submit time (#117). Submitting a field
177
163
  // the user cannot see is a silent write — and with `default`
178
164
  // set it writes a value they never chose. Omitting the key
@@ -299,13 +285,13 @@ export function useResourceEditor<T = any>(
299
285
  if (!editForm.value) {
300
286
  const formData: Record<string, any> = {};
301
287
  submittableEditFields.value.forEach(field => {
302
- formData[field.key] = defaultValueFor(field);
288
+ formData[field.key] = resolveFieldDefault(field);
303
289
  });
304
290
  editForm.value = useForm(formData);
305
291
  } else {
306
292
  // Reset existing form to defaults
307
293
  submittableEditFields.value.forEach(field => {
308
- editForm.value.data[field.key] = defaultValueFor(field);
294
+ editForm.value.data[field.key] = resolveFieldDefault(field);
309
295
  });
310
296
  editForm.value.clearErrors();
311
297
  }
@@ -110,9 +110,20 @@ export { default as DTooltip } from "./components/base/DTooltip.vue";
110
110
  export { useForm } from "./composables/useForm";
111
111
  export { defineForm } from "./composables/defineForm";
112
112
  export { useToast } from "./composables/useToast";
113
+ export { useContainerWidth } from "./composables/useContainerWidth";
113
114
 
114
115
  // Utils
115
116
  export { api } from "./utils/api";
117
+ // Pure form-schema helpers (#134) — the single source of defaulting, cloning,
118
+ // predicate resolution, visibility and submission-eligibility.
119
+ export {
120
+ defaultValueForType,
121
+ cloneDefault,
122
+ resolveFieldDefault,
123
+ resolvePredicate,
124
+ isFieldVisible,
125
+ isSubmittableField,
126
+ } from "./utils/formSchema";
116
127
 
117
128
  // Types
118
129
  export type {
@@ -122,6 +133,27 @@ export type {
122
133
  FormTab,
123
134
  MaybeFn,
124
135
  OptionsLoader,
136
+ // Strict, discriminated field types (#131) — opt-in; canonical in a future
137
+ // major. `FieldDef` is the union; the rest are its per-type members.
138
+ FieldDef,
139
+ BaseFieldDef,
140
+ TextFieldDef,
141
+ PasswordFieldDef,
142
+ NumberFieldDef,
143
+ DateFieldDef,
144
+ CurrencyFieldDef,
145
+ PercentageFieldDef,
146
+ TextareaFieldDef,
147
+ SelectFieldDef,
148
+ AutocompleteFieldDef,
149
+ CheckboxFieldDef,
150
+ CheckboxGroupFieldDef,
151
+ SwitchFieldDef,
152
+ SwitchListFieldDef,
153
+ RadioFieldDef,
154
+ FileFieldDef,
155
+ ComponentFieldDef,
156
+ RepeaterFieldDef,
125
157
  } from "./types";
126
158
 
127
159
  export type {
@@ -137,6 +169,12 @@ export type {
137
169
  DefineFormReturn,
138
170
  } from "./composables/defineForm";
139
171
 
172
+ export type {
173
+ ContainerWidthTarget,
174
+ UseContainerWidthOptions,
175
+ UseContainerWidthReturn,
176
+ } from "./composables/useContainerWidth";
177
+
140
178
  export type {
141
179
  ApiError,
142
180
  ApiResponse,
@@ -149,6 +187,8 @@ export type {
149
187
  BTableProvider,
150
188
  EditTab,
151
189
  BTableProviderContext,
190
+ DXTableApiAdapter,
191
+ DXTableSource,
152
192
  } from "./components/extended/DXTable.vue";
153
193
 
154
194
  export type {
@@ -380,6 +380,237 @@ export interface FieldDefinition {
380
380
  show?: () => boolean;
381
381
  }
382
382
 
383
+ // ───────────────────────────────────────────────────────────────────────────
384
+ // Strict, discriminated field types (#131)
385
+ //
386
+ // The permissive `FieldDefinition` above lets every option appear on every
387
+ // field type: `currencySymbol` compiles on a checkbox, `options` on a text
388
+ // input, `component` without `type: "component"`. The types below are the
389
+ // strict, `type`-keyed replacement — each variant permits only its own
390
+ // options, so the compiler catches an invalid field config at the call site.
391
+ //
392
+ // ADDITIVE for now: `FieldDefinition` stays canonical and these are opt-in
393
+ // (`import type { FieldDef } from "@omnitend/dashboard-for-laravel"`). A future
394
+ // major (#131 Phase 3) makes `FieldDef` canonical and removes the permissive
395
+ // interface with a codemod. **Until then, keep the two in step** — a prop added
396
+ // to `FieldDefinition` must be added to the matching variant(s) below. The doc
397
+ // comments live on `FieldDefinition`; variants stay terse to keep them aligned.
398
+ //
399
+ // Known limitation: the union rejects invalid FRESH object literals (excess
400
+ // property checking — the common `fields: [{ type: "checkbox", … }]` call site),
401
+ // but a PRE-DECLARED object with excess props (`const f = { type: "checkbox",
402
+ // currencySymbol: "£" }`) stays structurally assignable. Full exclusivity would
403
+ // need `never`-typed exclusion keys on every variant (or a StrictUnion helper) —
404
+ // heavy machinery with confusing errors for marginal gain on an opt-in type, so
405
+ // it's deliberately not done here.
406
+ // ───────────────────────────────────────────────────────────────────────────
407
+
408
+ /** Options common to every field variant (the non-`type`-specific props). */
409
+ export interface BaseFieldDef {
410
+ key: string;
411
+ label?: MaybeFn<string>;
412
+ required?: boolean;
413
+ help?: string;
414
+ hint?: MaybeFn<string>;
415
+ info?: MaybeFn<string>;
416
+ class?: string;
417
+ inputProps?: Record<string, any>;
418
+ span?: boolean;
419
+ layout?: "vertical" | "horizontal";
420
+ labelCols?: LabelCols;
421
+ hideLabel?: boolean;
422
+ default?: any;
423
+ disabled?: MaybeFn<boolean>;
424
+ disabledWhen?: (model: any) => boolean;
425
+ readonly?: MaybeFn<boolean>;
426
+ plaintext?: MaybeFn<boolean>;
427
+ submit?: boolean;
428
+ when?: MaybeFn<boolean>;
429
+ show?: () => boolean;
430
+ }
431
+
432
+ /** Mixins shared by several variants (not exported — internal composition). */
433
+ interface WithPlaceholder {
434
+ placeholder?: string;
435
+ }
436
+ interface WithNumericBounds {
437
+ step?: number | string;
438
+ min?: number | string;
439
+ max?: number | string;
440
+ }
441
+ interface WithSelectableOptions {
442
+ options?: FieldOption[];
443
+ optionsLoader?: OptionsLoader;
444
+ reloadOptionsOnChange?: boolean;
445
+ }
446
+
447
+ /** `text` / `email` / `url` / `tel` — a plain text-like `<input>`. */
448
+ export interface TextFieldDef extends BaseFieldDef, WithPlaceholder {
449
+ type: "text" | "email" | "url" | "tel";
450
+ }
451
+
452
+ /** `password` — text input with an optional reveal (eye) toggle. */
453
+ export interface PasswordFieldDef extends BaseFieldDef, WithPlaceholder {
454
+ type: "password";
455
+ revealable?: boolean;
456
+ }
457
+
458
+ /** `number` — numeric input with optional bounds. */
459
+ export interface NumberFieldDef
460
+ extends BaseFieldDef,
461
+ WithPlaceholder,
462
+ WithNumericBounds {
463
+ type: "number";
464
+ }
465
+
466
+ /** `date` / `datetime` / `datetime-local` / `time` — native date/time controls.
467
+ * `min`/`max`/`step` are valid on these inputs (e.g. `min="2024-01-01"`) and
468
+ * DXField forwards them, so the variant permits them. */
469
+ export interface DateFieldDef extends BaseFieldDef, WithNumericBounds {
470
+ type: "date" | "datetime" | "datetime-local" | "time";
471
+ }
472
+
473
+ /** `currency` — numeric input with a symbol affix and minor-unit handling. */
474
+ export interface CurrencyFieldDef
475
+ extends BaseFieldDef,
476
+ WithPlaceholder,
477
+ WithNumericBounds {
478
+ type: "currency";
479
+ currencySymbol?: string;
480
+ decimals?: number;
481
+ minorUnits?: boolean;
482
+ }
483
+
484
+ /** `percentage` — numeric input shown as a 0–100 percentage. */
485
+ export interface PercentageFieldDef
486
+ extends BaseFieldDef,
487
+ WithPlaceholder,
488
+ WithNumericBounds {
489
+ type: "percentage";
490
+ asFraction?: boolean;
491
+ }
492
+
493
+ /** `textarea` — multi-line text input. */
494
+ export interface TextareaFieldDef extends BaseFieldDef, WithPlaceholder {
495
+ type: "textarea";
496
+ rows?: number;
497
+ }
498
+
499
+ /** `select` — dropdown; `searchable` upgrades it to a filterable select. */
500
+ export interface SelectFieldDef
501
+ extends BaseFieldDef,
502
+ WithPlaceholder,
503
+ WithSelectableOptions {
504
+ type: "select";
505
+ searchable?: boolean;
506
+ }
507
+
508
+ /** `autocomplete` — free-text input with datalist suggestions. */
509
+ export interface AutocompleteFieldDef
510
+ extends BaseFieldDef,
511
+ WithPlaceholder,
512
+ WithSelectableOptions {
513
+ type: "autocomplete";
514
+ }
515
+
516
+ /** `checkbox` — a single boolean checkbox. */
517
+ export interface CheckboxFieldDef extends BaseFieldDef {
518
+ type: "checkbox";
519
+ }
520
+
521
+ /** `checkbox-group` — multiple checkboxes; model is an array of values. */
522
+ export interface CheckboxGroupFieldDef
523
+ extends BaseFieldDef,
524
+ WithSelectableOptions {
525
+ type: "checkbox-group";
526
+ }
527
+
528
+ /** `switch` — a toggle with contextual on/off text and on-state colour. */
529
+ export interface SwitchFieldDef extends BaseFieldDef {
530
+ type: "switch";
531
+ textWhenTrue?: MaybeFn<string>;
532
+ textWhenFalse?: MaybeFn<string>;
533
+ switchVariant?: "success" | "neutral";
534
+ }
535
+
536
+ /** `switch-list` — a labelled toggle row per option; model is an array. */
537
+ export interface SwitchListFieldDef
538
+ extends BaseFieldDef,
539
+ WithSelectableOptions {
540
+ type: "switch-list";
541
+ switchVariant?: "success" | "neutral";
542
+ }
543
+
544
+ /** `radio` — a radio group. */
545
+ export interface RadioFieldDef extends BaseFieldDef, WithSelectableOptions {
546
+ type: "radio";
547
+ }
548
+
549
+ /** `image` / `file` — file input (`image` also shows a preview). */
550
+ export interface FileFieldDef extends BaseFieldDef {
551
+ type: "image" | "file";
552
+ accept?: string;
553
+ }
554
+
555
+ /** `component` — escape hatch rendering `field.component`. */
556
+ export interface ComponentFieldDef extends BaseFieldDef {
557
+ type: "component";
558
+ /** Required on the strict variant: a `component` field with no component
559
+ * falls through to the generic input branch, so the union enforces it. */
560
+ component: Component;
561
+ }
562
+
563
+ /** `repeater` — a nested, repeatable sub-form driven by `fields`. */
564
+ export interface RepeaterFieldDef extends BaseFieldDef {
565
+ type: "repeater";
566
+ fields?: FieldDef[];
567
+ addLabel?: string;
568
+ minItems?: number;
569
+ maxItems?: number;
570
+ repeaterLayout?: "cards" | "table";
571
+ showRowIndex?: boolean;
572
+ softDeleteKey?: string;
573
+ }
574
+
575
+ /**
576
+ * Strict, discriminated replacement for `FieldDefinition`. Keyed on `type`,
577
+ * so each field only permits its own options. Opt-in today; canonical in a
578
+ * future major (#131). See the banner above.
579
+ */
580
+ export type FieldDef =
581
+ | TextFieldDef
582
+ | PasswordFieldDef
583
+ | NumberFieldDef
584
+ | DateFieldDef
585
+ | CurrencyFieldDef
586
+ | PercentageFieldDef
587
+ | TextareaFieldDef
588
+ | SelectFieldDef
589
+ | AutocompleteFieldDef
590
+ | CheckboxFieldDef
591
+ | CheckboxGroupFieldDef
592
+ | SwitchFieldDef
593
+ | SwitchListFieldDef
594
+ | RadioFieldDef
595
+ | FileFieldDef
596
+ | ComponentFieldDef
597
+ | RepeaterFieldDef;
598
+
599
+ /**
600
+ * Compile-time drift guard (#131): every strict variant must remain assignable
601
+ * to the permissive `FieldDefinition`. If a `FieldDef` variant grows a prop that
602
+ * `FieldDefinition` doesn't have (or types incompatibly), this fails to satisfy
603
+ * its `true` constraint and the build goes red. Purely type-level — no runtime.
604
+ * (The reverse direction — a prop added to `FieldDefinition` but forgotten on a
605
+ * variant — can't be asserted this way; the banner above is the reminder.)
606
+ */
607
+ type _AssertTrue<T extends true> = T;
608
+ // Exported (under an internal underscore name, not re-exported by the barrel)
609
+ // only so `noUnusedLocals` treats it as used — its value is the constraint check.
610
+ export type _FieldDefStaysASubset = _AssertTrue<
611
+ FieldDef extends FieldDefinition ? true : false
612
+ >;
613
+
383
614
  /**
384
615
  * A tab in a tabbed form. Groups a subset of fields and can be shown
385
616
  * conditionally or lazily mounted.
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Pure form-schema semantics — defaulting, cloning, predicate resolution,
3
+ * visibility, and submission-eligibility — extracted so the rule is defined
4
+ * ONCE and tested once (#134).
5
+ *
6
+ * These lived, drifted, in three-plus places: `defineForm`, `useResourceEditor`,
7
+ * `DXForm`, and `DXRepeater` each re-implemented seeding/visibility and were
8
+ * "kept in step" by hand — the exact shape of bug behind #117/#122/#125. Each
9
+ * copy had drifted: `DXRepeater`'s type map was missing the array-valued
10
+ * `checkbox-group`/`switch-list` cases, and `useResourceEditor` wasn't
11
+ * type-aware at all (a flat `""` fallback). Consolidating here fixes that drift.
12
+ *
13
+ * Everything is a plain function of its inputs (no Vue reactivity), so it unit
14
+ * tests directly.
15
+ */
16
+ import { toRaw } from "vue";
17
+ import type { FieldType, MaybeFn } from "../types";
18
+
19
+ /**
20
+ * The canonical starting value for a field TYPE, used when a field declares no
21
+ * explicit `default`. Superset of the previous copies:
22
+ * - `checkbox` / `switch` → `false`
23
+ * - `number` / `currency` / `percentage` → `0`
24
+ * - `repeater` / `checkbox-group` / `switch-list` → `[]` (array-valued models)
25
+ * - `image` / `file` → `null`
26
+ * - everything else (text family, `select`, `radio`, `autocomplete`, …) → `""`
27
+ */
28
+ export function defaultValueForType(type: FieldType): any {
29
+ switch (type) {
30
+ case "checkbox":
31
+ case "switch":
32
+ return false;
33
+ case "number":
34
+ case "currency":
35
+ case "percentage":
36
+ return 0;
37
+ case "repeater":
38
+ case "checkbox-group":
39
+ case "switch-list":
40
+ return [];
41
+ case "image":
42
+ case "file":
43
+ return null;
44
+ default:
45
+ return "";
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Deep-clone a default so object/array defaults aren't shared BY REFERENCE
51
+ * across form instances or repeater rows (mutating one row would otherwise
52
+ * mutate the field's declared default and every sibling row).
53
+ *
54
+ * Mirrors `useForm`'s `deepClone`: `toRaw` unwraps a Vue reactive proxy, then
55
+ * `structuredClone` (which preserves Date/File/Map — a plain JSON round-trip
56
+ * would flatten a `Date` default to a string) with a JSON fallback for a
57
+ * residual nested proxy or otherwise-uncloneable value. This matters because
58
+ * `defineForm`/`useResourceEditor` now clone here BEFORE the value reaches
59
+ * `useForm`, so a lossy clone would corrupt a rich default the raw path used
60
+ * to preserve (#134 review).
61
+ */
62
+ export function cloneDefault<TValue>(value: TValue): TValue {
63
+ if (value === null || typeof value !== "object") return value;
64
+ const raw = toRaw(value);
65
+ if (typeof structuredClone === "function") {
66
+ try {
67
+ return structuredClone(raw);
68
+ } catch {
69
+ // Reactive proxy or otherwise-uncloneable — fall through to JSON.
70
+ }
71
+ }
72
+ return JSON.parse(JSON.stringify(raw));
73
+ }
74
+
75
+ /**
76
+ * A field's starting value: its explicit `default` (deep-cloned) when one is
77
+ * DEFINED, else the type default. Definedness — not nullishness — decides, so a
78
+ * legitimate `null` default (e.g. a "none" select option whose value is `null`)
79
+ * survives instead of being replaced by the type default (#122/#125). An absent
80
+ * or explicitly-`undefined` default falls back to the type default rather than
81
+ * seeding `undefined`, which would drop the key from the JSON payload.
82
+ */
83
+ export function resolveFieldDefault(field: {
84
+ type: FieldType;
85
+ default?: any;
86
+ }): any {
87
+ return field.default !== undefined
88
+ ? cloneDefault(field.default)
89
+ : defaultValueForType(field.type);
90
+ }
91
+
92
+ /**
93
+ * Resolve a `MaybeFn<boolean>` predicate (`when` on a field or tab) against the
94
+ * live model. `undefined` yields `fallback` (the field/tab is unconditioned).
95
+ */
96
+ export function resolvePredicate(
97
+ when: MaybeFn<boolean> | undefined,
98
+ model: any,
99
+ fallback: boolean,
100
+ ): boolean {
101
+ if (when === undefined) return fallback;
102
+ return typeof when === "function" ? when(model) : when;
103
+ }
104
+
105
+ /**
106
+ * Whether a field is VISIBLE against a model, by the one rule every renderer
107
+ * shares: the `when` predicate (default visible) AND the legacy no-arg `show`
108
+ * predicate. A field is visible only if both pass.
109
+ */
110
+ export function isFieldVisible(
111
+ field: { when?: MaybeFn<boolean>; show?: () => boolean },
112
+ model: any,
113
+ ): boolean {
114
+ const whenOk = resolvePredicate(field.when, model, true);
115
+ const showOk = field.show ? field.show() : true;
116
+ return whenOk && showOk;
117
+ }
118
+
119
+ /**
120
+ * Whether a field's value belongs in the submitted payload. A presentational
121
+ * field (`submit: false`) — a header, an alert, an explanatory `span` block —
122
+ * renders but holds no data and must not be POSTed just because it was declared.
123
+ */
124
+ export function isSubmittableField(field: { submit?: boolean }): boolean {
125
+ return field.submit !== false;
126
+ }