@omnitend/dashboard-for-laravel 0.42.0 → 0.42.1

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.
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-10-04T09:15:57.974Z",
2
+ "generated": "2026-10-05T05:06:30.259Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.42.0"
5
+ "version": "0.42.1"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-10-04T09:15:58.053Z
4
+ > Last updated: 2026-10-05T05:06:30.315Z
5
5
 
6
6
  This file provides a complete map of all available documentation for AI agents and developers.
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.42.0",
3
+ "version": "0.42.1",
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",
@@ -562,6 +562,47 @@ h6,
562
562
  }
563
563
  }
564
564
 
565
+ // A horizontal row whose content column holds NO form control shows a
566
+ // display-only value: text, a link, a badge, a flex row of text, a small
567
+ // table, a stack of lines. Such a row is baseline-aligned instead of
568
+ // top-aligned, so the first line of text in the value, whatever its shape,
569
+ // sits on the label's first line (the label's padding just moves the shared
570
+ // baseline down). No class or field flag is needed; `.dx-form-plaintext`
571
+ // below still works and is now redundant inside a horizontal form.
572
+ //
573
+ // Every row that holds a control keeps the top alignment above: a checkbox,
574
+ // switch or file input exposes a synthesized or box-edge baseline, so
575
+ // baseline alignment would move them. The list is what the library renders
576
+ // as a control plus the native ones; a `.btn` counts because it is
577
+ // input-height and already lines its text up with the label when
578
+ // top-aligned. Content with no text at all (an image, an icon) sits its
579
+ // bottom edge on the label's baseline, as an inline image would in a line of
580
+ // text. The rule needs `:has()` (Chrome 105, Safari 15.4, Firefox 121); a
581
+ // browser without it falls back to the top alignment.
582
+ $dx-display-only-row: '.dx-form--horizontal .row:has(> .col-form-label):not(:has(> .col-form-label ~ * :is(input:not([type="hidden"]), select, textarea, button, .btn, .form-control, .form-select, .form-check, .input-group, [contenteditable])))';
583
+
584
+ #{$dx-display-only-row} {
585
+ align-items: baseline;
586
+
587
+ // A table's baseline is its first row's, and a row's baseline is its
588
+ // first BASELINE-aligned cell's; with every cell `middle` (the theme's
589
+ // default) CSS falls back to the bottom of the cell, ~6px below the text.
590
+ // Baseline-align the first row's cells so the table offers its text.
591
+ > .col-form-label ~ * table tr:first-child > * {
592
+ vertical-align: baseline;
593
+ }
594
+
595
+ // A badge's own baseline is its smaller text's, which lands its box ~2px
596
+ // off the label's centre; centre it on its line instead, as
597
+ // `.dx-form-plaintext .badge` does below.
598
+ > .col-form-label ~ * .badge {
599
+ vertical-align: top;
600
+ margin-top: calc(
601
+ (#{math.div($input-line-height, math.div($badge-font-size, 1em)) * 1em} - 1em - #{$badge-padding-y * 2}) / 2
602
+ );
603
+ }
604
+ }
605
+
565
606
  // A display-only value in a form row: a badge, a line of text, a short stack
566
607
  // of lines. It has no input box, so on its own it sits at the top of the row
567
608
  // while the label column (`.col-form-label`) is padded down to an input's text
@@ -1125,13 +1166,18 @@ fieldset:disabled .btn {
1125
1166
  }
1126
1167
  }
1127
1168
 
