@omnitend/dashboard-for-laravel 0.4.14 → 0.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.
Files changed (35) hide show
  1. package/dist/components/base/DButton.vue.d.ts +2 -3
  2. package/dist/components/base/DTable.vue.d.ts +11 -3
  3. package/dist/components/extended/DXBasicForm.vue.d.ts +4 -33
  4. package/dist/components/extended/DXField.vue.d.ts +88 -0
  5. package/dist/components/extended/DXForm.vue.d.ts +34 -8
  6. package/dist/components/extended/DXRepeater.vue.d.ts +30 -0
  7. package/dist/components/extended/DXTable.vue.d.ts +10 -17
  8. package/dist/dashboard-for-laravel.js +31131 -16052
  9. package/dist/dashboard-for-laravel.js.map +1 -1
  10. package/dist/dashboard-for-laravel.umd.cjs +10 -7
  11. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  12. package/dist/index.d.ts +12 -4
  13. package/dist/style.css +1 -1
  14. package/dist/types/index.d.ts +114 -9
  15. package/dist/utils/objectPath.d.ts +18 -0
  16. package/docs/public/api-reference.json +354 -130
  17. package/docs/public/docs-map.md +5 -4
  18. package/docs/public/llms.txt +8 -7
  19. package/package.json +3 -3
  20. package/resources/js/components/base/DButton.vue +2 -3
  21. package/resources/js/components/base/{DCarousel.vue → DFormRadioGroup.vue} +5 -5
  22. package/resources/js/components/base/DTable.vue +39 -3
  23. package/resources/js/components/base/DToaster.vue +5 -3
  24. package/resources/js/components/extended/DXBasicForm.vue +35 -184
  25. package/resources/js/components/extended/DXField.vue +402 -0
  26. package/resources/js/components/extended/DXForm.vue +282 -17
  27. package/resources/js/components/extended/DXRepeater.vue +216 -0
  28. package/resources/js/components/extended/DXTable.vue +96 -210
  29. package/resources/js/composables/defineForm.ts +7 -0
  30. package/resources/js/index.ts +18 -3
  31. package/resources/js/types/index.ts +146 -9
  32. package/resources/js/utils/objectPath.ts +59 -0
  33. package/dist/components/base/DCarouselSlide.vue.d.ts +0 -12
  34. package/resources/js/components/base/DCarouselSlide.vue +0 -14
  35. /package/dist/components/base/{DCarousel.vue.d.ts → DFormRadioGroup.vue.d.ts} +0 -0
@@ -455,128 +455,70 @@
455
455
  :title="computedModalTitle"
456
456
  :size="editModalSize"
457
457
  >
