@omnitend/dashboard-for-laravel 0.33.0 → 0.34.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 (50) 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/DXNumericField.vue.d.ts +23 -0
  14. package/dist/components/extended/DXRepeater.vue.d.ts +4 -4
  15. package/dist/components/extended/DXRepeaterCards.vue.d.ts +45 -0
  16. package/dist/components/extended/DXTable.vue.d.ts +91 -2
  17. package/dist/components/extended/DXTableEditorModal.vue.d.ts +17 -2
  18. package/dist/components/extended/DXUserAvatar.vue.d.ts +1 -1
  19. package/dist/composables/useAsyncOptions.d.ts +20 -0
  20. package/dist/composables/useResourceEditor.d.ts +2 -1
  21. package/dist/dashboard-for-laravel.js +9987 -10054
  22. package/dist/dashboard-for-laravel.js.map +1 -1
  23. package/dist/dashboard-for-laravel.umd.cjs +7 -7
  24. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  25. package/dist/index.d.ts +3 -2
  26. package/dist/style.css +1 -1
  27. package/dist/types/index.d.ts +146 -0
  28. package/dist/utils/formSchema.d.ts +59 -0
  29. package/docs/public/api-reference.json +464 -30
  30. package/docs/public/docs-map.md +1 -1
  31. package/docs/public/llms.txt +5 -2
  32. package/package.json +1 -1
  33. package/resources/js/components/charts/DXBarChart.vue +4 -43
  34. package/resources/js/components/charts/DXLineChart.vue +4 -43
  35. package/resources/js/components/charts/useThemedChart.ts +69 -0
  36. package/resources/js/components/extended/DXChoiceField.vue +114 -0
  37. package/resources/js/components/extended/DXField.vue +328 -615
  38. package/resources/js/components/extended/DXFieldShell.vue +124 -0
  39. package/resources/js/components/extended/DXForm.vue +9 -5
  40. package/resources/js/components/extended/DXNumericField.vue +144 -0
  41. package/resources/js/components/extended/DXRepeater.vue +53 -175
  42. package/resources/js/components/extended/DXRepeaterCards.vue +133 -0
  43. package/resources/js/components/extended/DXTable.vue +250 -37
  44. package/resources/js/components/extended/DXTableEditorModal.vue +34 -2
  45. package/resources/js/composables/defineForm.ts +8 -44
  46. package/resources/js/composables/useAsyncOptions.ts +61 -0
  47. package/resources/js/composables/useResourceEditor.ts +24 -38
  48. package/resources/js/index.ts +33 -0
  49. package/resources/js/types/index.ts +231 -0
  50. package/resources/js/utils/formSchema.ts +126 -0