1128
- // The one exception: DXSaveButton's saved state ("✓ Saved") is a REAL
1129
- // disabled button, but it is a confirmation, not an unavailable action, so it
1130
- // keeps its variant's own colours (by default the navy primary fill, #151e2d /
1131
- // #e9f0f8) at full opacity rather than the neutral grey above, so saving
1132
- // changes the label and the disabled state but not the colour. Three classes
1133
- // plus the state, so it outranks the neutral rule (and `fieldset:disabled .btn`).
1134
- .btn.dx-save-button--saved:is(:disabled, .disabled) {
1169
+ // The exceptions: two states that are REALLY disabled but are not unavailable
1170
+ // actions, so they keep their variant's own colours (by default the navy
1171
+ // primary fill, #151e2d / #e9f0f8) at full opacity rather than the neutral grey
1172
+ // above:
1173
+ // - a busy button (DButton `loading`, marked `aria-busy="true"`), disabled only
1174
+ // so it can't be pressed twice. Greying it made every save blink navy → grey
1175
+ // → navy, since the grey lasted exactly as long as the request;
1176
+ // - DXSaveButton's saved state ("✓ Saved"), a confirmation.
1177
+ // So saving changes the label and the disabled state but never the colour.
1178
+ // `.btn` + the `:is()` (its most specific argument counts) + the state outranks
1179
+ // the neutral rule (and `fieldset:disabled .btn`).
1180
+ .btn:is(.dx-save-button--saved, [aria-busy="true"]):is(:disabled, .disabled) {
1135
1181
  --bs-btn-disabled-opacity: 1;
1136
1182
  --bs-btn-disabled-bg: var(--bs-btn-bg);
1137
1183
  --bs-btn-disabled-color: var(--bs-btn-color);
@@ -9,7 +9,7 @@
9
9
  * searchable single-select can always reach "none" in-list (#138).
10
10
  */
11
11
  <script setup lang="ts">
12
- import { computed, useAttrs } from "vue";
12
+ import { computed, ref, useAttrs, watch } from "vue";
13
13
  import { BAutocomplete } from "bootstrap-vue-next";
14
14
  import type { SelectOption } from "bootstrap-vue-next";
15
15
 
@@ -127,6 +127,75 @@ const hasValue = computed(() => {
127
127
  const noClearButton = computed(
128
128
  () => attr("noClearButton", "no-clear-button") === true || !hasValue.value,
129
129
  );
130
+
131
+ /*
132
+ * Late options (0.42.1). bvn's BAutocomplete writes the input's text from the
133
+ * model value through reka-ui's ComboboxInput, which does so on mount and
134
+ * again only when the MODEL changes. A value set before its options load (a
135
+ * DXTable select filter seeded from the URL while the option list is still
136
+ * being fetched) therefore kept showing the raw value ("78") after its label
137
+ * ("Alcopop") arrived. bvn exposes no "resync" method, but the input's text
138
+ * is its `search` v-model, so this wrapper owns that model and, whenever the
139
+ * options change, puts the matching option's label in it.
140
+ *
141
+ * It never overwrites a search the user is typing: while the input has
142
+ * focus, it relabels only text that is still exactly the raw value. Multiple
143
+ * mode needs nothing (bvn derives the chips from the options on every
144
+ * render). A consumer that binds its own `search` owns the text, so it is
145
+ * left alone. A `:key` remount would also work but drops focus and the open
146
+ * menu, so the `search` model is the cleaner hook.
147
+ */
148
+ const rootElement = ref<HTMLElement | null>(null);
149
+ const searchText = ref("");
150
+ const consumerOwnsSearch = computed(() => attr("search", "search") !== undefined);
151
+ const boundSearch = computed<string>(() =>
152
+ consumerOwnsSearch.value ? String(attr("search", "search")) : searchText.value,
153
+ );
154
+
155
+ const optionValueAndText = (option: unknown): { value: unknown; text: unknown } => {
156
+ if (option === null || typeof option !== "object") return { value: option, text: option };
157
+ const valueField = (attr("valueField", "value-field") as string | undefined) ?? "value";
158
+ const textField = (attr("textField", "text-field") as string | undefined) ?? "text";
159
+ const record = option as Record<string, unknown>;
160
+ return { value: record[valueField], text: record[textField] };
161
+ };
162
+
163
+ // The same equality bvn uses to find the selected option (strict, or by key).
164
+ const sameValue = (optionValue: unknown, modelValue: unknown) => {
165
+ const by = attr("by", "by");
166
+ if (typeof by === "string") {
167
+ return (
168
+ (optionValue as Record<string, unknown> | null)?.[by] ===
169
+ (modelValue as Record<string, unknown> | null)?.[by]
170
+ );
171
+ }
172
+ return optionValue === modelValue;
173
+ };
174
+
175
+ // A bare `multiple` in a template arrives as "" (it is not a declared prop).
176
+ const isMultiple = computed(() => {
177
+ const multiple = attr("multiple", "multiple");
178
+ return multiple !== undefined && multiple !== null && multiple !== false;
179
+ });
180
+
181
+ const relabelFromOptions = () => {
182
+ if (isMultiple.value || consumerOwnsSearch.value) return;
183
+ const modelValue = attr("modelValue", "model-value");
184
+ if (modelValue === null || modelValue === undefined || modelValue === "") return;
185
+ const match = computedOptions.value
186
+ .map(optionValueAndText)
187
+ .find((option) => sameValue(option.value, modelValue));
188
+ if (!match) return;
189
+ const label = String(match.text ?? "");
190
+ if (searchText.value === label) return;
191
+ const input = rootElement.value?.querySelector("input");
192
+ const userIsTyping =
193
+ input !== null && input !== undefined && document.activeElement === input;
194
+ if (userIsTyping && searchText.value !== String(modelValue)) return;
195
+ searchText.value = label;
196
+ };
197
+
198
+ watch(computedOptions, relabelFromOptions);
130
199
  </script>
131
200
 
132
201
  <template>
@@ -138,12 +207,14 @@ const noClearButton = computed(
138
207
  root, so `[data-v-x] .input-group` matches nothing in a consumer build
139
208
  (#53). A plain element root makes it deterministic.
140
209
  -->
141
- <div class="d-autocomplete">
210
+ <div ref="rootElement" class="d-autocomplete">
142
211
  <BAutocomplete
143
212
  v-bind="$attrs"
213
+ :search="boundSearch"
144
214
  :options="computedOptions"
145
215
  :filter-function="effectiveFilterFunction"
146
216
  :no-clear-button="noClearButton"
217
+ @update:search="searchText = $event"
147
218
  >
148
219
  <!-- Dynamically pass through all named slots with their props -->
149
220
  <template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">