458
- <!-- Tabbed view (if editTabs provided) -->
459
- <template v-if="editTabs && editTabs.length > 0 && editForm">
460
- <DTabs v-model="activeTabIndex">
461
- <DTab
462
- v-for="(tab, index) in visibleTabs"
463
- :key="tab.key"
464
- :title="tab.label || tab.key"
465
- :lazy="tab.lazy"
466
- :active="index === 0"
467
- >
468
- <!-- Custom tab content slot -->
469
- <slot
470
- v-if="$slots[`tab-content(${tab.key})`]"
471
- :name="`tab-content(${tab.key})`"
472
- :item="selectedItem"
473
- :tab="tab"
474
- />
475
-
476
- <!-- Default: render fields for this tab -->
477
- <div v-else class="p-3">
478
- <!-- Before slot -->
479
- <slot :name="`tab-before(${tab.key})`" :item="selectedItem" :tab="tab" />
480
-
481
- <!-- Form fields for this tab -->
482
- <template v-for="fieldKey in tab.fieldKeys" :key="fieldKey">
483
- <div v-if="getField(fieldKey).span" class="mb-3">
484
- <!-- Full-width span field -->
485
- <slot
486
- :name="`edit-span(${fieldKey})`"
487
- :item="selectedItem"
488
- :value="editForm.data[fieldKey]"
489
- :update="(v: any) => editForm.data[fieldKey] = v"
490
- :close="handleEditCancel"
491
- />
492
- </div>
493
- <!-- Checkbox (no label wrapper needed) -->
494
- <div v-else-if="getField(fieldKey).type === 'checkbox'" class="mb-3">
495
- <!-- Custom value slot -->
496
- <slot
497
- v-if="$slots[`edit-value(${fieldKey})`]"
498
- :name="`edit-value(${fieldKey})`"
499
- :item="selectedItem"
500
- :value="editForm.data[fieldKey]"
501
- :update="(v: any) => editForm.data[fieldKey] = v"
502
- :field="getField(fieldKey)"
503
- />
504
- <DFormCheckbox
505
- v-else
506
- v-model="editForm.data[fieldKey]"
507
- >
508
- {{ getField(fieldKey).label || fieldKey }}
509
- </DFormCheckbox>
510
- </div>
511
- <!-- Other field types with label -->
512
- <DFormGroup
513
- v-else
514
- :label="getFieldLabel(fieldKey)"
515
- class="mb-3"
516
- >
517
- <!-- Custom value slot -->
518
- <slot
519
- v-if="$slots[`edit-value(${fieldKey})`]"
520
- :name="`edit-value(${fieldKey})`"
521
- :item="selectedItem"
522
- :value="editForm.data[fieldKey]"
523
- :update="(v: any) => editForm.data[fieldKey] = v"
524
- :field="getField(fieldKey)"
525
- />
526
- <DFormTextarea
527
- v-else-if="getField(fieldKey).type === 'textarea'"
528
- v-model="editForm.data[fieldKey]"
529
- :required="getField(fieldKey).required"
530
- :rows="getField(fieldKey).rows || 3"
531
- :state="editForm.getState(fieldKey)"
532
- :disabled="isFieldDisabled(fieldKey)"
533
- @input="editForm.clearError(fieldKey)"
534
- />
535
- <DFormSelect
536
- v-else-if="getField(fieldKey).type === 'select'"
537
- v-model="editForm.data[fieldKey]"
538
- :required="getField(fieldKey).required"
539
- :options="getField(fieldKey).options"
540
- :state="editForm.getState(fieldKey)"
541
- :disabled="isFieldDisabled(fieldKey)"
542
- @change="editForm.clearError(fieldKey)"
543
- />
544
- <DFormInput
545
- v-else
546
- v-model="editForm.data[fieldKey]"
547
- :type="getField(fieldKey).type || 'text'"
548
- :required="getField(fieldKey).required"
549
- :step="getField(fieldKey).step"
550
- :state="editForm.getState(fieldKey)"
551
- :disabled="isFieldDisabled(fieldKey)"
552
- @input="editForm.clearError(fieldKey)"
553
- />
554
- <!-- Validation error -->
555
- <DFormInvalidFeedback v-if="editForm.hasError(fieldKey)">
556
- {{ editForm.getError(fieldKey) }}
557
- </DFormInvalidFeedback>
558
- <!-- Hint text -->
559
- <DFormText v-if="getFieldHint(fieldKey)" class="text-muted">
560
- {{ getFieldHint(fieldKey) }}
561
- </DFormText>
562
- </DFormGroup>
563
- </template>
564
-
565
- <!-- After slot -->
566
- <slot :name="`tab-after(${tab.key})`" :item="selectedItem" :tab="tab" />
567
- </div>
568
- </DTab>
569
- </DTabs>
570
- </template>
571
-
572
- <!-- Fallback: no tabs, render flat form (current behavior) -->
573
- <DXBasicForm
574
- v-else-if="editForm"
458
+ <!-- Edit/create form (tabbed when editTabs provided, flat otherwise) -->
459
+ <DXForm
460
+ v-if="editForm"
461
+ v-model:active-tab="activeTabIndex"
575
462
  :form="editForm"
576
- :fields="resolvedEditFields"
463
+ :fields="editFields"
464
+ :tabs="editTabs"
465
+ :context="selectedItem ?? undefined"
577
466
  :show-submit="false"
578
467
  @submit="handleEditSave"