@@ -0,0 +1,124 @@
1
+ <!--
2
+ @component (internal — not exported)
3
+ The chrome every DXField branch shares: the optional label (with its info
4
+ affordance and, in horizontal layout, its hint), the control (default slot),
5
+ and the trailing validation feedback + info block + hint + static help. Before
6
+ #135 this markup was re-rendered separately in each of DXField's ~7 branches;
7
+ it now lives here once.
8
+
9
+ Two shapes:
10
+ - `mode="group"` — root is a `DFormGroup` that owns the `#label` column and
11
+ holds the control + trailing block inside it (standard / horizontal
12
+ checkbox / horizontal switch).
13
+ - `mode="plain"` — root is a plain `div`; the control renders its own inline
14
+ label (vertical checkbox/switch), or a heading/rows (switch-list), or a
15
+ nested group (repeater). The trailing block follows the control in the div.
16
+
17
+ The `hint` and `info` slots are forwarded down from DXField (content AND slot
18
+ props). `hint` falls back to `{{ resolvedHint }}` when no slot is supplied.
19
+ -->
20
+ <template>
21
+ <DFormGroup
22
+ v-if="mode === 'group'"
23
+ :class="wrapperClass"
24
+ v-bind="groupAttrs"
25
+ >
26
+ <template v-if="!hideLabel" #label>
27
+ <DXFieldLabel :label="label" :info="info" />
28
+ <small
29
+ v-if="hintInLabel && (resolvedHint || $slots.hint)"
30
+ class="form-text text-muted d-block dx-field-hint"
31
+ >
32
+ <slot name="hint" :field="field" :model="model">{{
33
+ resolvedHint
34
+ }}</slot>
35
+ </small>
36
+ </template>
37
+
38
+ <slot />
39
+
40
+ <DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
41
+ {{ form.getError(errorKey) }}
42
+ </DFormInvalidFeedback>
43
+ <slot name="info" :field="field" :model="model" />
44
+ <DFormText
45
+ v-if="!hintInLabel && (resolvedHint || $slots.hint)"
46
+ class="text-muted"
47
+ >
48
+ <slot name="hint" :field="field" :model="model">{{
49
+ resolvedHint
50
+ }}</slot>
51
+ </DFormText>
52
+ <DFormText v-if="field.help">{{ field.help }}</DFormText>
53
+ </DFormGroup>
54
+
55
+ <div v-else :class="wrapperClass">
56
+ <slot />
57
+
58
+ <DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
59
+ {{ form.getError(errorKey) }}
60
+ </DFormInvalidFeedback>
61
+ <slot name="info" :field="field" :model="model" />
62
+ <DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
63
+ <slot name="hint" :field="field" :model="model">{{
64
+ resolvedHint
65
+ }}</slot>
66
+ </DFormText>
67
+ <DFormText v-if="field.help">{{ field.help }}</DFormText>
68
+ </div>
69
+ </template>
70
+
71
+ <script setup lang="ts">
72
+ import DFormGroup from "../base/DFormGroup.vue";
73
+ import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
74
+ import DFormText from "../base/DFormText.vue";
75
+ import DXFieldLabel from "./DXFieldLabel.vue";
76
+ import type { UseFormReturn } from "../../composables/useForm";
77
+ import type { FieldDefinition } from "../../types";
78
+
79
+ interface Props {
80
+ /** The field definition (for `field.help` and slot props). */
81
+ field: FieldDefinition;
82
+
83
+ /** Form instance owning the field's errors. */
84
+ form: UseFormReturn<any>;
85
+
86
+ /** Error key for validation lookups. */
87
+ errorKey: string;
88
+
89
+ /** Model passed to `hint`/`info` slot props. */
90
+ model?: any;
91
+
92
+ /**
93
+ * Layout shape. `group` = DFormGroup root with a label column; `plain` =
94
+ * plain div, the control owning its own inline label/heading.
95
+ */
96
+ mode: "group" | "plain";
97
+
98
+ /** Wrapper class (`field.class || 'mb-3'`). */
99
+ wrapperClass?: string;
100
+
101
+ /** BFormGroup attrs (horizontal label-cols etc.); group mode only. */
102
+ groupAttrs?: Record<string, any>;
103
+
104
+ /** Resolved label text (group mode). Always supplied by DXField. */
105
+ label: string;
106
+
107
+ /** Resolved info text for the label popover (group mode). */
108
+ info?: string;
109
+
110
+ /** Skip the label column entirely (group mode). */
111
+ hideLabel?: boolean;
112
+
113
+ /**
114
+ * Whether the hint sits in the label column (horizontal) rather than below
115
+ * the control. Group mode only; plain mode always renders the hint below.
116
+ */
117
+ hintInLabel?: boolean;
118
+
119
+ /** Resolved hint text (fallback when no `hint` slot is supplied). */
120
+ resolvedHint?: string;
121
+ }
122
+
123
+ defineProps<Props>();
124
+ </script>
@@ -177,6 +177,10 @@ import DXFormField from "./DXFormField.vue";
177
177
  import type { UseFormReturn } from "../../composables/useForm";
178
178
  import type { DefineFormReturn } from "../../composables/defineForm";
179
179
  import type { FieldDefinition, FormTab, LabelCols, MaybeFn } from "../../types";
180
+ import {
181
+ resolvePredicate as resolvePredicateFor,
182
+ isFieldVisible as isFieldVisibleFor,
183
+ } from "../../utils/formSchema";
180
184
 
