@omnitend/dashboard-for-laravel 0.32.0 → 0.33.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.
- package/dist/components/extended/DXCurrencyInput.vue.d.ts +27 -0
- package/dist/components/extended/DXField.vue.d.ts +30 -15
- package/dist/components/extended/DXTable.vue.d.ts +39 -5
- package/dist/composables/defineForm.d.ts +8 -2
- package/dist/dashboard-for-laravel.js +8503 -8351
- 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 +1 -0
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +21 -9
- package/docs/public/api-reference.json +85 -5
- package/docs/public/docs-map.md +3 -2
- package/docs/public/llms.txt +3 -2
- package/package.json +1 -5
- package/resources/css/theme.scss +34 -0
- package/resources/js/components/extended/DXCurrencyInput.vue +143 -0
- package/resources/js/components/extended/DXDashboardSidebar.vue +21 -16
- package/resources/js/components/extended/DXField.vue +142 -71
- package/resources/js/components/extended/DXFormField.vue +1 -0
- package/resources/js/components/extended/DXTable.vue +164 -53
- package/resources/js/composables/defineForm.ts +12 -2
- package/resources/js/composables/useResourceEditor.ts +3 -5
- package/resources/js/index.ts +1 -0
- package/resources/js/types/index.ts +22 -8
- package/resources/js/utils/api.ts +13 -5
package/dist/types/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { Component } from "vue";
|
|
|
13
13
|
* - `switch` — a toggle checkbox with contextual on/off text and an
|
|
14
14
|
* on-state (filled) style; see `textWhenTrue` / `textWhenFalse`.
|
|
15
15
|
*/
|
|
16
|
-
export type FieldType = "text" | "email" | "password" | "number" | "url" | "tel" | "date" | "datetime-local" | "datetime" | "time" | "currency" | "percentage" | "textarea" | "select" | "autocomplete" | "checkbox" | "checkbox-group" | "switch" | "radio" | "image" | "file" | "component" | "repeater";
|
|
16
|
+
export type FieldType = "text" | "email" | "password" | "number" | "url" | "tel" | "date" | "datetime-local" | "datetime" | "time" | "currency" | "percentage" | "textarea" | "select" | "autocomplete" | "checkbox" | "checkbox-group" | "switch" | "switch-list" | "radio" | "image" | "file" | "component" | "repeater";
|
|
17
17
|
/**
|
|
18
18
|
* A value that may be supplied directly or computed from the live form
|
|
19
19
|
* model. Predicates receive the current model so fields can react to
|
|
@@ -72,9 +72,11 @@ export interface FieldDefinition {
|
|
|
72
72
|
/** Whether field is required (optional) */
|
|
73
73
|
required?: boolean;
|
|
74
74
|
/**
|
|
75
|
-
* Options for `select`, `radio`
|
|
76
|
-
* `autocomplete` these are the datalist
|
|
77
|
-
* accepts free text not present in the list.
|
|
75
|
+
* Options for `select`, `radio`, `checkbox-group`, `switch-list` and
|
|
76
|
+
* `autocomplete` fields. For `autocomplete` these are the datalist
|
|
77
|
+
* suggestions; the field still accepts free text not present in the list.
|
|
78
|
+
* For `switch-list` each option renders a labelled toggle row
|
|
79
|
+
* (`description` becomes the row label's tooltip).
|
|
78
80
|
*/
|
|
79
81
|
options?: FieldOption[];
|
|
80
82
|
/**
|
|
@@ -112,6 +114,14 @@ export interface FieldDefinition {
|
|
|
112
114
|
* `3.80`). Typing is never reformatted mid-edit.
|
|
113
115
|
*/
|
|
114
116
|
decimals?: number;
|
|
117
|
+
/**
|
|
118
|
+
* For `currency` fields: the model stores integer MINOR units (pence) but
|
|
119
|
+
* the input shows/edits MAJOR units (pounds) — display = value / 10^decimals,
|
|
120
|
+
* submit = round(input × 10^decimals). `min`/`max`/`step` stay in major
|
|
121
|
+
* units. Off by default (the model is the major-unit amount). Use for
|
|
122
|
+
* legacy pence-integer columns (#116).
|
|
123
|
+
*/
|
|
124
|
+
minorUnits?: boolean;
|
|
115
125
|
/**
|
|
116
126
|
* For `percentage` fields: treat the underlying model value as a 0–1
|
|
117
127
|
* fraction while showing/editing it as a 0–100 percentage. The model keeps
|
|
@@ -150,11 +160,13 @@ export interface FieldDefinition {
|
|
|
150
160
|
*/
|
|
151
161
|
textWhenFalse?: MaybeFn<string>;
|
|
152
162
|
/**
|
|
153
|
-
* For `switch` fields: the on-state colour. `"success"`
|
|
154
|
-
* house green-on / red-off style for an
|
|
155
|
-
* `"neutral"` (brand primary on / grey off) is
|
|
156
|
-
* switches that shouldn't imply good/bad ("contains
|
|
157
|
-
* toggle, "hidden on web shop").
|
|
163
|
+
* For `switch` and `switch-list` fields: the on-state colour. `"success"`
|
|
164
|
+
* (default) is the house green-on / red-off style for an
|
|
165
|
+
* active/enabled/good switch; `"neutral"` (brand primary on / grey off) is
|
|
166
|
+
* for semantically-mixed switches that shouldn't imply good/bad ("contains
|
|
167
|
+
* alcohol", an allergen toggle, "hidden on web shop"). A `switch-list`
|
|
168
|
+
* usually wants `"neutral"` — a wall of red-off toggles over-signals when
|
|
169
|
+
* off simply means "not present". See DXSwitch `onVariant` (#158).
|
|
158
170
|
*/
|
|
159
171
|
switchVariant?: "success" | "neutral";
|
|
160
172
|
/** CSS class for the form group */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-07-
|
|
2
|
+
"generated": "2026-07-20T14:11:55.309Z",
|
|
3
3
|
"package": {
|
|
4
4
|
"name": "@omnitend/dashboard-for-laravel",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.33.1"
|
|
6
6
|
},
|
|
7
7
|
"components": {
|
|
8
8
|
"base": [
|
|
@@ -1385,6 +1385,58 @@
|
|
|
1385
1385
|
],
|
|
1386
1386
|
"methods": []
|
|
1387
1387
|
},
|
|
1388
|
+
{
|
|
1389
|
+
"name": "DXCurrencyInput",
|
|
1390
|
+
"category": "extended",
|
|
1391
|
+
"filePath": "resources/js/components/extended/DXCurrencyInput.vue",
|
|
1392
|
+
"description": "DXCurrencyInput — the money input leaf (#152). A currency-symbol-prefixed numeric input with the display/model split from #69: the MODEL is always a plain number (or null), while the displayed text is padded to the minor-unit precision on blur/seed (`3.8` -> `3.80`) and never reformatted mid-edit. Clearing the input emits `null` — never `NaN`, never `\"\"` — so \"no value\" is one predictable shape (#152). `minorUnits` (#116) edits an integer-minor-unit model (pence) as major units (pounds): display = model / 10^decimals, emit = round(input * 10^decimals). `min`/`max`/`step` and the displayed text are always in MAJOR units — only the model is scaled. DXField's `currency` field type renders this leaf; it exists standalone so a money input outside a form (an inline table correction, a quick filter) doesn't re-inline the `£`-prefix + toFixed + parse foot-guns.",
|
|
1393
|
+
"props": [
|
|
1394
|
+
{
|
|
1395
|
+
"name": "modelValue",
|
|
1396
|
+
"type": "union",
|
|
1397
|
+
"required": false,
|
|
1398
|
+
"default": "null",
|
|
1399
|
+
"description": "The numeric model. In `minorUnits` mode this is the integer minor-unit\n amount (e.g. pence); otherwise the major-unit amount. `null` = empty."
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
"name": "currencySymbol",
|
|
1403
|
+
"type": "string",
|
|
1404
|
+
"required": false,
|
|
1405
|
+
"default": "\"£\"",
|
|
1406
|
+
"description": "Symbol shown in the prepend (default \"£\")."
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
"name": "decimals",
|
|
1410
|
+
"type": "number",
|
|
1411
|
+
"required": false,
|
|
1412
|
+
"default": "2",
|
|
1413
|
+
"description": "Minor-unit decimal places the display pads to (default 2; 0 for a\n currency with no minor unit, e.g. JPY)."
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
"name": "minorUnits",
|
|
1417
|
+
"type": "boolean",
|
|
1418
|
+
"required": false,
|
|
1419
|
+
"default": "false",
|
|
1420
|
+
"description": "Model is stored in integer minor units (pence) but edited in major\n units (pounds). Scale is 10^decimals."
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
"name": "step",
|
|
1424
|
+
"type": "union",
|
|
1425
|
+
"required": false,
|
|
1426
|
+
"default": "undefined",
|
|
1427
|
+
"description": "Input step in MAJOR units (default: one minor unit, e.g. 0.01)."
|
|
1428
|
+
}
|
|
1429
|
+
],
|
|
1430
|
+
"events": [
|
|
1431
|
+
{
|
|
1432
|
+
"name": "update:modelValue",
|
|
1433
|
+
"description": "",
|
|
1434
|
+
"properties": []
|
|
1435
|
+
}
|
|
1436
|
+
],
|
|
1437
|
+
"slots": [],
|
|
1438
|
+
"methods": []
|
|
1439
|
+
},
|
|
1388
1440
|
{
|
|
1389
1441
|
"name": "DXDashboard",
|
|
1390
1442
|
"category": "extended",
|
|
@@ -2033,6 +2085,28 @@
|
|
|
2033
2085
|
}
|
|
2034
2086
|
]
|
|
2035
2087
|
},
|
|
2088
|
+
{
|
|
2089
|
+
"name": "switch-list-item",
|
|
2090
|
+
"description": "Extra per-row content beside a switch-list row's toggle (e.g. a notes input the consumer binds to their own model).",
|
|
2091
|
+
"bindings": [
|
|
2092
|
+
{
|
|
2093
|
+
"title": "binding",
|
|
2094
|
+
"type": {
|
|
2095
|
+
"name": "FieldOption"
|
|
2096
|
+
},
|
|
2097
|
+
"name": "option",
|
|
2098
|
+
"description": "The option this row renders."
|
|
2099
|
+
},
|
|
2100
|
+
{
|
|
2101
|
+
"title": "binding",
|
|
2102
|
+
"type": {
|
|
2103
|
+
"name": "boolean"
|
|
2104
|
+
},
|
|
2105
|
+
"name": "on",
|
|
2106
|
+
"description": "Whether the row's switch is currently on."
|
|
2107
|
+
}
|
|
2108
|
+
]
|
|
2109
|
+
},
|
|
2036
2110
|
{
|
|
2037
2111
|
"name": "repeater-row",
|
|
2038
2112
|
"description": "Custom layout for a single repeater row, forwarded to `DXRepeater`'s `row` slot with its row props (row index, item, remove handler, etc.).",
|
|
@@ -2733,6 +2807,12 @@
|
|
|
2733
2807
|
"required": false,
|
|
2734
2808
|
"description": "API endpoint URL for auto-provider mode (alternative to provider function)"
|
|
2735
2809
|
},
|
|
2810
|
+
{
|
|
2811
|
+
"name": "apiAdapter",
|
|
2812
|
+
"type": "DXTableApiAdapter",
|
|
2813
|
+
"required": false,
|
|
2814
|
+
"description": "Adapts the built-in provider's request params / response body to a\n backend convention that differs from dfl's (see DXTableApiAdapter)."
|
|
2815
|
+
},
|
|
2736
2816
|
{
|
|
2737
2817
|
"name": "fields",
|
|
2738
2818
|
"type": "Array",
|
|
@@ -2749,7 +2829,7 @@
|
|
|
2749
2829
|
"name": "filters",
|
|
2750
2830
|
"type": "Record",
|
|
2751
2831
|
"required": false,
|
|
2752
|
-
"description": "Filter values (v-model support) - key is field key, value is filter string"
|
|
2832
|
+
"description": "Filter values (v-model support) - key is field key, value is the filter\n string, or an ARRAY of values for a `filterMultiple` column (#51)."
|
|
2753
2833
|
},
|
|
2754
2834
|
{
|
|
2755
2835
|
"name": "filterValues",
|
|
@@ -3563,8 +3643,8 @@
|
|
|
3563
3643
|
]
|
|
3564
3644
|
},
|
|
3565
3645
|
"stats": {
|
|
3566
|
-
"totalComponents":
|
|
3646
|
+
"totalComponents": 77,
|
|
3567
3647
|
"baseComponents": 57,
|
|
3568
|
-
"extendedComponents":
|
|
3648
|
+
"extendedComponents": 20
|
|
3569
3649
|
}
|
|
3570
3650
|
}
|
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-20T14:11:55.365Z
|
|
5
5
|
|
|
6
6
|
This file provides a complete map of all available documentation for AI agents and developers.
|
|
7
7
|
|
|
@@ -73,6 +73,7 @@ This file provides a complete map of all available documentation for AI agents a
|
|
|
73
73
|
### Extended
|
|
74
74
|
|
|
75
75
|
- [Charts](/components/extended/DXChart): Thin, themed wrappers around [Chart.js](https://www.chartjs.org/) via
|
|
76
|
+
- [DXCurrencyInput](/components/extended/DXCurrencyInput): The money input leaf: a currency-symbol-prefixed numeric input whose **model is
|
|
76
77
|
- [DXDashboard](/components/extended/DXDashboard): Complete dashboard layout component that combines sidebar navigation, top navbar, and content area.
|
|
77
78
|
- [DXDashboardNavbar](/components/extended/DXDashboardNavbar): A responsive top navigation bar component for Laravel dashboards with user menu, search functionalit
|
|
78
79
|
- [DXDashboardSidebar](/components/extended/DXDashboardSidebar): A collapsible sidebar navigation component for Laravel dashboards with support for navigation groups
|
|
@@ -101,4 +102,4 @@ This file provides a complete map of all available documentation for AI agents a
|
|
|
101
102
|
|
|
102
103
|
---
|
|
103
104
|
|
|
104
|
-
**Total Pages**:
|
|
105
|
+
**Total Pages**: 78
|
package/docs/public/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Vue 3 dashboard components for Laravel with Bootstrap Vue Next
|
|
4
4
|
>
|
|
5
|
-
> A dual-package library (NPM + Composer) providing
|
|
5
|
+
> A dual-package library (NPM + Composer) providing 70 Vue 3 components for building Laravel dashboards with Bootstrap Vue Next.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -81,11 +81,12 @@ Lightweight type-safe wrappers around Bootstrap Vue Next components providing AP
|
|
|
81
81
|
- [DToaster](/components/base/dtoaster): Bootstrap Vue Next Toaster wrapper
|
|
82
82
|
- [DTooltip](/components/base/dtooltip): Bootstrap Vue Next Tooltip wrapper
|
|
83
83
|
|
|
84
|
-
## Extended Components (
|
|
84
|
+
## Extended Components (13 components)
|
|
85
85
|
|
|
86
86
|
Custom dashboard components with advanced functionality beyond Bootstrap Vue Next.
|
|
87
87
|
|
|
88
88
|
- [DXBasicForm](/components/extended/dxbasicform): Extended dashboard component
|
|
89
|
+
- [DXCurrencyInput](/components/extended/dxcurrencyinput): Extended dashboard component
|
|
89
90
|
- [DXDashboard](/components/extended/dxdashboard): Complete dashboard layout with sidebar and navbar
|
|
90
91
|
- [DXDashboardNavbar](/components/extended/dxdashboardnavbar): Top navbar with user dropdown
|
|
91
92
|
- [DXDashboardSidebar](/components/extended/dxdashboardsidebar): Collapsible sidebar with navigation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnitend/dashboard-for-laravel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.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",
|
|
@@ -55,15 +55,11 @@
|
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@inertiajs/vue3": "^2.0.0",
|
|
58
|
-
"axios": "^1.15.3",
|
|
59
58
|
"chart.js": "^4.4.0",
|
|
60
59
|
"vue": "^3.5.13",
|
|
61
60
|
"vue-chartjs": "^5.3.0"
|
|
62
61
|
},
|
|
63
62
|
"peerDependenciesMeta": {
|
|
64
|
-
"axios": {
|
|
65
|
-
"optional": true
|
|
66
|
-
},
|
|
67
63
|
"bootstrap-vue-next": {
|
|
68
64
|
"optional": true
|
|
69
65
|
},
|
package/resources/css/theme.scss
CHANGED
|
@@ -543,6 +543,19 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
543
543
|
--bs-alert-link-color: #{$emphasis};
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
+
// Progress bars — large FILLS use the vivid `solid-bg`, not the dark
|
|
547
|
+
// emphasis Bootstrap wires into `.bg-*` (#154). A big solid block of the
|
|
548
|
+
// emphasis olive reads muddy and clashes with the soft badge green; the
|
|
549
|
+
// vivid fill is the same family as the switch-ON colour, so badge + switch +
|
|
550
|
+
// bar read as one system. Only success and warning actually change hex
|
|
551
|
+
// (danger/info's solid IS their emphasis). The variant-less default bar
|
|
552
|
+
// stays `$progress-bar-bg` (brand navy). `!important` beats the `.bg-*`
|
|
553
|
+
// utility, which sets its colour `!important` by design.
|
|
554
|
+
.progress-bar.bg-#{$name} {
|
|
555
|
+
background-color: #{$solid-bg} !important;
|
|
556
|
+
color: #{$solid-text};
|
|
557
|
+
}
|
|
558
|
+
|
|
546
559
|
// `.text-#{$name}` and `.link-#{$name}` need no override: each variant's base
|
|
547
560
|
// colour IS its emphasis shade, so Bootstrap already renders them legible.
|
|
548
561
|
}
|
|
@@ -559,6 +572,10 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
559
572
|
--dx-#{$name}-solid-bg: #{map.get($v, "solid-bg")};
|
|
560
573
|
--dx-#{$name}-emphasis: #{map.get($v, "emphasis")};
|
|
561
574
|
}
|
|
575
|
+
|
|
576
|
+
// DXTable header titles: muted by default (#157) so the table's CONTENT is
|
|
577
|
+
// the loud layer. Override to re-louden (e.g. `var(--bs-body-color)`).
|
|
578
|
+
--dx-table-header-color: var(--bs-secondary-color);
|
|
562
579
|
}
|
|
563
580
|
|
|
564
581
|
// primary is near-black navy, so Bootstrap's darken-on-hover is imperceptible —
|
|
@@ -692,12 +709,29 @@ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cd
|
|
|
692
709
|
// fallbacks for SSR / missing-CSS cases — keep the two lists in sync).
|
|
693
710
|
$dx-chart-palette: (#2563eb, #65a30d, #7c3aed, #0d9488, #ea580c, #0891b2, #d97706, #db2777);
|
|
694
711
|
|
|
712
|
+
// Dark-surface remap (#145). SAME hue slots — the CVD-derived order above is
|
|
713
|
+
// load-bearing, so only lightness moves: each hue lifted into the dark band so
|
|
714
|
+
// every step clears the 3:1 non-text guideline on Bootstrap's dark body
|
|
715
|
+
// (#212529), where two of the light steps dipped below. Validated with the
|
|
716
|
+
// same method as the light set (OKLab ΔE ×100, Viénot protan/deutan): min
|
|
717
|
+
// adjacent CVD ΔE 15.1 (light set: 13.8), min normal ΔE 25.3, contrast
|
|
718
|
+
// 5.67–10.23:1 on #212529. Swapping a step needs that validation re-run.
|
|
719
|
+
// getPalette() reads the CSS variables live, so charts follow the theme with
|
|
720
|
+
// no JS change; the TS fallbacks stay the light set (SSR/no-CSS default).
|
|
721
|
+
$dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #fbbf24, #f472b6);
|
|
722
|
+
|
|
695
723
|
:root {
|
|
696
724
|
@for $i from 1 through list.length($dx-chart-palette) {
|
|
697
725
|
--dx-chart-#{$i}: #{list.nth($dx-chart-palette, $i)};
|
|
698
726
|
}
|
|
699
727
|
}
|
|
700
728
|
|
|
729
|
+
[data-bs-theme="dark"] {
|
|
730
|
+
@for $i from 1 through list.length($dx-chart-palette-dark) {
|
|
731
|
+
--dx-chart-#{$i}: #{list.nth($dx-chart-palette-dark, $i)};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
701
735
|
// The chart container used by DXBarChart/DXLineChart/DXDoughnutChart (shipped
|
|
702
736
|
// from the `@omnitend/dashboard-for-laravel/charts` entry, #142). Global rather
|
|
703
737
|
// than a component scoped style so the charts entry emits NO CSS — chart
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
DXCurrencyInput — the money input leaf (#152).
|
|
3
|
+
|
|
4
|
+
A currency-symbol-prefixed numeric input with the display/model split from
|
|
5
|
+
#69: the MODEL is always a plain number (or null), while the displayed text
|
|
6
|
+
is padded to the minor-unit precision on blur/seed (`3.8` -> `3.80`) and
|
|
7
|
+
never reformatted mid-edit. Clearing the input emits `null` — never `NaN`,
|
|
8
|
+
never `""` — so "no value" is one predictable shape (#152).
|
|
9
|
+
|
|
10
|
+
`minorUnits` (#116) edits an integer-minor-unit model (pence) as major units
|
|
11
|
+
(pounds): display = model / 10^decimals, emit = round(input * 10^decimals).
|
|
12
|
+
`min`/`max`/`step` and the displayed text are always in MAJOR units — only
|
|
13
|
+
the model is scaled.
|
|
14
|
+
|
|
15
|
+
DXField's `currency` field type renders this leaf; it exists standalone so a
|
|
16
|
+
money input outside a form (an inline table correction, a quick filter)
|
|
17
|
+
doesn't re-inline the `£`-prefix + toFixed + parse foot-guns.
|
|
18
|
+
-->
|
|
19
|
+
<template>
|
|
20
|
+
<DInputGroup>
|
|
21
|
+
<template #prepend>
|
|
22
|
+
<span class="input-group-text">{{ currencySymbol }}</span>
|
|
23
|
+
</template>
|
|
24
|
+
<DFormInput
|
|
25
|
+
:model-value="displayValue"
|
|
26
|
+
type="number"
|
|
27
|
+
:step="step ?? defaultStep"
|
|
28
|
+
v-bind="$attrs"
|
|
29
|
+
@input="handleInput"
|
|
30
|
+
@focus="handleFocus"
|
|
31
|
+
@blur="handleBlur"
|
|
32
|
+
/>
|
|
33
|
+
</DInputGroup>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { computed, ref, watch } from "vue";
|
|
38
|
+
import DInputGroup from "../base/DInputGroup.vue";
|
|
39
|
+
import DFormInput from "../base/DFormInput.vue";
|
|
40
|
+
|
|
41
|
+
defineOptions({ inheritAttrs: false });
|
|
42
|
+
|
|
43
|
+
interface Props {
|
|
44
|
+
/** The numeric model. In `minorUnits` mode this is the integer minor-unit
|
|
45
|
+
* amount (e.g. pence); otherwise the major-unit amount. `null` = empty. */
|
|
46
|
+
modelValue?: number | null;
|
|
47
|
+
/** Symbol shown in the prepend (default "£"). */
|
|
48
|
+
currencySymbol?: string;
|
|
49
|
+
/** Minor-unit decimal places the display pads to (default 2; 0 for a
|
|
50
|
+
* currency with no minor unit, e.g. JPY). */
|
|
51
|
+
decimals?: number;
|
|
52
|
+
/** Model is stored in integer minor units (pence) but edited in major
|
|
53
|
+
* units (pounds). Scale is 10^decimals. */
|
|
54
|
+
minorUnits?: boolean;
|
|
55
|
+
/** Input step in MAJOR units (default: one minor unit, e.g. 0.01). */
|
|
56
|
+
step?: string | number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
60
|
+
modelValue: null,
|
|
61
|
+
currencySymbol: "£",
|
|
62
|
+
decimals: 2,
|
|
63
|
+
minorUnits: false,
|
|
64
|
+
step: undefined,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const emit = defineEmits<{
|
|
68
|
+
"update:modelValue": [value: number | null];
|
|
69
|
+
}>();
|
|
70
|
+
|
|
71
|
+
// `decimals` is public input — clamp it to a supported non-negative integer
|
|
72
|
+
// so a hostile value can't throw (`10 ** -1` scaling vs `toFixed(101)`
|
|
73
|
+
// RangeError) or make display precision and scale disagree.
|
|
74
|
+
const safeDecimals = computed(() => {
|
|
75
|
+
const truncated = Math.trunc(props.decimals);
|
|
76
|
+
return Number.isFinite(truncated) ? Math.min(8, Math.max(0, truncated)) : 2;
|
|
77
|
+
});
|
|
78
|
+
const scale = computed(() => Math.pow(10, safeDecimals.value));
|
|
79
|
+
const defaultStep = computed(() => (1 / scale.value).toString());
|
|
80
|
+
|
|
81
|
+
// Model (possibly minor units) -> the major-unit display string.
|
|
82
|
+
function format(value: number | null | undefined): string {
|
|
83
|
+
if (value === null || value === undefined || (value as any) === "") return "";
|
|
84
|
+
const num = Number(value);
|
|
85
|
+
if (!Number.isFinite(num)) return "";
|
|
86
|
+
const major = props.minorUnits ? num / scale.value : num;
|
|
87
|
+
return major.toFixed(safeDecimals.value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The shown text tracks a local ref so typing is never reformatted mid-edit
|
|
91
|
+
// (reactive reformatting would round "3." to "3.00" under the user's cursor);
|
|
92
|
+
// it only resyncs from the model while the input isn't focused. See #69.
|
|
93
|
+
const isFocused = ref(false);
|
|
94
|
+
const displayValue = ref("");
|
|
95
|
+
|
|
96
|
+
watch(
|
|
97
|
+
() => [props.modelValue, props.decimals, props.minorUnits] as const,
|
|
98
|
+
() => {
|
|
99
|
+
if (isFocused.value) return;
|
|
100
|
+
displayValue.value = format(props.modelValue);
|
|
101
|
+
},
|
|
102
|
+
{ immediate: true },
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
function handleFocus(): void {
|
|
106
|
+
isFocused.value = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handleInput(event: Event): void {
|
|
110
|
+
const raw = (event.target as HTMLInputElement).value;
|
|
111
|
+
displayValue.value = raw;
|
|
112
|
+
if (raw.trim() === "") {
|
|
113
|
+
emit("update:modelValue", null);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const num = Number(raw);
|
|
117
|
+
if (!Number.isFinite(num)) {
|
|
118
|
+
emit("update:modelValue", null);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
emit("update:modelValue", props.minorUnits ? toMinorUnits(num) : num);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Major units -> integer minor units, decimal-safely. A bare
|
|
125
|
+
// `Math.round(num * scale)` is wrong at half-unit boundaries: 19.99 * 100 is
|
|
126
|
+
// 1998.9999… (fine, rounds up) but 1.005 * 100 is 100.4999… and silently
|
|
127
|
+
// drops to 100, and negative halves round toward +∞ (-0.005 → -0). So the
|
|
128
|
+
// float product is first snapped back to its decimal value (toPrecision), and
|
|
129
|
+
// halves round AWAY FROM ZERO — the accounting convention, symmetric in sign.
|
|
130
|
+
function toMinorUnits(major: number): number {
|
|
131
|
+
const scaled = Number((major * scale.value).toPrecision(12));
|
|
132
|
+
const rounded = Math.sign(scaled) * Math.round(Math.abs(scaled));
|
|
133
|
+
return rounded === 0 ? 0 : rounded; // normalise -0
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Consumer listeners (@blur, @focus, @input) arrive through `$attrs` and are
|
|
137
|
+
// merged onto the inner input by Vue alongside these internal handlers, so no
|
|
138
|
+
// re-emit is needed (re-emitting would double-fire them).
|
|
139
|
+
function handleBlur(): void {
|
|
140
|
+
isFocused.value = false;
|
|
141
|
+
displayValue.value = format(props.modelValue);
|
|
142
|
+
}
|
|
143
|
+
</script>
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
<template v-for="(group, groupIndex) in navigation" :key="groupIndex">
|
|
40
40
|
<div
|
|
41
41
|
v-if="group.visible !== false"
|
|
42
|
-
class="nav-group
|
|
42
|
+
class="nav-group"
|
|
43
43
|
:class="{ 'nav-group-open': isGroupExpanded(groupIndex, group) }"
|
|
44
44
|
>
|
|
45
45
|
<!-- Collapsible group header (accordion toggle) -->
|
|
46
46
|
<button
|
|
47
47
|
v-if="isGroupToggle(group)"
|
|
48
48
|
type="button"
|
|
49
|
-
class="nav-group-toggle
|
|
49
|
+
class="nav-group-toggle fw-semibold mb-2 px-2"
|
|
50
50
|
:aria-expanded="isGroupExpanded(groupIndex, group)"
|
|
51
51
|
:aria-controls="groupItemsId(groupIndex)"
|
|
52
52
|
@click="toggleGroup(groupKey(group, groupIndex))"
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
<!-- Static group label (non-collapsible, expanded sidebar) -->
|
|
76
76
|
<div
|
|
77
77
|
v-else-if="group.label && !collapsed"
|
|
78
|
-
class="nav-group-label
|
|
78
|
+
class="nav-group-label fw-semibold mb-2 px-2"
|
|
79
79
|
>
|
|
80
80
|
{{ group.label }}
|
|
81
81
|
</div>
|
|
@@ -426,9 +426,15 @@ watch(activeGroupIndex, () => {
|
|
|
426
426
|
overflow-x: hidden;
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
/* Dense rhythm (#95): greendragon's cutover values, adopted as the default —
|
|
430
|
+
natural-case headers at 0.875rem (no uppercase/letter-spacing), tight group
|
|
431
|
+
spacing. */
|
|
432
|
+
.nav-group {
|
|
433
|
+
margin-bottom: 0.25rem;
|
|
434
|
+
}
|
|
435
|
+
|
|
429
436
|
.nav-group-label {
|
|
430
|
-
font-size: 0.
|
|
431
|
-
letter-spacing: 0.5px;
|
|
437
|
+
font-size: 0.875rem;
|
|
432
438
|
}
|
|
433
439
|
|
|
434
440
|
/* When collapsible groups are on, give the static (non-collapsible) group
|
|
@@ -437,7 +443,7 @@ watch(activeGroupIndex, () => {
|
|
|
437
443
|
.sidebar-collapsible-groups .nav-group-label {
|
|
438
444
|
display: flex;
|
|
439
445
|
align-items: center;
|
|
440
|
-
min-height:
|
|
446
|
+
min-height: 2rem;
|
|
441
447
|
}
|
|
442
448
|
|
|
443
449
|
/* Collapsible group toggle header */
|
|
@@ -446,14 +452,13 @@ watch(activeGroupIndex, () => {
|
|
|
446
452
|
align-items: center;
|
|
447
453
|
justify-content: space-between;
|
|
448
454
|
width: 100%;
|
|
449
|
-
/* Comfortable
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
min-height:
|
|
453
|
-
padding-top: 0.
|
|
454
|
-
padding-bottom: 0.
|
|
455
|
-
font-size: 0.
|
|
456
|
-
letter-spacing: 0.5px;
|
|
455
|
+
/* Comfortable click target at the dense rhythm (#95). `px-2` on the element
|
|
456
|
+
sets only left/right padding, so vertical padding here does not fight
|
|
457
|
+
Bootstrap's utility `!important`. */
|
|
458
|
+
min-height: 2rem;
|
|
459
|
+
padding-top: 0.25rem;
|
|
460
|
+
padding-bottom: 0.25rem;
|
|
461
|
+
font-size: 0.875rem;
|
|
457
462
|
background: transparent;
|
|
458
463
|
border: 0;
|
|
459
464
|
/* No `color` here on purpose: theme.scss sets `.nav-group-toggle` to
|
|
@@ -543,7 +548,7 @@ watch(activeGroupIndex, () => {
|
|
|
543
548
|
}
|
|
544
549
|
|
|
545
550
|
:deep(.nav-link) {
|
|
546
|
-
padding: 0.
|
|
551
|
+
padding: 0.3rem 0.75rem;
|
|
547
552
|
transition: all 0.2s ease;
|
|
548
553
|
text-decoration: none;
|
|
549
554
|
white-space: nowrap;
|
|
@@ -562,7 +567,7 @@ watch(activeGroupIndex, () => {
|
|
|
562
567
|
}
|
|
563
568
|
|
|
564
569
|
.sidebar-collapsed :deep(.nav-link) {
|
|
565
|
-
padding: 0.625rem;
|
|
570
|
+
padding: 0.3rem 0.625rem;
|
|
566
571
|
}
|
|
567
572
|
|
|
568
573
|
/* Custom scrollbar (the nav is the scroll region) */
|