@omnitend/dashboard-for-laravel 0.8.0 → 0.9.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.
@@ -10,8 +10,10 @@ import type { Component } from "vue";
10
10
  * - `image` / `file` — file input (`image` additionally shows a preview).
11
11
  * - `component` — escape hatch that renders `field.component`.
12
12
  * - `repeater` — nested, repeatable sub-form driven by `field.fields`.
13
+ * - `switch` — a toggle checkbox with contextual on/off text and an
14
+ * on-state (filled) style; see `textWhenTrue` / `textWhenFalse`.
13
15
  */
14
- export type FieldType = "text" | "email" | "password" | "number" | "url" | "tel" | "date" | "datetime-local" | "datetime" | "time" | "currency" | "percentage" | "textarea" | "select" | "checkbox" | "radio" | "image" | "file" | "component" | "repeater";
16
+ export type FieldType = "text" | "email" | "password" | "number" | "url" | "tel" | "date" | "datetime-local" | "datetime" | "time" | "currency" | "percentage" | "textarea" | "select" | "checkbox" | "switch" | "radio" | "image" | "file" | "component" | "repeater";
15
17
  /**
16
18
  * A value that may be supplied directly or computed from the live form
17
19
  * model. Predicates receive the current model so fields can react to
@@ -68,6 +70,14 @@ export interface FieldDefinition {
68
70
  max?: number | string;
69
71
  /** Symbol shown for `currency` fields (default: the locale's, "£"). */
70
72
  currencySymbol?: string;
73
+ /**
74
+ * For `percentage` fields: treat the underlying model value as a 0–1
75
+ * fraction while showing/editing it as a 0–100 percentage. The model keeps
76
+ * the fraction (e.g. `0.2`), the input shows `20`. Off by default (the value
77
+ * is taken as a whole percentage). Use for fields stored as ratios (VAT
78
+ * rates, discounts, …).
79
+ */
80
+ asFraction?: boolean;
71
81
  /** `accept` attribute for `image`/`file` inputs (e.g. "image/*"). */
72
82
  accept?: string;
73
83
  /** Help text displayed below the field (always visible). */
@@ -77,6 +87,23 @@ export interface FieldDefinition {
77
87
  * function of the model for dynamic hints.
78
88
  */
79
89
  hint?: MaybeFn<string>;
90
+ /**
91
+ * Longer help text revealed in a popover from a small info affordance
92
+ * on the field's label (on hover/focus). Complements `hint` (which is
93
+ * always-visible muted text below the control). May be a function of
94
+ * the model. For rich content, use the `#info` slot instead.
95
+ */
96
+ info?: MaybeFn<string>;
97
+ /**
98
+ * For `switch` fields: contextual label shown when the toggle is on.
99
+ * Falls back to `label` when omitted. May be a function of the model.
100
+ */
101
+ textWhenTrue?: MaybeFn<string>;
102
+ /**
103
+ * For `switch` fields: contextual label shown when the toggle is off.
104
+ * Falls back to `label` when omitted. May be a function of the model.
105
+ */
106
+ textWhenFalse?: MaybeFn<string>;
80
107
  /** CSS class for the form group */
81
108
  class?: string;
82
109
  /** Additional props to pass to the input component */
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-07-04T04:12:03.097Z",
2
+ "generated": "2026-07-04T10:39:25.077Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.8.0"
5
+ "version": "0.9.0"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -1605,6 +1605,29 @@
1605
1605
  ],
1606
1606
  "methods": []
1607
1607
  },
1608
+ {
1609
+ "name": "DXFieldLabel",
1610
+ "category": "extended",
1611
+ "filePath": "resources/js/components/extended/DXFieldLabel.vue",
1612
+ "description": "",
1613
+ "props": [
1614
+ {
1615
+ "name": "label",
1616
+ "type": "string",
1617
+ "required": true,
1618
+ "description": "Visible label text."
1619
+ },
1620
+ {
1621
+ "name": "info",
1622
+ "type": "string",
1623
+ "required": false,
1624
+ "description": "Optional help text revealed in a popover from an info affordance."
1625
+ }
1626
+ ],
1627
+ "events": [],
1628
+ "slots": [],
1629
+ "methods": []
1630
+ },
1608
1631
  {
1609
1632
  "name": "DXForm",
1610
1633
  "category": "extended",
@@ -2244,8 +2267,8 @@
2244
2267
  ]
