@omnitend/dashboard-for-laravel 0.18.0 → 0.19.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.
@@ -157,6 +157,16 @@ export interface FieldDefinition {
157
157
  * layout. Falls back to the form-level `labelCols` when omitted.
158
158
  */
159
159
  labelCols?: LabelCols;
160
+ /**
161
+ * Skip rendering the field's own label. In horizontal layout no label
162
+ * column is reserved either, so the control spans the full width. Useful
163
+ * for `switch` (and `checkbox`) fields, which render their own label
164
+ * inside the control — the outer row label would otherwise duplicate it,
165
+ * and the self-label then lines up with sibling row labels. Also used
166
+ * internally by `DXRepeater`'s `table` layout, where a column header
167
+ * already names the field.
168
+ */
169
+ hideLabel?: boolean;
160
170
  /**
161
171
  * Component rendered for `type: "component"` fields. Receives
162
172
  * `modelValue`, `field`, `model` props and emits `update:modelValue`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",
@@ -79,13 +79,18 @@ $btn-padding-x-lg: 1.5rem; // 24px
79
79
  // Forms
80
80
  // ----------------------------------------------------------------------------
81
81
 
82
- $input-padding-y: 0.625rem;
82
+ $input-padding-y: 0.375rem; // compact fields (Bootstrap's default; 0.625rem read oversized)
83
83
  $input-padding-x: 1rem;
84
84
  $input-border-radius: $border-radius;
85
85
  $input-border-color: #d1d5db; // Gray-300
86
86
  $input-focus-border-color: $primary;
87
87
  $input-focus-box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
88
88
 
89
+ // Check/radio inputs default their border to `$border-color` (Gray-200), not
90
+ // `$input-border-color` — so a bare checkbox looks faint beside the (darker,
91
+ // Gray-300) text inputs. Share the input border colour across all controls.
92
+ $form-check-input-border: 1px solid $input-border-color;
93
+
89
94
  // ----------------------------------------------------------------------------
90
95
  // Cards
91
96
  // ----------------------------------------------------------------------------
@@ -188,6 +193,36 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
188
193
  );
189
194
  }
190
195
 
196
+ // Bigger check/radio boxes — Bootstrap's 1em default reads small next to the
197
+ // theme's inputs and is a fiddly touch target.
198
+ .form-check-input {
199
+ width: 1.25rem;
200
+ height: 1.25rem;
201
+ }
202
+
203
+ // Horizontal ("label-left") forms: a compact settings-form rhythm, with the
204
+ // label + control top-aligned so a checkbox/switch sits on the label's first
205
+ // line rather than floating above it (the label column carries a top offset to
206
+ // line up with text inputs; the checkbox is given the same offset). Vertical
207
+ // forms are unaffected.
208
+ .dx-form--horizontal {
209
+ .row {
210
+ align-items: flex-start;
211
+ }
212
+
213
+ .col-form-label {
214
+ padding-top: 0.4rem;
215
+ }
216
+
217
+ .form-check {
218
+ margin-top: 0.4rem;
219
+ }
220
+
221
+ .mb-3 {
222
+ margin-bottom: 0.5rem;
223
+ }
224
+ }
225
+
191
226
  // Bootstrap's `.input-group` defaults to `flex-wrap: wrap`, letting a
192
227
  // narrow container (e.g. a compact table cell, a small screen) stack the
193
228
  // prepend/append affix onto its own line below the input. That breaks the
@@ -243,6 +278,10 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
243
278
 
244
279
  .dashboard-navbar {
245
280
  height: $dashboard-navbar-height;
281
+ // A light grey top bar over the white content panel (higher contrast than a
282
+ // grey-everywhere layout). The navbar markup drops its `bg-white` class so
283
+ // this wins without `!important`.
284
+ background-color: #f8f9fa;
246
285
  }
247
286
 
248
287
  // ----------------------------------------------------------------------------
@@ -247,5 +247,8 @@ const toggleSidebar = () => {
247
247
  .dashboard-main {
248
248
  flex: 1;
249
249
  max-width: 100%;
250
+ /* A white content panel under the light grey top bar / layout, so forms and
251
+ content sit on white (higher contrast than a grey-everywhere dashboard). */
252
+ background-color: #fff;
250
253
  }
251
254
  </style>
@@ -5,7 +5,7 @@
5
5
  `DXDashboard`, which forwards these slots with a `navbar-` prefix.
