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
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
* deliberately NOT forwarded — a responsive object on the public prop is exactly the seam that let
|
|
15
15
|
* five call sites disagree.
|
|
16
16
|
*
|
|
17
|
+
* **The law keys on the CONTAINER, not the viewport.** `WidgetGrid` renders its own
|
|
18
|
+
* `container-type: inline-size` box around the grid, so the counts follow the width the grid
|
|
19
|
+
* ACTUALLY has. The shell moves that width without moving the viewport — a `PageAside` claiming its
|
|
20
|
+
* 300px, the sidebar collapsing 256px → its 48px rail, a consumer's split pane — and a `@media`
|
|
21
|
+
* query sees none of it: it kept three chart columns in a 956px content box and every chart in the
|
|
22
|
+
* row went narrower than `VX.phoneChartWidth`, which is the exact condition the `lg` rung exists to
|
|
23
|
+
* avoid. See `widget-grid.module.css`'s header for the fallback.
|
|
24
|
+
*
|
|
17
25
|
* **This is the sanctioned place `lg` enters the package.** `sm` is still the only breakpoint a
|
|
18
26
|
* consumer writes (`docs/CONTROLS-SPEC.md` §2); `WidgetGrid` and `StatGroup` own `lg` internally so
|
|
19
27
|
* nobody else has to.
|
|
@@ -77,18 +85,28 @@ export function WidgetGrid({ cols = 2, children, className, style }: WidgetGridP
|
|
|
77
85
|
|
|
78
86
|
return (
|
|
79
87
|
<WidgetGridColsContext.Provider value={resolved}>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
style
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
{/*
|
|
89
|
+
Two boxes, and the outer one is not decoration: a size container is a container for its
|
|
90
|
+
DESCENDANTS, never for itself, so the grid whose tracks the query decides has to sit inside
|
|
91
|
+
the box being measured. `className`/`style` land on the OUTER box because that is the root
|
|
92
|
+
element a consumer positions (`BasaltProps`) — and because measuring the box the consumer
|
|
93
|
+
sized is the whole point. `container-type: inline-size` contains the inline axis only, so
|
|
94
|
+
the wrapper's block size still comes from the grid: percentage heights resolve exactly as
|
|
95
|
+
before, and there is no paint containment, so nothing inside is clipped.
|
|
96
|
+
*/}
|
|
97
|
+
<div className={cx(classes.container, className)} style={style}>
|
|
98
|
+
<div
|
|
99
|
+
className={classes.root}
|
|
100
|
+
data-cols={cols}
|
|
101
|
+
style={
|
|
102
|
+
{
|
|
103
|
+
'--basalt-widget-grid-cols-sm': resolved.sm,
|
|
104
|
+
'--basalt-widget-grid-cols-lg': resolved.lg,
|
|
105
|
+
} as CSSProperties
|
|
106
|
+
}
|
|
107
|
+
>
|
|
108
|
+
{children}
|
|
109
|
+
</div>
|
|
92
110
|
</div>
|
|
93
111
|
</WidgetGridColsContext.Provider>
|
|
94
112
|
)
|
package/src/guard/index.ts
CHANGED
|
@@ -517,6 +517,33 @@ const PAGE_TITLE_TAG = /<Title(?![\w.])(?:=>|[^>])*?>/g
|
|
|
517
517
|
/** `order={1}` / `order={2}` — the two orders that name a PAGE rather than a section. */
|
|
518
518
|
const PAGE_TITLE_ORDER_PROP = /\border\s*=\s*\{\s*([12])\s*\}/
|
|
519
519
|
|
|
520
|
+
/**
|
|
521
|
+
* The text-lane twin of the plugin's `hasStaticTitleText`: does the body between `<Title …>` and
|
|
522
|
+
* `</Title>` spell a NAME out, or render a value?
|
|
523
|
+
*
|
|
524
|
+
* The law is "one NAME per page", and a name is written in words — so a `<Title order={1}>{
|
|
525
|
+
* knot.title}</Title>` on a nav-less detail route, and a card's hero value
|
|
526
|
+
* `<Title order={2}>{RANK_LABEL[rank]}</Title>`, are data rather than a second page name and stop
|
|
527
|
+
* reporting in BOTH lanes. Kept in step with the plugin deliberately: the two share one id, so one
|
|
528
|
+
* `theme-allow` covers both and they must not disagree about what they cover.
|
|
529
|
+
*
|
|
530
|
+
* Text-lane approximation, and the direction of its error is the safe one: everything outside the
|
|
531
|
+
* outermost `{…}` runs is stripped of braces' contents and what remains has to be blank. A
|
|
532
|
+
* LAUNDERED literal (`{'Users'}`, a substitution-free template) is read as the text it is, so the
|
|
533
|
+
* escape is the expression, not the braces.
|
|
534
|
+
*/
|
|
535
|
+
const PAGE_TITLE_BRACED_RUN = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g
|
|
536
|
+
const PAGE_TITLE_LAUNDERED_LITERAL = /^\s*['"`][^'"`]*['"`]\s*$/
|
|
537
|
+
|
|
538
|
+
function titleBodyNamesThePage(body: string): boolean {
|
|
539
|
+
let outsideBraces = body
|
|
540
|
+
for (const braced of body.match(PAGE_TITLE_BRACED_RUN) ?? []) {
|
|
541
|
+
if (PAGE_TITLE_LAUNDERED_LITERAL.test(braced.slice(1, -1))) return true
|
|
542
|
+
outsideBraces = outsideBraces.replace(braced, '')
|
|
543
|
+
}
|
|
544
|
+
return outsideBraces.replace(/<[^>]*>/g, '').trim() !== ''
|
|
545
|
+
}
|
|
546
|
+
|
|
520
547
|
/**
|
|
521
548
|
* A raw Mantine selection control's opening tag — the text-lane twin of the oxlint plugin's
|
|
522
549
|
* RAW_FILTER_TAGS. `Chip.Group` carries a dot, so the name class allows one.
|
|
@@ -713,19 +740,22 @@ export type GraceEntry = { since: string; promote: string; why: string }
|
|
|
713
740
|
export const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, GraceEntry>> = {
|
|
714
741
|
'raw-selection-control': {
|
|
715
742
|
since: '1.26.0',
|
|
716
|
-
promote: '1.
|
|
743
|
+
promote: '1.31.0',
|
|
717
744
|
why:
|
|
718
745
|
'new in the wave-6 control guards (docs/CONTROLS-SPEC.md §6, law C1). The text lane cannot ' +
|
|
719
746
|
'see ancestry, so "no home" is approximated by a 12-line host-tag window — the loosest ' +
|
|
720
747
|
'reading in the guard, and the reason this one lands warn rather than error even though its ' +
|
|
721
|
-
'law is settled.
|
|
722
|
-
'home`, whose PLUGIN_RULE_GRACE entry carries the
|
|
723
|
-
'
|
|
724
|
-
'
|
|
725
|
-
'
|
|
726
|
-
|
|
727
|
-
'
|
|
728
|
-
'
|
|
748
|
+
'law is settled. ONE LAW, TWO LANES, ONE PROMOTION: it moves with its AST twin, ' +
|
|
749
|
+
'`basalt/control-outside-home`, whose PLUGIN_RULE_GRACE entry carries the full argument and ' +
|
|
750
|
+
'the measurement, and it must never be promoted or extended alone — a consumer waiving one ' +
|
|
751
|
+
'spelling and failing the other is the exact confusion `WAIVER_ID_ALIASES` exists to close. ' +
|
|
752
|
+
'RE-MEASURED 2026-09-09 across the whole fleet on 1.29.2: 29 AST-lane incumbents in five ' +
|
|
753
|
+
"repos, plus three of basalt's own (data-table, notifications/center, theme-lab). Extended " +
|
|
754
|
+
'to 1.31.0 because 1.30.0 is the FIRST release in which either lane names a home a ' +
|
|
755
|
+
'provider-only consumer can reach — every home both messages listed was a shell slot, and ' +
|
|
756
|
+
'the shell-less `PageBar` sentence ships in this minor. Promoting in the same minor that ' +
|
|
757
|
+
'makes a rule satisfiable gives nobody a window to act. 1.31.0 is when the 29 are ' +
|
|
758
|
+
're-measured against that number.',
|
|
729
759
|
},
|
|
730
760
|
}
|
|
731
761
|
|
|
@@ -940,6 +970,27 @@ export const PLUGIN_RULE_IDS: ReadonlySet<string> = new Set([
|
|
|
940
970
|
'query-dual-import',
|
|
941
971
|
])
|
|
942
972
|
|
|
973
|
+
/**
|
|
974
|
+
* ONE LAW, TWO RULE IDS — the ids that waive each other, in BOTH directions.
|
|
975
|
+
*
|
|
976
|
+
* `raw-selection-control` (this lane) and `basalt/control-outside-home` (the oxlint plugin) are the
|
|
977
|
+
* same law read by two engines, and this lane's own message already says so. They keep separate ids
|
|
978
|
+
* because the two engines disagree often enough that a consumer needs to be able to name which one
|
|
979
|
+
* they are waiving — but a consumer waiving the LAW should not have to know that, and before this
|
|
980
|
+
* map they did: `theme-allow control-outside-home` parsed here (the plugin's ids are all known,
|
|
981
|
+
* see {@link PLUGIN_RULE_IDS}) and then covered nothing, so the only working annotation was
|
|
982
|
+
* `theme-allow raw-selection-control control-outside-home` — two ids, undocumented, one silent
|
|
983
|
+
* debug cycle per consumer. `in-body-page-title` needs no entry: it is one id in both lanes.
|
|
984
|
+
*
|
|
985
|
+
* Mirrored VERBATIM in `configs/oxlint-plugin.js` (which cannot import from this package — see its
|
|
986
|
+
* header); `check-source.test.ts` asserts the two agree. An alias only ever WIDENS what an
|
|
987
|
+
* annotation covers, so a waiver can never become stricter by being spelled the other way.
|
|
988
|
+
*/
|
|
989
|
+
export const WAIVER_ID_ALIASES: Readonly<Record<string, readonly string[]>> = {
|
|
990
|
+
'control-outside-home': ['raw-selection-control'],
|
|
991
|
+
'raw-selection-control': ['control-outside-home'],
|
|
992
|
+
}
|
|
993
|
+
|
|
943
994
|
/** The shortest string accepted as a written reason — enough to exclude a stray separator. */
|
|
944
995
|
const MIN_ALLOW_REASON_LENGTH = 4
|
|
945
996
|
|
|
@@ -1346,6 +1397,7 @@ function collectAllowAnnotations(
|
|
|
1346
1397
|
*/
|
|
1347
1398
|
function annotationCovers(annotation: AllowAnnotation, kind: GuardKind): boolean {
|
|
1348
1399
|
if (annotation.rules.includes(kind)) return true
|
|
1400
|
+
if ((WAIVER_ID_ALIASES[kind] ?? []).some((alias) => annotation.rules.includes(alias))) return true
|
|
1349
1401
|
if (annotation.scope === 'file') return false
|
|
1350
1402
|
return annotation.rules.length === 0 && annotation.unknownRules.length === 0
|
|
1351
1403
|
}
|
|
@@ -1998,7 +2050,7 @@ export const GUARD_RULES = {
|
|
|
1998
2050
|
// JSX-tag-shaped (`<Title order={1}>`) — never appears in CSS text.
|
|
1999
2051
|
appliesTo: (relPath) => !relPath.endsWith('.css'),
|
|
2000
2052
|
message:
|
|
2001
|
-
'In-body page title — a page is named ONCE
|
|
2053
|
+
'In-body page title — a page is named ONCE: by the breadcrumb (staticData.title), by PageBar.title, or by <PageTitle> on a shell-less surface (a route error, an auth gate, a standalone screen). Every section/card/table title is a WidgetHeader (docs/CONTROLS-SPEC.md law C8). An <Title order={1|2}> spelling a name out in the body is a second name for the same page, and it drifts. A Title rendering a VALUE — {doc.title}, {RANK_LABEL[rank]} — never reports: that is data, not a name. Prose / ArticleLayout / an overlay and anything under a content/ path never report either. Same law the oxlint plugin enforces as basalt/in-body-page-title — one id, two lanes, so one theme-allow covers both.',
|
|
2002
2054
|
},
|
|
2003
2055
|
'raw-selection-control': {
|
|
2004
2056
|
kind: 'raw-selection-control',
|
|
@@ -2006,7 +2058,7 @@ export const GUARD_RULES = {
|
|
|
2006
2058
|
// JSX-tag-shaped (`<Select …>`) — never appears in CSS text.
|
|
2007
2059
|
appliesTo: (relPath) => !relPath.endsWith('.css'),
|
|
2008
2060
|
message:
|
|
2009
|
-
'Raw selection control with no home — a filter or tab belongs in a PageBar / Section / WidgetHeader slot and takes a `field` (a FieldHandle), so it owns the URL write and the localStorage mirror instead of carrying value/onChange (docs/CONTROLS-SPEC.md laws C1–C3). Use the bound control from basalt-ui/controls. A settings row, an overlay and an @mantine/form file are the declared non-homes and never report. This is the TEXT lane of basalt/control-outside-home, which answers the same question against the real AST — where the two disagree, the plugin is right.',
|
|
2061
|
+
'Raw selection control with no home — a filter or tab belongs in a PageBar / Section / WidgetHeader slot and takes a `field` (a FieldHandle), so it owns the URL write and the localStorage mirror instead of carrying value/onChange (docs/CONTROLS-SPEC.md laws C1–C3). Use the bound control from basalt-ui/controls. A settings row, an overlay and an @mantine/form file are the declared non-homes and never report. No shell? PageBar needs no BasaltShell: outside one it renders in flow, sticky, with its own title — so its slots are a reachable home for a provider-only app. This is the TEXT lane of basalt/control-outside-home, which answers the same question against the real AST — where the two disagree, the plugin is right.',
|
|
2010
2062
|
},
|
|
2011
2063
|
'inline-font-size': {
|
|
2012
2064
|
kind: 'inline-font-size',
|
|
@@ -2125,6 +2177,12 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
|
|
|
2125
2177
|
for (const annotation of fileScoped) {
|
|
2126
2178
|
for (const rule of annotation.rules) {
|
|
2127
2179
|
if (Object.hasOwn(GUARD_RULES, rule)) fileWaived.add(rule as GuardKind)
|
|
2180
|
+
// The plugin-id spelling of a shared law reaches file scope too — a `theme-allow-file
|
|
2181
|
+
// control-outside-home — <why>` names a rule this lane knows under another id, and honouring
|
|
2182
|
+
// it on one lane only is the same silent half-application the line scope had.
|
|
2183
|
+
for (const alias of WAIVER_ID_ALIASES[rule] ?? []) {
|
|
2184
|
+
if (Object.hasOwn(GUARD_RULES, alias)) fileWaived.add(alias as GuardKind)
|
|
2185
|
+
}
|
|
2128
2186
|
}
|
|
2129
2187
|
}
|
|
2130
2188
|
|
|
@@ -2432,6 +2490,16 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
|
|
|
2432
2490
|
for (const m of codeText.matchAll(PAGE_TITLE_TAG)) {
|
|
2433
2491
|
const order = PAGE_TITLE_ORDER_PROP.exec(m[0])
|
|
2434
2492
|
if (order === null) continue
|
|
2493
|
+
// A self-closing `<Title order={1} />` renders no name at all, so it never reports — and the
|
|
2494
|
+
// test has to be explicit rather than fall out of the body scan, which would otherwise run to
|
|
2495
|
+
// the NEXT `</Title>` in the file and judge unrelated code. The plugin agrees by construction
|
|
2496
|
+
// (an element with no children has no static text), and one id must not mean two verdicts.
|
|
2497
|
+
if (m[0].endsWith('/>')) continue
|
|
2498
|
+
const bodyStart = (m.index ?? 0) + m[0].length
|
|
2499
|
+
const closeAt = codeText.indexOf('</Title>', bodyStart)
|
|
2500
|
+
// No closing tag at all: no body to judge, so nothing names the page. See below for what a
|
|
2501
|
+
// body has to hold before this kind reports.
|
|
2502
|
+
if (closeAt === -1 || !titleBodyNamesThePage(codeText.slice(bodyStart, closeAt))) continue
|
|
2435
2503
|
const startLine = codeText.slice(0, m.index ?? 0).split('\n').length
|
|
2436
2504
|
const endLine = startLine + (m[0].split('\n').length - 1)
|
|
2437
2505
|
if (isAllowedInRange(startLine, endLine, 'in-body-page-title')) continue
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,15 @@ export { createBasaltTheme, baseTheme, cssVariablesResolver } from './theme'
|
|
|
37
37
|
export type { BasaltFontsConfig, CreateBasaltThemeOptions } from './theme'
|
|
38
38
|
export { CTL_THEME, CtlSlot } from './theme'
|
|
39
39
|
export type { CtlSlotProps } from './theme'
|
|
40
|
+
/**
|
|
41
|
+
* The ACTIVE resolved spacing off the running theme, not the frozen level-0 `SPACE`/`SPACE_STEP`
|
|
42
|
+
* snapshot on `basalt-ui/tokens`. Public because a consumer laying out ITS OWN chrome beside the
|
|
43
|
+
* shell's (a sticky sub-header under the app header, a fixed rail beside the navbar) has to hit the
|
|
44
|
+
* same rhythm the shell resolved, and every one of those numbers is JS-consumed — an AppShell
|
|
45
|
+
* dimension or a `<Menu width={…}>` — so there is no `--vx-*` var to read it from instead. See
|
|
46
|
+
* `theme/use-basalt-spacing.ts` for the level-0 fallback contract.
|
|
47
|
+
*/
|
|
48
|
+
export { useBasaltSpacing } from './theme'
|
|
40
49
|
export { ThemeToggle, type ThemeToggleProps } from './theme/theme-toggle'
|
|
41
50
|
/**
|
|
42
51
|
* The action vocabulary every home's `actions` slot takes (`docs/CONTROLS-SPEC.md` §2.1). The
|
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
* state and lands its trailing edge exactly on the sidebar|main seam. The negative
|
|
11
11
|
* `margin-inline-start` cancels `AppShell.Header`'s own `px` inset so the zone starts at the
|
|
12
12
|
* viewport edge and re-applies that inset as padding — which is what puts the brand lead on the
|
|
13
|
-
* same x as the sidebar
|
|
13
|
+
* same x as the sidebar's nav ICON column below it. The arithmetic, now that the page gutter is
|
|
14
|
+
* `SPACE.appShellInset`: the zone re-applies `--app-shell-padding` = 20, and a nav icon sits at
|
|
15
|
+
* 10px of `.root` frame inset plus 10px of NavLink inset (`rowInsetX`) = 20. One vertical line
|
|
16
|
+
* across the seam — a claim this comment used to make while the gutter was 13, and that the
|
|
17
|
+
* 13 -> 20 move made true.
|
|
14
18
|
*/
|
|
15
19
|
.zone {
|
|
16
20
|
flex: 0 0 var(--app-shell-navbar-offset, 0rem);
|
|
@@ -58,11 +62,16 @@
|
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/* Workspace switcher (`brand.menu`) — the brand name plus a chevron, as a row-shaped trigger. The
|
|
61
|
-
* chevron is faint until hover so the brand still reads as the brand, not as a control.
|
|
65
|
+
* chevron is faint until hover so the brand still reads as the brand, not as a control.
|
|
66
|
+
*
|
|
67
|
+
* `control-gap`, not the `sidebar-section-label-gap` this used to borrow: it is a HORIZONTAL
|
|
68
|
+
* name-to-glyph gap inside one control, and it has nothing to do with a sidebar label's vertical
|
|
69
|
+
* proximity pair — which is exactly why it moved when that token was retuned for an unrelated
|
|
70
|
+
* reason. Same 6px, correct by meaning. */
|
|
62
71
|
.brandButton {
|
|
63
72
|
display: flex;
|
|
64
73
|
align-items: center;
|
|
65
|
-
gap: var(--vx-space-
|
|
74
|
+
gap: var(--vx-space-control-gap);
|
|
66
75
|
min-width: 0;
|
|
67
76
|
padding: 0;
|
|
68
77
|
color: var(--vx-faint);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* App-shell top bar layout. ONE
|
|
1
|
+
/* App-shell top bar layout. ONE 44px row on every viewport, spanning the FULL viewport width —
|
|
2
2
|
* [brand | collapse] [breadcrumb] ········ [page bar row 1] | [global actions].
|
|
3
3
|
*
|
|
4
4
|
* The leading zone is `app-brand.module.css`'s `.zone`: exactly `--app-shell-navbar-offset` wide,
|
|
@@ -36,17 +36,48 @@
|
|
|
36
36
|
* overflows towards its START, so the sync button painted 30px INTO the page title. Measured in
|
|
37
37
|
* Chrome: the title spanned 119.7–197.1 and the sync button 158.3–188.3.
|
|
38
38
|
*
|
|
39
|
-
* So the lead is now the elastic side (`flex: 1 1 auto`,
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* So the lead is now the elastic side (`flex: 1 1 auto`, each crumb truncating via its own
|
|
40
|
+
* `truncate` prop) and the control side is rigid. A row of buttons that genuinely does not fit is
|
|
41
|
+
* not a shrinking problem — it is the OVERFLOW FOLD's problem, and basalt already owns that (law
|
|
42
|
+
* C7: below `sm` row 1 is the primary plus ONE kebab).
|
|
43
|
+
*
|
|
44
|
+
* `min-width` is a FLOOR, not the `0` it used to be: elastic still, but never elastic to nothing.
|
|
45
|
+
* With `0` the breadcrumb was the row's entire shock absorber and it absorbed itself out of
|
|
46
|
+
* existence — and then the row overflowed anyway, because `.pageBar` and `.global` below are both
|
|
47
|
+
* rigid. Measured on /dashboard at exactly 768px (the `sm` breakpoint, where the navbar appears):
|
|
48
|
+
* 768 − 256 navbar = 512px of header left, against ~554px of row 1 + ~102px of globals + ~48px of
|
|
49
|
+
* gaps ≈ 704px. ~192px over, with the page title already at zero — so the fold bought nothing and
|
|
50
|
+
* cost the reader the one word saying where they are.
|
|
51
|
+
*
|
|
52
|
+
* 96px, because the current crumb is `VX.text.lg` (16px) at `font-stretch: 88%`, i.e. ~7px of
|
|
53
|
+
* average advance: 96px is ~13 characters ("Study progress") before the crumb's own `truncate`
|
|
54
|
+
* starts eliding, and a short title ("Dashboard", "Settings") fits with room to spare. Below that
|
|
55
|
+
* the crumb is an ellipsis with a letter in front of it, which is the state this floor exists to
|
|
56
|
+
* forbid.
|
|
57
|
+
*
|
|
58
|
+
* It reads a TOKEN (`SPACE_STEP.appShellHeaderLeadMin`) rather than the raw `96px` it shipped as,
|
|
59
|
+
* because the derivation above is entirely in units that move with density — the crumb's type size
|
|
60
|
+
* and its average advance — so a frozen floor would buy ~13 characters at level 0 and fewer at
|
|
61
|
+
* every level up. The rem fallback is the level-0 number, for a stylesheet loaded before the
|
|
62
|
+
* provider's.
|
|
63
|
+
*
|
|
64
|
+
* Unscoped by width on purpose — it costs nothing on a phone. At 390px the row is the breadcrumb
|
|
65
|
+
* plus ~66px of mobile bar, ~66px of globals and ~26px of gaps inside a 374px content box, so the
|
|
66
|
+
* floor is never the binding constraint there.
|
|
67
|
+
*
|
|
68
|
+
* The floor is NOT free above `sm`: on its own it adds its own 96px to that ~192px overflow. The
|
|
69
|
+
* `md` label fold in `controls/actions.tsx` (`BarEntry`, same 62em tier) is what pays for it —
|
|
70
|
+
* dropping up to `DESKTOP_SECONDARY_MAX` (3) labelled secondaries to `ctl`-tier icon buttons returns
|
|
71
|
+
* roughly 180-240px on that same row. The two are one change; neither is correct alone, and
|
|
72
|
+
* NEITHER is the real answer, which is measuring the row (a ResizeObserver fold) rather than
|
|
73
|
+
* declaring breakpoints for it. This pair is the cheap, declared interim until that exists.
|
|
43
74
|
*/
|
|
44
75
|
.lead {
|
|
45
76
|
display: flex;
|
|
46
77
|
align-items: center;
|
|
47
78
|
gap: var(--mantine-spacing-sm);
|
|
48
79
|
flex: 1 1 auto;
|
|
49
|
-
min-width:
|
|
80
|
+
min-width: var(--vx-space-app-shell-header-lead-min, 96px);
|
|
50
81
|
}
|
|
51
82
|
|
|
52
83
|
/* No `overflow: hidden` here or on `.lead` above: Mantine's focus ring is `outline: 2px solid;
|
|
@@ -81,7 +112,8 @@
|
|
|
81
112
|
}
|
|
82
113
|
|
|
83
114
|
/*
|
|
84
|
-
* The shell globals sit at the SAME
|
|
115
|
+
* The shell globals sit at the SAME 32px `ctl` tier as everything else in the row, and they get there
|
|
116
|
+
* the
|
|
85
117
|
* same way a home's slot does — `HeaderGlobalActions` wraps this region in a `CtlSlot`, so a
|
|
86
118
|
* consumer's bare `<ActionIcon>` in `globalActions` defaults to `size="ctl"` with no prop at the call
|
|
87
119
|
* site (law C5). They rendered at Mantine's `md` 28px before, which is 2px under the page group
|
|
@@ -98,9 +130,17 @@
|
|
|
98
130
|
flex-shrink: 0;
|
|
99
131
|
}
|
|
100
132
|
|
|
133
|
+
/* Glyph BOX, 1.125rem — the SAME decoration-class fixed size `page-bar.module.css` sets on row 1's
|
|
134
|
+
* own glyphs, and deliberately NOT a `--vx-space-*` token (an icon is not a gap; the shipped icon
|
|
135
|
+
* set draws on a 24px grid and the box is a fraction of the type size, not of the spacing scale).
|
|
136
|
+
* Do not re-litigate these in a token sweep.
|
|
137
|
+
* 1.125rem rather than the old 1rem for the same reason the page bar's grew — `controlHeightCtl`
|
|
138
|
+
* went 30 -> 32, and a glyph held at 16px inside a 32px control reads EMPTIER, not bigger. The two
|
|
139
|
+
* rows are ONE control family (`HeaderGlobalActions` and `PageBar` row 1 sit in the same 44px
|
|
140
|
+
* band), so they had to stop disagreeing by 2px on the same glyph. */
|
|
101
141
|
.global :global(.mantine-ActionIcon-root) > svg {
|
|
102
|
-
width:
|
|
103
|
-
height:
|
|
142
|
+
width: 1.125rem;
|
|
143
|
+
height: 1.125rem;
|
|
104
144
|
}
|
|
105
145
|
|
|
106
146
|
/*
|
|
@@ -89,7 +89,23 @@
|
|
|
89
89
|
margin-bottom: calc(var(--app-shell-footer-offset, 0rem) + env(safe-area-inset-bottom, 0px));
|
|
90
90
|
margin-inline-start: var(--app-shell-navbar-offset, 0rem);
|
|
91
91
|
margin-inline-end: var(--app-shell-aside-offset, 0rem);
|
|
92
|
-
|
|
92
|
+
/*
|
|
93
|
+
* The scrollport's own inset, SPLIT by axis — a gap under a divider is not a gutter beside a
|
|
94
|
+
* seam.
|
|
95
|
+
*
|
|
96
|
+
* Inline stays on `--app-shell-padding`, the one Mantine var this module still reads for a
|
|
97
|
+
* value rather than an offset, because Mantine owns the responsive switch (`shell/index.tsx`
|
|
98
|
+
* hands `AppShell` the `appShellInset`/`appShellInsetMobile` pair) and the header, the band and
|
|
99
|
+
* Main all have to land on the SAME number — re-deriving it here would fork it. It is an
|
|
100
|
+
* ALIGNMENT constraint: 20 at `sm`+ puts the page's first column on the sidebar nav icon
|
|
101
|
+
* column's x across the seam (`app-brand.module.css` states that arithmetic).
|
|
102
|
+
*
|
|
103
|
+
* Block is `stack-md` and is only breathing room under the band's single `--vx-divider` line —
|
|
104
|
+
* nothing aligns to it, so it has no business tracking a gutter that grew for an alignment
|
|
105
|
+
* reason. Holding it at 12 keeps the total chrome above the first card at ~97px (44 header + 40
|
|
106
|
+
* band + 1 seam + 12) — DOWN from 100 — even though the inline gutter grew 13 -> 20.
|
|
107
|
+
*/
|
|
108
|
+
padding: var(--vx-space-stack-md) var(--app-shell-padding);
|
|
93
109
|
/* Without this a wide flex/grid child (a bare table, a nowrap control row) sizes Main to ITSELF. */
|
|
94
110
|
min-width: 0;
|
|
95
111
|
overflow-y: auto;
|
|
@@ -107,9 +123,13 @@
|
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
/* Installed PWA (C5 consolidation, seeded from argo's `styles/native.css`): clear the landscape
|
|
110
|
-
* notch on the side Main touches. `max()` is a FLOOR over `.main`'s
|
|
111
|
-
* — same mechanism as `app-sidebar.module.css`'s left counterpart
|
|
112
|
-
*
|
|
126
|
+
* notch on the side Main touches. `max()` is a FLOOR over the INLINE value of `.main`'s `padding`
|
|
127
|
+
* shorthand above — same mechanism as `app-sidebar.module.css`'s left counterpart, and it still
|
|
128
|
+
* floors correctly now that the shorthand is two axes and the inline one is responsive: this rule
|
|
129
|
+
* reads the same `--app-shell-padding`, so the floor is against whichever of 20/8 is live, and on
|
|
130
|
+
* the phone half (8) it is doing MORE work than before, not less. It overrides only
|
|
131
|
+
* `padding-right`; the block padding keeps coming from the shorthand. The bottom inset already
|
|
132
|
+
* rides `margin-bottom` above (`env(safe-area-inset-bottom)`); the PWA's top inset needs no rule here
|
|
113
133
|
* (the status bar uses the opaque `default` style, per argo's own seed comment). */
|
|
114
134
|
@media (display-mode: standalone) {
|
|
115
135
|
.main {
|
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
align-items: stretch;
|
|
10
10
|
width: 100%;
|
|
11
11
|
height: 100%;
|
|
12
|
-
|
|
12
|
+
/* NOT `--mantine-color-body`: that is the EXACT fill the scrollport above paints (both resolve
|
|
13
|
+
* to `--vx-surface-bg`), so the only thing separating the bar from the page was the region
|
|
14
|
+
* seam's 9%-alpha hairline — invisible on a phone, which is the "it could also have a border"
|
|
15
|
+
* report. `--vx-surface-panel` is a derived step above bg on both schemes and is the same
|
|
16
|
+
* relation `.sheet` already uses, so the edge is a real tonal one with no new token, no drop
|
|
17
|
+
* shadow (DESIGN-CORE pins depth to three tokens, none of them an upward-cast footer shadow)
|
|
18
|
+
* and no `withBorder` on the shell region (basalt-mantine.md forbids drawing that ourselves). */
|
|
19
|
+
background-color: var(--vx-surface-panel);
|
|
13
20
|
-webkit-tap-highlight-color: transparent;
|
|
14
21
|
/* NO padding-bottom / env() here: Mantine's own AppShell `.footer` rule already does
|
|
15
22
|
* `height: calc(var(--app-shell-footer-height) + env(safe-area-inset-bottom))` +
|
|
@@ -90,7 +97,8 @@
|
|
|
90
97
|
height: 8px;
|
|
91
98
|
border-radius: 999px;
|
|
92
99
|
background-color: var(--vx-accent);
|
|
93
|
-
|
|
100
|
+
/* The halo punches the dot out of whatever the BAR paints, so it tracks `.bar`'s fill. */
|
|
101
|
+
box-shadow: 0 0 0 2px var(--vx-surface-panel);
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
/* `text-overflow: clip`, never an ellipsis — a truncated word with a "…" in a 72px tab reads as
|
|
@@ -106,10 +114,25 @@
|
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
/* The menu pops UP out of its tab and is configured `flip: false` — flipping a bottom-anchored
|
|
109
|
-
* menu would put it under the footer
|
|
117
|
+
* menu would put it under the footer, so something has to guarantee it fits. That something is the
|
|
118
|
+
* `max-height` below, not `menuMax` (§2.2 as it used to be written). */
|
|
110
119
|
.menuDropdown {
|
|
111
|
-
padding:
|
|
112
|
-
|
|
120
|
+
padding: var(--vx-space-stack-xs);
|
|
121
|
+
/* THIS is what guarantees the menu never renders below the fold — not `menuMax`, which is only
|
|
122
|
+
* the menu-vs-sheet taste threshold (`mobile-nav-model.ts`'s module doc has the accounting).
|
|
123
|
+
*
|
|
124
|
+
* `min(62dvh, 440px)`, down from `min(72dvh, 560px)`. The old pair fit — but fitting was never
|
|
125
|
+
* the whole ask: MEASURED in headless Chrome at 390x844 the dropdown rendered 300x560 at
|
|
126
|
+
* y=221..781, i.e. 66% of the screen, which reads as a panel rather than as the "subtle, small
|
|
127
|
+
* menu" the More surface is supposed to be. At 440 the same phone gets 52%.
|
|
128
|
+
*
|
|
129
|
+
* The accounting, at both ends of the supported range. 390x844: headroom above the bar is 772px,
|
|
130
|
+
* 62dvh resolves to 523px, so the 440px term binds — 57% of the headroom. 320x568: headroom is
|
|
131
|
+
* ~496px, 62dvh resolves to 352px, so THAT term binds — 71% of the headroom. Either way the
|
|
132
|
+
* binding term is one of the two written here and it lands under the headroom, which is the
|
|
133
|
+
* property that matters: the cap, not `menuMax`, is the guarantee. Past it the dropdown scrolls
|
|
134
|
+
* inside itself instead of growing, so a long More list costs scroll, never fold. */
|
|
135
|
+
max-height: min(62dvh, 440px);
|
|
113
136
|
overflow-y: auto;
|
|
114
137
|
overscroll-behavior: contain;
|
|
115
138
|
}
|
|
@@ -135,13 +158,16 @@
|
|
|
135
158
|
/*
|
|
136
159
|
* A disabled destination still RENDERS in the menu (§2.3 rule 11) — it just must not be reachable.
|
|
137
160
|
* Mantine's `Menu.css` styles `[data-disabled]` with a dimmed color and nothing else, and the row
|
|
138
|
-
* is polymorphic (`component="a"`), so the native `disabled` attribute does not apply either
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
161
|
+
* is polymorphic (`component="a"`), so the native `disabled` attribute does not apply either.
|
|
162
|
+
*
|
|
163
|
+
* `pointer-events: none` USED to be the guard here and is deliberately gone. It made the row
|
|
164
|
+
* swallow a tap with zero feedback — no `:active`, no press state, nothing — which on a phone is
|
|
165
|
+
* indistinguishable from a button that is simply broken, and "I tap and nothing happens" is
|
|
166
|
+
* literally the report this change answers. The guard is now entirely in the handlers (`menuRow`,
|
|
167
|
+
* `sheetRow`), which cover the pointer path and every path a pointer never takes; the row still
|
|
168
|
+
* depresses under a finger and still goes nowhere, which is the honest version of dead.
|
|
142
169
|
*/
|
|
143
170
|
.menuItem[data-disabled] {
|
|
144
|
-
pointer-events: none;
|
|
145
171
|
cursor: not-allowed;
|
|
146
172
|
}
|
|
147
173
|
|
|
@@ -151,7 +177,10 @@
|
|
|
151
177
|
padding-inline-start: calc(var(--mantine-spacing-md) + 12px) !important;
|
|
152
178
|
}
|
|
153
179
|
|
|
154
|
-
/* Overflow section heading
|
|
180
|
+
/* Overflow section heading, in the menu AND in the sheet — inherits the theme's mono micro-label.
|
|
181
|
+
* The inset matches the rows' own (`--vx-space-row-inset-x`) so the heading and the labels under
|
|
182
|
+
* it share one left edge; the vertical padding is asymmetric on purpose — a heading belongs to
|
|
183
|
+
* what follows it, so it sits closer to its own rows than to the section above. */
|
|
155
184
|
.menuLabel {
|
|
156
185
|
font-family: var(--basalt-font-mono);
|
|
157
186
|
font-size: var(--vx-text-micro);
|
|
@@ -159,6 +188,65 @@
|
|
|
159
188
|
text-transform: uppercase;
|
|
160
189
|
letter-spacing: 0.08em;
|
|
161
190
|
color: var(--vx-faint);
|
|
191
|
+
padding: 6px var(--vx-space-row-inset-x) 2px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/*
|
|
195
|
+
* The popover's SECTION RHYTHM — what replaced the single `Menu.Divider`, and it is SPACE, not a
|
|
196
|
+
* line.
|
|
197
|
+
*
|
|
198
|
+
* The Drawer's bulk was never its row height (40px is a WCAG 2.5.5 floor pinned by
|
|
199
|
+
* `theme/density-relations.test.ts`); it was the stack rhythm around the rows — a 12px section
|
|
200
|
+
* gap, 18px of Drawer padding and a header row. A popover with one hairline through the middle and
|
|
201
|
+
* no rhythm at all is the other extreme, so each group opens its own band instead.
|
|
202
|
+
*
|
|
203
|
+
* A `border-top: 1px solid var(--vx-divider)` here was the obvious move and is deliberately NOT
|
|
204
|
+
* what shipped: `theme/divider-law.test.ts` inventories every horizontal hairline in the package
|
|
205
|
+
* and classifies it as `between-rows`, `region-boundary` or (inside `content/**` only)
|
|
206
|
+
* `prose-typography` — and it says in as many words that a rule under a HEADING in the chrome lane
|
|
207
|
+
* is none of the three, and that the fix is to delete it and let the type scale and the space do
|
|
208
|
+
* the work. A mono uppercase micro-label at `--vx-faint` over its own band of air is already a
|
|
209
|
+
* section boundary; a line under it would be the second one.
|
|
210
|
+
*
|
|
211
|
+
* `:not(:first-child)` keeps the first group flush with the dropdown's own padding instead of
|
|
212
|
+
* opening a band under the top edge with nothing above it — the same "a separator separates"
|
|
213
|
+
* property the guarded `Menu.Divider` used to carry in JSX. Scoped to `.menuDropdown` because
|
|
214
|
+
* `.menuLabel` is also the SHEET's group heading (`sheetGroupLabel`), where each label is its
|
|
215
|
+
* group Stack's first child and the rhythm is the Drawer's own to draw.
|
|
216
|
+
*
|
|
217
|
+
* The band is 6/4, down from 8/8 — the second half of the same "still too big" verdict the
|
|
218
|
+
* `max-height` above answers, and the cheaper half. MEASURED per heading at 8/8: 8px margin + 8px
|
|
219
|
+
* padding-top + 2px padding-bottom + a 17px line = a 35px box, and there are FIVE of them in the
|
|
220
|
+
* shipped More surface — 175px, 28% of the whole dropdown spent on five words. 6/4 takes each
|
|
221
|
+
* banded heading to 29px and hands 24px back to the rows. It does NOT touch the row height (40px
|
|
222
|
+
* is the WCAG 2.5.5 floor named at the top of this comment) and it does not add the line the
|
|
223
|
+
* paragraph above rules out: this is the same space that argument asked to carry the boundary,
|
|
224
|
+
* spent tighter.
|
|
225
|
+
*/
|
|
226
|
+
.menuDropdown .menuLabel:not(:first-child) {
|
|
227
|
+
margin-top: 6px;
|
|
228
|
+
padding-top: 4px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* The same band, on the first row of the UNLABELLED tail (blocks + account + settings) — the one
|
|
232
|
+
* boundary in the More surface with no heading to open it. Margin rather than padding: a row is a
|
|
233
|
+
* touch target with a `min-height` floor and symmetric inset, so growing one side of it would make
|
|
234
|
+
* that row read as a different size from its siblings. */
|
|
235
|
+
.menuDropdown .menuSectionStart {
|
|
236
|
+
margin-top: 8px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/*
|
|
240
|
+
* A sticky heading over a scrolling dropdown must be OPAQUE. `theme/index.ts` documents the same
|
|
241
|
+
* fix for the Drawer's header: basalt dropped that background to `transparent`, which reads fine
|
|
242
|
+
* until rows actually scroll under it and then shows every one of them straight through the title.
|
|
243
|
+
* `--vx-surface-overlay` is the dropdown's own fill, so the heading matches the box it sits in.
|
|
244
|
+
*/
|
|
245
|
+
.menuDropdown .menuLabel {
|
|
246
|
+
position: sticky;
|
|
247
|
+
top: 0;
|
|
248
|
+
z-index: 1;
|
|
249
|
+
background-color: var(--vx-surface-overlay);
|
|
162
250
|
}
|
|
163
251
|
|
|
164
252
|
/*
|