basalt-ui 1.17.0 → 1.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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +14 -0
- package/agent/rules/basalt-charts.md +12 -0
- package/agent/rules/basalt-mantine.md +71 -6
- package/agent/rules/basalt-router.md +129 -30
- package/agent/rules/basalt-tokens.md +2 -1
- package/agent/skills/basalt-design/SKILL.md +2 -2
- package/dist/charts/kinds/DualPanel.d.ts +7 -0
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +10 -3
- package/dist/charts/primitives/CartesianChart.d.ts +17 -0
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +5 -2
- package/dist/charts/primitives/ChartTooltip.d.ts +6 -1
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +2 -1
- package/dist/charts/series.d.ts +5 -0
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/nav/types.d.ts +110 -0
- package/dist/nav/types.d.ts.map +1 -0
- package/dist/nav/types.js +0 -0
- package/dist/router-tanstack/index.d.ts +9 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +7 -0
- package/dist/router-tanstack/nav.d.ts +163 -0
- package/dist/router-tanstack/nav.d.ts.map +1 -0
- package/dist/router-tanstack/nav.js +38 -0
- package/dist/router-tanstack/nav.test.d.ts +2 -0
- package/dist/router-tanstack/nav.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-param-store.d.ts +19 -9
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.d.ts +30 -0
- package/dist/router-tanstack/use-nav.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.js +71 -0
- package/dist/shell/app-breadcrumbs.d.ts +7 -9
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +8 -4
- package/dist/shell/app-mobile-nav.d.ts +23 -24
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +290 -68
- package/dist/shell/app-mobile-nav.module.css +166 -11
- package/dist/shell/app-mobile-nav.module.css.d.ts +11 -2
- package/dist/shell/app-mobile-nav.test.d.ts +2 -0
- package/dist/shell/app-mobile-nav.test.d.ts.map +1 -0
- package/dist/shell/app-sidebar.d.ts +3 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +40 -174
- package/dist/shell/app-sidebar.module.css +3 -2
- package/dist/shell/app-sidebar.module.css.d.ts +0 -2
- package/dist/shell/index.d.ts +22 -65
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +38 -76
- package/dist/shell/mobile-nav-model.d.ts +41 -0
- package/dist/shell/mobile-nav-model.d.ts.map +1 -0
- package/dist/shell/mobile-nav-model.js +244 -0
- package/dist/shell/mobile-nav-model.test.d.ts +2 -0
- package/dist/shell/mobile-nav-model.test.d.ts.map +1 -0
- package/dist/shell/nav-count-badge.d.ts +11 -0
- package/dist/shell/nav-count-badge.d.ts.map +1 -0
- package/dist/shell/nav-count-badge.js +29 -0
- package/dist/surfaces.d.ts +1 -1
- package/dist/surfaces.js +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +8 -7
- package/dist/theme/nav-link.module.css +4 -2
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +2 -0
- package/dist/tokens/palette.d.ts +15 -0
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +13 -0
- package/dist/tokens.css +2 -0
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/charts/kinds/DualPanel.test.tsx +81 -1
- package/src/charts/kinds/DualPanel.tsx +26 -4
- package/src/charts/primitives/CartesianChart.test.tsx +173 -1
- package/src/charts/primitives/CartesianChart.tsx +29 -5
- package/src/charts/primitives/ChartTooltip.test.tsx +22 -1
- package/src/charts/primitives/ChartTooltip.tsx +7 -1
- package/src/charts/series.ts +5 -0
- package/src/index.ts +15 -5
- package/src/nav/types.ts +115 -0
- package/src/router-tanstack/index.ts +21 -2
- package/src/router-tanstack/nav.test.ts +111 -0
- package/src/router-tanstack/nav.ts +205 -0
- package/src/router-tanstack/search-param-store.ts +19 -9
- package/src/router-tanstack/use-nav.ts +146 -0
- package/src/shell/app-breadcrumbs.tsx +21 -19
- package/src/shell/app-mobile-nav.module.css +166 -11
- package/src/shell/app-mobile-nav.module.css.d.ts +11 -2
- package/src/shell/app-mobile-nav.test.tsx +369 -0
- package/src/shell/app-mobile-nav.tsx +436 -102
- package/src/shell/app-sidebar.module.css +3 -2
- package/src/shell/app-sidebar.module.css.d.ts +0 -2
- package/src/shell/app-sidebar.test.tsx +6 -10
- package/src/shell/app-sidebar.tsx +49 -206
- package/src/shell/index.test.tsx +62 -7
- package/src/shell/index.tsx +105 -164
- package/src/shell/mobile-nav-model.test.ts +296 -0
- package/src/shell/mobile-nav-model.ts +389 -0
- package/src/shell/nav-count-badge.tsx +39 -0
- package/src/surfaces.ts +1 -1
- package/src/theme/density-relations.test.ts +19 -1
- package/src/theme/index.ts +8 -7
- package/src/theme/nav-link.module.css +4 -2
- package/src/theme/spacing.test.ts +11 -0
- package/src/tokens/density.test.ts +2 -0
- package/src/tokens/index.ts +8 -5
- package/src/tokens/palette.ts +16 -0
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The mobile bottom bar. Slots are DESTINATIONS: a tap navigates, so the bar must read as a tab
|
|
3
|
+
* bar rather than as chrome — hence a pill behind the icon for the active slot instead of a
|
|
4
|
+
* full-tab fill, and a press that depresses the icon without moving the hit box.
|
|
5
|
+
*/
|
|
1
6
|
.bar {
|
|
2
7
|
display: flex;
|
|
3
8
|
align-items: stretch;
|
|
@@ -5,6 +10,21 @@
|
|
|
5
10
|
height: 100%;
|
|
6
11
|
background: var(--mantine-color-body);
|
|
7
12
|
border-top: 1px solid var(--mantine-color-default-border);
|
|
13
|
+
-webkit-tap-highlight-color: transparent;
|
|
14
|
+
/* NO padding-bottom / env() here: Mantine's own AppShell `.footer` rule already does
|
|
15
|
+
* `height: calc(var(--app-shell-footer-height) + env(safe-area-inset-bottom))` +
|
|
16
|
+
* `padding-bottom: env(safe-area-inset-bottom)`. Adding it here double-counts the inset. */
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Mantine sets --app-shell-footer-OFFSET to the raw height WITHOUT the inset, so Main's
|
|
20
|
+
* padding-bottom is short by exactly one inset. This is the only real safe-area gap. */
|
|
21
|
+
@media (max-width: 47.99375em) {
|
|
22
|
+
.mainSafeArea {
|
|
23
|
+
padding-bottom: calc(
|
|
24
|
+
var(--app-shell-footer-offset, 0rem) + var(--app-shell-padding) +
|
|
25
|
+
env(safe-area-inset-bottom, 0px)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
8
28
|
}
|
|
9
29
|
|
|
10
30
|
.tab {
|
|
@@ -16,18 +36,127 @@
|
|
|
16
36
|
justify-content: center;
|
|
17
37
|
gap: var(--vx-space-mobile-nav-tab-gap);
|
|
18
38
|
color: var(--mantine-color-dimmed);
|
|
39
|
+
/* Kills the 300ms double-tap-zoom delay and the text-selection flash on a fast repeat tap. */
|
|
40
|
+
touch-action: manipulation;
|
|
41
|
+
user-select: none;
|
|
19
42
|
}
|
|
20
43
|
|
|
21
|
-
|
|
44
|
+
.tabIcon {
|
|
45
|
+
position: relative;
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
/* Deliberately sub-scale: the active pill hugs a 24px glyph, so it is a shape inset rather than
|
|
50
|
+
* layout spacing — the xs..xl scale has no step this tight. */
|
|
51
|
+
padding: 2px 12px; /* theme-allow */
|
|
52
|
+
border-radius: var(--vx-radius-ctrl);
|
|
53
|
+
transition: transform 140ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* A consumer's sidebar icons are typically 18px; the bar normalizes them in CSS so the same
|
|
57
|
+
* `SidebarItem.icon` renders at slot size with zero consumer change. */
|
|
58
|
+
.tabIcon > svg {
|
|
59
|
+
width: var(--vx-space-mobile-nav-icon-size);
|
|
60
|
+
height: var(--vx-space-mobile-nav-icon-size);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.tab:active .tabIcon {
|
|
64
|
+
transform: scale(0.9);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Active = a neutral pill behind the ICON only, plus plain text color. A full-tab fill is what
|
|
68
|
+
* makes the bar read as chrome instead of a tab bar. Never the identity blue (DESIGN.md). */
|
|
22
69
|
.tab[data-active] {
|
|
23
70
|
color: var(--mantine-color-text);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.tab[data-active] .tabIcon {
|
|
24
74
|
background-color: color-mix(in srgb, var(--vx-ink) 10%, transparent);
|
|
25
75
|
}
|
|
26
76
|
|
|
77
|
+
.tab[data-active] .label {
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Unread dot. This is a BADGE, not the active state — the "never the identity blue" rule above
|
|
82
|
+
* governs the active fill and is unchanged. A count GLYPH is unreadable in a 56px slot. */
|
|
83
|
+
.tabDot {
|
|
84
|
+
position: absolute;
|
|
85
|
+
top: 0;
|
|
86
|
+
inset-inline-end: 8px;
|
|
87
|
+
width: 8px;
|
|
88
|
+
height: 8px;
|
|
89
|
+
border-radius: 999px;
|
|
90
|
+
background-color: var(--vx-accent);
|
|
91
|
+
box-shadow: 0 0 0 2px var(--mantine-color-body);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* `text-overflow: clip`, never an ellipsis — a truncated word with a "…" in a 72px tab reads as
|
|
95
|
+
* broken rather than as abbreviated. `SidebarItem.short` is the fix, not the ellipsis. */
|
|
27
96
|
.label {
|
|
28
97
|
font-size: var(--vx-text-micro);
|
|
29
98
|
line-height: 1;
|
|
30
99
|
font-weight: 500;
|
|
100
|
+
max-width: 100%;
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
text-overflow: clip;
|
|
103
|
+
white-space: nowrap;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* The menu pops UP out of its tab and is configured `flip: false` — flipping a bottom-anchored
|
|
107
|
+
* menu would put it under the footer. `menuMax` is what guarantees it fits (§2.2). */
|
|
108
|
+
.menuDropdown {
|
|
109
|
+
padding: 4px;
|
|
110
|
+
max-height: min(60dvh, 360px);
|
|
111
|
+
overflow-y: auto;
|
|
112
|
+
overscroll-behavior: contain;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* The theme's `Menu.extend` pins `styles.item` padding + font-size to the dense DESKTOP values
|
|
116
|
+
* INLINE, so a touch-sized mobile row needs `!important` — same cascade problem, same reason, as
|
|
117
|
+
* `.row` below. */
|
|
118
|
+
.menuItem {
|
|
119
|
+
min-height: var(--vx-space-mobile-nav-row-height) !important;
|
|
120
|
+
align-items: center;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.menuItem :global(.mantine-Menu-itemLabel) {
|
|
124
|
+
font-size: var(--mantine-font-size-md) !important;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* An active destination reads the same in the More surface as it does in the sheet. */
|
|
128
|
+
.menuItem[aria-current='page'] {
|
|
129
|
+
background-color: color-mix(in srgb, var(--vx-ink) 13%, transparent);
|
|
130
|
+
font-weight: 600;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* A disabled destination still RENDERS in the menu (§2.3 rule 11) — it just must not be reachable.
|
|
135
|
+
* Mantine's `Menu.css` styles `[data-disabled]` with a dimmed color and nothing else, and the row
|
|
136
|
+
* is polymorphic (`component="a"`), so the native `disabled` attribute does not apply either: with
|
|
137
|
+
* no rule here the row stays fully clickable. `pointer-events: none` is the real guard; `menuRow`
|
|
138
|
+
* carries a matching handler guard for the paths pointers do not cover (keyboard, synthetic
|
|
139
|
+
* clicks, a consumer stylesheet that resets this).
|
|
140
|
+
*/
|
|
141
|
+
.menuItem[data-disabled] {
|
|
142
|
+
pointer-events: none;
|
|
143
|
+
cursor: not-allowed;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Nesting survives to mobile — a child destination is an indented row, not a sibling of its own
|
|
147
|
+
* parent (which is what today's flattened projection makes it). */
|
|
148
|
+
.menuItemNested {
|
|
149
|
+
padding-inline-start: calc(var(--mantine-spacing-md) + 12px) !important;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Overflow section heading inside the menu — inherits the theme's mono micro-label. */
|
|
153
|
+
.menuLabel {
|
|
154
|
+
font-family: var(--basalt-font-mono);
|
|
155
|
+
font-size: var(--vx-text-micro);
|
|
156
|
+
font-weight: 500;
|
|
157
|
+
text-transform: uppercase;
|
|
158
|
+
letter-spacing: 0.08em;
|
|
159
|
+
color: var(--vx-faint);
|
|
31
160
|
}
|
|
32
161
|
|
|
33
162
|
/*
|
|
@@ -39,6 +168,8 @@
|
|
|
39
168
|
box-shadow: var(--vx-shadow-card);
|
|
40
169
|
border-top-left-radius: var(--mantine-radius-lg);
|
|
41
170
|
border-top-right-radius: var(--mantine-radius-lg);
|
|
171
|
+
/* A Drawer is NOT an AppShell section, so Mantine adds no inset of its own here. */
|
|
172
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
42
173
|
}
|
|
43
174
|
|
|
44
175
|
/* Sheet title = a micro-label (docs/DESIGN-SPEC.md §3), matching the sidebar's `.sectionLabel`. */
|
|
@@ -51,7 +182,17 @@
|
|
|
51
182
|
color: var(--vx-faint);
|
|
52
183
|
}
|
|
53
184
|
|
|
54
|
-
/*
|
|
185
|
+
/* The dismiss affordance. There is no swipe-to-dismiss: Mantine ships no drag primitive and
|
|
186
|
+
* writing `transform` to Drawer.Content fights its own Transition on the same node. */
|
|
187
|
+
.grabber {
|
|
188
|
+
width: 36px;
|
|
189
|
+
height: 4px;
|
|
190
|
+
margin: 0 auto 8px;
|
|
191
|
+
border-radius: 999px;
|
|
192
|
+
background-color: color-mix(in srgb, var(--vx-ink) 18%, transparent);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Large tap rows inside the sheet. NavLink rows reuse the sidebar's neutral active fill.
|
|
55
196
|
* `padding` needs `!important`: the theme's `NavLink.extend` forces the dense desktop row padding
|
|
56
197
|
* (6px 10px) INLINE on every NavLink root, which would shrink these touch targets otherwise. */
|
|
57
198
|
.row {
|
|
@@ -59,13 +200,23 @@
|
|
|
59
200
|
align-items: center;
|
|
60
201
|
gap: var(--mantine-spacing-sm);
|
|
61
202
|
width: 100%;
|
|
203
|
+
min-height: var(--vx-space-mobile-nav-row-height);
|
|
62
204
|
padding: var(--mantine-spacing-sm) var(--mantine-spacing-md) !important;
|
|
63
205
|
border-radius: var(--mantine-radius-sm);
|
|
64
206
|
color: var(--mantine-color-text);
|
|
207
|
+
touch-action: manipulation;
|
|
208
|
+
user-select: none;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Hover must be gated: on touch an ungated :hover leaves the last-tapped row stuck lit. */
|
|
212
|
+
@media (hover: hover) {
|
|
213
|
+
.row:hover {
|
|
214
|
+
background-color: color-mix(in srgb, var(--vx-ink) 8%, transparent);
|
|
215
|
+
}
|
|
65
216
|
}
|
|
66
217
|
|
|
67
|
-
.row:
|
|
68
|
-
background-color: color-mix(in srgb, var(--vx-ink)
|
|
218
|
+
.row:active {
|
|
219
|
+
background-color: color-mix(in srgb, var(--vx-ink) 10%, transparent);
|
|
69
220
|
}
|
|
70
221
|
|
|
71
222
|
/*
|
|
@@ -76,8 +227,7 @@
|
|
|
76
227
|
* `classNames` are always injected, so the icon needs its own `!important` override here to hold
|
|
77
228
|
* the mobile-only "never accent" doctrine.
|
|
78
229
|
*/
|
|
79
|
-
.row[data-active]
|
|
80
|
-
.row[data-active]:hover {
|
|
230
|
+
.row[data-active] {
|
|
81
231
|
--nl-color: var(--mantine-color-text);
|
|
82
232
|
color: var(--mantine-color-text);
|
|
83
233
|
background-color: color-mix(in srgb, var(--vx-ink) 13%, transparent);
|
|
@@ -88,13 +238,18 @@
|
|
|
88
238
|
color: var(--mantine-color-text) !important;
|
|
89
239
|
}
|
|
90
240
|
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
line-height: 1.2;
|
|
241
|
+
.rowNested {
|
|
242
|
+
padding-inline-start: calc(var(--mantine-spacing-md) + 12px) !important;
|
|
94
243
|
}
|
|
95
244
|
|
|
96
|
-
/*
|
|
97
|
-
* INLINE (`styles.label`), so `!important` is required to restore
|
|
245
|
+
/* Sheet rows read at the mobile body size — the theme pins the label at the dense desktop 13px
|
|
246
|
+
* INLINE (`styles.label`), so `!important` is required to restore it. */
|
|
98
247
|
.row :global(.mantine-NavLink-label) {
|
|
99
248
|
font-size: var(--mantine-font-size-md) !important;
|
|
100
249
|
}
|
|
250
|
+
|
|
251
|
+
@media (prefers-reduced-motion: reduce) {
|
|
252
|
+
.tabIcon {
|
|
253
|
+
transition: none;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Typed CSS-module declaration (the S4 CSS-modules path). Named string props — not an index
|
|
3
|
-
* signature — so dot access stays verbatim under the package's strict flags.
|
|
3
|
+
* signature — so dot access stays verbatim under the package's strict flags. HAND-MAINTAINED:
|
|
4
|
+
* there is no generator, so a new class in `app-mobile-nav.module.css` must be added here too.
|
|
4
5
|
*/
|
|
5
6
|
declare const classes: {
|
|
6
7
|
readonly bar: string
|
|
8
|
+
readonly mainSafeArea: string
|
|
7
9
|
readonly tab: string
|
|
10
|
+
readonly tabIcon: string
|
|
11
|
+
readonly tabDot: string
|
|
8
12
|
readonly label: string
|
|
13
|
+
readonly menuDropdown: string
|
|
14
|
+
readonly menuItem: string
|
|
15
|
+
readonly menuItemNested: string
|
|
16
|
+
readonly menuLabel: string
|
|
9
17
|
readonly sheet: string
|
|
10
18
|
readonly sheetTitle: string
|
|
19
|
+
readonly grabber: string
|
|
11
20
|
readonly row: string
|
|
12
|
-
readonly
|
|
21
|
+
readonly rowNested: string
|
|
13
22
|
}
|
|
14
23
|
export default classes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-mobile-nav.test.d.ts","sourceRoot":"","sources":["../../src/shell/app-mobile-nav.test.tsx"],"names":[],"mappings":""}
|
|
@@ -1,36 +1,13 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { BrandConfig, SettingsMenuItem
|
|
2
|
+
import type { BrandConfig, SettingsMenuItem } from './index.js';
|
|
3
|
+
import type { SidebarSection } from '../nav/types.js';
|
|
3
4
|
import type { BasaltAccountProps } from './account-types.js';
|
|
4
5
|
import type { SidebarSearchConfig } from './sidebar-search.js';
|
|
5
|
-
/**
|
|
6
|
-
* Renders a single nav item. The consumer's router `Link` lives here; `active` is precomputed.
|
|
7
|
-
*
|
|
8
|
-
* `opts.close` (item 20) is the mobile "More" drawer's close handle — invoke-safe when absent, a
|
|
9
|
-
* no-op on desktop. Call it from the rendered link's own `onClick` (e.g. before/after navigating)
|
|
10
|
-
* so tapping a nav row in the mobile drawer also closes it; the built-in fallback `<a>` path does
|
|
11
|
-
* this automatically.
|
|
12
|
-
*
|
|
13
|
-
* `opts.navLinkClassName` (item 18) is the internal `.link` CSS-module class the fallback `<a>`
|
|
14
|
-
* path uses for inactive/hover treatment — present whenever `AppSidebar` renders this callback.
|
|
15
|
-
* Apply it to the rendered link's `className` to match that styling exactly instead of
|
|
16
|
-
* re-deriving it.
|
|
17
|
-
*/
|
|
18
|
-
export type NavLinkRenderer = (item: SidebarItem, opts: {
|
|
19
|
-
active: boolean;
|
|
20
|
-
close?: (() => void) | undefined;
|
|
21
|
-
navLinkClassName?: string | undefined;
|
|
22
|
-
}) => ReactNode;
|
|
23
6
|
export type AppSidebarProps = {
|
|
24
7
|
brand: BrandConfig;
|
|
25
8
|
sections: SidebarSection[];
|
|
26
9
|
collapsed: boolean;
|
|
27
10
|
onToggleCollapse: () => void;
|
|
28
|
-
onClose: () => void;
|
|
29
|
-
/**
|
|
30
|
-
* Optional consumer link renderer (e.g. a router `<Link>`). Receives the precomputed `active`
|
|
31
|
-
* flag. When omitted, items fall back to a plain `<a href>` + `item.onClick`.
|
|
32
|
-
*/
|
|
33
|
-
renderNavLink?: NavLinkRenderer;
|
|
34
11
|
/**
|
|
35
12
|
* Footer settings-menu entries (theme switcher, devtools, …) — supplied by the consumer.
|
|
36
13
|
* The pinned footer "Settings" row renders ONLY when this is a non-empty list — apps that put
|
|
@@ -38,8 +15,6 @@ export type AppSidebarProps = {
|
|
|
38
15
|
* `brand.version` label renders inside this menu, so it also only shows when the row does.
|
|
39
16
|
*/
|
|
40
17
|
settingsMenuItems?: SettingsMenuItem[];
|
|
41
|
-
/** Extra content appended to the sidebar footer, beside the settings menu (mobile close, etc.). */
|
|
42
|
-
footerExtra?: ReactNode;
|
|
43
18
|
/**
|
|
44
19
|
* Optional account row rendered below the settings menu in the sidebar footer (see
|
|
45
20
|
* `SidebarAccount` / `BasaltAccountProps`), separated by its own top hairline. Omitting it
|
|
@@ -70,5 +45,5 @@ export type AppSidebarProps = {
|
|
|
70
45
|
*/
|
|
71
46
|
navExtra?: ReactNode;
|
|
72
47
|
};
|
|
73
|
-
export declare function AppSidebar({ brand, sections, collapsed, onToggleCollapse,
|
|
48
|
+
export declare function AppSidebar({ brand, sections, collapsed, onToggleCollapse, settingsMenuItems, account, search, navExtra, }: AppSidebarProps): import("react").JSX.Element;
|
|
74
49
|
//# sourceMappingURL=app-sidebar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-sidebar.d.ts","sourceRoot":"","sources":["../../src/shell/app-sidebar.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app-sidebar.d.ts","sourceRoot":"","sources":["../../src/shell/app-sidebar.tsx"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC5D,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,cAAc,CAAA;AAG/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAK3D,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,WAAW,CAAA;IAClB,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,SAAS,EAAE,OAAO,CAAA;IAClB,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAA;IACtC;;;;OAIG;IACH,OAAO,CAAC,EAAE,kBAAkB,CAAA;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,CAAA;AAqQD,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,iBAAiB,EACjB,OAAO,EACP,MAAM,EACN,QAAQ,GACT,EAAE,eAAe,+BAuJjB"}
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
UnstyledButton
|
|
15
15
|
} from "@mantine/core";
|
|
16
16
|
import { useDisclosure } from "@mantine/hooks";
|
|
17
|
-
import { useRef, useState } from "react";
|
|
17
|
+
import { useEffect, useRef, useState } from "react";
|
|
18
|
+
import { NavCountBadge } from "./nav-count-badge.js";
|
|
18
19
|
import { SidebarAccount } from "./app-sidebar-account.js";
|
|
19
20
|
import { SidebarSearch } from "./sidebar-search.js";
|
|
20
21
|
import { VX } from "../tokens/index.js";
|
|
@@ -78,69 +79,34 @@ function IconGear() {
|
|
|
78
79
|
}
|
|
79
80
|
);
|
|
80
81
|
}
|
|
81
|
-
function IconClose() {
|
|
82
|
-
return /* @__PURE__ */ jsxs(
|
|
83
|
-
"svg",
|
|
84
|
-
{
|
|
85
|
-
width: 18,
|
|
86
|
-
height: 18,
|
|
87
|
-
viewBox: "0 0 24 24",
|
|
88
|
-
fill: "none",
|
|
89
|
-
stroke: "currentColor",
|
|
90
|
-
strokeWidth: 2,
|
|
91
|
-
strokeLinecap: "round",
|
|
92
|
-
strokeLinejoin: "round",
|
|
93
|
-
"aria-hidden": true,
|
|
94
|
-
children: [
|
|
95
|
-
/* @__PURE__ */ jsx("path", { d: "M18 6l-12 12" }),
|
|
96
|
-
/* @__PURE__ */ jsx("path", { d: "M6 6l12 12" })
|
|
97
|
-
]
|
|
98
|
-
}
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
82
|
const HOVER_OPEN_DELAY = 150;
|
|
102
83
|
const HOVER_CLOSE_DELAY = 200;
|
|
103
84
|
function hasActiveDescendant(item) {
|
|
104
85
|
if (item.active) return true;
|
|
105
86
|
return item.children?.some((c) => hasActiveDescendant(c)) ?? false;
|
|
106
87
|
}
|
|
107
|
-
function NavLinkBody({
|
|
108
|
-
item
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
88
|
+
function NavLinkBody({ item, active }) {
|
|
89
|
+
const Anchor = item.Anchor;
|
|
90
|
+
const shared = {
|
|
91
|
+
classNames: { root: classes.link },
|
|
92
|
+
label: item.label,
|
|
93
|
+
leftSection: item.icon,
|
|
94
|
+
rightSection: item.badge ?? (item.count ? /* @__PURE__ */ jsx(NavCountBadge, { count: item.count }) : void 0),
|
|
95
|
+
active,
|
|
96
|
+
...item.onClick !== void 0 && { onClick: item.onClick }
|
|
97
|
+
};
|
|
98
|
+
if (Anchor) {
|
|
99
|
+
return /* @__PURE__ */ jsx(NavLink, { component: Anchor, ...shared });
|
|
115
100
|
}
|
|
116
|
-
return /* @__PURE__ */ jsx(
|
|
117
|
-
NavLink,
|
|
118
|
-
{
|
|
119
|
-
classNames: { root: classes.link },
|
|
120
|
-
component: "a",
|
|
121
|
-
label: item.label,
|
|
122
|
-
leftSection: item.icon,
|
|
123
|
-
rightSection: item.badge,
|
|
124
|
-
active,
|
|
125
|
-
...item.href !== void 0 && { href: item.href },
|
|
126
|
-
onClick: (e) => {
|
|
127
|
-
item.onClick?.(e);
|
|
128
|
-
close?.();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
);
|
|
101
|
+
return /* @__PURE__ */ jsx(NavLink, { component: "a", ...item.href !== void 0 && { href: item.href }, ...shared });
|
|
132
102
|
}
|
|
133
103
|
function SectionLabel({ children, flush }) {
|
|
134
104
|
return /* @__PURE__ */ jsx(Text, { component: "div", px: flush ? 0 : "xs", mb: flush ? 0 : 4, className: classes.sectionLabel, children });
|
|
135
105
|
}
|
|
136
|
-
function NavItemRow({
|
|
137
|
-
item,
|
|
138
|
-
collapsed,
|
|
139
|
-
renderNavLink,
|
|
140
|
-
closeMobile
|
|
141
|
-
}) {
|
|
106
|
+
function NavItemRow({ item, collapsed }) {
|
|
142
107
|
const [opened, { open, close }] = useDisclosure(false);
|
|
143
108
|
const timer = useRef(void 0);
|
|
109
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
144
110
|
const scheduleOpen = () => {
|
|
145
111
|
clearTimeout(timer.current);
|
|
146
112
|
timer.current = setTimeout(() => open(), HOVER_OPEN_DELAY);
|
|
@@ -164,28 +130,12 @@ function NavItemRow({
|
|
|
164
130
|
) }) }, item.key);
|
|
165
131
|
}
|
|
166
132
|
const active2 = Boolean(item.active);
|
|
167
|
-
return /* @__PURE__ */ jsx(Tooltip, { label: item.label, position: "right", withArrow: true, disabled: !collapsed, children: /* @__PURE__ */ jsx(Box, { className: classes.navItem, children: /* @__PURE__ */ jsx(
|
|
168
|
-
NavLinkBody,
|
|
169
|
-
{
|
|
170
|
-
item,
|
|
171
|
-
active: active2,
|
|
172
|
-
renderNavLink,
|
|
173
|
-
close: closeMobile
|
|
174
|
-
}
|
|
175
|
-
) }) }, item.key);
|
|
133
|
+
return /* @__PURE__ */ jsx(Tooltip, { label: item.label, position: "right", withArrow: true, disabled: !collapsed, children: /* @__PURE__ */ jsx(Box, { className: classes.navItem, children: /* @__PURE__ */ jsx(NavLinkBody, { item, active: active2 }) }) }, item.key);
|
|
176
134
|
}
|
|
177
135
|
const active = Boolean(item.active);
|
|
178
136
|
if (isExpanded) {
|
|
179
137
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
180
|
-
/* @__PURE__ */ jsx(Tooltip, { label: item.label, position: "right", withArrow: true, disabled: !collapsed, children: /* @__PURE__ */ jsx(Box, { className: classes.navItem, children: /* @__PURE__ */ jsx(
|
|
181
|
-
NavLinkBody,
|
|
182
|
-
{
|
|
183
|
-
item,
|
|
184
|
-
active,
|
|
185
|
-
renderNavLink,
|
|
186
|
-
close: closeMobile
|
|
187
|
-
}
|
|
188
|
-
) }) }),
|
|
138
|
+
/* @__PURE__ */ jsx(Tooltip, { label: item.label, position: "right", withArrow: true, disabled: !collapsed, children: /* @__PURE__ */ jsx(Box, { className: classes.navItem, children: /* @__PURE__ */ jsx(NavLinkBody, { item, active }) }) }),
|
|
189
139
|
/* @__PURE__ */ jsx(Stack, { gap: 0, className: classes.childList, children: item.children.map((child) => {
|
|
190
140
|
if (child.disabled) {
|
|
191
141
|
return /* @__PURE__ */ jsx(Tooltip, { label: "Coming soon", position: "right", withArrow: true, children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
|
|
@@ -199,15 +149,7 @@ function NavItemRow({
|
|
|
199
149
|
) }) }, child.key);
|
|
200
150
|
}
|
|
201
151
|
const childActive = Boolean(child.active);
|
|
202
|
-
return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
|
|
203
|
-
NavLinkBody,
|
|
204
|
-
{
|
|
205
|
-
item: child,
|
|
206
|
-
active: childActive,
|
|
207
|
-
renderNavLink,
|
|
208
|
-
close: closeMobile
|
|
209
|
-
}
|
|
210
|
-
) }, child.key);
|
|
152
|
+
return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(NavLinkBody, { item: child, active: childActive }) }, child.key);
|
|
211
153
|
}) })
|
|
212
154
|
] }, item.key);
|
|
213
155
|
}
|
|
@@ -231,15 +173,7 @@ function NavItemRow({
|
|
|
231
173
|
onMouseLeave: scheduleClose,
|
|
232
174
|
onFocus: scheduleOpen,
|
|
233
175
|
onBlur: scheduleClose,
|
|
234
|
-
children: /* @__PURE__ */ jsx(
|
|
235
|
-
NavLinkBody,
|
|
236
|
-
{
|
|
237
|
-
item,
|
|
238
|
-
active,
|
|
239
|
-
renderNavLink,
|
|
240
|
-
close: closeMobile
|
|
241
|
-
}
|
|
242
|
-
)
|
|
176
|
+
children: /* @__PURE__ */ jsx(NavLinkBody, { item, active })
|
|
243
177
|
}
|
|
244
178
|
) }) }),
|
|
245
179
|
/* @__PURE__ */ jsx(
|
|
@@ -264,41 +198,10 @@ function NavItemRow({
|
|
|
264
198
|
) }) }, child.key);
|
|
265
199
|
}
|
|
266
200
|
const childActive = Boolean(child.active);
|
|
267
|
-
return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
|
|
268
|
-
NavLinkBody,
|
|
269
|
-
{
|
|
270
|
-
item: child,
|
|
271
|
-
active: childActive,
|
|
272
|
-
renderNavLink,
|
|
273
|
-
close: closeMobile
|
|
274
|
-
}
|
|
275
|
-
) }, child.key);
|
|
201
|
+
return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(NavLinkBody, { item: child, active: childActive }) }, child.key);
|
|
276
202
|
}) })
|
|
277
203
|
}
|
|
278
|
-
)
|
|
279
|
-
isExpanded && /* @__PURE__ */ jsx(Stack, { gap: 0, className: classes.childList, children: item.children.map((child) => {
|
|
280
|
-
if (child.disabled) {
|
|
281
|
-
return /* @__PURE__ */ jsx(Tooltip, { label: "Coming soon", position: "right", withArrow: true, children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
|
|
282
|
-
NavLink,
|
|
283
|
-
{
|
|
284
|
-
classNames: { root: classes.link },
|
|
285
|
-
label: child.label,
|
|
286
|
-
leftSection: child.icon,
|
|
287
|
-
"data-disabled": true
|
|
288
|
-
}
|
|
289
|
-
) }) }, child.key);
|
|
290
|
-
}
|
|
291
|
-
const childActive = Boolean(child.active);
|
|
292
|
-
return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
|
|
293
|
-
NavLinkBody,
|
|
294
|
-
{
|
|
295
|
-
item: child,
|
|
296
|
-
active: childActive,
|
|
297
|
-
renderNavLink,
|
|
298
|
-
close: closeMobile
|
|
299
|
-
}
|
|
300
|
-
) }, child.key);
|
|
301
|
-
}) })
|
|
204
|
+
)
|
|
302
205
|
]
|
|
303
206
|
},
|
|
304
207
|
item.key
|
|
@@ -309,10 +212,7 @@ function AppSidebar({
|
|
|
309
212
|
sections,
|
|
310
213
|
collapsed,
|
|
311
214
|
onToggleCollapse,
|
|
312
|
-
onClose,
|
|
313
|
-
renderNavLink,
|
|
314
215
|
settingsMenuItems,
|
|
315
|
-
footerExtra,
|
|
316
216
|
account,
|
|
317
217
|
search,
|
|
318
218
|
navExtra
|
|
@@ -321,59 +221,25 @@ function AppSidebar({
|
|
|
321
221
|
const [collapsedSections, setCollapsedSections] = useState(
|
|
322
222
|
() => Object.fromEntries(sections.map((s) => [s.label, Boolean(s.defaultCollapsed)]))
|
|
323
223
|
);
|
|
324
|
-
const renderSectionItems = (section) => section.items.map((item) => /* @__PURE__ */ jsx(
|
|
325
|
-
NavItemRow,
|
|
326
|
-
{
|
|
327
|
-
item,
|
|
328
|
-
collapsed,
|
|
329
|
-
renderNavLink,
|
|
330
|
-
closeMobile: onClose
|
|
331
|
-
},
|
|
332
|
-
item.key
|
|
333
|
-
));
|
|
334
|
-
const mobileControls = /* @__PURE__ */ jsxs(Group, { gap: 2, wrap: "nowrap", hiddenFrom: "sm", children: [
|
|
335
|
-
footerExtra,
|
|
336
|
-
/* @__PURE__ */ jsx(
|
|
337
|
-
ActionIcon,
|
|
338
|
-
{
|
|
339
|
-
variant: "subtle",
|
|
340
|
-
className: classes.ghostIcon,
|
|
341
|
-
onClick: onClose,
|
|
342
|
-
"aria-label": "Close navigation",
|
|
343
|
-
children: /* @__PURE__ */ jsx(IconClose, {})
|
|
344
|
-
}
|
|
345
|
-
)
|
|
346
|
-
] });
|
|
224
|
+
const renderSectionItems = (section) => section.items.map((item) => /* @__PURE__ */ jsx(NavItemRow, { item, collapsed }, item.key));
|
|
347
225
|
const hasSettingsMenu = (settingsMenuItems?.length ?? 0) > 0;
|
|
348
|
-
const settingsRow = hasSettingsMenu ? /* @__PURE__ */
|
|
349
|
-
/* @__PURE__ */
|
|
350
|
-
/* @__PURE__ */ jsx(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
/* @__PURE__ */
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
brand.version
|
|
362
|
-
] })
|
|
226
|
+
const settingsRow = hasSettingsMenu ? /* @__PURE__ */ jsx(Group, { ...account ? {} : { className: classes.footer }, gap: "xs", wrap: "nowrap", children: /* @__PURE__ */ jsxs(Menu, { position: "right-start", withArrow: true, width: step.sidebarSettingsMenuWidth, zIndex: 500, children: [
|
|
227
|
+
/* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsxs(UnstyledButton, { className: classes.footerBtn, "aria-label": "Settings", children: [
|
|
228
|
+
/* @__PURE__ */ jsx(IconGear, {}),
|
|
229
|
+
/* @__PURE__ */ jsx(Text, { className: classes.footerText, fz: VX.text.md, children: "Settings" })
|
|
230
|
+
] }) }),
|
|
231
|
+
/* @__PURE__ */ jsxs(Menu.Dropdown, { children: [
|
|
232
|
+
settingsMenuItems?.map((entry) => /* @__PURE__ */ jsx(Menu.Item, { leftSection: entry.icon, onClick: entry.onClick, children: entry.label }, entry.key)),
|
|
233
|
+
brand.version && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
234
|
+
/* @__PURE__ */ jsx(Menu.Divider, {}),
|
|
235
|
+
/* @__PURE__ */ jsxs(Menu.Label, { children: [
|
|
236
|
+
brand.name,
|
|
237
|
+
" v",
|
|
238
|
+
brand.version
|
|
363
239
|
] })
|
|
364
240
|
] })
|
|
365
|
-
] })
|
|
366
|
-
|
|
367
|
-
] }) : /* @__PURE__ */ jsx(
|
|
368
|
-
Group,
|
|
369
|
-
{
|
|
370
|
-
...account ? {} : { className: classes.footer },
|
|
371
|
-
gap: "xs",
|
|
372
|
-
wrap: "nowrap",
|
|
373
|
-
hiddenFrom: "sm",
|
|
374
|
-
children: mobileControls
|
|
375
|
-
}
|
|
376
|
-
);
|
|
241
|
+
] })
|
|
242
|
+
] }) }) : null;
|
|
377
243
|
return /* @__PURE__ */ jsxs(Stack, { gap: 0, h: "100%", className: classes.root, "data-collapsed": collapsed || void 0, children: [
|
|
378
244
|
/* @__PURE__ */ jsxs(Group, { className: classes.brand, gap: "sm", wrap: "nowrap", children: [
|
|
379
245
|
/* @__PURE__ */ jsxs(Group, { className: classes.brandLead, gap: "sm", wrap: "nowrap", children: [
|
|
@@ -395,7 +261,7 @@ function AppSidebar({
|
|
|
395
261
|
variant: "subtle",
|
|
396
262
|
size: 28,
|
|
397
263
|
visibleFrom: "sm",
|
|
398
|
-
className:
|
|
264
|
+
className: classes.ghostIcon,
|
|
399
265
|
onClick: onToggleCollapse,
|
|
400
266
|
"aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
|
|
401
267
|
children: /* @__PURE__ */ jsx(IconCollapse, { collapsed })
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The active tint and icon-accent color live in the Mantine theme
|
|
10
10
|
* (`NavLink.extend` + `theme/nav-link.module.css`) so they apply to EVERY render path, including a
|
|
11
|
-
* consumer's router `<Link>`
|
|
11
|
+
* consumer's router `<Link>` hosted through `SidebarItem.Anchor` (which never sees this CSS
|
|
12
|
+
* module). This module
|
|
12
13
|
* owns the shell-local layout (docs/DESIGN-SPEC.md §5) and the INACTIVE state (muted text, faint
|
|
13
14
|
* icon, ink-6% hover) — the active state is left to the theme.
|
|
14
15
|
*
|
|
@@ -382,7 +383,7 @@
|
|
|
382
383
|
}
|
|
383
384
|
|
|
384
385
|
/* Collapse every nav row to its icon. Targets `.navItem` — the wrapper Box present on EVERY render
|
|
385
|
-
* path — so it covers the shell's fallback NavLink AND a consumer's `
|
|
386
|
+
* path — so it covers the shell's fallback NavLink AND a consumer's `SidebarItem.Anchor` (e.g. a router
|
|
386
387
|
* <Link>), both of which render a Mantine NavLink inside. Keying off `.link` alone missed consumer
|
|
387
388
|
* links, so their labels overflowed the 64px rail. */
|
|
388
389
|
.root[data-collapsed] .navItem :global(.mantine-NavLink-root) {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Typed CSS-module declaration (the S4 CSS-modules path). Named string props — not an index
|
|
3
3
|
* signature — so dot access (`classes.link`) stays verbatim under the package's strict flags
|
|
4
4
|
* (`noUncheckedIndexedAccess`, `noPropertyAccessFromIndexSignature`, `exactOptionalPropertyTypes`).
|
|
5
|
-
* `collapseBtn` is referenced by the component for parity with argo though it has no CSS rule.
|
|
6
5
|
*/
|
|
7
6
|
declare const classes: {
|
|
8
7
|
readonly root: string
|
|
@@ -15,7 +14,6 @@ declare const classes: {
|
|
|
15
14
|
readonly navScroll: string
|
|
16
15
|
readonly navViewport: string
|
|
17
16
|
readonly nav: string
|
|
18
|
-
readonly collapseBtn: string
|
|
19
17
|
readonly footer: string
|
|
20
18
|
readonly footerBtn: string
|
|
21
19
|
readonly footerText: string
|