6
6
  -->
7
7
  <template>
8
- <header class="dashboard-navbar bg-white border-bottom">
8
+ <header class="dashboard-navbar border-bottom">
9
9
  <DContainer fluid>
10
10
  <!-- Flex bar that wraps: below `md` the search drops to its own
11
11
  full-width row beneath the toggle/title/user-menu row. -->
@@ -35,8 +35,20 @@
35
35
  :class="field.class || 'mb-3'"
36
36
  v-bind="horizontalAttrs"
37
37
  >
38
- <template #label>
38
+ <!-- The row label (left column) names the control; the checkbox's own
39
+ label is kept for screen readers but hidden visually so it doesn't
40
+ duplicate the row label. In horizontal layout the hint sits beneath
41
+ the label here rather than below the control. -->
42
+ <template v-if="!hideLabel" #label>
39
43
  <DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
44
+ <small
45
+ v-if="isHorizontal && (resolvedHint || $slots.hint)"
46
+ class="form-text text-muted d-block dx-field-hint"
47
+ >
48
+ <slot name="hint" :field="field" :model="model">{{
49
+ resolvedHint
50
+ }}</slot>
51
+ </small>
40
52
  </template>
41
53
  <slot
42
54
  v-if="$slots.value"
@@ -52,14 +64,14 @@
52
64
  :disabled="isDisabled || isReadonly"
53
65
  v-bind="field.inputProps"
54
66
  >
55
- <DXFieldLabel :label="resolvedLabel" />
67
+ <DXFieldLabel :label="resolvedLabel" class="visually-hidden" />
56
68
  </DFormCheckbox>
57
69
 
58
70
  <DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
59
71
  {{ form.getError(errorKey) }}
60
72
  </DFormInvalidFeedback>
61
73
  <slot name="info" :field="field" :model="model" />
62
- <DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
74
+ <DFormText v-if="!hintInLabel && (resolvedHint || $slots.hint)" class="text-muted">
63
75
  <slot name="hint" :field="field" :model="model">{{ resolvedHint }}</slot>
64
76
  </DFormText>
65
77
  <DFormText v-if="field.help">{{ field.help }}</DFormText>
@@ -119,8 +131,22 @@
119
131
  :class="[field.class || 'mb-3', 'dx-switch', { 'dx-switch--on': switchIsOn }]"
120
132
  v-bind="horizontalAttrs"
121
133
  >
122
- <template #label>
134
+ <!-- The row label (left column) names the control. The switch's own
135
+ inner text is shown only when the field opts into contextual on/off
136
+ text (e.g. "Product is current"); otherwise it falls back to the row
137
+ label and is hidden visually (kept for screen readers). In horizontal
138
+ layout the hint sits beneath the label here rather than below the
139
+ control. -->
140
+ <template v-if="!hideLabel" #label>
123
141
  <DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
142
+ <small
143
+ v-if="isHorizontal && (resolvedHint || $slots.hint)"
144
+ class="form-text text-muted d-block dx-field-hint"
145
+ >
146
+ <slot name="hint" :field="field" :model="model">{{
147
+ resolvedHint
148
+ }}</slot>
149
+ </small>
124
150
  </template>
125
151
  <slot
126
152
  v-if="$slots.value"
@@ -137,14 +163,17 @@
137
163
  :disabled="isDisabled || isReadonly"
138
164
  v-bind="field.inputProps"
139
165
  >
140
- <DXFieldLabel :label="switchText" />
166
+ <DXFieldLabel
167
+ :label="switchText"
168
+ :class="switchHasContextualText ? undefined : 'visually-hidden'"
169
+ />
141
170
  </DFormCheckbox>
142
171
 
143
172
  <DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
144
173
  {{ form.getError(errorKey) }}
145
174
  </DFormInvalidFeedback>
146
175
  <slot name="info" :field="field" :model="model" />
147
- <DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
176
+ <DFormText v-if="!hintInLabel && (resolvedHint || $slots.hint)" class="text-muted">
148
177
  <slot name="hint" :field="field" :model="model">{{ resolvedHint }}</slot>
149
178
  </DFormText>
150
179
  <DFormText v-if="field.help">{{ field.help }}</DFormText>
@@ -203,7 +232,7 @@
203
232
  <!-- Repeater: nested, repeatable sub-form -->
204
233
  <div v-else-if="field.type === 'repeater'" :class="field.class || 'mb-3'">
