@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.
@@ -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` and `autocomplete` fields. For
76
- * `autocomplete` these are the datalist suggestions; the field still
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"` (default) is the
154
- * house green-on / red-off style for an active/enabled/good switch;
155
- * `"neutral"` (brand primary on / grey off) is for semantically-mixed
156
- * switches that shouldn't imply good/bad ("contains alcohol", an allergen
157
- * toggle, "hidden on web shop"). See DXSwitch `onVariant` (#158).
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-20T09:56:11.731Z",
2
+ "generated": "2026-07-20T14:11:55.309Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.32.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": 76,
3646
+ "totalComponents": 77,
3567
3647
  "baseComponents": 57,
3568
- "extendedComponents": 19
3648
+ "extendedComponents": 20
3569
3649
  }
3570
3650
  }
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-07-20T09:56:11.791Z
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**: 77
105
+ **Total Pages**: 78
@@ -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 69 Vue 3 components for building Laravel dashboards with Bootstrap Vue Next.
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 (12 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.32.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
  },
@@ -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 mb-3"
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 text-uppercase small fw-semibold mb-2 px-2"
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 text-uppercase small fw-semibold mb-2 px-2"
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.75rem;
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: 2.5rem;
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, ergonomic click target (matches the nav links' vertical
450
- rhythm). `px-2` on the element sets only left/right padding, so vertical
451
- padding here does not fight Bootstrap's utility `!important`. */
452
- min-height: 2.5rem;
453
- padding-top: 0.5rem;
454
- padding-bottom: 0.5rem;
455
- font-size: 0.75rem;
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.625rem 0.75rem;
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) */