2245
2268
  },
2246
2269
  "stats": {
2247
- "totalComponents": 65,
2270
+ "totalComponents": 66,
2248
2271
  "baseComponents": 57,
2249
- "extendedComponents": 8
2272
+ "extendedComponents": 9
2250
2273
  }
2251
2274
  }
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-07-04T04:12:03.185Z
4
+ > Last updated: 2026-07-04T10:39:25.137Z
5
5
 
6
6
  This file provides a complete map of all available documentation for AI agents and developers.
7
7
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Vue 3 dashboard components for Laravel with Bootstrap Vue Next
4
4
  >
5
- > A dual-package library (NPM + Composer) providing 65 Vue 3 components for building Laravel dashboards with Bootstrap Vue Next.
5
+ > A dual-package library (NPM + Composer) providing 66 Vue 3 components for building Laravel dashboards with Bootstrap Vue Next.
6
6
 
7
7
  ## Installation
8
8
 
@@ -81,7 +81,7 @@ Lightweight type-safe wrappers around Bootstrap Vue Next components providing AP
81
81
  - [DToaster](/components/base/dtoaster): Bootstrap Vue Next Toaster wrapper
82
82
  - [DTooltip](/components/base/dtooltip): Bootstrap Vue Next Tooltip wrapper
83
83
 
84
- ## Extended Components (8 components)
84
+ ## Extended Components (9 components)
85
85
 
86
86
  Custom dashboard components with advanced functionality beyond Bootstrap Vue Next.
87
87
 
@@ -90,6 +90,7 @@ Custom dashboard components with advanced functionality beyond Bootstrap Vue Nex
90
90
  - [DXDashboardNavbar](/components/extended/dxdashboardnavbar): Top navbar with user dropdown
91
91
  - [DXDashboardSidebar](/components/extended/dxdashboardsidebar): Collapsible sidebar with navigation
92
92
  - [DXField](/components/extended/dxfield): Single-field renderer for any field type with value/span/info/hint slots
93
+ - [DXFieldLabel](/components/extended/dxfieldlabel): Extended dashboard component
93
94
  - [DXForm](/components/extended/dxform): Form renderer driven by field definitions, with optional tabs, conditional fields, per-field slots, async options, nested repeaters, and auto error-tab switching
94
95
  - [DXRepeater](/components/extended/dxrepeater): Repeatable nested sub-form (field array) primitive
95
96
  - [DXTable](/components/extended/dxtable): Data table with pagination, filtering, and sorting
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Vue 3 dashboard components for Laravel with Bootstrap Vue Next",
5
5
  "type": "module",
6
6
  "main": "./dist/dashboard-for-laravel.umd.cjs",
@@ -26,14 +26,47 @@
26
26
  :disabled="isDisabled || isReadonly"
27
27
  v-bind="field.inputProps"
28
28
  >
29
- {{ resolvedLabel }}
29
+ <DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
30
30
  </DFormCheckbox>
31
31
 
32
32
  <DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
33
33
  {{ form.getError(errorKey) }}
34
34
  </DFormInvalidFeedback>
35
35
  <slot name="info" :field="field" :model="model" />