205
234
  <DFormGroup v-bind="repeaterHorizontalAttrs">
206
- <template #label>
235
+ <template v-if="!hideLabel" #label>
207
236
  <DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
208
237
  </template>
209
238
  <DXRepeater
@@ -240,9 +269,19 @@
240
269
  <DFormGroup v-else :class="field.class || 'mb-3'" v-bind="horizontalAttrs">
241
270
  <!-- Label with optional info popover. Omitted entirely (not just
242
271
  emptied) when hideLabel is set, so BFormGroup doesn't reserve a
243
- label column/row for it (see DXRepeater's table layout). -->
272
+ label column/row for it (see DXRepeater's table layout). In
273
+ horizontal layout the hint sits beneath the label here rather than
274
+ below the control. -->
244
275
  <template v-if="!hideLabel" #label>
245
276
  <DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
277
+ <small
278
+ v-if="isHorizontal && (resolvedHint || $slots.hint)"
279
+ class="form-text text-muted d-block dx-field-hint"
280
+ >
281
+ <slot name="hint" :field="field" :model="model">{{
282
+ resolvedHint
283
+ }}</slot>
284
+ </small>
246
285
  </template>
247
286
 
248
287
  <!-- Custom value slot overrides the built-in control -->
@@ -429,8 +468,9 @@
429
468
  -->
430
469
  <slot name="info" :field="field" :model="model" />
431
470
 
432
- <!-- Hint (dynamic, slot-overridable) -->
433
- <DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
471
+ <!-- Hint (dynamic, slot-overridable). In horizontal layout it renders in
472
+ the label column instead (see the #label slot above). -->
473
+ <DFormText v-if="!hintInLabel && (resolvedHint || $slots.hint)" class="text-muted">
434
474
  <!--
435
475
  @slot Overrides the field's hint text shown below the control.
436
476
  @binding {FieldDefinition} field The field definition being rendered.
@@ -503,10 +543,14 @@ interface Props {
503
543
  labelCols?: LabelCols;
504
544
 
505
545
  /**
506
- * Skip rendering the field's own label entirely (only affects the
507
- * standard labelled-field branch). Used by DXRepeater's `table` layout,
508
- * where a column header already names the field and a per-row label
509
- * would waste space in the cell.
546
+ * Skip rendering the field's own (outer) label. Applies to the standard
547
+ * labelled-field branch and to the checkbox/switch/repeater branches. In
548
+ * horizontal layout no label column is reserved either, so the control
549
+ * spans the full width (a switch/checkbox self-label then lines up with
550
+ * sibling row labels). Used by DXRepeater's `table` layout (a column
551
+ * header already names the field) and by consumers hiding the redundant
552
+ * row label on a switch/checkbox, which render their own label inside the
553
+ * control (#78).
510
554
  */
511
555
  hideLabel?: boolean;
512
556
  }
@@ -545,9 +589,41 @@ function labelColsAttrs(cols: LabelCols | undefined): Record<string, any> {
545
589
  }
546
590
 
547
591
  // Default to a 3-column label when horizontal but no width was configured,
548
- // so `layout: "horizontal"` alone is enough to see the effect.
592
+ // so `layout: "horizontal"` alone is enough to see the effect. When the
593
+ // field's label is hidden, reserve no label column at all — BFormGroup would
594
+ // otherwise still emit an empty label column (it goes horizontal whenever any
595
+ // labelCols are set, regardless of whether a label exists), leaving the
596
+ // control indented in the input column beside dead space. Dropping the column
597
+ // lets the control span full width, so a switch/checkbox self-label lines up
598
+ // with sibling row labels (#78).
549
599
  const horizontalAttrs = computed<Record<string, any>>(() =>
550
- isHorizontal.value ? labelColsAttrs(props.labelCols ?? 3) : {},
600
+ isHorizontal.value && !props.hideLabel
601
+ ? {
602
+ ...labelColsAttrs(props.labelCols ?? 3),
603
+ // Right-align the label column so the label (and the hint that
604
+ // sits beneath it in horizontal layout) line up against the
605
+ // control column, like a classic label-left settings form.
606
+ labelClass: "text-sm-end dx-field-label-col",
607
+ }
608
+ : {},
609
+ );
610
+
611
+ // In horizontal layout the hint moves up into the label column (beneath the
612
+ // label), rather than below the control — so a row reads "label + hint | control".
613
+ // Vertical layout (and a hidden label, which reserves no label column) keeps the
614
+ // hint below the control.
615
+ const hintInLabel = computed(() => isHorizontal.value && !props.hideLabel);
616
+
617
+ // A `switch` renders visible text inside the control only when the field opts
618
+ // into contextual on/off text (textWhenTrue/textWhenFalse) — e.g. "Product is
619
+ // current". Without it, `switchText` falls back to the row label, which would
620
+ // duplicate the outer label, so the inner label is kept for screen readers but
621
+ // hidden visually (the row label names the control). A plain `checkbox` never
622
+ // shows visible inner text for the same reason.
623
+ const switchHasContextualText = computed(
624
+ () =>
625
+ props.field.textWhenTrue !== undefined ||
626
+ props.field.textWhenFalse !== undefined,
551
627
  );
552
628
 
553
629
  // A table-layout repeater is inherently wide — squeezing its own label into a
@@ -846,9 +922,12 @@ onBeforeUnmount(() => {
846
922
  `justify-content: space-between` on a full-width box used to leave a wide
847
923
  unclickable gap between the label and the toggle (only the tiny toggle
848
924
  itself was a real click target); shrinking the box to fit its content
849
- removes that dead zone entirely. */
925
+ removes that dead zone entirely. Uses block-level `flex` (not `inline-flex`)
926
+ so the box stays shrink-to-fit yet still takes its own line: an inline-level
927
+ box let a following `<small class="form-text">` hint flow inline beside it
928
+ instead of dropping below the control like every other field type (#79). */
850
929
  .dx-switch :deep(.form-check) {
851
- display: inline-flex;
930
+ display: flex;
852
931
  align-items: center;
853
932
  gap: 0.75rem;
854
933
  width: fit-content;
@@ -53,6 +53,13 @@ const infoId = `dx-field-info-${useId()}`;
53
53
  gap: 0.35rem;
54
54
  }
55
55
 
56
+ /* Soften the label from the near-black body colour so it doesn't dominate the
57
+ row — but keep it a step darker than the (lighter, secondary-grey) hint so the
58
+ label still leads. */
59
+ .dx-field-label__text {
60
+ color: #495057; /* gray-700 */
61
+ }
62
+
56
63
  .dx-field-label__info {
57
64
  display: inline-flex;
58
65
  align-items: center;
@@ -12,7 +12,10 @@
12
12
  (no Inertia required).
13
13
  -->
14
14
  <template>
15
- <BForm @submit.prevent="handleSubmit">
15
+ <BForm
16
+ @submit.prevent="handleSubmit"
17
+ :class="{ 'dx-form--horizontal': layout === 'horizontal' }"
18
+ >
16
19
  <!-- Form-level error message -->
17
20
  <DAlert
18
21
  v-if="resolvedForm.shouldShowMessage"
@@ -88,6 +91,7 @@
88
91
  :model="model"
89
92
  :layout="field.layout ?? layout"
90
93
  :label-cols="field.labelCols ?? labelCols"
94
+ :hide-label="field.hideLabel"
91
95
  >
92
96
  <template
93
97
  v-for="(slotName, target) in fieldSlotMap(field.key)"
@@ -151,6 +155,7 @@
151
155
  :model="model"
152
156
  :layout="field.layout ?? layout"
153
157
  :label-cols="field.labelCols ?? labelCols"
158
+ :hide-label="field.hideLabel"
154
159
  >
155
160
  <template
156
161
  v-for="(slotName, target) in fieldSlotMap(field.key)"
@@ -207,6 +207,17 @@ export interface FieldDefinition {
207
207
  */
208
208
  labelCols?: LabelCols;
209
209
 
210
+ /**
211
+ * Skip rendering the field's own label. In horizontal layout no label
212
+ * column is reserved either, so the control spans the full width. Useful
213
+ * for `switch` (and `checkbox`) fields, which render their own label
214
+ * inside the control — the outer row label would otherwise duplicate it,
215
+ * and the self-label then lines up with sibling row labels. Also used
216
+ * internally by `DXRepeater`'s `table` layout, where a column header
217
+ * already names the field.
218
+ */
219
+ hideLabel?: boolean;
220
+
210
221
  /**
211
222
  * Component rendered for `type: "component"` fields. Receives
212
223
  * `modelValue`, `field`, `model` props and emits `update:modelValue`.