@omnitend/dashboard-for-laravel 0.16.4 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/base/DButton.vue.d.ts +57 -2
- package/dist/components/extended/DXField.vue.d.ts +75 -25
- package/dist/components/extended/DXForm.vue.d.ts +23 -1
- package/dist/components/extended/DXRepeater.vue.d.ts +16 -0
- package/dist/dashboard-for-laravel.js +10611 -10154
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +6 -6
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/style.css +5 -1
- package/dist/types/index.d.ts +50 -1
- package/docs/public/api-reference.json +172 -4
- package/docs/public/docs-map.md +1 -1
- package/package.json +2 -1
- package/resources/css/theme.scss +28 -0
- package/resources/js/components/base/DButton.vue +170 -6
- package/resources/js/components/extended/DXField.vue +154 -7
- package/resources/js/components/extended/DXForm.vue +206 -80
- package/resources/js/components/extended/DXRepeater.vue +270 -4
- package/resources/js/components/extended/DXTable.vue +6 -7
- package/resources/js/types/index.ts +52 -1
|
@@ -5,7 +5,157 @@
|
|
|
5
5
|
field's `minItems`/`maxItems` limits.
|
|
6
6
|
-->
|
|
7
7
|
<template>
|
|
8
|
-
|
|
8
|
+
<!-- `repeaterLayout: "table"` is a PREFERENCE, not a guarantee: the table
|
|
9
|
+
needs real horizontal room for its columns, and the same viewport
|
|
10
|
+
width can put this repeater in a wide standalone form or a narrow
|
|
11
|
+
sidebar column — a viewport breakpoint can't tell those apart. A
|
|
12
|
+
ResizeObserver on THIS OUTER wrapper measures the space actually
|
|
13
|
+
available and falls back to the cards layout when it's not enough,
|
|
14
|
+
scaling to this repeater's own column count (see
|
|
15
|
+
`neededTableWidth`/`showTable` below). This outer wrapper is never
|
|
16
|
+
hidden (an element measures 0×0 once `display:none`, which would
|
|
17
|
+
leave a hidden table with no way to detect regained space and
|
|
18
|
+
un-hide itself) — but its two children below ARE mutually exclusive
|
|
19
|
+
via `v-if`/`v-else`, not `v-show`: only ever ONE of table/cards is
|
|
20
|
+
actually mounted, so a field with side effects (e.g. an async
|
|
21
|
+
`optionsLoader`) doesn't run twice forever just because table mode
|
|
22
|
+
is configured. -->
|
|
23
|
+
<div v-if="wantsTableLayout" ref="containerRef" class="dx-repeater-container">
|
|
24
|
+
<div v-if="showTable" class="dx-repeater-table-wrapper">
|
|
25
|
+
<table class="dx-repeater-table table">
|
|
26
|
+
<thead>
|
|
27
|
+
<tr>
|
|
28
|
+
<th v-for="subField in subFields" :key="subField.key">
|
|
29
|
+
{{ headerLabel(subField) }}
|
|
30
|
+
</th>
|
|
31
|
+
<th class="dx-repeater-table-remove-col"></th>
|
|
32
|
+
</tr>
|
|
33
|
+
</thead>
|
|
34
|
+
<tbody>
|
|
35
|
+
<tr
|
|
36
|
+
v-for="(entry, position) in visibleRows"
|
|
37
|
+
:key="rowKey(entry.row, position)"
|
|
38
|
+
>
|
|
39
|
+
<!--
|
|
40
|
+
@slot Custom layout for a single repeater row, replacing the default sub-field stack (including the built-in Remove control — call `remove()` yourself). In table layout, rendered as the `<tr>`'s children: supply one `<td>` per sub-field plus a trailing `<td>` to match the header's column count (the header always includes an empty column for the built-in remove button).
|
|
41
|
+
@binding {object} row The current row's data object.
|
|
42
|
+
@binding {number} index The row's zero-based position among visible rows.
|
|
43
|
+
@binding {FieldDefinition[]} fields The sub-field definitions for the row.
|
|
44
|
+
@binding {function} remove Removes this row (respects `minItems`; soft-deletes instead of splicing when `field.softDeleteKey` is set and the row has an `id`).
|
|
45
|
+
@binding {string} path The dot path into `form.data` for this row (e.g. `lines.0`).
|
|
46
|
+
-->
|
|
47
|
+
<slot
|
|
48
|
+
v-if="$slots.row"
|
|
49
|
+
name="row"
|
|
50
|
+
:row="entry.row"
|
|
51
|
+
:index="position"
|
|
52
|
+
:fields="subFields"
|
|
53
|
+
:remove="() => removeRow(entry.index)"
|
|
54
|
+
:path="rowPath(entry.index)"
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
<template v-else>
|
|
58
|
+
<td v-for="subField in subFields" :key="subField.key">
|
|
59
|
+
<DXField
|
|
60
|
+
:field="subField"
|
|
61
|
+
:form="form"
|
|
62
|
+
:model="entry.row"
|
|
63
|
+
:key-path="`${rowPath(entry.index)}.${subField.key}`"
|
|
64
|
+
:error-key="`${rowPath(entry.index)}.${subField.key}`"
|
|
65
|
+
hide-label
|
|
66
|
+
/>
|
|
67
|
+
</td>
|
|
68
|
+
<td class="dx-repeater-table-remove-col">
|
|
69
|
+
<DButton
|
|
70
|
+
variant="outline-danger"
|
|
71
|
+
class="dx-repeater-table-remove-btn"
|
|
72
|
+
:disabled="visibleRows.length <= minItems"
|
|
73
|
+
:aria-label="`Remove row ${position + 1}`"
|
|
74
|
+
@click="removeRow(entry.index)"
|
|
75
|
+
>
|
|
76
|
+
<i-lucide-trash-2 aria-hidden="true" />
|
|
77
|
+
</DButton>
|
|
78
|
+
</td>
|
|
79
|
+
</template>
|
|
80
|
+
</tr>
|
|
81
|
+
</tbody>
|
|
82
|
+
</table>
|
|
83
|
+
|
|
84
|
+
<DButton
|
|
85
|
+
variant="outline-primary"
|
|
86
|
+
size="sm"
|
|
87
|
+
:disabled="maxItems !== undefined && visibleRows.length >= maxItems"
|
|
88
|
+
@click="addRow"
|
|
89
|
+
>
|
|
90
|
+
{{ field.addLabel || "Add" }}
|
|
91
|
+
</DButton>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Cards fallback: identical to the default cards markup below,
|
|
95
|
+
shown instead of the table when there isn't enough room for it.
|
|
96
|
+
`v-else` (not `v-show`) — only the OUTER wrapper above needs to
|
|
97
|
+
stay mounted for measurement; unmounting whichever of these two
|
|
98
|
+
isn't current avoids permanently double-running every DXField
|
|
99
|
+
in both layouts at once (double async optionsLoader fetches,
|
|
100
|
+
double watchers, double everything else a field might do). -->
|
|
101
|
+
<div v-else class="dx-repeater">
|
|
102
|
+
<div
|
|
103
|
+
v-for="(entry, position) in visibleRows"
|
|
104
|
+
:key="rowKey(entry.row, position)"
|
|
105
|
+
class="dx-repeater-row"
|
|
106
|
+
>
|
|
107
|
+
<slot
|
|
108
|
+
v-if="$slots.row"
|
|
109
|
+
name="row"
|
|
110
|
+
:row="entry.row"
|
|
111
|
+
:index="position"
|
|
112
|
+
:fields="subFields"
|
|
113
|
+
:remove="() => removeRow(entry.index)"
|
|
114
|
+
:path="rowPath(entry.index)"
|
|
115
|
+
/>
|
|
116
|
+
|
|
117
|
+
<template v-else>
|
|
118
|
+
<div class="dx-repeater-row-header">
|
|
119
|
+
<span v-if="field.showRowIndex" class="dx-repeater-row-index">{{ position + 1 }}</span>
|
|
120
|
+
<DButton
|
|
121
|
+
variant="outline-danger"
|
|
122
|
+
size="sm"
|
|
123
|
+
:disabled="visibleRows.length <= minItems"
|
|
124
|
+
@click="removeRow(entry.index)"
|
|
125
|
+
>
|
|
126
|
+
Remove
|
|
127
|
+
</DButton>
|
|
128
|
+
</div>
|
|
129
|
+
<DXField
|
|
130
|
+
v-for="subField in subFields"
|
|
131
|
+
:key="subField.key"
|
|
132
|
+
:field="subField"
|
|
133
|
+
:form="form"
|
|
134
|
+
:model="entry.row"
|
|
135
|
+
:key-path="`${rowPath(entry.index)}.${subField.key}`"
|
|
136
|
+
:error-key="`${rowPath(entry.index)}.${subField.key}`"
|
|
137
|
+
/>
|
|
138
|
+
</template>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<DButton
|
|
142
|
+
variant="outline-primary"
|
|
143
|
+
size="sm"
|
|
144
|
+
:disabled="maxItems !== undefined && visibleRows.length >= maxItems"
|
|
145
|
+
@click="addRow"
|
|
146
|
+
>
|
|
147
|
+
{{ field.addLabel || "Add" }}
|
|
148
|
+
</DButton>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<!-- Cards layout (default, `repeaterLayout: "cards"` or unset): each row
|
|
153
|
+
is its own bordered card with sub-fields stacked vertically. Kept as
|
|
154
|
+
a separate, unconditional branch (rather than reusing the fallback
|
|
155
|
+
above) so this — the overwhelmingly common case — never pays for a
|
|
156
|
+
ResizeObserver or the extra wrapper div; its markup is byte-for-byte
|
|
157
|
+
what cards mode has always rendered. -->
|
|
158
|
+
<div v-else class="dx-repeater">
|
|
9
159
|
<div
|
|
10
160
|
v-for="(entry, position) in visibleRows"
|
|
11
161
|
:key="rowKey(entry.row, position)"
|
|
@@ -32,7 +182,7 @@
|
|
|
32
182
|
<!-- Default row: stack each sub-field -->
|
|
33
183
|
<template v-else>
|
|
34
184
|
<div class="dx-repeater-row-header">
|
|
35
|
-
<span class="dx-repeater-row-index">{{ position + 1 }}</span>
|
|
185
|
+
<span v-if="field.showRowIndex" class="dx-repeater-row-index">{{ position + 1 }}</span>
|
|
36
186
|
<DButton
|
|
37
187
|
variant="outline-danger"
|
|
38
188
|
size="sm"
|
|
@@ -66,7 +216,7 @@
|
|
|
66
216
|
</template>
|
|
67
217
|
|
|
68
218
|
<script setup lang="ts">
|
|
69
|
-
import { computed } from "vue";
|
|
219
|
+
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
|
70
220
|
import DButton from "../base/DButton.vue";
|
|
71
221
|
import DXField from "./DXField.vue";
|
|
72
222
|
import type { UseFormReturn } from "../../composables/useForm";
|
|
@@ -99,6 +249,79 @@ const minItems = computed(() => props.field.minItems ?? 0);
|
|
|
99
249
|
const maxItems = computed(() => props.field.maxItems);
|
|
100
250
|
const softDeleteKey = computed(() => props.field.softDeleteKey);
|
|
101
251
|
|
|
252
|
+
/** Column header text for a sub-field. No specific row context exists for a
|
|
253
|
+
* header, so a function-valued label resolves against the outer model. */
|
|
254
|
+
function headerLabel(subField: FieldDefinition): string {
|
|
255
|
+
const label = subField.label;
|
|
256
|
+
if (typeof label === "function") return label(props.model ?? {});
|
|
257
|
+
return label || subField.key;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ————————————————— table/cards responsive fallback (#68 follow-up)
|
|
261
|
+
|
|
262
|
+
const wantsTableLayout = computed(() => props.field.repeaterLayout === "table");
|
|
263
|
+
|
|
264
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
265
|
+
const containerWidth = ref<number | null>(null);
|
|
266
|
+
|
|
267
|
+
// Heuristic minimum width for the table to stay legible: per-sub-field
|
|
268
|
+
// column widths (not a pixel-measured fit, just a reasonable estimate —
|
|
269
|
+
// `currency`/`percentage` get extra room for their `£`/`%` affix, which
|
|
270
|
+
// otherwise crowds out the number itself) plus the trailing icon-only
|
|
271
|
+
// remove column. Scales with this repeater's own column count/types,
|
|
272
|
+
// unlike a single fixed viewport breakpoint.
|
|
273
|
+
const DEFAULT_COLUMN_WIDTH = 130;
|
|
274
|
+
const AFFIX_COLUMN_WIDTH = 160;
|
|
275
|
+
const REMOVE_COLUMN_WIDTH = 70;
|
|
276
|
+
|
|
277
|
+
function columnWidthFor(subField: FieldDefinition): number {
|
|
278
|
+
if (subField.type === "currency" || subField.type === "percentage") {
|
|
279
|
+
return AFFIX_COLUMN_WIDTH;
|
|
280
|
+
}
|
|
281
|
+
return DEFAULT_COLUMN_WIDTH;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const neededTableWidth = computed(
|
|
285
|
+
() =>
|
|
286
|
+
subFields.value.reduce((total, subField) => total + columnWidthFor(subField), 0) +
|
|
287
|
+
REMOVE_COLUMN_WIDTH,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// `null` (not yet measured) defaults to showing the table — the configured
|
|
291
|
+
// preference — rather than flashing cards first. Resolves as soon as the
|
|
292
|
+
// watcher below fires (see its comment for why that isn't simply onMounted).
|
|
293
|
+
const showTable = computed(
|
|
294
|
+
() => containerWidth.value === null || containerWidth.value >= neededTableWidth.value,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
298
|
+
|
|
299
|
+
// A `watch` on the ref itself, not `onMounted`: DXRepeater is loaded via
|
|
300
|
+
// `defineAsyncComponent` (DXField imports it lazily to break a circular
|
|
301
|
+
// import), and for an async-resolved component whose root element also
|
|
302
|
+
// carries `v-if`, the template ref is not always populated yet by the time
|
|
303
|
+
// `onMounted` fires — observed live as `containerRef.value === null` inside
|
|
304
|
+
// `onMounted` despite the element existing in the DOM at that point. `watch`
|
|
305
|
+
// instead reacts whenever the ref actually receives (or loses, e.g. if
|
|
306
|
+
// `wantsTableLayout` ever toggles false) its element, regardless of when
|
|
307
|
+
// that happens relative to lifecycle hooks.
|
|
308
|
+
watch(containerRef, (el) => {
|
|
309
|
+
resizeObserver?.disconnect();
|
|
310
|
+
resizeObserver = null;
|
|
311
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
312
|
+
containerWidth.value = el.getBoundingClientRect().width;
|
|
313
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
314
|
+
const entry = entries[0];
|
|
315
|
+
if (entry) containerWidth.value = entry.contentRect.width;
|
|
316
|
+
});
|
|
317
|
+
resizeObserver.observe(el);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
onBeforeUnmount(() => {
|
|
321
|
+
resizeObserver?.disconnect();
|
|
322
|
+
resizeObserver = null;
|
|
323
|
+
});
|
|
324
|
+
|
|
102
325
|
function isSoftDeleted(row: any): boolean {
|
|
103
326
|
return Boolean(
|
|
104
327
|
softDeleteKey.value &&
|
|
@@ -254,12 +477,55 @@ function removeRow(index: number): void {
|
|
|
254
477
|
.dx-repeater-row-header {
|
|
255
478
|
display: flex;
|
|
256
479
|
align-items: center;
|
|
257
|
-
justify-content: space-between;
|
|
258
480
|
margin-bottom: 0.75rem;
|
|
259
481
|
}
|
|
260
482
|
|
|
483
|
+
/* The Remove control is always this header's last child, whether or not the
|
|
484
|
+
(opt-in) row-index span is also present — push it to the end explicitly
|
|
485
|
+
rather than relying on `justify-content: space-between`, which centres a
|
|
486
|
+
single remaining child at the *start* once the index is off (the default). */
|
|
487
|
+
.dx-repeater-row-header > :last-child {
|
|
488
|
+
margin-inline-start: auto;
|
|
489
|
+
}
|
|
490
|
+
|
|
261
491
|
.dx-repeater-row-index {
|
|
262
492
|
font-weight: 600;
|
|
263
493
|
color: var(--bs-secondary-color);
|
|
264
494
|
}
|
|
495
|
+
|
|
496
|
+
.dx-repeater-table td {
|
|
497
|
+
vertical-align: middle;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* Sub-fields render with their default mb-3 (matching the standalone-field
|
|
501
|
+
default), which is unwanted spacing inside an already-compact table cell —
|
|
502
|
+
and leaves the field vertically off-centre against the trailing remove
|
|
503
|
+
button, since that residual margin-bottom is inside the td's own
|
|
504
|
+
vertical-align:middle box. Bootstrap's spacing utilities are `!important`
|
|
505
|
+
by design (same as the toast-variant override elsewhere in this project),
|
|
506
|
+
so overriding one requires `!important` too — a plain declaration silently
|
|
507
|
+
loses and the margin survives. */
|
|
508
|
+
.dx-repeater-table td :deep(.mb-3) {
|
|
509
|
+
margin-bottom: 0 !important;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.dx-repeater-table-remove-col {
|
|
513
|
+
width: 1%;
|
|
514
|
+
white-space: nowrap;
|
|
515
|
+
text-align: center;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/* Match the row's text-input height (same var the switch field uses).
|
|
519
|
+
`--dx-input-height`'s `em` term resolves against the CONSUMING element's
|
|
520
|
+
own font-size, not the input's — so this only lines up if the button's
|
|
521
|
+
font-size equals .form-control's (both derive from Bootstrap's base
|
|
522
|
+
font-size var, but a `size="sm"` DButton sets a smaller one). The button
|
|
523
|
+
is deliberately NOT `size="sm"` here for that reason; min-height still
|
|
524
|
+
keeps it visually compact regardless. */
|
|
525
|
+
.dx-repeater-table-remove-btn {
|
|
526
|
+
display: inline-flex;
|
|
527
|
+
align-items: center;
|
|
528
|
+
justify-content: center;
|
|
529
|
+
min-height: var(--dx-input-height, calc(1.5em + 0.75rem + 2px));
|
|
530
|
+
}
|
|
265
531
|
</style>
|
|
@@ -599,10 +599,12 @@
|
|
|
599
599
|
<DButton
|
|
600
600
|
v-if="deleteUrl && !isCreateMode"
|
|
601
601
|
variant="danger"
|
|
602
|
+
:loading="pendingAction === 'delete'"
|
|
603
|
+
loading-text="Deleting..."
|
|
602
604
|
:disabled="editForm?.processing || editLoading"
|
|
603
605
|
@click="handleDelete"
|
|
604
606
|
>
|
|
605
|
-
|
|
607
|
+
Delete
|
|
606
608
|
</DButton>
|
|
607
609
|
</div>
|
|
608
610
|
<div class="d-flex gap-2">
|
|
@@ -611,15 +613,12 @@
|
|
|
611
613
|
</DButton>
|
|
612
614
|
<DButton
|
|
613
615
|
variant="primary"
|
|
616
|
+
:loading="pendingAction === 'save'"
|
|
617
|
+
:loading-text="isCreateMode ? 'Creating...' : 'Saving...'"
|
|
614
618
|
:disabled="editForm?.processing || editLoading"
|
|
615
619
|
@click="handleEditSave"
|
|
616
620
|
>
|
|
617
|
-
|
|
618
|
-
{{ pendingAction === 'save' ? 'Creating...' : 'Create' }}
|
|
619
|
-
</template>
|
|
620
|
-
<template v-else>
|
|
621
|
-
{{ pendingAction === 'save' ? 'Saving...' : 'Save Changes' }}
|
|
622
|
-
</template>
|
|
621
|
+
{{ isCreateMode ? 'Create' : 'Save Changes' }}
|
|
623
622
|
</DButton>
|
|
624
623
|
</div>
|
|
625
624
|
</div>
|
|
@@ -61,6 +61,23 @@ export interface FieldOption extends Record<string, unknown> {
|
|
|
61
61
|
*/
|
|
62
62
|
export type OptionsLoader = (model: any) => Promise<FieldOption[]>;
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Label column width for horizontal layout, mirroring Bootstrap Vue Next's
|
|
66
|
+
* `BFormGroup` `labelCols`/`labelCols*` props: either a single width applied
|
|
67
|
+
* at all breakpoints, or a per-breakpoint object (omitted breakpoints fall
|
|
68
|
+
* back to BFormGroup's own defaults, which is how "collapse to vertical
|
|
69
|
+
* below a breakpoint" comes for free — e.g. `{ md: 3 }` stacks below `md`).
|
|
70
|
+
*
|
|
71
|
+
* Deliberately excludes `boolean` (which BFormGroup's own `labelCols` prop
|
|
72
|
+
* accepts): Vue's type-based `defineProps` auto-defaults an omitted prop to
|
|
73
|
+
* `false` (not `undefined`) whenever its declared type includes `boolean`,
|
|
74
|
+
* which would silently break the `field.labelCols ?? formLabelCols ?? 3`
|
|
75
|
+
* fallback chain used to resolve this value.
|
|
76
|
+
*/
|
|
77
|
+
export type LabelCols =
|
|
78
|
+
| number
|
|
79
|
+
| { sm?: number; md?: number; lg?: number; xl?: number };
|
|
80
|
+
|
|
64
81
|
/**
|
|
65
82
|
* Field definition shared by every form renderer.
|
|
66
83
|
*
|
|
@@ -172,10 +189,24 @@ export interface FieldDefinition {
|
|
|
172
189
|
|
|
173
190
|
/**
|
|
174
191
|
* Render the field full-width with no label wrapper, delegating its
|
|
175
|
-
* content to the `#span(<key>)` slot. Useful for custom blocks.
|
|
192
|
+
* content to the `#span(<key>)` slot. Useful for custom blocks. Also
|
|
193
|
+
* doubles as the "fill both columns" escape hatch in horizontal layout —
|
|
194
|
+
* a span field always bypasses the label/content column split.
|
|
176
195
|
*/
|
|
177
196
|
span?: boolean;
|
|
178
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Per-field override of DXForm's `layout` prop ("vertical" | "horizontal").
|
|
200
|
+
* Falls back to the form-level `layout` when omitted.
|
|
201
|
+
*/
|
|
202
|
+
layout?: "vertical" | "horizontal";
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Per-field override of DXForm's `labelCols` prop, for horizontal
|
|
206
|
+
* layout. Falls back to the form-level `labelCols` when omitted.
|
|
207
|
+
*/
|
|
208
|
+
labelCols?: LabelCols;
|
|
209
|
+
|
|
179
210
|
/**
|
|
180
211
|
* Component rendered for `type: "component"` fields. Receives
|
|
181
212
|
* `modelValue`, `field`, `model` props and emits `update:modelValue`.
|
|
@@ -199,6 +230,26 @@ export interface FieldDefinition {
|
|
|
199
230
|
minItems?: number;
|
|
200
231
|
maxItems?: number;
|
|
201
232
|
|
|
233
|
+
/**
|
|
234
|
+
* For `repeater` fields: `"cards"` (default) stacks each row as its own
|
|
235
|
+
* card with sub-fields listed vertically — best for rows with several
|
|
236
|
+
* or complex sub-fields. `"table"` renders one row per item with
|
|
237
|
+
* sub-fields as columns — far more compact for simple child-row lists
|
|
238
|
+
* (e.g. 2-3 short sub-fields). Named `repeaterLayout` (not `layout`) to
|
|
239
|
+
* avoid colliding with the unrelated form-level `layout` prop
|
|
240
|
+
* ("vertical" | "horizontal") every field type accepts.
|
|
241
|
+
*/
|
|
242
|
+
repeaterLayout?: "cards" | "table";
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* For `repeater` fields in `cards` layout: show each row's 1-based
|
|
246
|
+
* position (e.g. "1", "2") next to its Remove button. Off by default —
|
|
247
|
+
* only meaningful once a consumer explicitly wants it (e.g. rows the
|
|
248
|
+
* user thinks of as numbered/ordered); most repeaters don't need it.
|
|
249
|
+
* Not applicable to `table` layout, which has no equivalent column.
|
|
250
|
+
*/
|
|
251
|
+
showRowIndex?: boolean;
|
|
252
|
+
|
|
202
253
|
/**
|
|
203
254
|
* For `repeater` fields backed by an upsert-children API contract (rows
|
|
204
255
|
* carrying a persisted `id` are only deleted server-side when submitted
|