579
- />
468
+ >
469
+ <!-- Forward DXTable's edit-value(key) → DXForm value(key) -->
470
+ <template
471
+ v-for="key in editValueSlotKeys"
472
+ :key="`ev-${key}`"
473
+ #[`value(${key})`]="sp"
474
+ >
475
+ <slot
476
+ :name="`edit-value(${key})`"
477
+ :item="selectedItem"
478
+ :value="sp.value"
479
+ :update="sp.update"
480
+ :field="sp.field"
481
+ />
482
+ </template>
483
+
484
+ <!-- Forward edit-span(key) → span(key) -->
485
+ <template
486
+ v-for="key in editSpanSlotKeys"
487
+ :key="`es-${key}`"
488
+ #[`span(${key})`]="sp"
489
+ >
490
+ <slot
491
+ :name="`edit-span(${key})`"
492
+ :item="selectedItem"
493
+ :value="sp.value"
494
+ :update="sp.update"
495
+ :close="handleEditCancel"
496
+ />
497
+ </template>
498
+
499
+ <!-- Forward tab-content / tab-before / tab-after slots -->
500
+ <template
501
+ v-for="key in tabContentSlotKeys"
502
+ :key="`tc-${key}`"
503
+ #[`tab-content(${key})`]="sp"
504
+ >
505
+ <slot :name="`tab-content(${key})`" :item="selectedItem" :tab="sp.tab" />
506
+ </template>
507
+ <template
508
+ v-for="key in tabBeforeSlotKeys"
509
+ :key="`tb-${key}`"
510
+ #[`tab-before(${key})`]="sp"
511
+ >
512
+ <slot :name="`tab-before(${key})`" :item="selectedItem" :tab="sp.tab" />
513
+ </template>
514
+ <template
515
+ v-for="key in tabAfterSlotKeys"
516
+ :key="`taf-${key}`"
517
+ #[`tab-after(${key})`]="sp"
518
+ >
519
+ <slot :name="`tab-after(${key})`" :item="selectedItem" :tab="sp.tab" />
520
+ </template>
521
+ </DXForm>
580
522
 
581
523
  <template #footer>
582
524
  <div class="d-flex justify-content-between w-100">
@@ -614,7 +556,7 @@
614
556
  </template>
615
557
 
616
558
  <script setup lang="ts" generic="T = any">
617
- import { computed, ref, watch } from "vue";
559
+ import { computed, ref, watch, useSlots } from "vue";
618
560
  import { router } from "@inertiajs/vue3";
619
561
  import axios from "axios";
620
562
  import pluralize from "pluralize";
@@ -630,14 +572,7 @@ import DFormInput from "../base/DFormInput.vue";
630
572
  import DFormSelect from "../base/DFormSelect.vue";
631
573
  import DModal from "../base/DModal.vue";
632
574
  import DButton from "../base/DButton.vue";
633
- import DTabs from "../base/DTabs.vue";
634
- import DTab from "../base/DTab.vue";
635
- import DFormGroup from "../base/DFormGroup.vue";
636
- import DFormTextarea from "../base/DFormTextarea.vue";
637
- import DFormCheckbox from "../base/DFormCheckbox.vue";
638
- import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
639
- import DFormText from "../base/DFormText.vue";
640
- import DXBasicForm from "./DXBasicForm.vue";
575
+ import DXForm from "./DXForm.vue";
641
576
  export type FilterType = 'text' | 'select' | 'number' | 'date' | false;
642
577
 