181
185
  interface Props {
182
186
  /**
@@ -303,12 +307,14 @@ const model = computed(() => ({
303
307
  ...resolvedForm.value.data,
304
308
  }));
305
309
 
310
+ // Thin wrappers binding the shared formSchema predicates to this form's live
311
+ // model (context + form data), so field/tab visibility follows the one rule
312
+ // every renderer shares (#134).
306
313
  function resolvePredicate(
307
314
  when: MaybeFn<boolean> | undefined,
308
315
  fallback: boolean,
309
316
  ): boolean {
310
- if (when === undefined) return fallback;
311
- return typeof when === "function" ? when(model.value) : when;
317
+ return resolvePredicateFor(when, model.value, fallback);
312
318
  }
313
319
 
314
320
  /** Resolve a tab's (possibly function-valued) label against the live model. */
@@ -319,9 +325,7 @@ function resolveTabLabel(tab: FormTab): string {
319
325
  }
320
326
 
321
327
  function isFieldVisible(field: FieldDefinition): boolean {
322
- const whenOk = resolvePredicate(field.when, true);
323
- const showOk = field.show ? field.show() : true;
324
- return whenOk && showOk;
328
+ return isFieldVisibleFor(field, model.value);
325
329
  }
326
330
 
327
331
  // ————————————————— tabs
@@ -0,0 +1,144 @@
1
+ <!--
2
+ @component (internal — not exported)
3
+ The numeric control family for DXField: `currency`, `percentage`, `number`.
4
+ Extracted from DXField (#135) unchanged. The parent DXField still owns the
5
+ value/guards/`setValue` coercion — this component only renders the control and
6
+ emits the raw edited value back up. The `percentage` `asFraction` display
7
+ scaling (model holds a 0–1 fraction, input shows 0–100) lives here because it
8
+ is purely presentational to this control.
9
+ -->
10
+ <template>
11
+ <!-- Currency: the DXCurrencyInput leaf (#152) owns the symbol prepend, the
12
+ blur-padded display (#69), empty -> null, and the minor-units model
13
+ scaling (#116). -->
14
+ <DXCurrencyInput
15
+ v-if="field.type === 'currency'"
16
+ :model-value="modelValue"
17
+ :currency-symbol="field.currencySymbol || '£'"
18
+ :decimals="field.decimals ?? 2"
19
+ :minor-units="field.minorUnits ?? false"
20
+ :step="field.step"
21
+ :required="field.required"
22
+ :placeholder="field.placeholder"
23
+ :min="field.min"
24
+ :max="field.max"
25
+ :state="state"
26
+ :disabled="isDisabled"
27
+ :readonly="isReadonly || isPlaintext"
28
+ :plaintext="isPlaintext"
29
+ v-bind="inputProps"
30
+ @update:model-value="emit('update:modelValue', $event)"
31
+ />
32
+
33
+ <!-- Percentage: numeric input with an affix -->
34
+ <DInputGroup v-else-if="field.type === 'percentage'">
35
+ <DFormInput
36
+ v-model="numericInputValue"
37
+ type="number"
38
+ :required="field.required"
39
+ :placeholder="field.placeholder"
40
+ :step="field.step"
41
+ :min="field.min"
42
+ :max="field.max"
43
+ :state="state"
44
+ :disabled="isDisabled"
45
+ :readonly="isReadonly || isPlaintext"
46
+ :plaintext="isPlaintext"
47
+ v-bind="inputProps"
48
+ />
49
+
50
+ <template #append>
51
+ <span class="input-group-text">%</span>
52
+ </template>
53
+ </DInputGroup>
54
+
55
+ <!-- Number -->
56
+ <DFormInput
57
+ v-else
58
+ :model-value="modelValue"
59
+ type="number"
60
+ :required="field.required"
61
+ :placeholder="field.placeholder"
62
+ :step="field.step"
63
+ :min="field.min"
64
+ :max="field.max"
65
+ :state="state"
66
+ :disabled="isDisabled"
67
+ :readonly="isReadonly || isPlaintext"
68
+ :plaintext="isPlaintext"
69
+ v-bind="inputProps"
70
+ @update:model-value="emit('update:modelValue', $event)"
71
+ />
72
+ </template>
73
+
74
+ <script setup lang="ts">
75
+ import { computed } from "vue";
76
+ import DFormInput from "../base/DFormInput.vue";
77
+ import DInputGroup from "../base/DInputGroup.vue";
78
+ import DXCurrencyInput from "./DXCurrencyInput.vue";
79
+ import type { FieldDefinition } from "../../types";
80
+
81
+ interface Props {
82
+ /** Field definition (type currency/percentage/number + numeric options). */
83
+ field: FieldDefinition;
84
+
85
+ /** Current model value. */
86
+ modelValue: any;
87
+
88
+ /** Validation state passed to the control. */
89
+ state?: boolean | null;
90
+
91
+ /** Disabled state (`isDisabled`). */
92
+ isDisabled?: boolean;
93
+
94
+ /** Readonly state (`isReadonly`). */
95
+ isReadonly?: boolean;
96
+
97
+ /** Plaintext (display-only) state (`isPlaintext`). */
98
+ isPlaintext?: boolean;
99
+
100
+ /** Escape-hatch input props, already guard-merged by DXField. */
101
+ inputProps?: Record<string, any>;
102
+ }
103
+
104
+ const props = defineProps<Props>();
105
+
106
+ const emit = defineEmits<{
107
+ (event: "update:modelValue", value: any): void;
108
+ }>();
109
+
110
+ // For `percentage` fields with `asFraction`, the model holds a 0–1 fraction but
111
+ // the input shows/edits a 0–100 percentage. Scale on read/write, rounding away
112
+ // binary-float artefacts (0.2 * 100 = 20.000000000000004). Currency and plain
113
+ // percentage fields pass straight through.
114
+ const isFractionPercentage = computed(
115
+ () => props.field.type === "percentage" && props.field.asFraction === true,
116
+ );
117
+
118
+ const numericInputValue = computed({
119
+ get: () => {
120
+ const value = props.modelValue;
121
+ if (!isFractionPercentage.value) return value;
122
+ if (value === null || value === undefined || value === "") return value;
123
+ const num = Number(value);
124
+ if (!Number.isFinite(num)) return value;
125
+ return Math.round(num * 100 * 1e6) / 1e6;
126
+ },
127
+ set: (value: any) => {
128
+ if (!isFractionPercentage.value) {
129
+ emit("update:modelValue", value);
130
+ return;
131
+ }
132
+ if (value === null || value === undefined || value === "") {
133
+ emit("update:modelValue", value);
134
+ return;
135
+ }
136
+ const num = Number(value);
137
+ if (!Number.isFinite(num)) {
138
+ emit("update:modelValue", value);
139
+ return;
140
+ }
141
+ emit("update:modelValue", Math.round((num / 100) * 1e9) / 1e9);
142
+ },
143
+ });
144
+ </script>
@@ -91,137 +91,65 @@
91
91
  </DButton>
92
92
  </div>
93
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
94
+ <!-- Cards fallback: the same DXRepeaterCards as the default branch
95
+ below, shown instead of the table when there isn't enough room for
96
+ it. `v-else` (not `v-show`) — only the OUTER wrapper above needs to
97
97
  stay mounted for measurement; unmounting whichever of these two
98
98
  isn't current avoids permanently double-running every DXField
99
99
  in both layouts at once (double async optionsLoader fetches,
100
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">
159
- <div
160
- v-for="(entry, position) in visibleRows"
161
- :key="rowKey(entry.row, position)"
162
- class="dx-repeater-row"
101
+ <DXRepeaterCards
102
+ v-else
103
+ :visible-rows="visibleRows"
104
+ :sub-fields="subFields"
105
+ :form="form"
106
+ :field="field"
107
+ :min-items="minItems"
108
+ :max-items="maxItems"
109
+ :row-key="rowKey"
110
+ :row-path="rowPath"
111
+ :remove-row="removeRow"
112
+ :add-row="addRow"
163
113
  >
164
- <!--
165
- @slot Custom layout for a single repeater row, replacing the default sub-field stack.
166
- @binding {object} row The current row's data object.
167
- @binding {number} index The row's zero-based position among visible rows.
168
- @binding {FieldDefinition[]} fields The sub-field definitions for the row.
169
- @binding {function} remove Removes this row (respects `minItems`; soft-deletes instead of splicing when `field.softDeleteKey` is set and the row has an `id`).
170
- @binding {string} path The dot path into `form.data` for this row (e.g. `lines.0`).
171
- -->
172
- <slot
173
- v-if="$slots.row"
174
- name="row"
175
- :row="entry.row"
176
- :index="position"
177
- :fields="subFields"
178
- :remove="() => removeRow(entry.index)"
179
- :path="rowPath(entry.index)"
180
- />
181
-
182
- <!-- Default row: stack each sub-field -->
183
- <template v-else>
184
- <div class="dx-repeater-row-header">
185
- <span v-if="field.showRowIndex" class="dx-repeater-row-index">{{ position + 1 }}</span>
186
- <DButton
187
- variant="outline-danger"
188
- size="sm"
189
- :disabled="visibleRows.length <= minItems"
190
- @click="removeRow(entry.index)"
191
- >
192
- Remove
193
- </DButton>
194
- </div>
195
- <DXField
196
- v-for="subField in subFields"
197
- :key="subField.key"
198
- :field="subField"
199
- :form="form"
200
- :model="entry.row"
201
- :key-path="`${rowPath(entry.index)}.${subField.key}`"
202
- :error-key="`${rowPath(entry.index)}.${subField.key}`"
203
- />
114
+ <template v-if="$slots.row" #row="binds">
115
+ <slot name="row" v-bind="binds" />
204
116
  </template>
205
- </div>
206
-
207
- <DButton
208
- variant="outline-primary"
209
- size="sm"
210
- :disabled="maxItems !== undefined && visibleRows.length >= maxItems"
211
- @click="addRow"
212
- >
213
- {{ field.addLabel || "Add" }}
214
- </DButton>
117
+ </DXRepeaterCards>
215
118
  </div>
119
+
120
+ <!-- Cards layout (default, `repeaterLayout: "cards"` or unset): each row
121
+ is its own bordered card with sub-fields stacked vertically. The
122
+ overwhelmingly common case, rendered directly (no ResizeObserver, no
123
+ measurement wrapper) — only the `table` preference above pays for
124
+ those. Both branches render the same DXRepeaterCards (#135). -->
125
+ <DXRepeaterCards
126
+ v-else
127
+ :visible-rows="visibleRows"
128
+ :sub-fields="subFields"
129
+ :form="form"
130
+ :field="field"
131
+ :min-items="minItems"
132
+ :max-items="maxItems"
133
+ :row-key="rowKey"
134
+ :row-path="rowPath"
135
+ :remove-row="removeRow"
136
+ :add-row="addRow"
137
+ >
138
+ <template v-if="$slots.row" #row="binds">
139
+ <slot name="row" v-bind="binds" />
140
+ </template>
141
+ </DXRepeaterCards>
216
142
  </template>
217
143
 
218
144
  <script setup lang="ts">
219
145
  import { computed, onBeforeUnmount, ref, watch } from "vue";
220
146
  import DButton from "../base/DButton.vue";
221
147
  import DXField from "./DXField.vue";
148
+ import DXRepeaterCards from "./DXRepeaterCards.vue";
222
149
  import type { UseFormReturn } from "../../composables/useForm";
223
- import type { FieldDefinition, FieldType } from "../../types";
150
+ import type { FieldDefinition } from "../../types";
224
151
  import { getByPath, setByPath } from "../../utils/objectPath";
152
+ import { resolveFieldDefault } from "../../utils/formSchema";
225
153
 
226
154
  interface Props {
227
155
  /** Form instance owning the repeater array */
@@ -378,42 +306,15 @@ const rowKey = (row: any, fallbackPosition: number): string | number => {
378
306
  return fallbackPosition;
379
307
  };
380
308
 
381
- /** Default value for a freshly added row's sub-field. */
382
- function defaultForType(type: FieldType): any {
383
- switch (type) {
384
- case "checkbox":
385
- case "switch":
386
- return false;
387
- case "number":
388
- case "currency":
389
- case "percentage":
390
- return 0;
391
- case "repeater":
392
- return [];
393
- case "image":
394
- case "file":
395
- return null;
396
- default:
397
- return "";
398
- }
399
- }
400
-
401
- /** Deep-clone a sub-field default so object/array defaults aren't shared
402
- * (by reference) across rows. Uses JSON (not structuredClone) because the
403
- * default may be a Vue reactive proxy — which structuredClone can't clone —
404
- * and form defaults are JSON-serializable by construction. */
405
- function cloneDefault(value: any): any {
406
- if (value === null || typeof value !== "object") return value;
407
- return JSON.parse(JSON.stringify(value));
408
- }
409
-
309
+ /**
310
+ * A freshly-added row: each sub-field seeded by the shared `resolveFieldDefault`
311
+ * rule (explicit default deep-cloned when defined, else the type default), so
312
+ * repeater rows agree with every other seeding site (#134).
313
+ */
410
314
  function buildRow(): Record<string, any> {
411
315
  const row: Record<string, any> = {};
412
316
  for (const subField of subFields.value) {
413
- row[subField.key] =
414
- subField.default !== undefined
415
- ? cloneDefault(subField.default)
416
- : defaultForType(subField.type);
317
+ row[subField.key] = resolveFieldDefault(subField);
417
318
  }
418
319
  return row;
419
320
  }
@@ -467,32 +368,9 @@ function removeRow(index: number): void {
467
368
  </script>
468
369
 
469
370
  <style scoped>
470
- .dx-repeater-row {
471
- border: 1px solid var(--bs-border-color);
472
- border-radius: var(--bs-border-radius);
473
- padding: 1rem;
474
- margin-bottom: 0.75rem;
475
- }
476
-
477
- .dx-repeater-row-header {
478
- display: flex;
479
- align-items: center;
480
- margin-bottom: 0.75rem;
481
- }
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
-
491
- .dx-repeater-row-index {
492
- font-weight: 600;
493
- color: var(--bs-secondary-color);
494
- }
495
-
371
+ /* Cards-layout row styles (.dx-repeater-row*) live in DXRepeaterCards.vue with
372
+ the markup they target — a scoped rule can't reach another component (#135).
373
+ Only the table layout is rendered directly here, so only its styles remain. */
496
374
  .dx-repeater-table td {
497
375
  vertical-align: middle;
498
376
  }