@omnitend/dashboard-for-laravel 0.19.2 → 0.22.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/extended/DXDashboard.vue.d.ts +22 -6
- package/dist/components/extended/DXDashboardNavbar.vue.d.ts +16 -0
- package/dist/components/extended/DXFieldLabel.vue.d.ts +11 -1
- package/dist/components/extended/DXFormField.vue.d.ts +41 -0
- package/dist/components/extended/DXSwitch.vue.d.ts +30 -0
- package/dist/components/extended/dxFieldContext.d.ts +11 -0
- package/dist/dashboard-for-laravel.js +10913 -10827
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +8 -8
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/navigation.d.ts +13 -0
- package/dist/utils/api.d.ts +1 -1
- package/docs/public/api-reference.json +255 -103
- package/docs/public/docs-map.md +3 -2
- package/docs/public/llms.txt +3 -2
- package/package.json +9 -5
- package/resources/css/theme.scss +22 -3
- package/resources/js/components/extended/DXDashboard.vue +32 -10
- package/resources/js/components/extended/DXDashboardNavbar.vue +104 -26
- package/resources/js/components/extended/DXField.vue +66 -77
- package/resources/js/components/extended/DXFieldLabel.vue +18 -3
- package/resources/js/components/extended/DXForm.vue +31 -109
- package/resources/js/components/extended/DXFormField.vue +98 -0
- package/resources/js/components/extended/DXSwitch.vue +153 -0
- package/resources/js/components/extended/dxFieldContext.ts +14 -0
- package/resources/js/composables/useForm.ts +10 -3
- package/resources/js/index.ts +3 -0
- package/resources/js/types/index.ts +4 -1
- package/resources/js/types/navigation.ts +15 -0
- package/resources/js/utils/api.ts +19 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<template>
|
|
7
7
|
<span class="dx-field-label">
|
|
8
8
|
<span class="dx-field-label__text">{{ label }}</span>
|
|
9
|
-
<template v-if="info">
|
|
9
|
+
<template v-if="info || $slots.popover || popoverRender">
|
|
10
10
|
<button
|
|
11
11
|
:id="infoId"
|
|
12
12
|
type="button"
|
|
@@ -22,15 +22,24 @@
|
|
|
22
22
|
focus
|
|
23
23
|
placement="top"
|
|
24
24
|
>
|
|
25
|
-
|
|
25
|
+
<!--
|
|
26
|
+
@slot Rich popover body. Overrides the plain `info` text — use
|
|
27
|
+
it for lists, bold, paragraphs. Falls back to `{{ info }}`.
|
|
28
|
+
-->
|
|
29
|
+
<slot name="popover">
|
|
30
|
+
<!-- DXField forwards its `info-popover` slot here via inject. -->
|
|
31
|
+
<component v-if="popoverRender" :is="popoverRender" />
|
|
32
|
+
<template v-else>{{ info }}</template>
|
|
33
|
+
</slot>
|
|
26
34
|
</DPopover>
|
|
27
35
|
</template>
|
|
28
36
|
</span>
|
|
29
37
|
</template>
|
|
30
38
|
|
|
31
39
|
<script setup lang="ts">
|
|
32
|
-
import { useId } from "vue";
|
|
40
|
+
import { computed, inject, useId } from "vue";
|
|
33
41
|
import DPopover from "../base/DPopover.vue";
|
|
42
|
+
import { dxFieldInfoPopoverKey } from "./dxFieldContext";
|
|
34
43
|
|
|
35
44
|
interface Props {
|
|
36
45
|
/** Visible label text. */
|
|
@@ -42,6 +51,12 @@ interface Props {
|
|
|
42
51
|
|
|
43
52
|
defineProps<Props>();
|
|
44
53
|
|
|
54
|
+
// A rich popover body forwarded by a DXField ancestor (its `info-popover`
|
|
55
|
+
// slot), if any. The explicit `popover` slot still takes precedence for
|
|
56
|
+
// standalone use. Null when there's no DXField ancestor / no such slot.
|
|
57
|
+
const injectedPopover = inject(dxFieldInfoPopoverKey, null);
|
|
58
|
+
const popoverRender = computed(() => injectedPopover?.value ?? null);
|
|
59
|
+
|
|
45
60
|
// Stable, SSR-safe id so the popover can target the trigger button.
|
|
46
61
|
const infoId = `dx-field-info-${useId()}`;
|
|
47
62
|
</script>
|
|
@@ -65,52 +65,22 @@
|
|
|
65
65
|
-->
|
|
66
66
|
<slot :name="`tab-before(${tab.key})`" :tab="tab" :model="model" />
|
|
67
67
|
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<template v-
|
|
81
|
-
|
|
82
|
-
@slot Content inserted directly above a field, keyed by field key: `field-before(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
83
|
-
@binding {FieldDefinition} field The field about to be rendered.
|
|
84
|
-
@binding {object} model Live form data merged with `context`, for predicates.
|
|
85
|
-
-->
|
|
86
|
-
<slot :name="`field-before(${field.key})`" :field="field" :model="model" />
|
|
87
|
-
|
|
88
|
-
<DXField
|
|
89
|
-
:field="field"
|
|
90
|
-
:form="resolvedForm"
|
|
91
|
-
:model="model"
|
|
92
|
-
:layout="field.layout ?? layout"
|
|
93
|
-
:label-cols="field.labelCols ?? labelCols"
|
|
94
|
-
:hide-label="field.hideLabel"
|
|
95
|
-
>
|
|
96
|
-
<template
|
|
97
|
-
v-for="(slotName, target) in fieldSlotMap(field.key)"
|
|
98
|
-
:key="target"
|
|
99
|
-
#[target]="slotProps"
|
|
100
|
-
>
|
|
101
|
-
<!-- @slot Per-field overrides forwarded to DXField, keyed by field key: `value(<key>)`, `span(<key>)`, `info(<key>)`, `hint(<key>)`, `repeater-row(<key>)`. -->
|
|
102
|
-
<slot :name="slotName" v-bind="slotProps" />
|
|
103
|
-
</template>
|
|
104
|
-
</DXField>
|
|
105
|
-
|
|
106
|
-
<!--
|
|
107
|
-
@slot Content inserted directly below a field, keyed by field key: `field-after(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
108
|
-
@binding {FieldDefinition} field The field that was just rendered.
|
|
109
|
-
@binding {object} model Live form data merged with `context`, for predicates.
|
|
110
|
-
-->
|
|
111
|
-
<slot :name="`field-after(${field.key})`" :field="field" :model="model" />
|
|
68
|
+
<DXFormField
|
|
69
|
+
v-for="field in visibleFieldsFor(tab)"
|
|
70
|
+
:key="field.key"
|
|
71
|
+
:field="field"
|
|
72
|
+
:form="resolvedForm"
|
|
73
|
+
:model="model"
|
|
74
|
+
:layout="layout"
|
|
75
|
+
:label-cols="labelCols"
|
|
76
|
+
>
|
|
77
|
+
<!-- Forward every DXForm slot so the field can render
|
|
78
|
+
its keyed field(<key>)/field-before/field-after/
|
|
79
|
+
value/span/info/hint/repeater-row slots. -->
|
|
80
|
+
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
|
81
|
+
<slot :name="name" v-bind="slotProps" />
|
|
112
82
|
</template>
|
|
113
|
-
</
|
|
83
|
+
</DXFormField>
|
|
114
84
|
|
|
115
85
|
<!--
|
|
116
86
|
@slot Content inserted below a tab's fields, keyed by tab (slot name `tab-after(<tabKey>)`).
|
|
@@ -129,52 +99,22 @@
|
|
|
129
99
|
instead, above). -->
|
|
130
100
|
<template v-if="!hasTabs">
|
|
131
101
|
<component :is="card ? DCard : 'div'">
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<template v-
|
|
145
|
-
|
|
146
|
-
@slot Content inserted directly above a field, keyed by field key: `field-before(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
147
|
-
@binding {FieldDefinition} field The field about to be rendered.
|
|
148
|
-
@binding {object} model Live form data merged with `context`, for predicates.
|
|
149
|
-
-->
|
|
150
|
-
<slot :name="`field-before(${field.key})`" :field="field" :model="model" />
|
|
151
|
-
|
|
152
|
-
<DXField
|
|
153
|
-
:field="field"
|
|
154
|
-
:form="resolvedForm"
|
|
155
|
-
:model="model"
|
|
156
|
-
:layout="field.layout ?? layout"
|
|
157
|
-
:label-cols="field.labelCols ?? labelCols"
|
|
158
|
-
:hide-label="field.hideLabel"
|
|
159
|
-
>
|
|
160
|
-
<template
|
|
161
|
-
v-for="(slotName, target) in fieldSlotMap(field.key)"
|
|
162
|
-
:key="target"
|
|
163
|
-
#[target]="slotProps"
|
|
164
|
-
>
|
|
165
|
-
<!-- @slot Per-field overrides forwarded to DXField, keyed by field key: `value(<key>)`, `span(<key>)`, `info(<key>)`, `hint(<key>)`, `repeater-row(<key>)`. -->
|
|
166
|
-
<slot :name="slotName" v-bind="slotProps" />
|
|
167
|
-
</template>
|
|
168
|
-
</DXField>
|
|
169
|
-
|
|
170
|
-
<!--
|
|
171
|
-
@slot Content inserted directly below a field, keyed by field key: `field-after(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
172
|
-
@binding {FieldDefinition} field The field that was just rendered.
|
|
173
|
-
@binding {object} model Live form data merged with `context`, for predicates.
|
|
174
|
-
-->
|
|
175
|
-
<slot :name="`field-after(${field.key})`" :field="field" :model="model" />
|
|
102
|
+
<DXFormField
|
|
103
|
+
v-for="field in visibleFlatFields"
|
|
104
|
+
:key="field.key"
|
|
105
|
+
:field="field"
|
|
106
|
+
:form="resolvedForm"
|
|
107
|
+
:model="model"
|
|
108
|
+
:layout="layout"
|
|
109
|
+
:label-cols="labelCols"
|
|
110
|
+
>
|
|
111
|
+
<!-- Forward every DXForm slot so the field can render its
|
|
112
|
+
keyed field(<key>)/field-before/field-after/value/span/
|
|
113
|
+
info/hint/repeater-row slots. -->
|
|
114
|
+
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
|
115
|
+
<slot :name="name" v-bind="slotProps" />
|
|
176
116
|
</template>
|
|
177
|
-
</
|
|
117
|
+
</DXFormField>
|
|
178
118
|
|
|
179
119
|
<!-- Submit button -->
|
|
180
120
|
<DButton
|
|
@@ -233,7 +173,7 @@ import DButton from "../base/DButton.vue";
|
|
|
233
173
|
import DCard from "../base/DCard.vue";
|
|
234
174
|
import DTabs from "../base/DTabs.vue";
|
|
235
175
|
import DTab from "../base/DTab.vue";
|
|
236
|
-
import
|
|
176
|
+
import DXFormField from "./DXFormField.vue";
|
|
237
177
|
import type { UseFormReturn } from "../../composables/useForm";
|
|
238
178
|
import type { DefineFormReturn } from "../../composables/defineForm";
|
|
239
179
|
import type { FieldDefinition, FormTab, LabelCols, MaybeFn } from "../../types";
|
|
@@ -408,24 +348,6 @@ const visibleFlatFields = computed<FieldDefinition[]>(() =>
|
|
|
408
348
|
resolvedFields.value.filter(isFieldVisible),
|
|
409
349
|
);
|
|
410
350
|
|
|
411
|
-
// ————————————————— per-field slot forwarding
|
|
412
|
-
|
|
413
|
-
/** Map a DXField slot name to the parent's keyed slot, when present. */
|
|
414
|
-
function fieldSlotMap(key: string): Record<string, string> {
|
|
415
|
-
const map: Record<string, string> = {};
|
|
416
|
-
const candidates: Array<[string, string]> = [
|
|
417
|
-
["value", `value(${key})`],
|
|
418
|
-
["span", `span(${key})`],
|
|
419
|
-
["info", `info(${key})`],
|
|
420
|
-
["hint", `hint(${key})`],
|
|
421
|
-
["repeater-row", `repeater-row(${key})`],
|
|
422
|
-
];
|
|
423
|
-
for (const [target, source] of candidates) {
|
|
424
|
-
if (slots[source]) map[target] = source;
|
|
425
|
-
}
|
|
426
|
-
return map;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
351
|
// ————————————————— auto-switch to the first error tab
|
|
430
352
|
|
|
431
353
|
/** Keys that currently carry at least one validation error. */
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
DXFormField — internal per-field renderer for DXForm.
|
|
4
|
+
|
|
5
|
+
Owns the single per-field render block that DXForm's tabbed and flat layouts
|
|
6
|
+
both need: the `field(<key>)` full-replacement slot, the
|
|
7
|
+
`field-before(<key>)`/`field-after(<key>)` wrapper slots, and the `<DXField>`
|
|
8
|
+
element with its `value`/`span`/`info`/`hint`/`repeater-row` slot forwarding.
|
|
9
|
+
|
|
10
|
+
Extracted so those per-field props/slots are wired in ONE place — before this,
|
|
11
|
+
DXForm carried two near-identical copies (tabbed + flat) and a new per-field
|
|
12
|
+
prop had to be added to both, which was easy to miss (see #78/#83). DXForm
|
|
13
|
+
forwards all of its slots into each instance; this component only reads the
|
|
14
|
+
ones matching its field's key.
|
|
15
|
+
|
|
16
|
+
Internal: not exported from the package index — consumers use DXForm.
|
|
17
|
+
-->
|
|
18
|
+
<template>
|
|
19
|
+
<!-- Full replacement slot bypasses DXField entirely (mirrors tab-content):
|
|
20
|
+
also supersedes field-before/field-after for the same key. -->
|
|
21
|
+
<slot
|
|
22
|
+
v-if="$slots[`field(${field.key})`]"
|
|
23
|
+
:name="`field(${field.key})`"
|
|
24
|
+
:field="field"
|
|
25
|
+
:model="model"
|
|
26
|
+
/>
|
|
27
|
+
<template v-else>
|
|
28
|
+
<!-- Content inserted directly above the field. -->
|
|
29
|
+
<slot :name="`field-before(${field.key})`" :field="field" :model="model" />
|
|
30
|
+
|
|
31
|
+
<DXField
|
|
32
|
+
:field="field"
|
|
33
|
+
:form="form"
|
|
34
|
+
:model="model"
|
|
35
|
+
:layout="field.layout ?? layout"
|
|
36
|
+
:label-cols="field.labelCols ?? labelCols"
|
|
37
|
+
:hide-label="field.hideLabel"
|
|
38
|
+
>
|
|
39
|
+
<template
|
|
40
|
+
v-for="(slotName, target) in fieldSlotMap()"
|
|
41
|
+
:key="target"
|
|
42
|
+
#[target]="slotProps"
|
|
43
|
+
>
|
|
44
|
+
<!-- Per-field overrides forwarded to DXField: value/span/info/hint/repeater-row. -->
|
|
45
|
+
<slot :name="slotName" v-bind="slotProps" />
|
|
46
|
+
</template>
|
|
47
|
+
</DXField>
|
|
48
|
+
|
|
49
|
+
<!-- Content inserted directly below the field. -->
|
|
50
|
+
<slot :name="`field-after(${field.key})`" :field="field" :model="model" />
|
|
51
|
+
</template>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup lang="ts">
|
|
55
|
+
import { useSlots } from "vue";
|
|
56
|
+
import DXField from "./DXField.vue";
|
|
57
|
+
import type { UseFormReturn } from "../../composables/useForm";
|
|
58
|
+
import type { FieldDefinition, LabelCols } from "../../types";
|
|
59
|
+
|
|
60
|
+
interface Props {
|
|
61
|
+
/** The field to render. */
|
|
62
|
+
field: FieldDefinition;
|
|
63
|
+
/** The resolved form instance (state, errors, submit helpers). */
|
|
64
|
+
form: UseFormReturn<any>;
|
|
65
|
+
/** Live form data merged with context, for slot bindings/predicates. */
|
|
66
|
+
model: Record<string, any>;
|
|
67
|
+
/** Form-level layout; overridden per-field by `field.layout`. */
|
|
68
|
+
layout?: "vertical" | "horizontal";
|
|
69
|
+
/** Form-level label column width; overridden per-field by `field.labelCols`. */
|
|
70
|
+
labelCols?: LabelCols;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const props = defineProps<Props>();
|
|
74
|
+
const slots = useSlots();
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Map a DXField slot name to this field's keyed parent slot, when present.
|
|
78
|
+
* A plain function (re-read each render) rather than a computed, so a slot the
|
|
79
|
+
* consumer adds after mount is still picked up — matching DXForm's original
|
|
80
|
+
* per-render behaviour before this component was extracted.
|
|
81
|
+
*/
|
|
82
|
+
function fieldSlotMap(): Record<string, string> {
|
|
83
|
+
const key = props.field.key;
|
|
84
|
+
const map: Record<string, string> = {};
|
|
85
|
+
const candidates: Array<[string, string]> = [
|
|
86
|
+
["value", `value(${key})`],
|
|
87
|
+
["span", `span(${key})`],
|
|
88
|
+
["info", `info(${key})`],
|
|
89
|
+
["info-popover", `info-popover(${key})`],
|
|
90
|
+
["hint", `hint(${key})`],
|
|
91
|
+
["repeater-row", `repeater-row(${key})`],
|
|
92
|
+
];
|
|
93
|
+
for (const [target, source] of candidates) {
|
|
94
|
+
if (slots[source]) map[target] = source;
|
|
95
|
+
}
|
|
96
|
+
return map;
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
DXSwitch — a "filled box" toggle switch.
|
|
4
|
+
|
|
5
|
+
An opinionated switch built on `DFormCheckbox` (`switch`): a bordered box sized
|
|
6
|
+
like a `.form-control` with the label on the left and the toggle on the right,
|
|
7
|
+
colour-coding its state (a neutral box when off, a filled primary box when on).
|
|
8
|
+
Used standalone or, internally, by `DXField`'s `switch` field type so a form
|
|
9
|
+
switch and a lone switch share one look.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
```vue
|
|
13
|
+
<DXSwitch v-model="isCurrent" label="Product is current" />
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For a form field, prefer a `DXField`/`DXForm` `type: 'switch'`; reach for
|
|
17
|
+
`DXSwitch` directly when you want the styled toggle outside a form. Pass a
|
|
18
|
+
`label` prop for simple text, or the default slot for richer label content.
|
|
19
|
+
-->
|
|
20
|
+
<template>
|
|
21
|
+
<div class="dx-switch" :class="{ 'dx-switch--on': isOn }">
|
|
22
|
+
<DFormCheckbox v-model="model" switch v-bind="$attrs">
|
|
23
|
+
<!--
|
|
24
|
+
@slot Label content shown beside the toggle. Falls back to the contextual
|
|
25
|
+
text / `label` prop. Scoped with the current on-state so a caller can vary
|
|
26
|
+
rich content by state; DXField passes a richer label element here.
|
|
27
|
+
@binding {boolean} on Whether the switch is currently on.
|
|
28
|
+
-->
|
|
29
|
+
<slot :on="isOn"><span>{{ displayText }}</span></slot>
|
|
30
|
+
</DFormCheckbox>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import { computed } from "vue";
|
|
36
|
+
import DFormCheckbox from "../base/DFormCheckbox.vue";
|
|
37
|
+
|
|
38
|
+
interface Props {
|
|
39
|
+
/** Whether the switch is on. */
|
|
40
|
+
modelValue?: boolean;
|
|
41
|
+
/** Static text beside the toggle; the fallback when no contextual text applies. */
|
|
42
|
+
label?: string;
|
|
43
|
+
/** Text shown when on — overrides `label` in the on state (e.g. "Product is current"). */
|
|
44
|
+
textWhenTrue?: string;
|
|
45
|
+
/** Text shown when off — overrides `label` in the off state (e.g. "Product is not current"). */
|
|
46
|
+
textWhenFalse?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
50
|
+
modelValue: false,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const emit = defineEmits<{
|
|
54
|
+
"update:modelValue": [value: boolean];
|
|
55
|
+
}>();
|
|
56
|
+
|
|
57
|
+
const isOn = computed(() => props.modelValue);
|
|
58
|
+
|
|
59
|
+
// A switch (unlike a checkbox) can say what each state *means*: prefer the
|
|
60
|
+
// state-specific text, falling back to the static `label`. Ignored when the
|
|
61
|
+
// default slot supplies its own content.
|
|
62
|
+
const displayText = computed(() => {
|
|
63
|
+
const contextual = isOn.value ? props.textWhenTrue : props.textWhenFalse;
|
|
64
|
+
return contextual ?? props.label ?? "";
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Proxy the v-model so the underlying bvn checkbox's update re-emits as this
|
|
68
|
+
// component's `update:modelValue`. Expects a real boolean — callers that hold a
|
|
69
|
+
// truthy non-boolean (e.g. a Laravel-serialised `1`) should normalise first;
|
|
70
|
+
// DXField does this before binding.
|
|
71
|
+
const model = computed({
|
|
72
|
+
get: () => props.modelValue,
|
|
73
|
+
set: (value: boolean) => emit("update:modelValue", value),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
defineOptions({
|
|
77
|
+
inheritAttrs: false,
|
|
78
|
+
});
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<style scoped>
|
|
82
|
+
/* A compact "filled box" toggle, sized to its content — it doesn't stretch to
|
|
83
|
+
fill the form row — but matching standard input height so it lines up with
|
|
84
|
+
sibling text/select fields. Colour-codes its state: a neutral box when off, a
|
|
85
|
+
filled primary box when on. The label sits on the left and the toggle on the
|
|
86
|
+
right. Renders the underlying `.form-check` (from the switch checkbox) as the
|
|
87
|
+
box. `justify-content: space-between` on a full-width box used to leave a wide
|
|
88
|
+
unclickable gap between the label and the toggle (only the tiny toggle itself
|
|
89
|
+
was a real click target); shrinking the box to fit its content removes that
|
|
90
|
+
dead zone entirely. Uses block-level `flex` (not `inline-flex`) so the box
|
|
91
|
+
stays shrink-to-fit yet still takes its own line: an inline-level box let a
|
|
92
|
+
following `<small class="form-text">` hint flow inline beside it instead of
|
|
93
|
+
dropping below the control like every other field type (#79).
|
|
94
|
+
|
|
95
|
+
Scoped `:deep()` anchored on the plain `.dx-switch` root, so the scope-id
|
|
96
|
+
reliably reaches the bvn-rendered `.form-check` (see the scoped-deep-styles
|
|
97
|
+
guard test and CLAUDE.md). */
|
|
98
|
+
.dx-switch :deep(.form-check) {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: 0.75rem;
|
|
102
|
+
width: fit-content;
|
|
103
|
+
max-width: 100%; /* wrap/ellipsize rather than overflow a narrow field */
|
|
104
|
+
min-height: var(--dx-input-height, calc(1.5em + 0.75rem + 2px)); /* match .form-control height */
|
|
105
|
+
margin: 0;
|
|
106
|
+
/* Override Bootstrap `.form-switch`'s 2.5em left padding for the floated input. */
|
|
107
|
+
padding: 0.375rem 0.75rem;
|
|
108
|
+
border: 1px solid var(--bs-border-color);
|
|
109
|
+
border-radius: var(--bs-border-radius);
|
|
110
|
+
background-color: var(--bs-body-bg);
|
|
111
|
+
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Label on the left, toggle on the right (DOM order is input then label). */
|
|
115
|
+
.dx-switch :deep(.form-check-label) {
|
|
116
|
+
order: 1;
|
|
117
|
+
margin: 0;
|
|
118
|
+
color: var(--bs-secondary-color);
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
transition: color 0.15s ease-in-out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.dx-switch :deep(.form-check-input) {
|
|
124
|
+
order: 2;
|
|
125
|
+
flex-shrink: 0;
|
|
126
|
+
/* Override `.form-switch`'s negative left margin now that we're flex-laid-out. */
|
|
127
|
+
margin: 0;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* On-state: filled primary. Green is reserved for success semantics, so a
|
|
132
|
+
generic "on" toggle uses the brand action colour, matching the base switch,
|
|
133
|
+
checkboxes and radios. */
|
|
134
|
+
.dx-switch--on :deep(.form-check) {
|
|
135
|
+
background-color: var(--bs-primary-bg-subtle);
|
|
136
|
+
border-color: var(--bs-primary);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.dx-switch--on :deep(.form-check-label) {
|
|
140
|
+
color: var(--bs-primary-text-emphasis);
|
|
141
|
+
font-weight: 500;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.dx-switch--on :deep(.form-check-input:checked) {
|
|
145
|
+
background-color: var(--bs-primary);
|
|
146
|
+
border-color: var(--bs-primary);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.dx-switch--on :deep(.form-check-input:focus) {
|
|
150
|
+
border-color: var(--bs-primary);
|
|
151
|
+
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lets `DXField` forward its `info-popover` slot to the `DXFieldLabel` it
|
|
5
|
+
* renders — at any of its several label sites — without repeating the forward
|
|
6
|
+
* at each one. The provided value is a render function for the popover body, or
|
|
7
|
+
* `null` when the field has no `info-popover` slot.
|
|
8
|
+
*
|
|
9
|
+
* Optional: a `DXFieldLabel` used standalone (no `DXField` ancestor) injects the
|
|
10
|
+
* default `null` and falls back to its own `popover` slot / `info` text.
|
|
11
|
+
*/
|
|
12
|
+
export const dxFieldInfoPopoverKey: InjectionKey<
|
|
13
|
+
ComputedRef<(() => unknown) | null>
|
|
14
|
+
> = Symbol("dxFieldInfoPopover");
|
|
@@ -293,9 +293,16 @@ export function useForm<TData extends Record<string, any>>(
|
|
|
293
293
|
sendPayload as Record<string, unknown>,
|
|
294
294
|
{ signal: options?.signal },
|
|
295
295
|
)
|
|
296
|
-
:
|
|
297
|
-
|
|
298
|
-
|
|
296
|
+
: sendMethod === "delete"
|
|
297
|
+
// A record is deleted by its URL — don't submit the form
|
|
298
|
+
// fields as a request body. Passing the payload here would
|
|
299
|
+
// send the whole model on every delete.
|
|
300
|
+
? await api.delete<TResponse>(url, undefined, {
|
|
301
|
+
signal: options?.signal,
|
|
302
|
+
})
|
|
303
|
+
: await api[sendMethod]<TResponse>(url, sendPayload, {
|
|
304
|
+
signal: options?.signal,
|
|
305
|
+
});
|
|
299
306
|
|
|
300
307
|
state.recentlySuccessful = true;
|
|
301
308
|
setTimeout(() => (state.recentlySuccessful = false), 1500);
|
package/resources/js/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ 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
11
|
export { default as DXFieldLabel } from "./components/extended/DXFieldLabel.vue";
|
|
12
|
+
export { default as DXSwitch } from "./components/extended/DXSwitch.vue";
|
|
12
13
|
export { default as DXStatCard } from "./components/extended/DXStatCard.vue";
|
|
13
14
|
export { default as DXBarChart } from "./components/charts/DXBarChart.vue";
|
|
14
15
|
export { default as DXLineChart } from "./components/charts/DXLineChart.vue";
|
|
@@ -138,4 +139,6 @@ export type {
|
|
|
138
139
|
NavigationItem,
|
|
139
140
|
NavigationGroup,
|
|
140
141
|
Navigation,
|
|
142
|
+
NavbarSearchAlign,
|
|
143
|
+
NavbarActionsOnMobile,
|
|
141
144
|
} from "./types/navigation";
|
|
@@ -165,7 +165,10 @@ export interface FieldDefinition {
|
|
|
165
165
|
* Longer help text revealed in a popover from a small info affordance
|
|
166
166
|
* on the field's label (on hover/focus). Complements `hint` (which is
|
|
167
167
|
* always-visible muted text below the control). May be a function of
|
|
168
|
-
* the model.
|
|
168
|
+
* the model. Plain text only — for a rich popover body (lists, bold,
|
|
169
|
+
* paragraphs) use the `#info-popover(<key>)` slot. Note the separate
|
|
170
|
+
* `#info(<key>)` slot renders an always-visible block below the control,
|
|
171
|
+
* not popover content.
|
|
169
172
|
*/
|
|
170
173
|
info?: MaybeFn<string>;
|
|
171
174
|
|
|
@@ -30,3 +30,18 @@ export interface NavigationGroup {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export type Navigation = NavigationGroup[];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Horizontal alignment of the navbar search slot content within its region:
|
|
36
|
+
* `"start"` = flush left (after the title from `md` up), `"center"` = centred.
|
|
37
|
+
* Shared by DXDashboard (which forwards it) and DXDashboardNavbar.
|
|
38
|
+
*/
|
|
39
|
+
export type NavbarSearchAlign = "start" | "center";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What the navbar actions slot does below the `md` breakpoint: `"wrap"` moves
|
|
43
|
+
* it to its own full-width row (the bar grows), `"hide"` removes it entirely
|
|
44
|
+
* (for apps that relocate page actions into the page on phones).
|
|
45
|
+
* Shared by DXDashboard (which forwards it) and DXDashboardNavbar.
|
|
46
|
+
*/
|
|
47
|
+
export type NavbarActionsOnMobile = "wrap" | "hide";
|
|
@@ -173,9 +173,26 @@ class ApiClient {
|
|
|
173
173
|
|
|
174
174
|
async delete<T = unknown>(
|
|
175
175
|
url: string,
|
|
176
|
-
|
|
176
|
+
data: any = undefined,
|
|
177
|
+
options: Omit<RequestOptions, "method" | "body"> = {},
|
|
177
178
|
) {
|
|
178
|
-
|
|
179
|
+
// Consistent (url, data, options) signature with post/put/patch so the
|
|
180
|
+
// third argument is always the request options (e.g. `{ signal }`) — it
|
|
181
|
+
// used to be the second, so `useForm.submit`'s uniform call spread the
|
|
182
|
+
// form payload into the fetch config and dropped the abort signal.
|
|
183
|
+
// DELETE bodies are legal but unusual: only send one when data is given
|
|
184
|
+
// (a record is deleted by its URL — `useForm.delete()` sends none).
|
|
185
|
+
const body =
|
|
186
|
+
data === undefined || data === null
|
|
187
|
+
? undefined
|
|
188
|
+
: data instanceof FormData
|
|
189
|
+
? data
|
|
190
|
+
: JSON.stringify(data);
|
|
191
|
+
return this.request<T>(url, {
|
|
192
|
+
...options,
|
|
193
|
+
method: "DELETE",
|
|
194
|
+
...(body !== undefined ? { body } : {}),
|
|
195
|
+
});
|
|
179
196
|
}
|
|
180
197
|
}
|
|
181
198
|
|