@omnitend/dashboard-for-laravel 0.19.2 → 0.21.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.
@@ -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
- : await api[sendMethod]<TResponse>(url, sendPayload, {
297
- signal: options?.signal,
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);
@@ -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";
@@ -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. For rich content, use the `#info` slot instead.
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
 
@@ -173,9 +173,26 @@ class ApiClient {
173
173
 
174
174
  async delete<T = unknown>(
175
175
  url: string,
176
- options: Omit<RequestOptions, "method"> = {},
176
+ data: any = undefined,
177
+ options: Omit<RequestOptions, "method" | "body"> = {},
177
178
  ) {
178
- return this.request<T>(url, { ...options, method: "DELETE" });
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