@omnitend/dashboard-for-laravel 0.34.0 → 0.35.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/DXForm.vue.d.ts +28 -6
- package/dist/components/extended/DXTable.vue.d.ts +39 -1
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +7 -2
- package/dist/composables/useContainerWidth.d.ts +96 -0
- package/dist/dashboard-for-laravel.js +8138 -7998
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +7 -7
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/style.css +1 -1
- package/docs/public/api-reference.json +40 -7
- package/docs/public/docs-map.md +1 -1
- package/package.json +1 -1
- package/resources/css/theme.scss +80 -2
- package/resources/js/components/extended/DXDashboardSidebar.vue +1 -1
- package/resources/js/components/extended/DXForm.vue +83 -9
- package/resources/js/components/extended/DXTable.vue +217 -12
- package/resources/js/components/extended/DXTableEditorModal.vue +7 -2
- package/resources/js/components/extended/DXTablePagination.vue +19 -3
- package/resources/js/components/extended/DXTableShell.vue +70 -1
- package/resources/js/composables/useContainerWidth.ts +303 -0
- package/resources/js/index.ts +7 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-07-
|
|
2
|
+
"generated": "2026-07-21T12:16:47.073Z",
|
|
3
3
|
"package": {
|
|
4
4
|
"name": "@omnitend/dashboard-for-laravel",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.35.0"
|
|
6
6
|
},
|
|
7
7
|
"components": {
|
|
8
8
|
"base": [
|
|
@@ -2427,7 +2427,14 @@
|
|
|
2427
2427
|
"type": "union",
|
|
2428
2428
|
"required": false,
|
|
2429
2429
|
"default": "\"vertical\"",
|
|
2430
|
-
"description": "Form-wide field layout
|
|
2430
|
+
"description": "Form-wide field layout:\n\n- `\"vertical\"` (default) — label above input, always.\n- `\"horizontal\"` — label left, input right, always.\n- `\"auto\"` — horizontal when the form's **own container** is at least\n `layoutThreshold` px wide, vertical below that. Container-driven, not\n viewport-driven: a page narrowed by the dashboard sidebar, or a form\n inside a modal, stacks even though the window is wide (which no\n Bootstrap media query can see).\n\nOverridable per-field via `field.layout`. A field with `span: true`\nalways renders full-width, regardless of layout."
|
|
2431
|
+
},
|
|
2432
|
+
{
|
|
2433
|
+
"name": "layoutThreshold",
|
|
2434
|
+
"type": "number",
|
|
2435
|
+
"required": false,
|
|
2436
|
+
"default": "640",
|
|
2437
|
+
"description": "Container width (px) at or above which `layout: \"auto\"` goes horizontal.\nIgnored for the explicit `\"vertical\"`/`\"horizontal\"` layouts.\n\nDefault 640, measured rather than guessed: with the default 3-column\nlabel the label's text area is `containerWidth / 4 - 18` px, so 640 gives\nit 142px — enough for a ~20-character label (\"Unit price (ex VAT)\"\nmeasures 128px at the theme's label font) to stay on one line, with the\ncontrol column still 474px. Below ~584px that label starts wrapping,\nwhich is the cramped-label symptom this exists to avoid. Raise it if your\nlabels run longer, or lower `labelCols` instead."
|
|
2431
2438
|
},
|
|
2432
2439
|
{
|
|
2433
2440
|
"name": "labelCols",
|
|
@@ -3314,7 +3321,7 @@
|
|
|
3314
3321
|
"type": "boolean",
|
|
3315
3322
|
"required": false,
|
|
3316
3323
|
"default": "true",
|
|
3317
|
-
"description": "Wrap the table in a `DCard`. Defaults to `true`. Set `false` for a plain,\nborderless variant — the same header, filter row, table and pagination\nrendered directly on the page background (no card border/shadow), for\ndata-heavy admin index pages that read as busy inside a card."
|
|
3324
|
+
"description": "Wrap the table in a `DCard`. Defaults to `true`. Set `false` for a plain,\nborderless variant — the same header, filter row, table and pagination\nrendered directly on the page background (no card border/shadow), for\ndata-heavy admin index pages that read as busy inside a card.\n\nThe table renders FLUSH to the card border (rows and stripes reach the\nedge) while the header and pagination stay padded, which requires the card\nto clip to its radius (`overflow: hidden`).\n\n**Consequence for slot content:** anything you render into a slot that\npositions itself OUTSIDE the card and does not teleport is clipped at the\ncard edge. Library components are unaffected — `DDropdown` teleports to\n`<body>` by default, and the column-filter menus position themselves\n`fixed` — so this only bites a hand-rolled `position: absolute` overlay\n(a custom popover in a `cell(<key>)` or `#header` slot). Teleport it, or\nuse `:card=\"false\"`. Narrowing the clip to the table region is tracked\nas #166."
|
|
3318
3325
|
},
|
|
3319
3326
|
{
|
|
3320
3327
|
"name": "editFields",
|
|
@@ -3346,7 +3353,7 @@
|
|
|
3346
3353
|
"type": "union",
|
|
3347
3354
|
"required": false,
|
|
3348
3355
|
"default": "\"horizontal\"",
|
|
3349
|
-
"description": "Field layout for the edit/create modal's form, forwarded to its `DXForm`\n(\"vertical\" = label above input, \"horizontal\" = label left). Defaults to\n**\"horizontal\"** to match the Omni Tend form convention (a raw modal form\npreviously fell back to DXForm's vertical default, which looked out of\nplace next to horizontal page forms). Pass `\"vertical\"` to opt out."
|
|
3356
|
+
"description": "Field layout for the edit/create modal's form, forwarded to its `DXForm`\n(\"vertical\" = label above input, \"horizontal\" = label left). Defaults to\n**\"horizontal\"** to match the Omni Tend form convention (a raw modal form\npreviously fell back to DXForm's vertical default, which looked out of\nplace next to horizontal page forms). Pass `\"vertical\"` to opt out.\n\n`\"auto\"` hands the decision to `DXForm`'s container-width observer, which\nmeasures the FORM rather than the window — so a modal narrowed by its own\nbreakpoint stacks to vertical even on a wide viewport, where a media query\nwould still report \"plenty of room\". See `useContainerWidth`."
|
|
3350
3357
|
},
|
|
3351
3358
|
{
|
|
3352
3359
|
"name": "editLabelCols",
|
|
@@ -3578,6 +3585,32 @@
|
|
|
3578
3585
|
}
|
|
3579
3586
|
]
|
|
3580
3587
|
},
|
|
3588
|
+
{
|
|
3589
|
+
"name": "`head-end(${field.key})`",
|
|
3590
|
+
"description": "head-end(<fieldKey>) Additive content at the end of a column's own header — a period total above a numeric column, a small badge — keeping DXTable's sort indicator and field hint. Not a `head()` override.",
|
|
3591
|
+
"bindings": [
|
|
3592
|
+
{
|
|
3593
|
+
"name": "name",
|
|
3594
|
+
"title": "binding"
|
|
3595
|
+
},
|
|
3596
|
+
{
|
|
3597
|
+
"title": "binding",
|
|
3598
|
+
"type": {
|
|
3599
|
+
"name": "object"
|
|
3600
|
+
},
|
|
3601
|
+
"name": "field",
|
|
3602
|
+
"description": "The column's field definition."
|
|
3603
|
+
},
|
|
3604
|
+
{
|
|
3605
|
+
"title": "binding",
|
|
3606
|
+
"type": {
|
|
3607
|
+
"name": "string"
|
|
3608
|
+
},
|
|
3609
|
+
"name": "label",
|
|
3610
|
+
"description": "The column's resolved header label."
|
|
3611
|
+
}
|
|
3612
|
+
]
|
|
3613
|
+
},
|
|
3581
3614
|
{
|
|
3582
3615
|
"name": "name",
|
|
3583
3616
|
"description": "Any slot the underlying table supports, forwarded with its scope: `cell(<fieldKey>)` for a cell, `foot(<fieldKey>)` for a footer cell (needs `footClone`), `empty` for the no-rows message, `row-expansion` for expandable row detail, plus `custom-foot`, `top-row`, `bottom-row`, `thead-sub` and the `table-*` slots.",
|
|
@@ -3679,7 +3712,7 @@
|
|
|
3679
3712
|
"name": "layout",
|
|
3680
3713
|
"type": "union",
|
|
3681
3714
|
"required": false,
|
|
3682
|
-
"description": "Field layout forwarded to the modal's DXForm (defaults handled upstream)."
|
|
3715
|
+
"description": "Field layout forwarded to the modal's DXForm (defaults handled upstream).\nIncludes `\"auto\"`, which lets the form stack to vertical when the MODAL is\nnarrow even though the viewport is wide — the container-query case a\nviewport media query cannot see."
|
|
3683
3716
|
},
|
|
3684
3717
|
{
|
|
3685
3718
|
"name": "labelCols",
|
|
@@ -3991,7 +4024,7 @@
|
|
|
3991
4024
|
"name": "DXTableShell",
|
|
3992
4025
|
"category": "extended",
|
|
3993
4026
|
"filePath": "resources/js/components/extended/DXTableShell.vue",
|
|
3994
|
-
"description": "Internal chrome wrapper for `DXTable`. Renders its content either inside a `DCard` (the default) or as a plain, borderless block on the page background (`:card=\"false\"`). Extracted so `DXTable`'s ~460-line body and all its slots aren't duplicated across a card/plain `v-if`. Slots: - `#header` — the title / action row. In card mode it fills `DCard`'s header; in plain mode it renders above the table with a subtle bottom rule. - default — the table body (spinner/error/table/pagination).",
|
|
4027
|
+
"description": "Internal chrome wrapper for `DXTable`. Renders its content either inside a `DCard` (the default) or as a plain, borderless block on the page background (`:card=\"false\"`). Extracted so `DXTable`'s ~460-line body and all its slots aren't duplicated across a card/plain `v-if`. Slots: - `#header` — the title / action row. In card mode it fills `DCard`'s header; in plain mode it renders above the table with a subtle bottom rule. - default — the table body (spinner/error/table/pagination). Card mode renders `DCard` in **`no-body`** mode so the table sits FLUSH against the card border. With a `.card-body` in between, a striped table's rows were inset by `--bs-card-spacer-x` (24px) and left a white gutter between the stripes and the border. Without it, DXTable's children — error alert, loading block, table, pagination — become direct children of `.card`, which is already a `flex-direction: column` container, so they still stack in order: .card (overflow: hidden) ├─ .card-header (padded by Bootstrap's card-cap padding) ├─ .table-responsive ← FLUSH │ └─ table └─ .dx-table-pagination (pads itself — see DXTablePagination.vue) Bootstrap 5 has no `.card > .table` rounding rules (that was BS4), so the clipping is done with `overflow: hidden` on the card: the flush table's square top corners then follow the card's radius whether or not a header is present. bvn's popups position `fixed` by default, so they are not clipped by it.",
|
|
3995
4028
|
"props": [
|
|
3996
4029
|
{
|
|
3997
4030
|
"name": "card",
|
package/docs/public/docs-map.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Documentation Map
|
|
2
2
|
|
|
3
3
|
> Auto-generated hierarchical overview of all documentation
|
|
4
|
-
> Last updated: 2026-07-
|
|
4
|
+
> Last updated: 2026-07-21T12:16:47.128Z
|
|
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
package/resources/css/theme.scss
CHANGED
|
@@ -313,6 +313,32 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
313
313
|
flex-wrap: nowrap;
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
+
// ----------------------------------------------------------------------------
|
|
317
|
+
// Nav tabs
|
|
318
|
+
// ----------------------------------------------------------------------------
|
|
319
|
+
// Chrome recedes, content leads — the same ethos as the muted table headers
|
|
320
|
+
// (#157). Bootstrap gives an INACTIVE tab `--bs-nav-link-color:
|
|
321
|
+
// var(--bs-link-color)`, i.e. the saturated link-blue, so a row of tabs above a
|
|
322
|
+
// form reads as a row of links competing with the fields below. Mute the
|
|
323
|
+
// inactive tabs and let the ACTIVE one (Bootstrap's
|
|
324
|
+
// `--bs-nav-tabs-link-active-color`, the near-black emphasis colour) carry the
|
|
325
|
+
// selection on its own.
|
|
326
|
+
//
|
|
327
|
+
// Driven through Bootstrap's own `--bs-nav-*` variables rather than a raw
|
|
328
|
+
// `color:` — so a consumer can re-louden one tab set by resetting the same
|
|
329
|
+
// variable, `[data-bs-theme="dark"]` remaps the tokens for free, and Bootstrap
|
|
330
|
+
// keeps ownership of the `.active`/`.disabled` resolution order.
|
|
331
|
+
//
|
|
332
|
+
// `--bs-secondary-color` is `rgba(var(--bs-body-color-rgb), .75)` → ~#585b5e on
|
|
333
|
+
// white: 6.83:1, comfortably AA (AAA for body text); 6.53:1 on the theme's
|
|
334
|
+
// `$light` #f8fafc panel. Hover goes to the full body colour (#212529, 16.1:1)
|
|
335
|
+
// so hovering reads as "heading toward selected" rather than "turning into a
|
|
336
|
+
// link".
|
|
337
|
+
.nav-tabs {
|
|
338
|
+
--bs-nav-link-color: var(--bs-secondary-color);
|
|
339
|
+
--bs-nav-link-hover-color: var(--bs-body-color);
|
|
340
|
+
}
|
|
341
|
+
|
|
316
342
|
// ----------------------------------------------------------------------------
|
|
317
343
|
// Dashboard Custom Styles
|
|
318
344
|
// ----------------------------------------------------------------------------
|
|
@@ -433,6 +459,40 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
433
459
|
color: $secondary; // muted message text
|
|
434
460
|
}
|
|
435
461
|
|
|
462
|
+
// HEADER-LESS toast (`body` but no `title` — the shape every toast created
|
|
463
|
+
// through `useToast().create({ body })` takes). BVN renders it as
|
|
464
|
+
// .toast > .d-flex > (.toast-body + .btn-close)
|
|
465
|
+
// and BVN's own `.toast .btn-close-custom { margin: <pad> <pad> auto }` pins
|
|
466
|
+
// the close X to the TOP of that row, while the `.toast-body` above still
|
|
467
|
+
// carries the reduced `padding-top` that only makes sense when it sits under
|
|
468
|
+
// a `.toast-header`. Result: the message rides high, the X sits lower, and a
|
|
469
|
+
// one-line toast reads as misaligned.
|
|
470
|
+
//
|
|
471
|
+
// With no header there is nothing above the body, so restore symmetric
|
|
472
|
+
// padding (Bootstrap's `.toast-body` is `padding: var(--bs-toast-padding-x)`
|
|
473
|
+
// all round) and centre the row. The `margin-bottom: auto` has to go too —
|
|
474
|
+
// an auto margin outranks `align-items` in flex alignment, so centring alone
|
|
475
|
+
// would leave the X pinned; `margin-block: 0` hands alignment back to the
|
|
476
|
+
// container while keeping BVN's horizontal margin.
|
|
477
|
+
//
|
|
478
|
+
// `:has()` is the structural test — it is the only way to key off "this toast
|
|
479
|
+
// has no header" from CSS, and it is supported in every evergreen browser
|
|
480
|
+
// (Chrome/Edge 105+, Safari 15.4+, Firefox 121+). A browser without it simply
|
|
481
|
+
// keeps today's slightly-off alignment; nothing breaks.
|
|
482
|
+
&:not(:has(.toast-header)) {
|
|
483
|
+
> .d-flex {
|
|
484
|
+
align-items: center;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.toast-body {
|
|
488
|
+
padding-top: var(--bs-toast-padding-x);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.btn-close-custom {
|
|
492
|
+
margin-block: 0;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
436
496
|
// Hide the auto-dismiss countdown bar — the ticking animation reads as
|
|
437
497
|
// stressful/urgent, which is wrong for a passive notification. Auto-dismiss
|
|
438
498
|
// still works; only its visual indicator is removed.
|
|
@@ -530,7 +590,18 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
530
590
|
// (which sets its colours `!important` by design). `.toast` is excluded: it
|
|
531
591
|
// has its own soft handling (a fainter 50%-white mix via `--bs-toast-bg` for
|
|
532
592
|
// its large surface) and themed toasts don't carry `.text-bg-*` anyway.
|
|
533
|
-
|
|
593
|
+
//
|
|
594
|
+
// The exclusion is wrapped in `:where()` DELIBERATELY: `:where()` always
|
|
595
|
+
// contributes ZERO specificity, so this stays a single-class 0,1,0 selector
|
|
596
|
+
// while matching exactly what a bare `:not(.toast)` would. Written bare it is
|
|
597
|
+
// 0,2,0 — and because the declarations are `!important`, a consumer painting
|
|
598
|
+
// a fully custom badge (`.my-status-badge { background: …; }`) could not win
|
|
599
|
+
// with a single-class `!important` rule and had to escalate to a
|
|
600
|
+
// repeated-class hack. One class in, one class out: a consumer's own
|
|
601
|
+
// `!important` single-class rule now overrides us on source order, as it
|
|
602
|
+
// should. (Everything Bootstrap defines for `.text-bg-*` is also 0,1,0
|
|
603
|
+
// `!important` and is imported ABOVE this block, so we still win it.)
|
|
604
|
+
.text-bg-#{$name}:where(:not(.toast)) {
|
|
534
605
|
background-color: #{$soft-bg} !important;
|
|
535
606
|
color: #{$soft-text} !important;
|
|
536
607
|
}
|
|
@@ -551,7 +622,14 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
551
622
|
// (danger/info's solid IS their emphasis). The variant-less default bar
|
|
552
623
|
// stays `$progress-bar-bg` (brand navy). `!important` beats the `.bg-*`
|
|
553
624
|
// utility, which sets its colour `!important` by design.
|
|
554
|
-
|
|
625
|
+
//
|
|
626
|
+
// `.progress-bar` is only here to SCOPE the rule (large fills, not every
|
|
627
|
+
// `.bg-*` element), so it goes in `:where()` — zero specificity, same
|
|
628
|
+
// matching, and the selector stays 0,1,0 like Bootstrap's own `.bg-*`. Same
|
|
629
|
+
// reasoning as the `.text-bg-*` rule above: at 0,2,0 `!important` a consumer
|
|
630
|
+
// recolouring one bar had to out-specify us instead of just writing
|
|
631
|
+
// `.my-bar { background-color: … !important }`.
|
|
632
|
+
:where(.progress-bar).bg-#{$name} {
|
|
555
633
|
background-color: #{$solid-bg} !important;
|
|
556
634
|
color: #{$solid-text};
|
|
557
635
|
}
|
|
@@ -426,7 +426,7 @@ watch(activeGroupIndex, () => {
|
|
|
426
426
|
overflow-x: hidden;
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
/* Dense rhythm (#95):
|
|
429
|
+
/* Dense rhythm (#95): values proven in a production dashboard, adopted as the default —
|
|
430
430
|
natural-case headers at 0.875rem (no uppercase/letter-spacing), tight group
|
|
431
431
|
spacing. */
|
|
432
432
|
.nav-group {
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
-->
|
|
14
14
|
<template>
|
|
15
15
|
<BForm
|
|
16
|
+
ref="formRoot"
|
|
16
17
|
@submit.prevent="handleSubmit"
|
|
17
|
-
:class="{ 'dx-form--horizontal':
|
|
18
|
+
:class="{ 'dx-form--horizontal': resolvedLayout === 'horizontal' }"
|
|
18
19
|
>
|
|
19
20
|
<!-- Form-level error message -->
|
|
20
21
|
<DAlert
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
:field="field"
|
|
72
73
|
:form="resolvedForm"
|
|
73
74
|
:model="model"
|
|
74
|
-
:layout="
|
|
75
|
+
:layout="resolvedLayout"
|
|
75
76
|
:label-cols="labelCols"
|
|
76
77
|
>
|
|
77
78
|
<!-- Forward every DXForm slot so the field can render
|
|
@@ -105,7 +106,7 @@
|
|
|
105
106
|
:field="field"
|
|
106
107
|
:form="resolvedForm"
|
|
107
108
|
:model="model"
|
|
108
|
-
:layout="
|
|
109
|
+
:layout="resolvedLayout"
|
|
109
110
|
:label-cols="labelCols"
|
|
110
111
|
>
|
|
111
112
|
<!-- Forward every DXForm slot so the field can render its
|
|
@@ -166,7 +167,7 @@
|
|
|
166
167
|
</template>
|
|
167
168
|
|
|
168
169
|
<script setup lang="ts">
|
|
169
|
-
import { computed, watch } from "vue";
|
|
170
|
+
import { computed, ref, watch, type ComponentPublicInstance } from "vue";
|
|
170
171
|
import { BForm } from "bootstrap-vue-next";
|
|
171
172
|
import DAlert from "../base/DAlert.vue";
|
|
172
173
|
import DButton from "../base/DButton.vue";
|
|
@@ -176,6 +177,7 @@ import { BTab as DTab } from "bootstrap-vue-next"; // raw BTab: BTabs scans slot
|
|
|
176
177
|
import DXFormField from "./DXFormField.vue";
|
|
177
178
|
import type { UseFormReturn } from "../../composables/useForm";
|
|
178
179
|
import type { DefineFormReturn } from "../../composables/defineForm";
|
|
180
|
+
import { useContainerWidth } from "../../composables/useContainerWidth";
|
|
179
181
|
import type { FieldDefinition, FormTab, LabelCols, MaybeFn } from "../../types";
|
|
180
182
|
import {
|
|
181
183
|
resolvePredicate as resolvePredicateFor,
|
|
@@ -235,12 +237,34 @@ interface Props {
|
|
|
235
237
|
cardTabs?: boolean;
|
|
236
238
|
|
|
237
239
|
/**
|
|
238
|
-
* Form-wide field layout:
|
|
239
|
-
*
|
|
240
|
-
* `
|
|
241
|
-
*
|
|
240
|
+
* Form-wide field layout:
|
|
241
|
+
*
|
|
242
|
+
* - `"vertical"` (default) — label above input, always.
|
|
243
|
+
* - `"horizontal"` — label left, input right, always.
|
|
244
|
+
* - `"auto"` — horizontal when the form's **own container** is at least
|
|
245
|
+
* `layoutThreshold` px wide, vertical below that. Container-driven, not
|
|
246
|
+
* viewport-driven: a page narrowed by the dashboard sidebar, or a form
|
|
247
|
+
* inside a modal, stacks even though the window is wide (which no
|
|
248
|
+
* Bootstrap media query can see).
|
|
249
|
+
*
|
|
250
|
+
* Overridable per-field via `field.layout`. A field with `span: true`
|
|
251
|
+
* always renders full-width, regardless of layout.
|
|
242
252
|
*/
|
|
243
|
-
layout?: "vertical" | "horizontal";
|
|
253
|
+
layout?: "vertical" | "horizontal" | "auto";
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Container width (px) at or above which `layout: "auto"` goes horizontal.
|
|
257
|
+
* Ignored for the explicit `"vertical"`/`"horizontal"` layouts.
|
|
258
|
+
*
|
|
259
|
+
* Default 640, measured rather than guessed: with the default 3-column
|
|
260
|
+
* label the label's text area is `containerWidth / 4 - 18` px, so 640 gives
|
|
261
|
+
* it 142px — enough for a ~20-character label ("Unit price (ex VAT)"
|
|
262
|
+
* measures 128px at the theme's label font) to stay on one line, with the
|
|
263
|
+
* control column still 474px. Below ~584px that label starts wrapping,
|
|
264
|
+
* which is the cramped-label symptom this exists to avoid. Raise it if your
|
|
265
|
+
* labels run longer, or lower `labelCols` instead.
|
|
266
|
+
*/
|
|
267
|
+
layoutThreshold?: number;
|
|
244
268
|
|
|
245
269
|
/**
|
|
246
270
|
* Label column width for horizontal layout (mirrors BFormGroup's
|
|
@@ -258,6 +282,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
258
282
|
card: false,
|
|
259
283
|
cardTabs: true,
|
|
260
284
|
layout: "vertical",
|
|
285
|
+
layoutThreshold: 640,
|
|
261
286
|
});
|
|
262
287
|
|
|
263
288
|
const emit = defineEmits<{
|
|
@@ -270,6 +295,55 @@ const slots = defineSlots<Record<string, (props: any) => any>>();
|
|
|
270
295
|
/** v-model for the active tab index. */
|
|
271
296
|
const activeTab = defineModel<number>("activeTab", { default: 0 });
|
|
272
297
|
|
|
298
|
+
// ————————————————— container-driven layout (`layout: "auto"`)
|
|
299
|
+
|
|
300
|
+
const formRoot = ref<ComponentPublicInstance | HTMLElement | null>(null);
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The `<form>` element BForm renders. A template ref on a component yields the
|
|
304
|
+
* component instance, so unwrap `$el` — and tolerate either shape rather than
|
|
305
|
+
* assuming one, so a future BForm change (or a functional re-implementation)
|
|
306
|
+
* can't silently leave us observing nothing.
|
|
307
|
+
*/
|
|
308
|
+
function resolveFormElement(): HTMLElement | null {
|
|
309
|
+
const rootValue = formRoot.value;
|
|
310
|
+
if (rootValue === null || rootValue === undefined) return null;
|
|
311
|
+
if (rootValue instanceof HTMLElement) return rootValue;
|
|
312
|
+
const element = (rootValue as ComponentPublicInstance).$el;
|
|
313
|
+
return element instanceof HTMLElement ? element : null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Observe ONLY in auto mode. Returning null for the other layouts means no
|
|
317
|
+
// ResizeObserver is ever attached for the (overwhelmingly common) explicit
|
|
318
|
+
// layouts — existing consumers pay nothing and behave identically.
|
|
319
|
+
const { isBelow: containerIsNarrow } = useContainerWidth(
|
|
320
|
+
() => (props.layout === "auto" ? resolveFormElement() : null),
|
|
321
|
+
{
|
|
322
|
+
// Getter, so a consumer binding `:layout-threshold` to something
|
|
323
|
+
// reactive re-evaluates instead of latching the mount-time value.
|
|
324
|
+
threshold: () => props.layoutThreshold,
|
|
325
|
+
// Guard against an observer feedback loop at the boundary: the vertical
|
|
326
|
+
// layout is TALLER, so a form inside an `overflow:auto` ancestor can
|
|
327
|
+
// gain a scrollbar when it stacks, shrinking its own container by
|
|
328
|
+
// ~15-17px — straight back over the threshold, and it flips forever.
|
|
329
|
+
// A band wider than any scrollbar (24px) makes the crossing one-way
|
|
330
|
+
// until the container genuinely grows.
|
|
331
|
+
hysteresis: 24,
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The layout actually rendered. `vertical`/`horizontal` pass through
|
|
337
|
+
* unconditionally (unchanged behaviour); `auto` resolves from the measured
|
|
338
|
+
* container width, defaulting to `vertical` before the first measurement and
|
|
339
|
+
* under SSR — the stacked layout is legible at any width, so it is the safe
|
|
340
|
+
* thing to render when the width is unknown.
|
|
341
|
+
*/
|
|
342
|
+
const resolvedLayout = computed<"vertical" | "horizontal">(() => {
|
|
343
|
+
if (props.layout !== "auto") return props.layout;
|
|
344
|
+
return containerIsNarrow.value ? "vertical" : "horizontal";
|
|
345
|
+
});
|
|
346
|
+
|
|
273
347
|
// ————————————————— resolve form / fields (accept useForm or defineForm)
|
|
274
348
|
|
|
275
349
|
function isDefineForm(
|