basalt-ui 1.29.2 → 1.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/MIGRATING.md +901 -366
- 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/index.d.ts +1 -1
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.d.ts +5 -3
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.d.ts +4 -2
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.d.ts +4 -2
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.d.ts +4 -2
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.d.ts +5 -2
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.d.ts +4 -2
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.d.ts +4 -2
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.d.ts +4 -2
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts +4 -2
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.d.ts +4 -1
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- 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 +25 -4
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +23 -16
- package/dist/charts/primitives/chart-frame-layout.d.ts +115 -6
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +46 -6
- 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/index.ts +1 -0
- package/src/charts/kinds/BandStrip.tsx +5 -3
- package/src/charts/kinds/Bars.tsx +4 -2
- package/src/charts/kinds/Donut.tsx +4 -2
- package/src/charts/kinds/DualPanel.tsx +4 -2
- package/src/charts/kinds/Heatmap.tsx +5 -2
- package/src/charts/kinds/MirroredBars.tsx +4 -2
- package/src/charts/kinds/MultiLine.tsx +4 -2
- package/src/charts/kinds/StackedArea.tsx +4 -2
- package/src/charts/kinds/ZonedLine.tsx +4 -2
- package/src/charts/primitives/CartesianChart.tsx +4 -1
- package/src/charts/primitives/ChartCard.tsx +12 -7
- package/src/charts/primitives/ChartFrame.tsx +51 -37
- package/src/charts/primitives/chart-frame-layout.ts +172 -13
- 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
|
@@ -9,9 +9,23 @@
|
|
|
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 {
|
|
17
31
|
MobileNavConfig,
|
|
@@ -29,11 +43,16 @@ import { sidebarBlockMobile } from './sidebar-block-model'
|
|
|
29
43
|
export const MOBILE_MAX_TABS_DEFAULT = 5
|
|
30
44
|
|
|
31
45
|
/**
|
|
32
|
-
* Rows a slot may hold before it becomes a sheet instead of a menu.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
46
|
+
* Rows a slot may hold before it becomes a sheet instead of a menu.
|
|
47
|
+
*
|
|
48
|
+
* It was 6 on the theory that the count was what kept the menu above the fold. It is not — the
|
|
49
|
+
* dropdown's own `max-height` is (module doc), at every row count. What 6 actually did was flip
|
|
50
|
+
* the playground's ~11-row More slot into a full Drawer, which is the "big door on mobile" the
|
|
51
|
+
* owner reported. 12 keeps every realistic More surface a compact popover and leaves the sheet
|
|
52
|
+
* for the nav that has genuinely outgrown one. `mobileNav.menuMax` remains the consumer opt-out
|
|
53
|
+
* in both directions.
|
|
35
54
|
*/
|
|
36
|
-
export const MOBILE_MENU_MAX_DEFAULT =
|
|
55
|
+
export const MOBILE_MENU_MAX_DEFAULT = 12
|
|
37
56
|
|
|
38
57
|
/** The overflow slot's reserved key. A slot like any other — it just exists only when needed. */
|
|
39
58
|
export const MOBILE_MORE_KEY = '__more'
|
|
@@ -285,7 +304,13 @@ function sectionCandidate(section: SidebarSection, menuMax: number): Candidate {
|
|
|
285
304
|
// The section's own identity wins when it declares one — the consumer configured the
|
|
286
305
|
// SLOT — and falls back to the single destination's icon when it does not.
|
|
287
306
|
icon: mobile?.icon ?? section.icon ?? only.icon,
|
|
288
|
-
|
|
307
|
+
// `isActiveDestination`, not the rollup — this collapse produced a LINK, and a link slot
|
|
308
|
+
// covers exactly the one key it navigates to, exactly as `itemCandidate` does. The two
|
|
309
|
+
// predicates agree TODAY (this branch needs `rows === 1`, `countRows` counts descendants,
|
|
310
|
+
// so `only` is childless whenever it is taken) — which is the point: activeness now reads
|
|
311
|
+
// the slot's own `covered` set instead of a non-local fact about what `rows === 1`
|
|
312
|
+
// implies. Widen the collapse and the rollup would light a tab whose tap lands elsewhere.
|
|
313
|
+
active: isActiveDestination(only),
|
|
289
314
|
item: only,
|
|
290
315
|
}
|
|
291
316
|
}
|
|
@@ -474,7 +499,10 @@ export function projectMobileNav(
|
|
|
474
499
|
label: only.label,
|
|
475
500
|
short: only.short ?? only.label,
|
|
476
501
|
icon: only.icon,
|
|
477
|
-
|
|
502
|
+
// Same as `sectionCandidate`'s collapse, for the same reason and with the same caveat: a
|
|
503
|
+
// LINK slot reads only the destination its tap reaches, and `rows === 1` makes the two
|
|
504
|
+
// predicates agree today — state the law locally rather than inherit it.
|
|
505
|
+
active: isActiveDestination(only),
|
|
478
506
|
item: only,
|
|
479
507
|
})
|
|
480
508
|
return { slots }
|
|
@@ -49,12 +49,13 @@
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/* The portalled form's band matches the SHELL-OWNED page-bar band exactly, whenever one is
|
|
52
|
-
* present (`--basalt-page-bar-h`, `docs/DESIGN-SPEC.md` §5) — not a fixed
|
|
52
|
+
* present (`--basalt-page-bar-h`, `docs/DESIGN-SPEC.md` §5) — not a fixed header height. `PageBar`
|
|
53
53
|
* publishes that var on `documentElement` from a ResizeObserver on the band's own content
|
|
54
54
|
* (`shell/page-bar.tsx`), so it inherits down to every descendant, this header included, with no
|
|
55
55
|
* extra wiring. Two seams, one line: MEASURED on `/cbbi` 1440x900, the band's outer edge (its own
|
|
56
56
|
* `border-bottom`, `app-main.module.css` `.band:not(:empty)`) used to sit at y87 while this header
|
|
57
|
-
* (unconditionally `var(--app-shell-header-height)`, 48px
|
|
57
|
+
* (unconditionally `var(--app-shell-header-height)`, 48px at the time — the token is 44 since the
|
|
58
|
+
* desktop-shell pass) sat at y96 — 9px apart.
|
|
58
59
|
*
|
|
59
60
|
* The `+ 1px` is not decorative. `--basalt-page-bar-h` measures the band's CONTENT box only (the
|
|
60
61
|
* portalled row-2 wrapper `page-bar.tsx` attaches its ResizeObserver to); the band's own
|
|
@@ -63,9 +64,10 @@
|
|
|
63
64
|
* under the page's global `box-sizing: border-box` reset — so its OWN `border-bottom` is absorbed
|
|
64
65
|
* INSIDE whatever height is declared instead of adding on top. Feeding the raw var straight in
|
|
65
66
|
* therefore undershoots by exactly the band's border: `calc(38px + 1px)` = 39px, matching the
|
|
66
|
-
* band's true painted height, while the no-band fallback nets out unchanged (`calc((
|
|
67
|
-
* 1px)` =
|
|
68
|
-
* band seam to align to, so the header keeps closing the
|
|
67
|
+
* band's true painted height, while the no-band fallback nets out unchanged (`calc((44px - 1px) +
|
|
68
|
+
* 1px)` = 44px, reading today's `appShellHeaderHeight`) because the same `+ 1px` also wraps that
|
|
69
|
+
* branch. Without a `PageBar` there is no band seam to align to, so the header keeps closing the
|
|
70
|
+
* ordinary 44px top belt
|
|
69
71
|
* (`docs/DESIGN-SPEC.md` §5) the header and sidebar-brand bands also close.
|
|
70
72
|
*/
|
|
71
73
|
.panel[data-basalt-page-aside='shell'] .header {
|
|
@@ -81,7 +83,7 @@
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
/* A real title (docs/DESIGN-SPEC.md §5): mirrors `widget-header.module.css` `.heading` and the
|
|
84
|
-
* breadcrumb's current-page treatment — the three
|
|
86
|
+
* breadcrumb's current-page treatment — the three 44px bands each carry one 550-weight head-font
|
|
85
87
|
* name. */
|
|
86
88
|
.title {
|
|
87
89
|
font-family: var(--basalt-font-head);
|
|
@@ -96,10 +96,15 @@
|
|
|
96
96
|
margin-inline-end: var(--vx-space-control-gap);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/* Glyph BOX, 1.125rem — a decoration-class fixed size, deliberately NOT a `--vx-space-*` token
|
|
100
|
+
* (an icon is not a gap; the shipped icon set draws on a 24px grid and the box is a fraction of
|
|
101
|
+
* the type size, not of the spacing scale). Do not re-litigate these in a token sweep.
|
|
102
|
+
* 1.125rem rather than the old 1rem because `controlHeightCtl` grew 30 -> 32: a glyph held at
|
|
103
|
+
* 16px inside a 32px control reads EMPTIER, not bigger. */
|
|
99
104
|
.row1 :global(.mantine-Button-section) > svg,
|
|
100
105
|
.row1 :global(.mantine-ActionIcon-root) > svg {
|
|
101
|
-
width:
|
|
102
|
-
height:
|
|
106
|
+
width: 1.125rem;
|
|
107
|
+
height: 1.125rem;
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
/* Actions and sync are right-aligned; the lead (or the breadcrumb, in a shell) holds the left. */
|
|
@@ -114,21 +119,24 @@
|
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
/*
|
|
117
|
-
* Row 2's rhythm is 8px · pills · 8px, NOT a
|
|
122
|
+
* Row 2's rhythm is 8px · pills · 8px, NOT a reserved row-height box.
|
|
118
123
|
*
|
|
119
|
-
* `min-height: var(--vx-space-page-bar-row-height)`
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
124
|
+
* `min-height: var(--vx-space-page-bar-row-height)` was the defect, and the arithmetic is the whole
|
|
125
|
+
* reason it is gone. Measured when that token was 40 and `ctl` was 30: a 30px pill floated inside a
|
|
126
|
+
* 40px box with the 5px above and below it belonging to nothing — and because the row sat in a
|
|
127
|
+
* `Stack` that already spaced it, the visible gap between the header and the pills read as ~19px
|
|
128
|
+
* while the gap from the pills to the first card read as ~19px too. The desktop-shell pass has
|
|
129
|
+
* since moved both numbers (row height 40 -> 36, `ctl` 30 -> 32), which narrows that unowned band
|
|
130
|
+
* to 2px a side but does not close it: a HEIGHT token still hands its spare pixels to nobody.
|
|
131
|
+
* Symmetric padding on the row itself is one declaration owning both halves, so the sticky bar's
|
|
132
|
+
* published height covers exactly the band it paints — including below `sm`, where `.row2` becomes
|
|
133
|
+
* a COLUMN of `.tabs` then `.pills` and `padding-block` still owns both edges of the taller band.
|
|
126
134
|
*
|
|
127
135
|
* `stackXs` and not `stackSm`: the row sits inside the shell's own bordered band
|
|
128
136
|
* (`app-main.module.css`'s `.band`), so it no longer has to carry the visual separation in
|
|
129
|
-
* whitespace — 4px above and below a
|
|
137
|
+
* whitespace — 4px above and below a 32px `ctl` pill still clears it (40px total) and buys back 8px
|
|
130
138
|
* of content on every page.
|
|
131
|
-
* Row 1's
|
|
139
|
+
* Row 1's 36px `--vx-space-page-bar-row-height` is untouched: it is a HEIGHT token, not padding.
|
|
132
140
|
*/
|
|
133
141
|
.row2 {
|
|
134
142
|
display: flex;
|
package/src/shell/page-bar.tsx
CHANGED
|
@@ -41,6 +41,9 @@ import asideClasses from './page-aside.module.css'
|
|
|
41
41
|
/** The custom property row 2's measured height is published on. */
|
|
42
42
|
export const PAGE_BAR_HEIGHT_VAR = '--basalt-page-bar-h'
|
|
43
43
|
|
|
44
|
+
/** Dev-only diagnostics, constant-folded out of a production bundle — see {@link useAsidePanelSlot}. */
|
|
45
|
+
const DEV = process.env['NODE_ENV'] !== 'production'
|
|
46
|
+
|
|
44
47
|
/**
|
|
45
48
|
* What a `PageAside` publishes when it projects itself into row 2 below `sm`
|
|
46
49
|
* (`docs/ASIDE-SPEC.md` §0 "Desktop and mobile are one declaration"). Metadata only — the aside's
|
|
@@ -203,13 +206,36 @@ export function usePageKebabClaimed(): boolean {
|
|
|
203
206
|
* Internal — the seam `PageAside` projects itself through below `sm` (`docs/ASIDE-SPEC.md` §0).
|
|
204
207
|
* `host` says whether there is a row 2 to hang the pill off at all; `claim` publishes the aside's
|
|
205
208
|
* title and glyph; `target` is the node inside the opened sheet its children portal into.
|
|
209
|
+
*
|
|
210
|
+
* It also carries THE ONE BRANCH NOBODY PREDICTS, as a dev-only warning. `PageBar` only claims the
|
|
211
|
+
* panel host when it renders a row 2 (`tabs`, `filters` or a non-empty `filtersEnd`), so a page
|
|
212
|
+
* whose bar has actions ALONE gets `host: false` — and below `sm` that aside silently stops being
|
|
213
|
+
* reachable: no pill, no sheet, just the in-flow form at the bottom of a scrolled page. The
|
|
214
|
+
* behaviour is deliberate (there is genuinely nowhere to hang a trigger) and pinned by
|
|
215
|
+
* `page-aside.test.tsx`, so this warns rather than changes it. Nothing about the page LOOKS wrong
|
|
216
|
+
* on the desktop the author is building on, which is exactly why it needs saying out loud.
|
|
217
|
+
*
|
|
218
|
+
* `settled` is why the warning is not a one-shot mount effect: `panelHost` is published by a LAYOUT
|
|
219
|
+
* effect inside `PageBar`, so on the first pass it is false on every page, including the ones that
|
|
220
|
+
* do host. Deferring by one commit — the same deferral `PageAside` runs for the same reason — is
|
|
221
|
+
* what keeps this from firing on every aside in the library.
|
|
206
222
|
*/
|
|
207
223
|
export function useAsidePanelSlot(): {
|
|
208
224
|
host: boolean
|
|
209
225
|
claim: (panel: AsidePanelClaim) => () => void
|
|
210
226
|
target: HTMLElement | null
|
|
211
227
|
} {
|
|
212
|
-
const { panelHost, claimPanel, panelTarget } = useContext(PageBarContext)
|
|
228
|
+
const { panelHost, claimPanel, panelTarget, inShell } = useContext(PageBarContext)
|
|
229
|
+
const [settled, setSettled] = useState(false)
|
|
230
|
+
useIsomorphicLayoutEffect(() => {
|
|
231
|
+
setSettled(true)
|
|
232
|
+
}, [])
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (!DEV || !settled || !inShell || panelHost) return
|
|
235
|
+
console.warn(
|
|
236
|
+
"[basalt] PageAside: this page's PageBar renders no row 2 (no `tabs`, no `filters`, no `filtersEnd`), so below `sm` there is no pill to open the aside — it renders in flow at the bottom of the page instead. Give the bar a row 2, or expect the in-flow form on a phone.",
|
|
237
|
+
)
|
|
238
|
+
}, [settled, inShell, panelHost])
|
|
213
239
|
return { host: panelHost, claim: claimPanel, target: panelTarget }
|
|
214
240
|
}
|
|
215
241
|
|
|
@@ -419,10 +445,18 @@ export function PageBar({
|
|
|
419
445
|
)}
|
|
420
446
|
{panel !== null && (
|
|
421
447
|
<FilterPill
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
448
|
+
// THE ASIDE'S OWN TITLE, not the word "Panel" — the title names the CONTENT, never
|
|
449
|
+
// the region (`docs/ASIDE-SPEC.md` §0), which is the rule the desktop header already
|
|
450
|
+
// obeys by painting `panel.title` at the top of the region. A phone user reading
|
|
451
|
+
// "Panel" for an aside called "Inspector" or "Session detail" is being told which
|
|
452
|
+
// BOX it is, which they can see, instead of what is in it, which they cannot. The
|
|
453
|
+
// separate `ariaLabel` went with it: it existed only to put the real title back for a
|
|
454
|
+
// screen reader, so once the visible label IS the title the two would say the same
|
|
455
|
+
// thing twice.
|
|
456
|
+
//
|
|
457
|
+
// Still no count: unlike `Filters (n)` an aside has no census to count (its children
|
|
458
|
+
// are not a `FilterSet`), and a number nobody can derive is worse than none.
|
|
459
|
+
label={panel.title}
|
|
426
460
|
icon={panel.icon}
|
|
427
461
|
className={classes.panelPill}
|
|
428
462
|
hideGlyph
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* and every named spot reads identically regardless of which one a given surface uses.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/* Title over subtitle is the tightest pair the 4px vertical rhythm has a step for — `stack-xs`,
|
|
9
|
+
* not the literal 4px it duplicated. */
|
|
8
10
|
.root {
|
|
9
11
|
display: flex;
|
|
10
12
|
flex-direction: column;
|
|
11
|
-
gap:
|
|
13
|
+
gap: var(--vx-space-stack-xs);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
.titleRow {
|
|
@@ -22,12 +22,14 @@
|
|
|
22
22
|
* as the AppShell dimensions above (see `sidebarAccountMenuWidth`'s doc in `../tokens/palette`
|
|
23
23
|
* for why a fixed dropdown width needs to track density at all).
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* PUBLIC since the desktop-shell spacing pass — re-exported by the root `.` barrel
|
|
26
|
+
* (`src/index.ts`), on exactly the trigger this doc used to name as the condition for promoting it:
|
|
27
|
+
* a consumer outside the framework's own components needs the ACTIVE resolved spacing. Laying its
|
|
28
|
+
* own chrome out beside the shell's — a sticky sub-header under the app header, a fixed rail beside
|
|
29
|
+
* the navbar — means matching numbers that are ALL JS-consumed (an AppShell dimension, a
|
|
30
|
+
* `<Menu width={…}>`), so unlike a CSS inset there is no `--vx-*` var to read them from instead;
|
|
31
|
+
* without this hook the consumer's only option was to hardcode the level-0 constants and silently
|
|
32
|
+
* desync at any other density.
|
|
31
33
|
*/
|
|
32
34
|
import { useMantineTheme } from '@mantine/core'
|
|
33
35
|
import { DEFAULT_SPACE_VALUES } from '../tokens/palette'
|
package/src/tokens/index.ts
CHANGED
|
@@ -556,8 +556,9 @@ function spaceDecls(space: SpaceValues): string[] {
|
|
|
556
556
|
decl('space-stack-xl', `${space.anchors.stackXl}px`),
|
|
557
557
|
// The CSS-module spacing sweep's one-offs (docs/STATUS.md) — same single-source reasoning,
|
|
558
558
|
// grouped to match `SPACE_STEP_BASE`'s own grouping in tokens/palette.ts.
|
|
559
|
-
// ONE value at every viewport since 1.26.0 — the AppShell header is a single
|
|
560
|
-
// C14), so the `-mobile` override of the old Decision-3
|
|
559
|
+
// ONE value at every viewport since 1.26.0 — the AppShell header is a single
|
|
560
|
+
// `appShellHeaderHeight` row (law C14), so the `-mobile` override of the old Decision-3
|
|
561
|
+
// responsive pair is gone.
|
|
561
562
|
decl('space-sticky-header-clearance', `${space.step.stickyHeaderClearance}px`),
|
|
562
563
|
decl('space-nav-icon-gap', `${space.step.navIconGap}px`),
|
|
563
564
|
decl('space-sidebar-region-gap', `${space.step.sidebarRegionGap}px`),
|
|
@@ -634,6 +635,24 @@ function spaceDecls(space: SpaceValues): string[] {
|
|
|
634
635
|
decl('space-sidebar-child-list-indent', `${space.step.sidebarChildListIndent}px`),
|
|
635
636
|
decl('space-sidebar-child-row-inset-y', `${space.step.sidebarChildRowInsetY}px`),
|
|
636
637
|
decl('space-sidebar-child-row-indent', `${space.step.sidebarChildRowIndent}px`),
|
|
638
|
+
// The page gutter — the ONE `appShell*` dimension that IS emitted, against the JS-number-only
|
|
639
|
+
// rule its five neighbours follow (see the note at the bottom of this list). It earns the
|
|
640
|
+
// exception because it is the most visible spacing number the framework ships — every page's
|
|
641
|
+
// left edge — and it was, until now, the one number the `--vx-*` layer did not expose at all, so
|
|
642
|
+
// a consumer retuning its identity through `createBasaltTheme` could not reach it and had to
|
|
643
|
+
// reach past the token layer into `AppShell.Main`'s own styles. Emitted in PLAIN PX, like every
|
|
644
|
+
// other inset/gap in this list and unlike the `pxRem` heights above it: the rem-plus-
|
|
645
|
+
// `var(--mantine-scale)` reconstruction exists so a CONTROL's box tracks the root font size and
|
|
646
|
+
// Mantine's global scale knob; a page gutter is a layout margin against a viewport, not a box
|
|
647
|
+
// sized to text, so scaling it with the font would make a large-type user's content column
|
|
648
|
+
// narrower rather than more legible.
|
|
649
|
+
decl('space-app-shell-inset', `${space.step.appShellInset}px`),
|
|
650
|
+
decl('space-app-shell-inset-mobile', `${space.step.appShellInsetMobile}px`),
|
|
651
|
+
// The header's breadcrumb floor — emitted for the same reason as the gutter pair above it and
|
|
652
|
+
// not for the JS-number reason its `appShell*` neighbours below are withheld for: this one has
|
|
653
|
+
// a real CSS consumer (`shell/app-header.module.css`'s `.lead`), which is the whole test this
|
|
654
|
+
// list applies. Plain px, like every other layout dimension here.
|
|
655
|
+
decl('space-app-shell-header-lead-min', `${space.step.appShellHeaderLeadMin}px`),
|
|
637
656
|
decl('space-mobile-nav-tab-gap', `${space.step.mobileNavTabGap}px`),
|
|
638
657
|
decl('space-mobile-nav-icon-size', `${space.step.mobileNavIconSize}px`),
|
|
639
658
|
decl('space-mobile-nav-tab-inset-y', `${space.step.mobileNavTabInsetY}px`),
|
|
@@ -651,6 +670,11 @@ function spaceDecls(space: SpaceValues): string[] {
|
|
|
651
670
|
decl('space-badge-inset-y', `${space.step.badgeInsetY}px`),
|
|
652
671
|
decl('space-badge-inset-x', `${space.step.badgeInsetX}px`),
|
|
653
672
|
decl('space-stat-card-gap', `${space.step.statCardGap}px`),
|
|
673
|
+
// The card/section inset ladder — the three values `spaceMobileDecls` below re-points at `< sm`
|
|
674
|
+
// (see its doc for where the law lives and why it lives there).
|
|
675
|
+
decl('space-card-inset-y', `${space.step.cardInsetY}px`),
|
|
676
|
+
decl('space-card-inset-x', `${space.step.cardInsetX}px`),
|
|
677
|
+
decl('space-section-gap', `${space.step.sectionGap}px`),
|
|
654
678
|
decl('space-virtual-row-inset-y', `${space.step.virtualRowInsetY}px`),
|
|
655
679
|
decl('space-virtual-row-inset-x', `${space.step.virtualRowInsetX}px`),
|
|
656
680
|
// Controls tier row/gap heights (`docs/CONTROLS-SPEC.md` §5) — the current commit's consumers
|
|
@@ -666,7 +690,8 @@ function spaceDecls(space: SpaceValues): string[] {
|
|
|
666
690
|
// `chartDotR`/`progressBarSize`/`timelineBullet` are DELIBERATELY absent — see this function's
|
|
667
691
|
// doc for why (JS-number-only consumers, zero `var()` reads). `appShellHeaderHeight`/
|
|
668
692
|
// `appShellNavbarWidth`/`appShellNavbarRailWidth`/`appShellAsideWidth`/`appShellAsideRailWidth`
|
|
669
|
-
// join that list —
|
|
693
|
+
// join that list — but NOT `appShellInset`/`appShellInsetMobile`/`appShellHeaderLeadMin`, which
|
|
694
|
+
// are emitted above and carry their own reason for the exception —
|
|
670
695
|
// `shell/index.tsx` reads all five as JS numbers via `useBasaltSpacing()` (Mantine's `AppShell`
|
|
671
696
|
// `header`/`navbar`/`aside` props take numbers, not `var()` strings), so a `--vx-space-app-shell-*`
|
|
672
697
|
// decl here would have had zero consumers, framework-wide, the same dead-weight shape this function's
|
|
@@ -912,6 +937,54 @@ const indent = (block: string): string =>
|
|
|
912
937
|
.map((line) => (line === '' ? line : ` ${line}`))
|
|
913
938
|
.join('\n')
|
|
914
939
|
|
|
940
|
+
/**
|
|
941
|
+
* The `< sm` breakpoint, written the way a stylesheet has to write it — the SAME literal every
|
|
942
|
+
* basalt CSS module spells (`shell/app-header.module.css`, `dashboard/section.module.css`, …), and
|
|
943
|
+
* for the same reason `BREAKPOINTS` above exists: a `@media` condition is resolved before custom
|
|
944
|
+
* properties exist, so the number cannot be a `var()`. Held here as a constant rather than inlined
|
|
945
|
+
* so the emitter and the modules can be grepped as one set.
|
|
946
|
+
*/
|
|
947
|
+
const MOBILE_MEDIA = '@media (max-width: 47.99375em)'
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* The MOBILE INSET LADDER, and this function is its ONE home — framework-wide, not per module.
|
|
951
|
+
*
|
|
952
|
+
* The problem it answers: the desktop-shell pass took the page gutter from 20 to 8 below `sm`
|
|
953
|
+
* (`appShellInsetMobile`), and the card ladder INSIDE that gutter never moved — card insets stayed
|
|
954
|
+
* `xs`/`sm` and a `Section`'s gaps stayed `stackMd` at every width — so a 390px phone still read
|
|
955
|
+
* "content spacing too big" after the gutter fix. Two rungs close it: the card's horizontal inset
|
|
956
|
+
* steps `sm` -> `xs`, and the Section's gap steps `stackMd` -> `stackSm`.
|
|
957
|
+
*
|
|
958
|
+
* Why it lives HERE and not in the three CSS files that consume it: two of those three sites are
|
|
959
|
+
* INLINE styles in TSX (`dashboard/stat-card.tsx`, `charts/primitives/ChartCard.tsx`), and an
|
|
960
|
+
* inline style cannot carry a media query at all. The only surface that reaches all three is a
|
|
961
|
+
* custom property whose VALUE moves at the breakpoint, and the only stylesheet guaranteed present
|
|
962
|
+
* wherever those components render is this one — `BasaltProvider` injects `buildPaletteCss()`, and
|
|
963
|
+
* a component's own CSS module is loaded only if a bundler kept that module. So the law is one
|
|
964
|
+
* `@media` block emitted beside the base declarations it overrides, and every consuming site is a
|
|
965
|
+
* plain `var()` read with no breakpoint knowledge. A media query copied into three files (or into
|
|
966
|
+
* one component stylesheet reaching for `:global(:root)`) was the alternative, and it fails the
|
|
967
|
+
* same way twice: three copies drift, and one copy in a tree-shakeable module is a law that
|
|
968
|
+
* sometimes does not ship.
|
|
969
|
+
*
|
|
970
|
+
* This does NOT make the token layer responsive in general. It stays what `BREAKPOINTS`'s doc says
|
|
971
|
+
* it is — the layer that owns the NUMBER a media query has to write — and this is the one law whose
|
|
972
|
+
* consuming sites cannot write that query themselves. A dimension whose consumer is a CSS module
|
|
973
|
+
* keeps making the choice in that module (`appShellInset`/`appShellInsetMobile` still do).
|
|
974
|
+
*/
|
|
975
|
+
function spaceMobileDecls(space: SpaceValues): string[] {
|
|
976
|
+
return [
|
|
977
|
+
decl('space-card-inset-x', `${space.scale.xs}px`),
|
|
978
|
+
decl('space-section-gap', `${space.anchors.stackSm}px`),
|
|
979
|
+
]
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** {@link spaceMobileDecls} as the `@media` block that overrides the base `:root` declarations —
|
|
983
|
+
* same specificity, later in source order, so it wins below `sm` and nowhere else. */
|
|
984
|
+
function mobileLadderBlock(space: SpaceValues): string {
|
|
985
|
+
return `${MOBILE_MEDIA} {\n :root {\n${indent(spaceMobileDecls(space).join('\n'))}\n }\n}`
|
|
986
|
+
}
|
|
987
|
+
|
|
915
988
|
/**
|
|
916
989
|
* Emit the `--vx-*` stylesheet: a `:root` block of theme-independent scalars, then the
|
|
917
990
|
* dark (default) and light primitive blocks under `html[data-mantine-color-scheme='…']`.
|
|
@@ -942,8 +1015,9 @@ export function buildPaletteCss(
|
|
|
942
1015
|
): string {
|
|
943
1016
|
const groups = opts.groups ?? {}
|
|
944
1017
|
const legacyAliases = opts.legacyAliases ?? true
|
|
1018
|
+
const only = opts.only ?? 'all'
|
|
945
1019
|
const extraDerived = (opts.derived ?? []).map((d) => ` ${d}`).join('\n')
|
|
946
|
-
const derivedBlock = frameworkDerived(data,
|
|
1020
|
+
const derivedBlock = frameworkDerived(data, only, legacyAliases)
|
|
947
1021
|
const derived = extraDerived ? `${derivedBlock}\n${extraDerived}` : derivedBlock
|
|
948
1022
|
const side = (s: Side): string => {
|
|
949
1023
|
const extra = Object.entries(groups)
|
|
@@ -952,7 +1026,14 @@ export function buildPaletteCss(
|
|
|
952
1026
|
const framework = frameworkPrimitives(s, data, legacyAliases)
|
|
953
1027
|
return extra ? `${framework}\n${extra}` : framework
|
|
954
1028
|
}
|
|
955
|
-
|
|
1029
|
+
// The scalars, then the ONE `@media` block that re-points two of them below `sm` (see
|
|
1030
|
+
// `spaceMobileDecls`). Dropped under `only: 'core'` for the same reason the declarations it
|
|
1031
|
+
// overrides are: both vars are component-named one-offs, not `SPACE` anchors, so a core-only
|
|
1032
|
+
// consumer never receives the base pair the block exists to override.
|
|
1033
|
+
const rootBlock =
|
|
1034
|
+
only === 'core'
|
|
1035
|
+
? `:root {\n${derived}\n}`
|
|
1036
|
+
: `:root {\n${derived}\n}\n${mobileLadderBlock(DEFAULT_SPACE_VALUES)}`
|
|
956
1037
|
|
|
957
1038
|
// The legacy shape — kept verbatim, and only reachable when the caller asked for nothing.
|
|
958
1039
|
if (opts.scheme === undefined && opts.defaultScheme === undefined && !opts.mediaFallback) {
|
|
@@ -1073,5 +1154,8 @@ export function buildRadiusCss(radius: RadiusValues | undefined): string {
|
|
|
1073
1154
|
*/
|
|
1074
1155
|
export function buildDensityCss(space: SpaceValues | undefined): string {
|
|
1075
1156
|
if (!space) return ''
|
|
1076
|
-
|
|
1157
|
+
// The `< sm` ladder rides along, at the retuned level — without it a phone at `density: 2` would
|
|
1158
|
+
// take the level-0 mobile inset from the static stylesheet and the level-2 base from this one,
|
|
1159
|
+
// which is the only way the two rungs of one ladder could ever come from different levels.
|
|
1160
|
+
return `:root {\n${spaceDecls(space).join('\n')}\n}\n${mobileLadderBlock(space)}`
|
|
1077
1161
|
}
|
package/src/tokens/palette.ts
CHANGED
|
@@ -312,8 +312,15 @@ const SPACE_ANCHORS_BASE = {
|
|
|
312
312
|
controlHeightWidget: 24,
|
|
313
313
|
/** The `size="ctl"` Mantine tier's resolved height (`docs/CONTROLS-SPEC.md` §5, C5) —
|
|
314
314
|
* `PageBar`/`Section`/table toolbar/sidebar-block controls. Floored at 28px in
|
|
315
|
-
* {@link deriveSpacing}.
|
|
316
|
-
|
|
315
|
+
* {@link deriveSpacing}.
|
|
316
|
+
*
|
|
317
|
+
* 32, up from 30. At 30 this tier was the ONLY chrome number in the shell that did not agree with
|
|
318
|
+
* its neighbours: the sidebar search trigger is 32 (`sidebarSearchTriggerHeight`), a sidebar block
|
|
319
|
+
* row is 32 (`sidebarBlockRowHeight`), and a NavLink row resolves to ~32 (`2 * rowInsetY +
|
|
320
|
+
* rowLineHeight * text.md`) — so a `size="ctl"` button sitting beside any of them read 2px short
|
|
321
|
+
* and the whole band looked unresolved. 32 lands the entire chrome tier on ONE number, which is
|
|
322
|
+
* also the smallest square that still holds a 16px icon with a 8px surround. */
|
|
323
|
+
controlHeightCtl: 32,
|
|
317
324
|
/** The touch hit-area size (`::before`) every home control expands to below `sm` (C15,
|
|
318
325
|
* `docs/CONTROLS-SPEC.md` §5) — independent of the visible `controlHeightCtl` box, same shape as
|
|
319
326
|
* `mobileNavBarHeight`/`mobileNavRowHeight`'s Apple HIG 44pt / WCAG 2.5.5 floors elsewhere in this
|
|
@@ -378,7 +385,7 @@ const SPACE_STEP_BASE = {
|
|
|
378
385
|
// literal could drift from that header, which is exactly what happened. `SpaceValues.step` still
|
|
379
386
|
// carries the key — see that type's own doc — it is just not part of this BASE table. The
|
|
380
387
|
// `stickyHeaderClearanceMobile` sibling went with the two-row mobile header in 1.26.0: the header
|
|
381
|
-
// is one
|
|
388
|
+
// is one `appShellHeaderHeight` row at every viewport now, so one clearance covers both.
|
|
382
389
|
/** NavLink leftSection icon-to-label gap — shared by `theme/nav-link.module.css` (every render
|
|
383
390
|
* path) and `shell/app-sidebar.module.css`'s own `.link` rule (belt-and-suspenders on the same
|
|
384
391
|
* DOM), and reused verbatim for `.footerBtn`/`.accountRow`'s icon gap (their own doc comments call
|
|
@@ -386,8 +393,14 @@ const SPACE_STEP_BASE = {
|
|
|
386
393
|
navIconGap: 10,
|
|
387
394
|
/** Sidebar region-to-region gap: `.brand`'s bottom padding and `.searchSlot`'s bottom padding (in
|
|
388
395
|
* both the expanded and collapsed rail variants) — the searchSlot comment explicitly says its gap
|
|
389
|
-
* opens the nav region "the same way the brand row's own bottom padding does".
|
|
390
|
-
|
|
396
|
+
* opens the nav region "the same way the brand row's own bottom padding does".
|
|
397
|
+
*
|
|
398
|
+
* 12, up from 10. This is the ONE gap the sidebar spends at all three of its seams (brand → search,
|
|
399
|
+
* search → nav, nav → account), so it is what separates the column's regions from each other; at 10
|
|
400
|
+
* it was indistinguishable from the gaps WITHIN a region (`sidebarSectionLabelGap`, the child-list
|
|
401
|
+
* margins), and the column read as one undifferentiated list of rows. 12 is the first step that
|
|
402
|
+
* reads as a region break without opening a hole. */
|
|
403
|
+
sidebarRegionGap: 12,
|
|
391
404
|
|
|
392
405
|
// ── content/prose.module.css ──────────────────────────────────────────────────────────────────
|
|
393
406
|
/** Blockquote rail's own vertical inset. */
|
|
@@ -549,8 +562,11 @@ const SPACE_STEP_BASE = {
|
|
|
549
562
|
// ── shell/app-sidebar.module.css (beyond the shared/reused anchors above) ────────────────────
|
|
550
563
|
/** Brand row's horizontal inset. */
|
|
551
564
|
sidebarBrandInsetX: 7,
|
|
552
|
-
/** Gap between nav sections.
|
|
553
|
-
|
|
565
|
+
/** Gap between nav sections. Paired with `sidebarSectionLabelGap` below: together they are the
|
|
566
|
+
* section's proximity law — MORE space above a section label than below it, so the label reads as
|
|
567
|
+
* belonging to the list it heads. 16/6, up from 12/2: at 12 above and 2 below the pair was
|
|
568
|
+
* INVERTED, gluing each label to the last row of the section before it. */
|
|
569
|
+
sidebarSectionGap: 16,
|
|
554
570
|
/** Account row's top inset. */
|
|
555
571
|
sidebarAccountInsetTop: 9,
|
|
556
572
|
/** Account row's horizontal inset. */
|
|
@@ -559,8 +575,10 @@ const SPACE_STEP_BASE = {
|
|
|
559
575
|
sidebarAccountInsetBottom: 2,
|
|
560
576
|
/** Identity-initials avatar block's fixed size (width and height). */
|
|
561
577
|
sidebarAvatarSize: 28,
|
|
562
|
-
/** Section-label row's bottom gap (before the first nav row)
|
|
563
|
-
|
|
578
|
+
/** Section-label row's bottom gap (before the first nav row) — the smaller half of the
|
|
579
|
+
* proximity pair documented on `sidebarSectionGap` above. 6, up from 2: 2 was hairline enough that
|
|
580
|
+
* the label looked welded to the first row rather than titling the list. */
|
|
581
|
+
sidebarSectionLabelGap: 6,
|
|
564
582
|
/** Collapsible child-list wrapper's top margin. */
|
|
565
583
|
sidebarChildListGapTop: 2,
|
|
566
584
|
/** Collapsible child-list wrapper's bottom margin. */
|
|
@@ -588,15 +606,22 @@ const SPACE_STEP_BASE = {
|
|
|
588
606
|
|
|
589
607
|
// ── shell/index.tsx (AppShell dimensions) ──────────────────────────────────────────────
|
|
590
608
|
/** AppShell desktop header bar height — sized to hold one row of `size="md"` controls, so it
|
|
591
|
-
* tracks `controlHeight`.
|
|
592
|
-
*
|
|
593
|
-
* is a vertical rail's WIDTH (governed by icon footprint).
|
|
609
|
+
* tracks `controlHeight`. It no longer coincides with `appShellNavbarRailWidth` below, which is
|
|
610
|
+
* the point that entry's own note was always making: this is a horizontal bar's HEIGHT (governed
|
|
611
|
+
* by control height), that is a vertical rail's WIDTH (governed by icon footprint). Two separate
|
|
612
|
+
* entries, and now visibly so. */
|
|
594
613
|
/** AppShell header bar height — ONE value at every viewport since 1.26.0 (law C14,
|
|
595
614
|
* `docs/CONTROLS-SPEC.md` §2.1). The pre-1.26.0 `appShellHeaderMobileHeight` (97) existed only
|
|
596
615
|
* because the mobile header wrapped to a second, always-reserved page-actions row; `PageBar`
|
|
597
616
|
* folds that row's overflow into a kebab and moves its filters/tabs into the page flow, so the
|
|
598
|
-
* bar is
|
|
599
|
-
|
|
617
|
+
* bar is one row on a phone too and there is no sum to keep in step any more.
|
|
618
|
+
*
|
|
619
|
+
* 44, down from 48. The band holds ONE `ctl`-tier row (32 since this pass), so 48 left 8px of
|
|
620
|
+
* dead air above and below it — the "too much spacing at the top bar" the header reads as. 44
|
|
621
|
+
* keeps 6px of breathing room a side, still clears `touchControlHeight` (36) below `sm` and
|
|
622
|
+
* `controlHeight` (42) for a `size="md"` control, and is the Apple HIG 44pt figure the rest of
|
|
623
|
+
* this table already floors its touch targets at. */
|
|
624
|
+
appShellHeaderHeight: 44,
|
|
600
625
|
/** AppShell navbar width, expanded — ONE entry for both the `base` (mobile, where the navbar is
|
|
601
626
|
* permanently collapsed and unused — see `MobileNav`) and the expanded `sm` value: the same
|
|
602
627
|
* `.root` at full width, genuinely one concept.
|
|
@@ -608,8 +633,9 @@ const SPACE_STEP_BASE = {
|
|
|
608
633
|
* (`sidebarAccountMenuWidth` 220 + the row inset), so the menu no longer overhangs the column it
|
|
609
634
|
* belongs to. */
|
|
610
635
|
appShellNavbarWidth: 256,
|
|
611
|
-
/** AppShell navbar width, collapsed icon rail. Separate from `appShellHeaderHeight` above
|
|
612
|
-
*
|
|
636
|
+
/** AppShell navbar width, collapsed icon rail. Separate from `appShellHeaderHeight` above, which
|
|
637
|
+
* used to share 48 at level 0 and no longer does (see that entry's note) — the two were always
|
|
638
|
+
* independent concepts and the shared literal was only ever hiding it. */
|
|
613
639
|
appShellNavbarRailWidth: 48,
|
|
614
640
|
/** AppShell ASIDE width, unfolded — the right-hand panel region a route claims by rendering
|
|
615
641
|
* `PageAside` (`docs/ASIDE-SPEC.md` §0). ONE entry for every viewport, same reasoning as
|
|
@@ -624,6 +650,36 @@ const SPACE_STEP_BASE = {
|
|
|
624
650
|
* `appShellNavbarRailWidth` (48) on purpose: the navbar rail hosts a full icon nav row and its
|
|
625
651
|
* hover targets, this one hosts exactly one `ctl`-tier icon button and nothing else. */
|
|
626
652
|
appShellAsideRailWidth: 36,
|
|
653
|
+
/** The PAGE GUTTER — `AppShell.Main`'s own horizontal inset, i.e. the distance from the sidebar
|
|
654
|
+
* seam (and from the viewport's right edge) to the first pixel of page content. Unlike its
|
|
655
|
+
* `appShell*` neighbours above this one IS emitted as a `--vx-*` var (see `spaceDecls`'s doc in
|
|
656
|
+
* `tokens/index.ts` for why), because it is the single most visible spacing number the framework
|
|
657
|
+
* ships and a consumer retuning its identity has to be able to reach it.
|
|
658
|
+
*
|
|
659
|
+
* 20, up from the 13 (`SPACE_SCALE.sm`) the main column used to borrow. The sidebar's nav ICON
|
|
660
|
+
* column already sits at x=20 — 10px of `.root` frame inset plus 10px of NavLink inset
|
|
661
|
+
* (`rowInsetX`) — and the brand row claims to align with it while sitting at 13. 20 makes that
|
|
662
|
+
* claim true across the seam: brand mark, nav icons and the page's first column all start on one
|
|
663
|
+
* vertical line, and the content gets a seam-side gutter that reads deliberate instead of
|
|
664
|
+
* incidental. */
|
|
665
|
+
appShellInset: 20,
|
|
666
|
+
/** The page gutter BELOW `sm` — the phone half of `appShellInset` above. A separate entry rather
|
|
667
|
+
* than one responsive value for the same reason `appShellNavbarWidth` is one: there is no sidebar
|
|
668
|
+
* seam to align to on a phone, so the number is answering a different question (how much of a
|
|
669
|
+
* 390px viewport to spend on margin) and must be free to move alone.
|
|
670
|
+
*
|
|
671
|
+
* 8, down from the same borrowed 13. 13 a side spends 26 of 390px — 6.7% of the viewport — on
|
|
672
|
+
* margin before a single card is drawn, which is the "content spacing is too big" a phone reads
|
|
673
|
+
* as. 8 keeps the content off the bezel without competing with the cards' own insets. */
|
|
674
|
+
appShellInsetMobile: 8,
|
|
675
|
+
/** The app header's BREADCRUMB FLOOR — `shell/app-header.module.css`'s `.lead` `min-width`, the
|
|
676
|
+
* width below which the row's one elastic side may not shrink. A step, not the raw `96px` it
|
|
677
|
+
* shipped as: every other dimension of that row (its height, its control tier, its gaps) tracks
|
|
678
|
+
* density, so a frozen floor would grow relatively narrower at every level up — and it is a floor
|
|
679
|
+
* measured in CHARACTERS of the crumb (~13 at level 0), which is exactly the quantity that moves
|
|
680
|
+
* when the type and the row around it move. The full derivation of 96 is on the `.lead` rule
|
|
681
|
+
* itself; this entry only owns the number. */
|
|
682
|
+
appShellHeaderLeadMin: 96,
|
|
627
683
|
|
|
628
684
|
// ── shell/app-mobile-nav.module.css ───────────────────────────────────────────────────────────
|
|
629
685
|
/** Tab's icon-to-label gap. */
|
|
@@ -709,11 +765,48 @@ const SPACE_STEP_BASE = {
|
|
|
709
765
|
/** `data/virtual-list.tsx`'s skeleton row horizontal inset. */
|
|
710
766
|
virtualRowInsetX: 12,
|
|
711
767
|
|
|
768
|
+
// ── The card/section inset ladder (`tokens/index.ts`'s `spaceMobileDecls` is its ONE home) ────
|
|
769
|
+
// The three numbers a page's CONTENT spends on itself, as opposed to the shell gutter around it
|
|
770
|
+
// (`appShellInset`/`appShellInsetMobile` above). They are grouped, and named, because they step
|
|
771
|
+
// TOGETHER below `sm` — the gutter went 20 -> 8 in the desktop-shell pass while the ladder inside
|
|
772
|
+
// it never moved, so a 390px phone still read "content spacing too big" after the gutter fix.
|
|
773
|
+
//
|
|
774
|
+
// Each value is deliberately the rung of an existing ladder rather than a fresh literal: the card
|
|
775
|
+
// inset IS `SPACE_SCALE.xs`/`.sm` (which is what `StatCard`/`ChartCard` spelled as
|
|
776
|
+
// `--mantine-spacing-xs`/`-sm` before they were named here) and the Section gap IS
|
|
777
|
+
// `SPACE.stackMd`. Naming them buys the ROLE, not a new number — a role the `< sm` override can
|
|
778
|
+
// then re-point without touching the scale every other consumer of `xs`/`sm` reads.
|
|
779
|
+
//
|
|
780
|
+
// There is deliberately NO `*Mobile` sibling for the two that step, unlike `appShellInsetMobile`:
|
|
781
|
+
// that one is a separate entry because a phone gutter answers a different question from a desktop
|
|
782
|
+
// one (how much of 390px to spend on margin, with no sidebar seam to align to). These two answer
|
|
783
|
+
// the SAME question one rung down, so the override reads the neighbouring rung (`scale.xs`,
|
|
784
|
+
// `anchors.stackSm`) and a second literal would only be a place for the ladder to drift apart.
|
|
785
|
+
/** Card inset, VERTICAL — `dashboard/stat-card.tsx` and `charts/primitives/ChartCard.tsx`, which
|
|
786
|
+
* must render identically (a StatCard and a ChartCard sit in the same grid row). Does NOT step
|
|
787
|
+
* below `sm`: the phone is short of WIDTH, and shaving the vertical inset of a card that already
|
|
788
|
+
* hugs its content buys a few pixels at the cost of the card's own breathing room. */
|
|
789
|
+
cardInsetY: 11,
|
|
790
|
+
/** Card inset, HORIZONTAL — same two files, same must-match reason. This IS the half that steps
|
|
791
|
+
* below `sm` (to `SPACE_SCALE.xs`), because horizontal is where a 390px viewport is short. */
|
|
792
|
+
cardInsetX: 13,
|
|
793
|
+
/** `dashboard/section.module.css`'s header→body and body-child gap — a page-level GROUPING
|
|
794
|
+
* rhythm, which is why it reads the stack scale rather than the card-inset one. Steps to
|
|
795
|
+
* `SPACE.stackSm` below `sm`. Distinct from `sidebarSectionGap` above, which is the gap between
|
|
796
|
+
* two NAV sections inside the sidebar column. */
|
|
797
|
+
sectionGap: 12,
|
|
798
|
+
|
|
712
799
|
// ── Controls — homes/tiers (`docs/CONTROLS-SPEC.md` §5) ──────────────────────────────────────
|
|
713
800
|
/** `PageBar` row height (row 1 or row 2, each measured independently — the bar's own overall
|
|
714
801
|
* height is the sum published as `--basalt-page-bar-h`, computed by `PageBar` itself, not a
|
|
715
|
-
* static token).
|
|
716
|
-
|
|
802
|
+
* static token).
|
|
803
|
+
*
|
|
804
|
+
* 36, down from 40. Row 1 portals into a shell-owned band directly under the app header
|
|
805
|
+
* (`shell/page-bar.tsx`), so the two stack as visible chrome: at 40 against a 44 header the
|
|
806
|
+
* secondary row all but matched the primary one, and the eye could not tell which band owned the
|
|
807
|
+
* page. 36 puts it a clear step below the header while still holding a 32px `ctl` row with 2px a
|
|
808
|
+
* side. */
|
|
809
|
+
pageBarRowHeight: 36,
|
|
717
810
|
/** `WidgetHeader tier="section"` row height. */
|
|
718
811
|
sectionHeaderHeight: 36,
|
|
719
812
|
/** `WidgetHeader tier="widget"` row height. */
|