643
578
  export interface FilterOption {
@@ -1455,67 +1390,35 @@ try {
1455
1390
  createToast = undefined;
1456
1391
  }
1457
1392
 
1458
- // Computed: Visible tabs (respects when condition)
1459
- const visibleTabs = computed(() => {
1460
- if (!props.editTabs || props.editTabs.length === 0) return [];
1461
-
1462
- return props.editTabs.filter(tab => {
1463
- if (tab.when === undefined) return true;
1464
- return typeof tab.when === 'function'
1465
- ? tab.when(selectedItem.value)
1466
- : tab.when;
1467
- });
1468
- });
1469
-
1470
- // Helper: Get field by key
1471
- const getField = (key: string) => {
1472
- return props.editFields?.find(f => f.key === key) || { key };
1473
- };
1474
-
1475
- // Helper: Get field label (supports function for dynamic labels)
1476
- const getFieldLabel = (key: string): string => {
1477
- const field = getField(key);
1478
- if (typeof field.label === 'function') {
1479
- return field.label(selectedItem.value);
1480
- }
1481
- return field.label || key;
1482
- };
1483
-
1484
- // Helper: Get field hint (supports function for dynamic hints)
1485
- const getFieldHint = (key: string): string | undefined => {
1486
- const field = getField(key);
1487
- if (typeof field.hint === 'function') {
1488
- return field.hint(selectedItem.value);
1489
- }
1490
- return field.hint;
1491
- };
1492
-
1493
- // Helper: Check if field is disabled (supports disabledWhen function)
1494
- const isFieldDisabled = (key: string): boolean => {
1495
- const field = getField(key);
1496
- if (typeof field.disabledWhen === 'function') {
1497
- return field.disabledWhen(selectedItem.value);
1498
- }
1499
- return field.disabled || false;
1500
- };
1501
-
1502
- // Computed: Resolve edit fields with dynamic labels/hints for DXBasicForm
1503
- const resolvedEditFields = computed(() => {
1504
- if (!props.editFields) return [];
1505
-
1506
- return props.editFields.map(field => ({
1507
- ...field,
1508
- label: typeof field.label === 'function'
1509
- ? field.label(selectedItem.value)
1510
- : field.label,
1511
- hint: typeof field.hint === 'function'
1512
- ? field.hint(selectedItem.value)
1513
- : field.hint,
1514
- disabled: typeof field.disabledWhen === 'function'
1515
- ? field.disabledWhen(selectedItem.value)
1516
- : field.disabled,
1517
- }));
1518
- });
1393
+ // The edit/create form rendering is delegated to DXForm, which
1394
+ // owns field/tab visibility, dynamic labels/hints, conditional fields,
1395
+ // and auto-switching to the first tab with a validation error.
1396
+
1397
+ // Forward only the keyed edit slots the consumer actually provided, so
1398
+ // DXForm doesn't mistake an always-present (but empty) wrapper for
1399
+ // a real custom-value override.
1400
+ const tableSlots = useSlots();
1401
+ const editFieldKeys = computed<string[]>(() =>
1402
+ (props.editFields ?? []).map((field: any) => field.key),
1403
+ );
1404
+ const tabKeys = computed<string[]>(() =>
1405
+ (props.editTabs ?? []).map((tab) => tab.key),
1406
+ );
1407
+ const editValueSlotKeys = computed(() =>
1408
+ editFieldKeys.value.filter((key) => !!tableSlots[`edit-value(${key})`]),
1409
+ );
1410
+ const editSpanSlotKeys = computed(() =>
1411
+ editFieldKeys.value.filter((key) => !!tableSlots[`edit-span(${key})`]),
1412
+ );
1413
+ const tabContentSlotKeys = computed(() =>
1414
+ tabKeys.value.filter((key) => !!tableSlots[`tab-content(${key})`]),
1415
+ );
1416
+ const tabBeforeSlotKeys = computed(() =>
1417
+ tabKeys.value.filter((key) => !!tableSlots[`tab-before(${key})`]),
1418
+ );
1419
+ const tabAfterSlotKeys = computed(() =>
1420
+ tabKeys.value.filter((key) => !!tableSlots[`tab-after(${key})`]),
1421
+ );
1519
1422
 
1520
1423
  // Computed: Singular and plural item names
1521
1424
  const singularItemName = computed(() => props.itemName);
@@ -1653,16 +1556,8 @@ const handleEditSave = async () => {
1653
1556
  modelValue: 5000,
1654
1557
  });
1655
1558
 
1656
- if (props.editTabs && props.editTabs.length > 0) {
1657
- const errorKeys = Object.keys(errors);
1658
- const tabIndex = visibleTabs.value.findIndex(tab =>
1659
- tab.fieldKeys.some(key => errorKeys.includes(key))
1660
- );
1661
- if (tabIndex !== -1) {
1662
- activeTabIndex.value = tabIndex;
1663
- }
1664
- }
1665
-
1559
+ // DXForm switches to the first errored tab via its
1560
+ // own watcher on editForm.errors.
1666
1561
  emit('createError', errors);
1667
1562
  }
1668
1563
  });
@@ -1716,17 +1611,8 @@ const handleEditSave = async () => {
1716
1611
  modelValue: 5000, // Auto-dismiss after 5 seconds
1717
1612
  });