36
- <DFormText v-if="resolvedHint" class="text-muted">
36
+ <DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
37
+ <slot name="hint" :field="field" :model="model">{{ resolvedHint }}</slot>
38
+ </DFormText>
39
+ <DFormText v-if="field.help">{{ field.help }}</DFormText>
40
+ </div>
41
+
42
+ <!-- Switch: toggle with contextual on/off text and an on-state style -->
43
+ <div
44
+ v-else-if="field.type === 'switch'"
45
+ :class="[field.class || 'mb-3', 'dx-switch', { 'dx-switch--on': switchIsOn }]"
46
+ >
47
+ <slot
48
+ v-if="$slots.value"
49
+ name="value"
50
+ :field="field"
51
+ :model="model"
52
+ :value="fieldValue"
53
+ :update="setValue"
54
+ />
55
+ <DFormCheckbox
56
+ v-else
57
+ v-model="switchModel"
58
+ switch
59
+ :disabled="isDisabled || isReadonly"
60
+ v-bind="field.inputProps"
61
+ >
62
+ <DXFieldLabel :label="switchText" :info="resolvedInfo" />
63
+ </DFormCheckbox>
64
+
65
+ <DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
66
+ {{ form.getError(errorKey) }}
67
+ </DFormInvalidFeedback>
68
+ <slot name="info" :field="field" :model="model" />
69
+ <DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
37
70
  <slot name="hint" :field="field" :model="model">{{ resolvedHint }}</slot>
38
71
  </DFormText>
39
72
  <DFormText v-if="field.help">{{ field.help }}</DFormText>
@@ -41,7 +74,10 @@
41
74
 
42
75
  <!-- Repeater: nested, repeatable sub-form -->
43
76
  <div v-else-if="field.type === 'repeater'" :class="field.class || 'mb-3'">
44
- <DFormGroup :label="resolvedLabel">
77
+ <DFormGroup>
78
+ <template #label>
79
+ <DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
80
+ </template>
45
81
  <DXRepeater
46
82
  :form="form"
47
83
  :field="field"
@@ -55,14 +91,19 @@
55
91
  </DXRepeater>
56
92
  </DFormGroup>
57
93
  <slot name="info" :field="field" :model="model" />
58
- <DFormText v-if="resolvedHint" class="text-muted">
94
+ <DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
59
95
  <slot name="hint" :field="field" :model="model">{{ resolvedHint }}</slot>
60
96
  </DFormText>
61
97
  <DFormText v-if="field.help">{{ field.help }}</DFormText>
62
98
  </div>
63
99
 
64
100
  <!-- Standard labelled field -->
65
- <DFormGroup v-else :label="resolvedLabel" :class="field.class || 'mb-3'">
101
+ <DFormGroup v-else :class="field.class || 'mb-3'">
102
+ <!-- Label with optional info popover -->
103
+ <template #label>
104
+ <DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
105
+ </template>
106
+
66
107
  <!-- Custom value slot overrides the built-in control -->
67
108
  <slot
68
109
  v-if="$slots.value"
@@ -144,7 +185,7 @@
144
185
  <span class="input-group-text">{{ field.currencySymbol || "£" }}</span>
145
186
  </template>
146
187
  <DFormInput
147
- v-model="fieldValue"
188
+ v-model="numericInputValue"
148
189
  type="number"
149
190
  :required="field.required"
150
191
  :placeholder="field.placeholder"
@@ -213,6 +254,7 @@ import DFormCheckbox from "../base/DFormCheckbox.vue";
213
254
  import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
214
255
  import DFormText from "../base/DFormText.vue";
215
256
  import DInputGroup from "../base/DInputGroup.vue";
257
+ import DXFieldLabel from "./DXFieldLabel.vue";
216
258
  import type { UseFormReturn } from "../../composables/useForm";
217
259
  import type { FieldDefinition, FieldOption, FieldType } from "../../types";
218
260
  import { getByPath, setByPath } from "../../utils/objectPath";
@@ -273,6 +315,41 @@ const fieldValue = computed({
273
315
  set: (value: any) => setValue(value),
274
316
  });
275
317
 
