basalt-ui 1.29.1 → 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 +16 -0
- package/MIGRATING.md +701 -369
- 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/query-client.d.ts +20 -2
- package/dist/query-client.d.ts.map +1 -1
- 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/query-client.ts +17 -5
- 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
|
@@ -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
|
/*
|
package/src/shell/index.tsx
CHANGED
|
@@ -291,10 +291,6 @@ function collapseStore(key: string): () => readonly [boolean, (next: boolean) =>
|
|
|
291
291
|
return store
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
// The shared inset for the shell padding and the header's inline padding, so the breadcrumb's
|
|
295
|
-
// left edge and the global actions' right edge land on the card column's edges.
|
|
296
|
-
const SHELL_INSET = 'sm' as const
|
|
297
|
-
|
|
298
294
|
/**
|
|
299
295
|
* The shell's two page-level regions are providers, and both wrap the frame rather than living
|
|
300
296
|
* inside it: `PageBarProvider` owns the header portal and the single-kebab claim, `AsideProvider`
|
|
@@ -334,6 +330,30 @@ function ShellFrame({
|
|
|
334
330
|
// (controls, the search trigger/avatar, nav labels) already track density, so a fixed literal
|
|
335
331
|
// container squeezes progressively worse as density rises.
|
|
336
332
|
const { step } = useBasaltSpacing()
|
|
333
|
+
// THE PAGE GUTTER, responsive — the shared inset for `AppShell`'s own padding and the header's
|
|
334
|
+
// inline padding, so the breadcrumb's left edge and the global actions' right edge land on the
|
|
335
|
+
// card column's edges. It used to be the literal `'sm'` spacing key, which is 13px at EVERY
|
|
336
|
+
// width: 13 on a 360px phone and 13 on a 2560px monitor, with no token behind it. Now it is the
|
|
337
|
+
// `appShellInset` / `appShellInsetMobile` pair (see their entries in `tokens/palette.ts`).
|
|
338
|
+
//
|
|
339
|
+
// On a 390px phone that is 8 a side instead of 13 — 10px more line width, and 10px less vertical
|
|
340
|
+
// chrome, since `padding` is Main's vertical inset too. From `sm` up it is 20, which is the x the
|
|
341
|
+
// sidebar's nav icon column already sits at, so the brand mark, the nav icons and the page's
|
|
342
|
+
// first column all start on one vertical line.
|
|
343
|
+
//
|
|
344
|
+
// VERIFIED against the installed Mantine 9.3 source, because both props take the object by a
|
|
345
|
+
// DIFFERENT road and both had to be checked:
|
|
346
|
+
// - `AppShell padding`: `AppShellMediaStyles/assign-padding-variables.mjs` puts `base` in the
|
|
347
|
+
// `:root` rule and every other key in `get-variables.mjs`'s MIN-width block
|
|
348
|
+
// (`(min-width: <breakpoint>)`), so the pair is mobile-first and needs no max-width twin.
|
|
349
|
+
// - `AppShell.Header px`: a Box style prop, `Box/style-props/parse-style-props.mjs` — same
|
|
350
|
+
// `{ base, … }` shape, same `(min-width: theme.breakpoints.sm)` query, emitted on a generated
|
|
351
|
+
// class. Both resolve `sm` from `theme.breakpoints`, so the two gutters step at one width.
|
|
352
|
+
// A NUMBER on either road is CONVERTED by Mantine's `rem()` — 8 becomes
|
|
353
|
+
// `calc(0.5rem * var(--mantine-scale))` — rather than looked up, which is exactly why these are
|
|
354
|
+
// numbers: a spacing KEY resolves to `var(--mantine-spacing-sm)` and hands the gutter back to
|
|
355
|
+
// Mantine's own scale, which is where the 13px came from.
|
|
356
|
+
const inset = { base: step.appShellInsetMobile, sm: step.appShellInset }
|
|
337
357
|
const aside = useAsideRegion()
|
|
338
358
|
const [storedCollapsed, setStoredCollapsed] = collapseStore(storageKey)()
|
|
339
359
|
// Controlled/uncontrolled seam (item 19): an explicit `collapsed` prop overrides the internal
|
|
@@ -410,7 +430,7 @@ function ShellFrame({
|
|
|
410
430
|
// A plain number, NOT a `calc(... + env(safe-area-inset-bottom))` string: Mantine's own
|
|
411
431
|
// `.footer` rule already adds the inset to both the height and the padding (§2.7).
|
|
412
432
|
footer={{ height: { base: step.mobileNavBarHeight, sm: 0 } }}
|
|
413
|
-
padding={
|
|
433
|
+
padding={inset}
|
|
414
434
|
// `mainClasses.shell` is not decoration — see its rule for why the root has to establish a
|
|
415
435
|
// block formatting context now that Main is offset with margins.
|
|
416
436
|
className={cx(mainClasses.shell, className)}
|
|
@@ -419,7 +439,7 @@ function ShellFrame({
|
|
|
419
439
|
{/* Region seams (docs/DESIGN-SPEC.md §5, §8 #12): Mantine's `[data-with-border]` painted in
|
|
420
440
|
* `--vx-divider` by the theme's `AppShell.extend({ vars })`. No shell module draws a region
|
|
421
441
|
* edge; never opt a section back out of its border here. */}
|
|
422
|
-
<AppShell.Header px={
|
|
442
|
+
<AppShell.Header px={inset}>
|
|
423
443
|
<div className={headerClasses.bar}>
|
|
424
444
|
{/* The header's LEADING ZONE — exactly `--app-shell-navbar-offset` wide, so its trailing
|
|
425
445
|
* edge lands on the sidebar|main seam and the breadcrumb after it starts on Main's own
|
|
@@ -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
|
}
|