basalt-ui 1.29.2 → 1.30.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/CHANGELOG.md +9 -0
- package/MIGRATING.md +693 -368
- package/README.md +23 -2
- package/agent/rules/basalt-controls.md +34 -34
- package/agent/skills/basalt-app/SKILL.md +4 -2
- package/agent/templates/query-client.ts.tpl +18 -4
- package/configs/lefthook.yml +51 -19
- package/configs/oxlint-plugin.js +210 -84
- package/configs/oxlint-plugin.test.ts +182 -5
- package/configs/oxlint.json +4 -4
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +2 -2
- package/dist/charts/primitives/ChartFrame.d.ts +23 -3
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +4 -4
- package/dist/charts/primitives/chart-frame-layout.d.ts +55 -0
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +16 -0
- package/dist/cli/check-theme.d.ts.map +1 -1
- package/dist/cli/check-theme.js +6 -0
- package/dist/cli/doctor.d.ts +10 -4
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +28 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +2 -1
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +29 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +4 -15
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +8 -14
- package/dist/controls/actions.d.ts.map +1 -1
- package/dist/controls/actions.js +12 -2
- package/dist/dashboard/section.module.css +15 -7
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +7 -2
- package/dist/dashboard/stat-group.d.ts +8 -2
- package/dist/dashboard/stat-group.d.ts.map +1 -1
- package/dist/dashboard/stat-group.js +23 -15
- package/dist/dashboard/stat-group.module.css +85 -9
- package/dist/dashboard/stat-group.module.css.d.ts +1 -0
- package/dist/dashboard/widget-grid.d.ts.map +1 -1
- package/dist/dashboard/widget-grid.js +4 -5
- package/dist/dashboard/widget-grid.module.css +64 -21
- package/dist/dashboard/widget-grid.module.css.d.ts +1 -0
- package/dist/guard/index.d.ts +19 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +27 -4
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/shell/app-brand.module.css +12 -3
- package/dist/shell/app-header.module.css +49 -9
- package/dist/shell/app-main.module.css +24 -4
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -51
- package/dist/shell/app-mobile-nav.module.css +99 -11
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.module.css +55 -2
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +3 -3
- package/dist/shell/mobile-nav-model.d.ts +26 -7
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +12 -3
- package/dist/shell/page-aside.module.css +8 -6
- package/dist/shell/page-bar.d.ts +13 -0
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +13 -3
- package/dist/shell/page-bar.module.css +20 -12
- package/dist/shell/page-title.module.css +3 -1
- package/dist/theme/use-basalt-spacing.d.ts.map +1 -1
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +51 -7
- package/dist/tokens/palette.d.ts +98 -17
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +110 -18
- package/dist/tokens.css +17 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/primitives/ChartCard.tsx +12 -7
- package/src/charts/primitives/ChartFrame.tsx +30 -15
- package/src/charts/primitives/chart-frame-layout.ts +86 -0
- package/src/cli/check-theme.ts +13 -0
- package/src/cli/doctor.ts +49 -8
- package/src/cli/help.ts +5 -3
- package/src/cli/index.ts +49 -0
- package/src/cli/init.ts +6 -27
- package/src/cli/sync.ts +11 -22
- package/src/controls/actions.tsx +51 -0
- package/src/dashboard/section.module.css +15 -7
- package/src/dashboard/stat-card.tsx +7 -2
- package/src/dashboard/stat-group.module.css +85 -9
- package/src/dashboard/stat-group.module.css.d.ts +1 -0
- package/src/dashboard/stat-group.tsx +40 -20
- package/src/dashboard/widget-grid.module.css +64 -21
- package/src/dashboard/widget-grid.module.css.d.ts +1 -0
- package/src/dashboard/widget-grid.tsx +30 -12
- package/src/guard/index.ts +79 -11
- package/src/index.ts +9 -0
- package/src/shell/app-brand.module.css +12 -3
- package/src/shell/app-header.module.css +49 -9
- package/src/shell/app-main.module.css +24 -4
- package/src/shell/app-mobile-nav.module.css +99 -11
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +225 -83
- package/src/shell/app-sidebar.module.css +55 -2
- package/src/shell/index.tsx +26 -6
- package/src/shell/mobile-nav-model.ts +37 -9
- package/src/shell/page-aside.module.css +8 -6
- package/src/shell/page-bar.module.css +20 -12
- package/src/shell/page-bar.tsx +39 -5
- package/src/shell/page-title.module.css +3 -1
- package/src/theme/use-basalt-spacing.ts +8 -6
- package/src/tokens/index.ts +90 -6
- package/src/tokens/palette.ts +111 -18
|
@@ -106,45 +106,27 @@ function settingsRows(items) {
|
|
|
106
106
|
...item.active !== void 0 && { active: item.active }
|
|
107
107
|
}));
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
function deepestScrolledDescendant(root) {
|
|
110
|
+
if (root === null) return null;
|
|
111
|
+
let best = null;
|
|
112
|
+
let bestDepth = -1;
|
|
113
|
+
for (const node of root.querySelectorAll("*")) {
|
|
114
|
+
if (node.scrollTop <= 0) continue;
|
|
115
|
+
if (node.getClientRects().length === 0) continue;
|
|
116
|
+
let depth = 0;
|
|
117
|
+
for (let parent = node.parentElement; parent !== null; parent = parent.parentElement) depth += 1;
|
|
118
|
+
if (depth > bestDepth) {
|
|
119
|
+
best = node;
|
|
120
|
+
bestDepth = depth;
|
|
114
121
|
}
|
|
115
|
-
item.onClick?.(event);
|
|
116
|
-
};
|
|
117
|
-
const shared = {
|
|
118
|
-
className: depth > 0 ? `${classes.menuItem} ${classes.menuItemNested}` : classes.menuItem,
|
|
119
|
-
leftSection: item.icon,
|
|
120
|
-
disabled: Boolean(item.disabled),
|
|
121
|
-
// Mantine emits `data-disabled` (a STYLING hook) and nothing else, so the ARIA state is ours
|
|
122
|
-
// to set — a disabled destination still renders (§2.3 rule 11), so it has to announce itself.
|
|
123
|
-
"aria-disabled": item.disabled || void 0,
|
|
124
|
-
"aria-current": item.active ? "page" : void 0,
|
|
125
|
-
rightSection: item.count ? /* @__PURE__ */ jsx(NavCountBadge, { count: item.count }) : void 0,
|
|
126
|
-
onClick
|
|
127
|
-
};
|
|
128
|
-
const Anchor = item.Anchor;
|
|
129
|
-
if (Anchor && !item.disabled) {
|
|
130
|
-
return /* @__PURE__ */ jsx(Menu.Item, { component: Anchor, ...shared, children: item.label }, item.key);
|
|
131
122
|
}
|
|
132
|
-
return
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
component: "a",
|
|
136
|
-
...item.href !== void 0 && !item.disabled && { href: item.href },
|
|
137
|
-
...shared,
|
|
138
|
-
children: item.label
|
|
139
|
-
},
|
|
140
|
-
item.key
|
|
141
|
-
);
|
|
142
|
-
};
|
|
123
|
+
return best;
|
|
124
|
+
}
|
|
143
125
|
const rows = (items, depth, render) => items.flatMap((item) => [render(item, depth), ...rows(item.children ?? [], depth + 1, render)]);
|
|
144
|
-
const menuActionRow = (row) => /* @__PURE__ */ jsx(
|
|
126
|
+
const menuActionRow = (row, sectionStart = false) => /* @__PURE__ */ jsx(
|
|
145
127
|
Menu.Item,
|
|
146
128
|
{
|
|
147
|
-
className: classes.menuItem,
|
|
129
|
+
className: sectionStart ? `${classes.menuItem} ${classes.menuSectionStart}` : classes.menuItem,
|
|
148
130
|
leftSection: row.icon,
|
|
149
131
|
rightSection: row.active ? /* @__PURE__ */ jsx(IconCheck, {}) : void 0,
|
|
150
132
|
...row.danger ? { color: "red" } : {},
|
|
@@ -170,24 +152,49 @@ function MobileNav({
|
|
|
170
152
|
const reduceMotion = useReducedMotion();
|
|
171
153
|
const { step } = useBasaltSpacing();
|
|
172
154
|
const close = () => setOpenKey(null);
|
|
173
|
-
const
|
|
174
|
-
|
|
155
|
+
const scrollElementToTop = (target) => {
|
|
156
|
+
if (!target || target.scrollTop <= 0) return false;
|
|
175
157
|
target.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" });
|
|
158
|
+
return true;
|
|
159
|
+
};
|
|
160
|
+
const scrollToTop = () => {
|
|
161
|
+
const declared = config?.getScrollElement?.() ?? document.querySelector(`[${SCROLLPORT_ATTRIBUTE}]`);
|
|
162
|
+
if (scrollElementToTop(declared)) return true;
|
|
163
|
+
if (scrollElementToTop(deepestScrolledDescendant(declared ?? document.body))) return true;
|
|
164
|
+
return scrollElementToTop(document.scrollingElement ?? document.documentElement);
|
|
165
|
+
};
|
|
166
|
+
const onActivate = (item, event) => {
|
|
167
|
+
if (item.disabled) {
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (item.active === true && scrollToTop()) event.preventDefault();
|
|
172
|
+
item.onClick?.(event);
|
|
173
|
+
return true;
|
|
176
174
|
};
|
|
177
175
|
const extraRows = [...accountRows(account), ...settingsRows(settingsMenuItems)];
|
|
178
176
|
const moreBlocks = (blocks ?? []).filter(
|
|
179
177
|
(block) => block.kind !== "custom" && sidebarBlockMobile(block) === "more" && (block.kind !== "list" || block.items.length > 0)
|
|
180
178
|
);
|
|
181
179
|
const listBlocks = moreBlocks.filter((block) => block.kind === "list");
|
|
180
|
+
const tailRule = (slot) => moreBlocks.length + extraRows.length > 0 && slotHasRows(slot);
|
|
182
181
|
const activateBlock = (block) => {
|
|
183
182
|
close();
|
|
184
183
|
if (block.kind === "list") {
|
|
185
|
-
setOpenBlockKey(block.key);
|
|
184
|
+
requestAnimationFrame(() => setOpenBlockKey(block.key));
|
|
186
185
|
return;
|
|
187
186
|
}
|
|
188
187
|
block.onClick?.();
|
|
189
188
|
};
|
|
190
|
-
const menuBlockRow = (block) => /* @__PURE__ */ jsx(
|
|
189
|
+
const menuBlockRow = (block, sectionStart = false) => /* @__PURE__ */ jsx(
|
|
190
|
+
Menu.Item,
|
|
191
|
+
{
|
|
192
|
+
className: sectionStart ? `${classes.menuItem} ${classes.menuSectionStart}` : classes.menuItem,
|
|
193
|
+
onClick: () => activateBlock(block),
|
|
194
|
+
children: blockRowLabel(block)
|
|
195
|
+
},
|
|
196
|
+
block.key
|
|
197
|
+
);
|
|
191
198
|
const sheetBlockRow = (block) => /* @__PURE__ */ jsx(
|
|
192
199
|
NavLink,
|
|
193
200
|
{
|
|
@@ -238,24 +245,20 @@ function MobileNav({
|
|
|
238
245
|
] }),
|
|
239
246
|
/* @__PURE__ */ jsx(Text, { className: classes.label, children: slot.short })
|
|
240
247
|
] });
|
|
241
|
-
const surfaceTab = (slot, onClick) => /* @__PURE__ */ jsx(
|
|
248
|
+
const surfaceTab = (slot, onClick, expanded) => /* @__PURE__ */ jsx(
|
|
242
249
|
UnstyledButton,
|
|
243
250
|
{
|
|
244
251
|
className: classes.tab,
|
|
245
252
|
"data-active": slot.active || void 0,
|
|
246
253
|
"aria-current": slot.active ? "page" : void 0,
|
|
247
254
|
"aria-label": slot.label,
|
|
248
|
-
...onClick ? { onClick } : {},
|
|
255
|
+
...onClick ? { onClick, "aria-haspopup": "dialog", "aria-expanded": Boolean(expanded) } : {},
|
|
249
256
|
children: tabInner(slot)
|
|
250
257
|
}
|
|
251
258
|
);
|
|
252
259
|
const linkTab = (slot) => {
|
|
253
260
|
const onClick = (e) => {
|
|
254
|
-
|
|
255
|
-
e.preventDefault();
|
|
256
|
-
scrollToTop();
|
|
257
|
-
}
|
|
258
|
-
slot.item.onClick?.(e);
|
|
261
|
+
onActivate(slot.item, e);
|
|
259
262
|
};
|
|
260
263
|
const shared = {
|
|
261
264
|
className: classes.tab,
|
|
@@ -279,9 +282,38 @@ function MobileNav({
|
|
|
279
282
|
slot.key
|
|
280
283
|
);
|
|
281
284
|
};
|
|
285
|
+
const menuRow = (item, depth) => {
|
|
286
|
+
const shared = {
|
|
287
|
+
className: depth > 0 ? `${classes.menuItem} ${classes.menuItemNested}` : classes.menuItem,
|
|
288
|
+
leftSection: item.icon,
|
|
289
|
+
disabled: Boolean(item.disabled),
|
|
290
|
+
// Mantine emits `data-disabled` (a STYLING hook) and nothing else, so the ARIA state is ours
|
|
291
|
+
// to set — a disabled destination still renders (§2.3 rule 11), so it has to announce itself.
|
|
292
|
+
"aria-disabled": item.disabled || void 0,
|
|
293
|
+
"aria-current": item.active ? "page" : void 0,
|
|
294
|
+
rightSection: item.count ? /* @__PURE__ */ jsx(NavCountBadge, { count: item.count }) : void 0,
|
|
295
|
+
onClick: (event) => {
|
|
296
|
+
onActivate(item, event);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const Anchor = item.Anchor;
|
|
300
|
+
if (Anchor && !item.disabled) {
|
|
301
|
+
return /* @__PURE__ */ jsx(Menu.Item, { component: Anchor, ...shared, children: item.label }, item.key);
|
|
302
|
+
}
|
|
303
|
+
return /* @__PURE__ */ jsx(
|
|
304
|
+
Menu.Item,
|
|
305
|
+
{
|
|
306
|
+
component: "a",
|
|
307
|
+
...item.href !== void 0 && !item.disabled && { href: item.href },
|
|
308
|
+
...shared,
|
|
309
|
+
children: item.label
|
|
310
|
+
},
|
|
311
|
+
item.key
|
|
312
|
+
);
|
|
313
|
+
};
|
|
282
314
|
const sheetRow = (item, depth) => {
|
|
283
315
|
const onClick = (e) => {
|
|
284
|
-
item
|
|
316
|
+
if (!onActivate(item, e)) return;
|
|
285
317
|
close();
|
|
286
318
|
};
|
|
287
319
|
const shared = {
|
|
@@ -335,7 +367,7 @@ function MobileNav({
|
|
|
335
367
|
onChange: (opened) => setOpenKey(opened ? slot.key : null),
|
|
336
368
|
position: slot.isMore ? "top-end" : "top",
|
|
337
369
|
offset: 8,
|
|
338
|
-
width: step.mobileNavMenuWidth,
|
|
370
|
+
width: slot.isMore ? "min(78vw, 300px)" : step.mobileNavMenuWidth,
|
|
339
371
|
withinPortal: true,
|
|
340
372
|
zIndex: 400,
|
|
341
373
|
middlewares: MENU_MIDDLEWARES,
|
|
@@ -358,22 +390,29 @@ function MobileNav({
|
|
|
358
390
|
...group.label ? [/* @__PURE__ */ jsx(Menu.Label, { children: group.label }, `${group.key}-label`)] : [],
|
|
359
391
|
...rows(group.items, 0, menuRow)
|
|
360
392
|
]),
|
|
361
|
-
slot.isMore
|
|
362
|
-
slot.isMore ?
|
|
363
|
-
|
|
393
|
+
slot.isMore ? moreBlocks.map((block, index) => menuBlockRow(block, tailRule(slot) && index === 0)) : null,
|
|
394
|
+
slot.isMore ? extraRows.map(
|
|
395
|
+
(row, index) => menuActionRow(row, tailRule(slot) && moreBlocks.length === 0 && index === 0)
|
|
396
|
+
) : null
|
|
364
397
|
] })
|
|
365
398
|
]
|
|
366
399
|
},
|
|
367
400
|
slot.key
|
|
368
401
|
);
|
|
369
402
|
const sheetSlot = (slot) => /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
370
|
-
surfaceTab(
|
|
403
|
+
surfaceTab(
|
|
404
|
+
slot,
|
|
405
|
+
() => setOpenKey(openKey === slot.key ? null : slot.key),
|
|
406
|
+
openKey === slot.key
|
|
407
|
+
),
|
|
371
408
|
/* @__PURE__ */ jsx(
|
|
372
409
|
Drawer,
|
|
373
410
|
{
|
|
374
411
|
opened: openKey === slot.key,
|
|
375
412
|
onClose: close,
|
|
376
413
|
position: "bottom",
|
|
414
|
+
zIndex: 400,
|
|
415
|
+
overlayProps: { zIndex: 400 },
|
|
377
416
|
padding: "md",
|
|
378
417
|
title: slot.label,
|
|
379
418
|
classNames: {
|
|
@@ -384,6 +423,10 @@ function MobileNav({
|
|
|
384
423
|
transitionProps: {
|
|
385
424
|
transition: "slide-up",
|
|
386
425
|
duration: reduceMotion ? 0 : 220,
|
|
426
|
+
// Asymmetric on purpose: the entrance may take its time, the EXIT is a hit-test window.
|
|
427
|
+
// Mantine's default exit equals the duration, so a dismissed sheet left 220ms of live
|
|
428
|
+
// overlay over the bar — long enough to eat the next tab tap.
|
|
429
|
+
exitDuration: reduceMotion ? 0 : 120,
|
|
387
430
|
timingFunction: "cubic-bezier(0.32, 0.72, 0, 1)"
|
|
388
431
|
},
|
|
389
432
|
children: /* @__PURE__ */ jsx(ScrollArea.Autosize, { mah: "62dvh", type: "scroll", offsetScrollbars: "present", scrollbarSize: 8, children: /* @__PURE__ */ jsxs(Stack, { gap: "var(--vx-space-sidebar-section-gap)", children: [
|
|
@@ -403,6 +446,8 @@ function MobileNav({
|
|
|
403
446
|
opened: openBlockKey === block.key,
|
|
404
447
|
onClose: () => setOpenBlockKey(null),
|
|
405
448
|
position: "bottom",
|
|
449
|
+
zIndex: 400,
|
|
450
|
+
overlayProps: { zIndex: 400 },
|
|
406
451
|
padding: "md",
|
|
407
452
|
title: block.label,
|
|
408
453
|
classNames: {
|
|
@@ -413,6 +458,8 @@ function MobileNav({
|
|
|
413
458
|
transitionProps: {
|
|
414
459
|
transition: "slide-up",
|
|
415
460
|
duration: reduceMotion ? 0 : 220,
|
|
461
|
+
// Same reasoning as `sheetSlot`'s Drawer — a long exit leaves a live overlay over the bar.
|
|
462
|
+
exitDuration: reduceMotion ? 0 : 120,
|
|
416
463
|
timingFunction: "cubic-bezier(0.32, 0.72, 0, 1)"
|
|
417
464
|
},
|
|
418
465
|
children: /* @__PURE__ */ jsx(ScrollArea.Autosize, { mah: "62dvh", type: "scroll", offsetScrollbars: "present", scrollbarSize: 8, children: /* @__PURE__ */ jsx(Stack, { gap: 1, children: block.items.map(blockItemRow) }) })
|
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
align-items: stretch;
|
|
10
10
|
width: 100%;
|
|
11
11
|
height: 100%;
|
|
12
|
-
|
|
12
|
+
/* NOT `--mantine-color-body`: that is the EXACT fill the scrollport above paints (both resolve
|
|
13
|
+
* to `--vx-surface-bg`), so the only thing separating the bar from the page was the region
|
|
14
|
+
* seam's 9%-alpha hairline — invisible on a phone, which is the "it could also have a border"
|
|
15
|
+
* report. `--vx-surface-panel` is a derived step above bg on both schemes and is the same
|
|
16
|
+
* relation `.sheet` already uses, so the edge is a real tonal one with no new token, no drop
|
|
17
|
+
* shadow (DESIGN-CORE pins depth to three tokens, none of them an upward-cast footer shadow)
|
|
18
|
+
* and no `withBorder` on the shell region (basalt-mantine.md forbids drawing that ourselves). */
|
|
19
|
+
background-color: var(--vx-surface-panel);
|
|
13
20
|
-webkit-tap-highlight-color: transparent;
|
|
14
21
|
/* NO padding-bottom / env() here: Mantine's own AppShell `.footer` rule already does
|
|
15
22
|
* `height: calc(var(--app-shell-footer-height) + env(safe-area-inset-bottom))` +
|
|
@@ -90,7 +97,8 @@
|
|
|
90
97
|
height: 8px;
|
|
91
98
|
border-radius: 999px;
|
|
92
99
|
background-color: var(--vx-accent);
|
|
93
|
-
|
|
100
|
+
/* The halo punches the dot out of whatever the BAR paints, so it tracks `.bar`'s fill. */
|
|
101
|
+
box-shadow: 0 0 0 2px var(--vx-surface-panel);
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
/* `text-overflow: clip`, never an ellipsis — a truncated word with a "…" in a 72px tab reads as
|
|
@@ -106,10 +114,25 @@
|
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
/* The menu pops UP out of its tab and is configured `flip: false` — flipping a bottom-anchored
|
|
109
|
-
* menu would put it under the footer
|
|
117
|
+
* menu would put it under the footer, so something has to guarantee it fits. That something is the
|
|
118
|
+
* `max-height` below, not `menuMax` (§2.2 as it used to be written). */
|
|
110
119
|
.menuDropdown {
|
|
111
|
-
padding:
|
|
112
|
-
|
|
120
|
+
padding: var(--vx-space-stack-xs);
|
|
121
|
+
/* THIS is what guarantees the menu never renders below the fold — not `menuMax`, which is only
|
|
122
|
+
* the menu-vs-sheet taste threshold (`mobile-nav-model.ts`'s module doc has the accounting).
|
|
123
|
+
*
|
|
124
|
+
* `min(62dvh, 440px)`, down from `min(72dvh, 560px)`. The old pair fit — but fitting was never
|
|
125
|
+
* the whole ask: MEASURED in headless Chrome at 390x844 the dropdown rendered 300x560 at
|
|
126
|
+
* y=221..781, i.e. 66% of the screen, which reads as a panel rather than as the "subtle, small
|
|
127
|
+
* menu" the More surface is supposed to be. At 440 the same phone gets 52%.
|
|
128
|
+
*
|
|
129
|
+
* The accounting, at both ends of the supported range. 390x844: headroom above the bar is 772px,
|
|
130
|
+
* 62dvh resolves to 523px, so the 440px term binds — 57% of the headroom. 320x568: headroom is
|
|
131
|
+
* ~496px, 62dvh resolves to 352px, so THAT term binds — 71% of the headroom. Either way the
|
|
132
|
+
* binding term is one of the two written here and it lands under the headroom, which is the
|
|
133
|
+
* property that matters: the cap, not `menuMax`, is the guarantee. Past it the dropdown scrolls
|
|
134
|
+
* inside itself instead of growing, so a long More list costs scroll, never fold. */
|
|
135
|
+
max-height: min(62dvh, 440px);
|
|
113
136
|
overflow-y: auto;
|
|
114
137
|
overscroll-behavior: contain;
|
|
115
138
|
}
|
|
@@ -135,13 +158,16 @@
|
|
|
135
158
|
/*
|
|
136
159
|
* A disabled destination still RENDERS in the menu (§2.3 rule 11) — it just must not be reachable.
|
|
137
160
|
* Mantine's `Menu.css` styles `[data-disabled]` with a dimmed color and nothing else, and the row
|
|
138
|
-
* is polymorphic (`component="a"`), so the native `disabled` attribute does not apply either
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
161
|
+
* is polymorphic (`component="a"`), so the native `disabled` attribute does not apply either.
|
|
162
|
+
*
|
|
163
|
+
* `pointer-events: none` USED to be the guard here and is deliberately gone. It made the row
|
|
164
|
+
* swallow a tap with zero feedback — no `:active`, no press state, nothing — which on a phone is
|
|
165
|
+
* indistinguishable from a button that is simply broken, and "I tap and nothing happens" is
|
|
166
|
+
* literally the report this change answers. The guard is now entirely in the handlers (`menuRow`,
|
|
167
|
+
* `sheetRow`), which cover the pointer path and every path a pointer never takes; the row still
|
|
168
|
+
* depresses under a finger and still goes nowhere, which is the honest version of dead.
|
|
142
169
|
*/
|
|
143
170
|
.menuItem[data-disabled] {
|
|
144
|
-
pointer-events: none;
|
|
145
171
|
cursor: not-allowed;
|
|
146
172
|
}
|
|
147
173
|
|
|
@@ -151,7 +177,10 @@
|
|
|
151
177
|
padding-inline-start: calc(var(--mantine-spacing-md) + 12px) !important;
|
|
152
178
|
}
|
|
153
179
|
|
|
154
|
-
/* Overflow section heading
|
|
180
|
+
/* Overflow section heading, in the menu AND in the sheet — inherits the theme's mono micro-label.
|
|
181
|
+
* The inset matches the rows' own (`--vx-space-row-inset-x`) so the heading and the labels under
|
|
182
|
+
* it share one left edge; the vertical padding is asymmetric on purpose — a heading belongs to
|
|
183
|
+
* what follows it, so it sits closer to its own rows than to the section above. */
|
|
155
184
|
.menuLabel {
|
|
156
185
|
font-family: var(--basalt-font-mono);
|
|
157
186
|
font-size: var(--vx-text-micro);
|
|
@@ -159,6 +188,65 @@
|
|
|
159
188
|
text-transform: uppercase;
|
|
160
189
|
letter-spacing: 0.08em;
|
|
161
190
|
color: var(--vx-faint);
|
|
191
|
+
padding: 6px var(--vx-space-row-inset-x) 2px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/*
|
|
195
|
+
* The popover's SECTION RHYTHM — what replaced the single `Menu.Divider`, and it is SPACE, not a
|
|
196
|
+
* line.
|
|
197
|
+
*
|
|
198
|
+
* The Drawer's bulk was never its row height (40px is a WCAG 2.5.5 floor pinned by
|
|
199
|
+
* `theme/density-relations.test.ts`); it was the stack rhythm around the rows — a 12px section
|
|
200
|
+
* gap, 18px of Drawer padding and a header row. A popover with one hairline through the middle and
|
|
201
|
+
* no rhythm at all is the other extreme, so each group opens its own band instead.
|
|
202
|
+
*
|
|
203
|
+
* A `border-top: 1px solid var(--vx-divider)` here was the obvious move and is deliberately NOT
|
|
204
|
+
* what shipped: `theme/divider-law.test.ts` inventories every horizontal hairline in the package
|
|
205
|
+
* and classifies it as `between-rows`, `region-boundary` or (inside `content/**` only)
|
|
206
|
+
* `prose-typography` — and it says in as many words that a rule under a HEADING in the chrome lane
|
|
207
|
+
* is none of the three, and that the fix is to delete it and let the type scale and the space do
|
|
208
|
+
* the work. A mono uppercase micro-label at `--vx-faint` over its own band of air is already a
|
|
209
|
+
* section boundary; a line under it would be the second one.
|
|
210
|
+
*
|
|
211
|
+
* `:not(:first-child)` keeps the first group flush with the dropdown's own padding instead of
|
|
212
|
+
* opening a band under the top edge with nothing above it — the same "a separator separates"
|
|
213
|
+
* property the guarded `Menu.Divider` used to carry in JSX. Scoped to `.menuDropdown` because
|
|
214
|
+
* `.menuLabel` is also the SHEET's group heading (`sheetGroupLabel`), where each label is its
|
|
215
|
+
* group Stack's first child and the rhythm is the Drawer's own to draw.
|
|
216
|
+
*
|
|
217
|
+
* The band is 6/4, down from 8/8 — the second half of the same "still too big" verdict the
|
|
218
|
+
* `max-height` above answers, and the cheaper half. MEASURED per heading at 8/8: 8px margin + 8px
|
|
219
|
+
* padding-top + 2px padding-bottom + a 17px line = a 35px box, and there are FIVE of them in the
|
|
220
|
+
* shipped More surface — 175px, 28% of the whole dropdown spent on five words. 6/4 takes each
|
|
221
|
+
* banded heading to 29px and hands 24px back to the rows. It does NOT touch the row height (40px
|
|
222
|
+
* is the WCAG 2.5.5 floor named at the top of this comment) and it does not add the line the
|
|
223
|
+
* paragraph above rules out: this is the same space that argument asked to carry the boundary,
|
|
224
|
+
* spent tighter.
|
|
225
|
+
*/
|
|
226
|
+
.menuDropdown .menuLabel:not(:first-child) {
|
|
227
|
+
margin-top: 6px;
|
|
228
|
+
padding-top: 4px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* The same band, on the first row of the UNLABELLED tail (blocks + account + settings) — the one
|
|
232
|
+
* boundary in the More surface with no heading to open it. Margin rather than padding: a row is a
|
|
233
|
+
* touch target with a `min-height` floor and symmetric inset, so growing one side of it would make
|
|
234
|
+
* that row read as a different size from its siblings. */
|
|
235
|
+
.menuDropdown .menuSectionStart {
|
|
236
|
+
margin-top: 8px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/*
|
|
240
|
+
* A sticky heading over a scrolling dropdown must be OPAQUE. `theme/index.ts` documents the same
|
|
241
|
+
* fix for the Drawer's header: basalt dropped that background to `transparent`, which reads fine
|
|
242
|
+
* until rows actually scroll under it and then shows every one of them straight through the title.
|
|
243
|
+
* `--vx-surface-overlay` is the dropdown's own fill, so the heading matches the box it sits in.
|
|
244
|
+
*/
|
|
245
|
+
.menuDropdown .menuLabel {
|
|
246
|
+
position: sticky;
|
|
247
|
+
top: 0;
|
|
248
|
+
z-index: 1;
|
|
249
|
+
background-color: var(--vx-surface-overlay);
|
|
162
250
|
}
|
|
163
251
|
|
|
164
252
|
/*
|
|
@@ -142,12 +142,50 @@
|
|
|
142
142
|
|
|
143
143
|
/* Re-apply the bled inset INSIDE the viewport, so rows keep their exact 10px/10px column while the
|
|
144
144
|
* bar floats clear of them in the gutter. Static padding, not Mantine's `offsetScrollbars`: the
|
|
145
|
-
* latter pads only while overflowing, which reflows the rows the moment a section expands.
|
|
145
|
+
* latter pads only while overflowing, which reflows the rows the moment a section expands.
|
|
146
|
+
*
|
|
147
|
+
* `padding-block-end` is the scroll END-STOP: with only the inline inset, an overflowing column
|
|
148
|
+
* cut its last row flush against the viewport edge, which reads as a label whose list is CLIPPED
|
|
149
|
+
* rather than as something that scrolls. One `region-gap` of air below the last row is the same
|
|
150
|
+
* number the search slot opens at the column's other end.
|
|
151
|
+
*
|
|
152
|
+
* The end-stop is necessary and NOT sufficient, and the mask below is the other half. MEASURED at
|
|
153
|
+
* 1512x945 with the playground's nav: `.navScroll` is `flex: 1; min-height: 0` between the search
|
|
154
|
+
* slot and a `flex-shrink: 0` footer, so any nav taller than the leftover column overflows — and
|
|
155
|
+
* the end-stop only pads the BOTTOM OF THE CONTENT, which the reader sees exactly once, at the
|
|
156
|
+
* scroll end. Everywhere else the viewport's own edge cuts whatever row happens to cross it, and
|
|
157
|
+
* `.footer`'s progress block starts one `region-gap` under that cut. Two hard edges 12px apart:
|
|
158
|
+
* the column reads BROKEN rather than scrolled. Nothing else says otherwise, because the
|
|
159
|
+
* ScrollArea is `type="hover"` — at rest there is no bar at all.
|
|
160
|
+
*
|
|
161
|
+
* So: a scroll fade on the viewport's bottom edge, and deliberately NOT the other candidate, a
|
|
162
|
+
* `--vx-divider` seam on `.footer`. A rule there would answer a question nobody asked — the footer
|
|
163
|
+
* boundary is already carried by its own `padding-top` and the account row's inset (see `.footer`
|
|
164
|
+
* below) — and it would answer the real one WRONG: a hairline drawn across a row that is severed
|
|
165
|
+
* mid-glyph makes the severing crisper, not legible. A fade is the only one of the two that says
|
|
166
|
+
* "there is more, and it moves". It also keeps this module out of `theme/divider-law.test.ts`'s
|
|
167
|
+
* inventory, which is the correct outcome and not a dodge: that ledger exists so every hairline is
|
|
168
|
+
* a line someone chose, and this one would have been chosen to stand in for a scrollbar.
|
|
169
|
+
*
|
|
170
|
+
* The ramp is one `region-gap` tall, which is what pairs it with the end-stop instead of fighting
|
|
171
|
+
* it: at the scroll END those 12px hold the end-stop's padding — empty — so the last row lands
|
|
172
|
+
* fully opaque, and a column that does not overflow fades nothing but its own trailing air. Only
|
|
173
|
+
* mid-scroll, where a real row crosses the edge, does the fade have anything to dim.
|
|
174
|
+
*
|
|
175
|
+
* `black`/`transparent` here are alpha, not color: a mask reads only the alpha channel, so these
|
|
176
|
+
* carry no design meaning and deliberately are NOT `--vx-*` tokens. Do not route them through the
|
|
177
|
+
* palette in a token sweep. */
|
|
146
178
|
.navViewport {
|
|
147
179
|
padding-inline-end: var(--vx-space-row-inset-x);
|
|
180
|
+
padding-block-end: var(--vx-space-sidebar-region-gap);
|
|
181
|
+
mask-image: linear-gradient(
|
|
182
|
+
to bottom,
|
|
183
|
+
black calc(100% - var(--vx-space-sidebar-region-gap)),
|
|
184
|
+
transparent
|
|
185
|
+
);
|
|
148
186
|
}
|
|
149
187
|
|
|
150
|
-
/* Sections separate by SPACING only —
|
|
188
|
+
/* Sections separate by SPACING only — 16px between sections, nothing above the first (the brand
|
|
151
189
|
* band's centred `min-height` already opens the region's air, and the optional search slot's own
|
|
152
190
|
* `padding-block` opens more when it renders). */
|
|
153
191
|
.nav > div + div {
|
|
@@ -156,9 +194,24 @@
|
|
|
156
194
|
|
|
157
195
|
/*
|
|
158
196
|
* Footer region — no border; the account row's own top padding supplies the separation.
|
|
197
|
+
*
|
|
198
|
+
* The region owns its own air on both axes, because the `Stack` wrapping it is `gap={0}`
|
|
199
|
+
* (`app-sidebar.tsx`) — the footer's rhythm is single-sourced HERE, so the whole answer is
|
|
200
|
+
* readable in one place rather than split between a prop and a stylesheet. Without it a
|
|
201
|
+
* bottom-placed block (a `kind: 'progress'` block defaults to `'bottom'`) stacked FLUSH on the
|
|
202
|
+
* settings and account rows and on the scroll region above, which is the "cramped" footer.
|
|
203
|
+
*
|
|
204
|
+
* `region-gap` (12) above the cluster, `stack-sm` (8) between its parts — deliberately tighter
|
|
205
|
+
* than the nav's `section-gap` (16): the footer is ONE pinned cluster whose rows belong together,
|
|
206
|
+
* not a list of peer sections.
|
|
159
207
|
*/
|
|
160
208
|
.footer {
|
|
161
209
|
flex-shrink: 0;
|
|
210
|
+
padding-top: var(--vx-space-sidebar-region-gap);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.footer > * + * {
|
|
214
|
+
margin-top: var(--vx-space-stack-sm);
|
|
162
215
|
}
|
|
163
216
|
|
|
164
217
|
/*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shell/index.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shell/index.tsx"],"names":[],"mappings":"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"}
|
package/dist/shell/index.js
CHANGED
|
@@ -69,7 +69,6 @@ function collapseStore(key) {
|
|
|
69
69
|
collapseStores.set(key, store);
|
|
70
70
|
return store;
|
|
71
71
|
}
|
|
72
|
-
const SHELL_INSET = "sm";
|
|
73
72
|
function BasaltShell(props) {
|
|
74
73
|
assertRequiredProps("BasaltShell", props, ["brand", "sections"]);
|
|
75
74
|
return /* @__PURE__ */ jsx(PageBarProvider, { globalActions: props.globalActions ?? [], children: /* @__PURE__ */ jsx(AsideProvider, { children: /* @__PURE__ */ jsx(ShellFrame, { ...props }) }) });
|
|
@@ -92,6 +91,7 @@ function ShellFrame({
|
|
|
92
91
|
style
|
|
93
92
|
}) {
|
|
94
93
|
const { step } = useBasaltSpacing();
|
|
94
|
+
const inset = { base: step.appShellInsetMobile, sm: step.appShellInset };
|
|
95
95
|
const aside = useAsideRegion();
|
|
96
96
|
const [storedCollapsed, setStoredCollapsed] = collapseStore(storageKey)();
|
|
97
97
|
const isCollapseControlled = collapsedProp !== void 0;
|
|
@@ -136,11 +136,11 @@ function ShellFrame({
|
|
|
136
136
|
collapsed: { desktop: !aside.claimed, mobile: true }
|
|
137
137
|
},
|
|
138
138
|
footer: { height: { base: step.mobileNavBarHeight, sm: 0 } },
|
|
139
|
-
padding:
|
|
139
|
+
padding: inset,
|
|
140
140
|
className: cx(mainClasses.shell, className),
|
|
141
141
|
...style !== void 0 && { style },
|
|
142
142
|
children: [
|
|
143
|
-
/* @__PURE__ */ jsx(AppShell.Header, { px:
|
|
143
|
+
/* @__PURE__ */ jsx(AppShell.Header, { px: inset, children: /* @__PURE__ */ jsxs("div", { className: headerClasses.bar, children: [
|
|
144
144
|
/* @__PURE__ */ jsx("div", { className: brandClasses.zone, "data-collapsed": collapsed || void 0, children: /* @__PURE__ */ jsx(AppBrand, { brand, collapsed, onToggleCollapse: toggleCollapse }) }),
|
|
145
145
|
/* @__PURE__ */ jsx("div", { className: headerClasses.lead, children: /* @__PURE__ */ jsx(AppBreadcrumbs, { ...activeCrumb }) }),
|
|
146
146
|
/* @__PURE__ */ jsx(PageBarOutlet, { className: headerClasses.pageBar }),
|
|
@@ -9,19 +9,38 @@
|
|
|
9
9
|
* The law it implements is the interaction model, not a heuristic: a slot is a DESTINATION, so a
|
|
10
10
|
* tap navigates with no overlay to dismiss. An overlay exists only where a slot genuinely holds
|
|
11
11
|
* more than one destination, and its surface is INFERRED from how many rows it holds rather than
|
|
12
|
-
* configured —
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* configured — up to `menuMax` rows it is a content-sized menu popping out of the tab, past that a
|
|
13
|
+
* bottom sheet.
|
|
14
|
+
*
|
|
15
|
+
* WHAT KEEPS THE MENU ABOVE THE FOLD IS CSS, NOT THIS CONSTANT. `.menuDropdown` declares
|
|
16
|
+
* `max-height: min(72dvh, 560px)` + `overflow-y: auto` + `overscroll-behavior: contain`, so the
|
|
17
|
+
* dropdown is bounded at ANY row count and scrolls inside itself past it. Measured at 390x844 the
|
|
18
|
+
* headroom above the bar is 772px (bar top 788, less the 8px menu offset and the 8px shift
|
|
19
|
+
* padding), so the 560px cap uses 73% of it; on the smallest supported viewport (320x568) the
|
|
20
|
+
* headroom is ~496px and 72dvh is 409px — the cap is binding on every supported phone, and the row
|
|
21
|
+
* count never is. The old arithmetic here ("6 x 44px + 8px padding against 415px of headroom") was
|
|
22
|
+
* stale in almost every number: rows are 40px (`--vx-space-mobile-nav-row-height`), the dropdown's
|
|
23
|
+
* own padding is `2 * --vx-space-stack-xs` = 8px, and the headroom figures above are the measured
|
|
24
|
+
* ones.
|
|
25
|
+
*
|
|
26
|
+
* `menuMax` therefore survives for the case a menu genuinely should not hold: a 30-destination nav
|
|
27
|
+
* still deserves a sheet, where a title, a close button and 62dvh of scroll read better than a
|
|
28
|
+
* tall popover hanging off one tab.
|
|
15
29
|
*/
|
|
16
30
|
import type { MobileNavConfig, MobileNavModel, SidebarBlock, SidebarSection } from './nav-types.js';
|
|
17
31
|
/** Hard cap on bar slots, INCLUDING More. Five 72px slots is the widest a 360px viewport holds. */
|
|
18
32
|
export declare const MOBILE_MAX_TABS_DEFAULT = 5;
|
|
19
33
|
/**
|
|
20
|
-
* Rows a slot may hold before it becomes a sheet instead of a menu.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
34
|
+
* Rows a slot may hold before it becomes a sheet instead of a menu.
|
|
35
|
+
*
|
|
36
|
+
* It was 6 on the theory that the count was what kept the menu above the fold. It is not — the
|
|
37
|
+
* dropdown's own `max-height` is (module doc), at every row count. What 6 actually did was flip
|
|
38
|
+
* the playground's ~11-row More slot into a full Drawer, which is the "big door on mobile" the
|
|
39
|
+
* owner reported. 12 keeps every realistic More surface a compact popover and leaves the sheet
|
|
40
|
+
* for the nav that has genuinely outgrown one. `mobileNav.menuMax` remains the consumer opt-out
|
|
41
|
+
* in both directions.
|
|
23
42
|
*/
|
|
24
|
-
export declare const MOBILE_MENU_MAX_DEFAULT =
|
|
43
|
+
export declare const MOBILE_MENU_MAX_DEFAULT = 12;
|
|
25
44
|
/** The overflow slot's reserved key. A slot like any other — it just exists only when needed. */
|
|
26
45
|
export declare const MOBILE_MORE_KEY = "__more";
|
|
27
46
|
export type ProjectMobileNavOptions = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-nav-model.d.ts","sourceRoot":"","sources":["../../src/shell/mobile-nav-model.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mobile-nav-model.d.ts","sourceRoot":"","sources":["../../src/shell/mobile-nav-model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EACV,eAAe,EAEf,cAAc,EAGd,YAAY,EAEZ,cAAc,EACf,MAAM,aAAa,CAAA;AAGpB,mGAAmG;AACnG,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC,iGAAiG;AACjG,eAAO,MAAM,eAAe,WAAW,CAAA;AAEvC,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,CAAC,EAAE,eAAe,GAAG,SAAS,CAAA;IACpC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,SAAS,GAAG,MAAM,CAQjF;AA8VD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,cAAc,EAAE,EAC1B,IAAI,CAAC,EAAE,uBAAuB,GAC7B,cAAc,CA8EhB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sidebarBlockMobile } from "./sidebar-block-model.js";
|
|
2
2
|
const MOBILE_MAX_TABS_DEFAULT = 5;
|
|
3
|
-
const MOBILE_MENU_MAX_DEFAULT =
|
|
3
|
+
const MOBILE_MENU_MAX_DEFAULT = 12;
|
|
4
4
|
const MOBILE_MORE_KEY = "__more";
|
|
5
5
|
function blockRowCount(blocks) {
|
|
6
6
|
if (blocks === void 0) return 0;
|
|
@@ -117,7 +117,13 @@ function sectionCandidate(section, menuMax) {
|
|
|
117
117
|
// The section's own identity wins when it declares one — the consumer configured the
|
|
118
118
|
// SLOT — and falls back to the single destination's icon when it does not.
|
|
119
119
|
icon: mobile?.icon ?? section.icon ?? only.icon,
|
|
120
|
-
|
|
120
|
+
// `isActiveDestination`, not the rollup — this collapse produced a LINK, and a link slot
|
|
121
|
+
// covers exactly the one key it navigates to, exactly as `itemCandidate` does. The two
|
|
122
|
+
// predicates agree TODAY (this branch needs `rows === 1`, `countRows` counts descendants,
|
|
123
|
+
// so `only` is childless whenever it is taken) — which is the point: activeness now reads
|
|
124
|
+
// the slot's own `covered` set instead of a non-local fact about what `rows === 1`
|
|
125
|
+
// implies. Widen the collapse and the rollup would light a tab whose tap lands elsewhere.
|
|
126
|
+
active: isActiveDestination(only),
|
|
121
127
|
item: only
|
|
122
128
|
};
|
|
123
129
|
}
|
|
@@ -252,7 +258,10 @@ function projectMobileNav(sections, opts) {
|
|
|
252
258
|
label: only.label,
|
|
253
259
|
short: only.short ?? only.label,
|
|
254
260
|
icon: only.icon,
|
|
255
|
-
|
|
261
|
+
// Same as `sectionCandidate`'s collapse, for the same reason and with the same caveat: a
|
|
262
|
+
// LINK slot reads only the destination its tap reaches, and `rows === 1` makes the two
|
|
263
|
+
// predicates agree today — state the law locally rather than inherit it.
|
|
264
|
+
active: isActiveDestination(only),
|
|
256
265
|
item: only
|
|
257
266
|
});
|
|
258
267
|
return { slots };
|