318
+ // For `percentage` fields with `asFraction`, the model holds a 0–1 fraction but
319
+ // the input shows/edits a 0–100 percentage. Scale on read/write, rounding away
320
+ // binary-float artefacts (0.2 * 100 = 20.000000000000004). Currency and plain
321
+ // percentage fields pass straight through.
322
+ const isFractionPercentage = computed(
323
+ () => props.field.type === "percentage" && props.field.asFraction === true,
324
+ );
325
+
326
+ const numericInputValue = computed({
327
+ get: () => {
328
+ const value = fieldValue.value;
329
+ if (!isFractionPercentage.value) return value;
330
+ if (value === null || value === undefined || value === "") return value;
331
+ const num = Number(value);
332
+ if (!Number.isFinite(num)) return value;
333
+ return Math.round(num * 100 * 1e6) / 1e6;
334
+ },
335
+ set: (value: any) => {
336
+ if (!isFractionPercentage.value) {
337
+ setValue(value);
338
+ return;
339
+ }
340
+ if (value === null || value === undefined || value === "") {
341
+ setValue(value);
342
+ return;
343
+ }
344
+ const num = Number(value);
345
+ if (!Number.isFinite(num)) {
346
+ setValue(value);
347
+ return;
348
+ }
349
+ setValue(Math.round((num / 100) * 1e9) / 1e9);
350
+ },
351
+ });
352
+
276
353
  const NUMERIC_TYPES: ReadonlySet<FieldType> = new Set([
277
354
  "number",
278
355
  "currency",
@@ -307,6 +384,46 @@ const resolvedLabel = computed(
307
384
 
308
385
  const resolvedHint = computed(() => resolveMaybe(props.field.hint));
309
386
 
387
+ const resolvedInfo = computed(() => resolveMaybe(props.field.info));
388
+
389
+ // ————————————————— switch (toggle) field
390
+
391
+ // Whether a `switch` field is currently on. Coerces the model value to a
392
+ // boolean, but treats the common "falsey" string encodings a backend might send
393
+ // for a boolean ("0", "false", "") as off — plain `Boolean("0")` is `true`,
394
+ // which would wrongly render such a value on.
395
+ const switchIsOn = computed(() => {
396
+ const value = fieldValue.value;
397
+ if (typeof value === "string") {
398
+ const normalised = value.trim().toLowerCase();
399
+ return normalised !== "" && normalised !== "0" && normalised !== "false";
400
+ }
401
+ return Boolean(value);
402
+ });
403
+
404
+ // Bind the toggle to a normalised boolean. The underlying bvn checkbox only
405
+ // treats a literal `true` as checked, so a truthy non-boolean model (e.g.
406
+ // Laravel serialising a boolean column as `1`, or a `"1"` string) would render
407
+ // the toggle in the *off* position while `switchIsOn` styled it *on* — the
408
+ // control contradicting itself. Reading a real boolean keeps the checkbox
409
+ // position, the on-state style, and the contextual text in agreement; writing
410
+ // stores a clean boolean.
411
+ const switchModel = computed({
412
+ get: () => switchIsOn.value,
413
+ set: (value: boolean) => setValue(value),
414
+ });
415
+
416
+ /**
417
+ * Contextual label for a `switch` field: `textWhenTrue`/`textWhenFalse`
418
+ * for the current state, falling back to the field's label.
419
+ */
420
+ const switchText = computed(() => {
421
+ const contextual = switchIsOn.value
422
+ ? resolveMaybe(props.field.textWhenTrue)
423
+ : resolveMaybe(props.field.textWhenFalse);
424
+ return contextual ?? resolvedLabel.value;
425
+ });
426
+
310
427
  const isDisabled = computed(() => {
311
428
  if (props.field.disabledWhen) {
312
429
  return props.field.disabledWhen(effectiveModel.value);
@@ -399,4 +516,26 @@ onBeforeUnmount(() => {
399
516
  border: 1px solid var(--bs-border-color);
400
517
  border-radius: var(--bs-border-radius);
401
518
  }
519
+
520
+ /* Switch field: contextual styling that responds to the on/off state.
521
+ Off is muted; on turns the control and label a filled success green. */
522
+ .dx-switch :deep(.form-check-label) {
523
+ color: var(--bs-secondary-color);
524
+ transition: color 0.15s ease-in-out;
525
+ }
526
+
527
+ .dx-switch--on :deep(.form-check-label) {
528
+ color: var(--bs-success);
529
+ font-weight: 500;
530
+ }
531
+
532
+ .dx-switch--on :deep(.form-check-input:checked) {
533
+ background-color: var(--bs-success);
534
+ border-color: var(--bs-success);
535
+ }
536
+
537
+ .dx-switch--on :deep(.form-check-input:focus) {
538
+ border-color: var(--bs-success);
539
+ box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);
540
+ }
402
541
  </style>
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <span class="dx-field-label">
3
+ <span class="dx-field-label__text">{{ label }}</span>
4
+ <template v-if="info">
5
+ <button
6
+ :id="infoId"
7
+ type="button"
8
+ class="dx-field-label__info"
9
+ :aria-label="`More information: ${label}`"
10
+ @click.stop.prevent
11
+ >
12
+ <i-lucide-info aria-hidden="true" />
13
+ </button>
14
+ <DPopover
15
+ :target="infoId"
16
+ hover
17
+ focus
18
+ placement="top"
19
+ >
20
+ {{ info }}
21
+ </DPopover>
22
+ </template>
23
+ </span>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { useId } from "vue";
28
+ import DPopover from "../base/DPopover.vue";
29
+
30
+ interface Props {
31
+ /** Visible label text. */
32
+ label: string;
33
+
34
+ /** Optional help text revealed in a popover from an info affordance. */
35
+ info?: string;
36
+ }
37
+
38
+ defineProps<Props>();
39
+
40
+ // Stable, SSR-safe id so the popover can target the trigger button.
41
+ const infoId = `dx-field-info-${useId()}`;
42
+ </script>
43
+
44
+ <style scoped>
45
+ .dx-field-label {
46
+ display: inline-flex;
47
+ align-items: center;
48
+ gap: 0.35rem;
49
+ }
50
+
51
+ .dx-field-label__info {
52
+ display: inline-flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ padding: 0;
56
+ border: 0;
57
+ background: none;
58
+ line-height: 1;
59
+ color: var(--bs-secondary-color);
60
+ cursor: help;
61
+ }
62
+
63
+ .dx-field-label__info:hover,
64
+ .dx-field-label__info:focus-visible {
65
+ color: var(--bs-primary);
66
+ }
67
+
68
+ .dx-field-label__info svg {
69
+ width: 0.9em;
70
+ height: 0.9em;
71
+ }
72
+ </style>
@@ -29,7 +29,7 @@
29
29
  <DTab
30
30
  v-for="(tab, index) in visibleTabs"
31
31
  :key="tab.key"
32
- :title="tab.label || tab.key"
32
+ :title="resolveTabLabel(tab)"
33
33
  :lazy="tab.lazy"
34
34
  :active="index === 0"
35
35
  >
@@ -207,6 +207,13 @@ function resolvePredicate(
207
207
  return typeof when === "function" ? when(model.value) : when;
208
208
  }
209
209
 
210
+ /** Resolve a tab's (possibly function-valued) label against the live model. */
211
+ function resolveTabLabel(tab: FormTab): string {
212
+ const label =
213
+ typeof tab.label === "function" ? tab.label(model.value) : tab.label;
214
+ return label || tab.key;
215
+ }
216
+
210
217
  function isFieldVisible(field: FieldDefinition): boolean {
211
218
  const whenOk = resolvePredicate(field.when, true);
212
219
  const showOk = field.show ? field.show() : true;
@@ -124,6 +124,7 @@ const rowKey = (index: number): string | number => {
124
124
  function defaultForType(type: FieldType): any {
125
125
  switch (type) {
126
126
  case "checkbox":
127
+ case "switch":
127
128
  return false;
128
129
  case "number":
129
130
  case "currency":
@@ -529,7 +529,7 @@
529
529
  :disabled="editForm?.processing"
530
530
  @click="handleDelete"
531
531
  >
532
- {{ editForm?.processing ? 'Deleting...' : 'Delete' }}
532
+ {{ pendingAction === 'delete' ? 'Deleting...' : 'Delete' }}
533
533
  </DButton>
534
534
  </div>
535
535
  <div class="d-flex gap-2">
@@ -542,10 +542,10 @@
542
542
  @click="handleEditSave"
543
543
  >
544
544
  <template v-if="isCreateMode">
545
- {{ editForm?.processing ? 'Creating...' : 'Create' }}
545
+ {{ pendingAction === 'save' ? 'Creating...' : 'Create' }}
546
546
  </template>
547
547
  <template v-else>
548
- {{ editForm?.processing ? 'Saving...' : 'Save Changes' }}
548
+ {{ pendingAction === 'save' ? 'Saving...' : 'Save Changes' }}
549
549
  </template>
550
550
  </DButton>
551
551
  </div>
@@ -621,8 +621,12 @@ export interface EditTab {
621
621
  /** Unique key for this tab */
622
622
  key: string;
623
623
 
624
- /** Display label (optional, auto-derived from key if omitted) */
625
- label?: string;
624
+ /**
625
+ * Display label (optional, auto-derived from key if omitted). May be a
626
+ * function of the model (the edited row merged with the live form data),
627
+ * e.g. `label: (item) => \`Products (${item.products_count ?? 0})\``.
628
+ */
629
+ label?: string | ((item: any) => string);
626
630
 
627
631
  /** Field keys to display in this tab (from editFields) */
628
632
  fieldKeys: string[];
@@ -1380,6 +1384,11 @@ const editForm = ref<any>(null);
1380
1384
  const activeTabIndex = ref(0);
1381
1385
  const isCreateMode = ref(false);
1382
1386
 
1387
+ // Which modal action is in flight, so the Save and Delete buttons show their
1388
+ // own loading label independently. `editForm.processing` is shared by every
1389
+ // request the form makes, so it can't tell Save from Delete on its own.
1390
+ const pendingAction = ref<'save' | 'delete' | null>(null);
1391
+
1383
1392
  // Toast (may not be available in test environment)
1384
1393
  let createToast: ((obj: any) => any) | undefined;
1385
1394
  try {
@@ -1521,6 +1530,15 @@ const handleCreateNew = () => {
1521
1530
  const handleEditSave = async () => {
1522
1531
  if (!editForm.value) return;
1523
1532
 
1533
+ pendingAction.value = 'save';
1534
+ try {
1535
+ await performSave();
1536
+ } finally {
1537
+ pendingAction.value = null;
1538
+ }
1539
+ };
1540
+
1541
+ const performSave = async () => {
1524
1542
  // Create mode: POST to createUrl
1525
1543
  if (isCreateMode.value && props.createUrl) {
1526
1544
  try {
@@ -1648,6 +1666,7 @@ const handleDelete = async () => {
1648
1666
 
1649
1667
  if (!confirmed) return;
1650
1668
 
1669
+ pendingAction.value = 'delete';
1651
1670
  try {
1652
1671
  const itemId = (selectedItem.value as any).id;
1653
1672
  const url = props.deleteUrl.replace(':id', itemId);
@@ -1687,6 +1706,8 @@ const handleDelete = async () => {
1687
1706
  });
1688
1707
  } catch (error) {
1689
1708
  emit('deleteError', selectedItem.value as T, error);
1709
+ } finally {
1710
+ pendingAction.value = null;
1690
1711
  }
1691
1712
  };
1692
1713
 
@@ -30,6 +30,7 @@ export interface DefineFormReturn<TData extends Record<string, any>> {
30
30
  function getDefaultValueForType(type: FieldType): any {
31
31
  switch (type) {
32
32
  case "checkbox":
33
+ case "switch":
33
34
  return false;
34
35
  case "number":
35
36
  case "currency":
@@ -8,6 +8,7 @@ export * from 'bootstrap-vue-next';
8
8
  export { default as DXDashboard } from "./components/extended/DXDashboard.vue";
9
9
  export { default as DXForm } from "./components/extended/DXForm.vue";
10
10
  export { default as DXField } from "./components/extended/DXField.vue";
11
+ export { default as DXFieldLabel } from "./components/extended/DXFieldLabel.vue";
11
12
  export { default as DXRepeater } from "./components/extended/DXRepeater.vue";
12
13
  /**
13
14
  * @deprecated Use `DXForm`. `DXBasicForm` is a thin wrapper around `DXForm`
@@ -11,6 +11,8 @@ import type { Component } from "vue";
11
11
  * - `image` / `file` — file input (`image` additionally shows a preview).
12
12
  * - `component` — escape hatch that renders `field.component`.
13
13
  * - `repeater` — nested, repeatable sub-form driven by `field.fields`.
14
+ * - `switch` — a toggle checkbox with contextual on/off text and an
15
+ * on-state (filled) style; see `textWhenTrue` / `textWhenFalse`.
14
16
  */
15
17
  export type FieldType =
16
18
  | "text"
@@ -28,6 +30,7 @@ export type FieldType =
28
30
  | "textarea"
29
31
  | "select"
30
32
  | "checkbox"
33
+ | "switch"
31
34
  | "radio"
32
35
  | "image"
33
36
  | "file"
@@ -105,6 +108,15 @@ export interface FieldDefinition {
105
108
  /** Symbol shown for `currency` fields (default: the locale's, "£"). */
106
109
  currencySymbol?: string;
107
110
 
111
+ /**
112
+ * For `percentage` fields: treat the underlying model value as a 0–1
113
+ * fraction while showing/editing it as a 0–100 percentage. The model keeps
114
+ * the fraction (e.g. `0.2`), the input shows `20`. Off by default (the value
115
+ * is taken as a whole percentage). Use for fields stored as ratios (VAT
116
+ * rates, discounts, …).
117
+ */
118
+ asFraction?: boolean;
119
+
108
120
  /** `accept` attribute for `image`/`file` inputs (e.g. "image/*"). */
109
121
  accept?: string;
110
122
 
@@ -117,6 +129,26 @@ export interface FieldDefinition {
117
129
  */
118
130
  hint?: MaybeFn<string>;
119
131
 
132
+ /**
133
+ * Longer help text revealed in a popover from a small info affordance
134
+ * on the field's label (on hover/focus). Complements `hint` (which is
135
+ * always-visible muted text below the control). May be a function of
136
+ * the model. For rich content, use the `#info` slot instead.
137
+ */
138
+ info?: MaybeFn<string>;
139
+
140
+ /**
141
+ * For `switch` fields: contextual label shown when the toggle is on.
142
+ * Falls back to `label` when omitted. May be a function of the model.
143
+ */
144
+ textWhenTrue?: MaybeFn<string>;
145
+
146
+ /**
147
+ * For `switch` fields: contextual label shown when the toggle is off.
148
+ * Falls back to `label` when omitted. May be a function of the model.
149
+ */
150
+ textWhenFalse?: MaybeFn<string>;
151
+
120
152
  /** CSS class for the form group */
121
153
  class?: string;
122
154
 
@@ -191,8 +223,13 @@ export interface FormTab {
191
223
  /** Unique key for this tab */
192
224
  key: string;
193
225
 
194
- /** Display label (optional, defaults to the key) */
195
- label?: string;
226
+ /**
227
+ * Display label (optional, defaults to the key). May be a function of the
228
+ * form model — the live form data merged with any `context` (e.g. the row
229
+ * DXTable is editing) — so a tab title can reflect the record, such as
230
+ * `label: (model) => \`Products (${model.products_count ?? 0})\``.
231
+ */
232
+ label?: MaybeFn<string>;
196
233
 
197
234
  /** Field keys (from the form's fields) to render in this tab */
198
235
  fieldKeys: string[];