1718
1613
 
1719
- // Switch to tab containing error field
1720
- if (props.editTabs && props.editTabs.length > 0) {
1721
- const errorKeys = Object.keys(errors);
1722
- const tabIndex = visibleTabs.value.findIndex(tab =>
1723
- tab.fieldKeys.some(key => errorKeys.includes(key))
1724
- );
1725
- if (tabIndex !== -1) {
1726
- activeTabIndex.value = tabIndex;
1727
- }
1728
- }
1729
-
1614
+ // DXForm switches to the first errored tab via its
1615
+ // own watcher on editForm.errors.
1730
1616
  emit('editError', selectedItem.value as T, errors);
1731
1617
  }
1732
1618
  });
@@ -32,7 +32,14 @@ function getDefaultValueForType(type: FieldType): any {
32
32
  case "checkbox":
33
33
  return false;
34
34
  case "number":
35
+ case "currency":
36
+ case "percentage":
35
37
  return 0;
38
+ case "repeater":
39
+ return [];
40
+ case "image":
41
+ case "file":
42
+ return null;
36
43
  case "select":
37
44
  case "radio":
38
45
  return "";
@@ -6,8 +6,15 @@ export * from 'bootstrap-vue-next';
6
6
 
7
7
  // Extended components (custom functionality beyond Bootstrap Vue Next)
8
8
  export { default as DXDashboard } from "./components/extended/DXDashboard.vue";
9
- export { default as DXBasicForm } from "./components/extended/DXBasicForm.vue";
10
9
  export { default as DXForm } from "./components/extended/DXForm.vue";
10
+ export { default as DXField } from "./components/extended/DXField.vue";
11
+ export { default as DXRepeater } from "./components/extended/DXRepeater.vue";
12
+ /**
13
+ * @deprecated Use `DXForm`. `DXBasicForm` is a thin wrapper around `DXForm`
14
+ * (a flat form is just `DXForm` without a `tabs` prop) that logs a one-time
15
+ * deprecation warning. It will be removed in a future major version.
16
+ */
17
+ export { default as DXBasicForm } from "./components/extended/DXBasicForm.vue";
11
18
  export { default as DXTable } from "./components/extended/DXTable.vue";
12
19
  export { default as DXDashboardSidebar } from "./components/extended/DXDashboardSidebar.vue";
13
20
  export { default as DXDashboardNavbar } from "./components/extended/DXDashboardNavbar.vue";
@@ -23,8 +30,12 @@ export { default as DButton } from "./components/base/DButton.vue";
23
30
  export { default as DButtonGroup } from "./components/base/DButtonGroup.vue";
24
31
  export { default as DButtonToolbar } from "./components/base/DButtonToolbar.vue";
25
32
  export { default as DCard } from "./components/base/DCard.vue";
26
- export { default as DCarousel } from "./components/base/DCarousel.vue";
27
- export { default as DCarouselSlide } from "./components/base/DCarouselSlide.vue";
33
+ // Re-export the real BCarousel/BCarouselSlide (not wrappers): BCarousel
34
+ // collects its slides by scanning slot vnodes for the BCarouselSlide
35
+ // component type, so wrapper components in between break slide registration
36
+ // in bvn 0.45. (Most components are safely wrapped; carousel is the exception.)
37
+ export { BCarousel as DCarousel } from "bootstrap-vue-next";
38
+ export { BCarouselSlide as DCarouselSlide } from "bootstrap-vue-next";
28
39
  export { default as DCol } from "./components/base/DCol.vue";
29
40
  export { default as DCollapse } from "./components/base/DCollapse.vue";
30
41
  export { default as DContainer } from "./components/base/DContainer.vue";
@@ -37,6 +48,7 @@ export { default as DFormGroup } from "./components/base/DFormGroup.vue";
37
48
  export { default as DFormInput } from "./components/base/DFormInput.vue";
38
49
  export { default as DFormInvalidFeedback } from "./components/base/DFormInvalidFeedback.vue";
39
50
  export { default as DFormRadio } from "./components/base/DFormRadio.vue";
51
+ export { default as DFormRadioGroup } from "./components/base/DFormRadioGroup.vue";
40
52
  export { default as DFormSelect } from "./components/base/DFormSelect.vue";
41
53
  export { default as DFormSpinbutton } from "./components/base/DFormSpinbutton.vue";
42
54
  export { default as DFormTags } from "./components/base/DFormTags.vue";
@@ -82,6 +94,9 @@ export type {
82
94
  FieldType,
83
95
  FieldOption,
84
96
  FieldDefinition,
97
+ FormTab,
98
+ MaybeFn,
99
+ OptionsLoader,
85
100
  } from "./types";
86
101
 
87
102
  export type {
@@ -1,5 +1,16 @@
1
+ import type { Component } from "vue";
2
+
1
3
  /**
2
- * Field types supported by OForm
4
+ * Field types supported by DXForm (and DXField, its per-field renderer).
5
+ *
6
+ * Text-like types render an `<input>`; the remainder render purpose-built
7
+ * controls:
8
+ * - `currency` / `percentage` — numeric input wrapped in an input-group
9
+ * with a symbol affix.
10
+ * - `datetime` — alias for the native `datetime-local` control.
11
+ * - `image` / `file` — file input (`image` additionally shows a preview).
12
+ * - `component` — escape hatch that renders `field.component`.
13
+ * - `repeater` — nested, repeatable sub-form driven by `field.fields`.
3
14
  */
4
15
  export type FieldType =
5
16
  | "text"
@@ -10,11 +21,25 @@ export type FieldType =
10
21
  | "tel"
11
22
  | "date"
12
23
  | "datetime-local"
24
+ | "datetime"
13
25
  | "time"
26
+ | "currency"
27
+ | "percentage"
14
28
  | "textarea"
15
29
  | "select"
16
30
  | "checkbox"
17
- | "radio";
31
+ | "radio"
32
+ | "image"
33
+ | "file"
34
+ | "component"
35
+ | "repeater";
36
+
37
+ /**
38
+ * A value that may be supplied directly or computed from the live form
39
+ * model. Predicates receive the current model so fields can react to
40
+ * other fields (cross-field reactivity).
41
+ */
42
+ export type MaybeFn<TValue> = TValue | ((model: any) => TValue);
18
43
 
19
44
  /**
20
45
  * Option for select or radio fields
@@ -26,7 +51,18 @@ export interface FieldOption extends Record<string, unknown> {
26
51
  }
27
52
 
28
53
  /**
29
- * Field definition for OForm
54
+ * Asynchronously resolves the options for a select/radio field from the
55
+ * current model (e.g. fetch a dependent list). Resolved on mount, and
56
+ * again on model change when `reloadOptionsOnChange` is set.
57
+ */
58
+ export type OptionsLoader = (model: any) => Promise<FieldOption[]>;
59
+
60
+ /**
61
+ * Field definition shared by every form renderer.
62
+ *
63
+ * DXForm and DXTable's edit modal honour `hint`, `span`, `when`,
64
+ * `readonly`, `disabledWhen`, function-valued `label`/`hint`, async
65
+ * options, the `component` escape hatch and nested `repeater` fields.
30
66
  */
31
67
  export interface FieldDefinition {
32
68
  /** Field key (must match form data key) */
@@ -35,8 +71,8 @@ export interface FieldDefinition {
35
71
  /** Field type */
36
72
  type: FieldType;
37
73
 
38
- /** Field label (optional) */
39
- label?: string;
74
+ /** Field label string or a function of the form model */
75
+ label?: MaybeFn<string>;
40
76
 
41
77
  /** Placeholder text (optional) */
42
78
  placeholder?: string;
@@ -47,12 +83,40 @@ export interface FieldDefinition {
47
83
  /** Options for select or radio fields */
48
84
  options?: FieldOption[];
49
85
 
86
+ /**
87
+ * Asynchronously load options for select/radio fields. Takes
88
+ * precedence over `options` once resolved.
89
+ */
90
+ optionsLoader?: OptionsLoader;
91
+
92
+ /** Re-run `optionsLoader` whenever the form model changes. */
93
+ reloadOptionsOnChange?: boolean;
94
+
50
95
  /** Number of rows for textarea (default: 3) */
51
96
  rows?: number;
52
97
 
53
- /** Help text displayed below field */
98
+ /** Step for numeric/currency/percentage inputs */
99
+ step?: number | string;
100
+
101
+ /** Min/max for numeric inputs */
102
+ min?: number | string;
103
+ max?: number | string;
104
+
105
+ /** Symbol shown for `currency` fields (default: the locale's, "£"). */
106
+ currencySymbol?: string;
107
+
108
+ /** `accept` attribute for `image`/`file` inputs (e.g. "image/*"). */
109
+ accept?: string;
110
+
111
+ /** Help text displayed below the field (always visible). */
54
112
  help?: string;
55
113
 
114
+ /**
115
+ * Hint text displayed below the field. Unlike `help`, may be a
116
+ * function of the model for dynamic hints.
117
+ */
118
+ hint?: MaybeFn<string>;
119
+
56
120
  /** CSS class for the form group */
57
121
  class?: string;
58
122
 
@@ -60,9 +124,82 @@ export interface FieldDefinition {
60
124
  inputProps?: Record<string, any>;
61
125
 
62
126
  /**
63
- * Conditionally show or hide this field. When omitted, the field
64
- * is always visible. The function is re-evaluated reactively, so
65
- * it can depend on form state or other reactive sources.
127
+ * Render the field full-width with no label wrapper, delegating its
128
+ * content to the `#span(<key>)` slot. Useful for custom blocks.
129
+ */
130
+ span?: boolean;
131
+
132
+ /**
133
+ * Component rendered for `type: "component"` fields. Receives
134
+ * `modelValue`, `field`, `model` props and emits `update:modelValue`.
135
+ */
136
+ component?: Component;
137
+
138
+ /** Sub-field definitions for `type: "repeater"` fields. */
139
+ fields?: FieldDefinition[];
140
+
141
+ /**
142
+ * Default/initial value. Used by `defineForm` to seed form data and by
143
+ * repeaters to seed a freshly-added row's sub-fields. (`defineForm`'s
144
+ * `FormFieldDefinition` re-declares this as required for inference.)
145
+ */
146
+ default?: any;
147
+
148
+ /** Label for a repeater's "add row" button (default: "Add"). */
149
+ addLabel?: string;
150
+
151
+ /** Minimum / maximum number of repeater rows. */
152
+ minItems?: number;
153
+ maxItems?: number;
154
+
155
+ /** Disable the field (static or computed from the model). */
156
+ disabled?: MaybeFn<boolean>;
157
+
158
+ /**
159
+ * Disable the field based on the model. Retained for backwards
160
+ * compatibility with DXTable; prefer `disabled` with a function.
161
+ */
162
+ disabledWhen?: (model: any) => boolean;
163
+
164
+ /**
165
+ * Render the field read-only (static or computed). For controls
166
+ * without a native readonly state (select/checkbox/radio) this is
167
+ * applied as `disabled`.
168
+ */
169
+ readonly?: MaybeFn<boolean>;
170
+
171
+ /**
172
+ * Conditionally show or hide this field. When omitted the field is
173
+ * always visible. Boolean or a function of the form model; evaluated
174
+ * reactively for cross-field conditional fields.
175
+ */
176
+ when?: MaybeFn<boolean>;
177
+
178
+ /**
179
+ * Legacy no-argument visibility predicate. Retained for backwards
180
+ * compatibility; prefer `when`. When both are present, a field is
181
+ * visible only if both pass.
66
182
  */
67
183
  show?: () => boolean;
68
184
  }
185
+
186
+ /**
187
+ * A tab in a tabbed form. Groups a subset of fields and can be shown
188
+ * conditionally or lazily mounted.
189
+ */
190
+ export interface FormTab {
191
+ /** Unique key for this tab */
192
+ key: string;
193
+
194
+ /** Display label (optional, defaults to the key) */
195
+ label?: string;
196
+
197
+ /** Field keys (from the form's fields) to render in this tab */
198
+ fieldKeys: string[];
199
+
200
+ /** Conditional display — boolean or a function of the form model */
201
+ when?: MaybeFn<boolean>;
202
+
203
+ /** Lazily mount tab content until first activated */
204
+ lazy?: boolean;
205
+ }