basalt-ui 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +15 -1
- package/CHANGELOG.md +27 -0
- package/MIGRATING.md +228 -0
- package/README.md +53 -9
- package/agent/rules/basalt-app.md +43 -0
- package/agent/rules/basalt-charts.md +26 -14
- package/agent/rules/basalt-mantine.md +25 -10
- package/agent/rules/basalt-router.md +22 -10
- package/agent/rules/basalt-tokens.md +75 -6
- package/agent/skills/basalt-app/SKILL.md +42 -6
- package/configs/check.yml +11 -4
- package/configs/lefthook.yml +21 -2
- package/configs/oxlint-plugin.js +515 -25
- package/configs/oxlint-plugin.test.ts +383 -10
- package/configs/oxlint.json +10 -1
- package/dist/agent/stick-to-bottom.js +1 -1
- package/dist/agent-chat/thread-detail-panel.js +1 -1
- package/dist/agent-chat/thread-message.js +2 -2
- package/dist/agent-chat/tool-chip.js +1 -1
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +1 -1
- package/dist/charts/primitives/ChartLegend.js +2 -2
- package/dist/charts/primitives/ChartTooltip.js +1 -1
- package/dist/cli/index.d.ts +107 -15
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +610 -72
- package/dist/cli/toolchain-wiring.test.d.ts +2 -0
- package/dist/cli/toolchain-wiring.test.d.ts.map +1 -0
- package/dist/dashboard/delta-badge.js +2 -2
- package/dist/dashboard/stat-card.js +1 -1
- package/dist/data/virtual-list.js +5 -5
- package/dist/guard/index.d.ts +56 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +326 -59
- package/dist/guard/types.d.ts +25 -5
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/register.d.ts +25 -0
- package/dist/register.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +14 -14
- package/dist/shell/app-mobile-nav.module.css +44 -11
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -1
- package/dist/surfaces.d.ts +1 -1
- package/dist/surfaces.js +1 -1
- package/dist/tokens/index.d.ts +3 -3
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +2 -0
- package/dist/tokens/palette.d.ts +35 -27
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +24 -18
- package/dist/tokens.css +19 -17
- package/dist/vite-color-mix.js +2 -2
- package/llms.txt +3 -3
- package/package.json +4 -3
- package/src/agent/stick-to-bottom.tsx +1 -1
- package/src/agent-chat/thread-detail-panel.tsx +1 -1
- package/src/agent-chat/thread-message.tsx +3 -3
- package/src/agent-chat/tool-chip.tsx +1 -1
- package/src/charts/kinds/Donut.tsx +2 -1
- package/src/charts/primitives/ChartLegend.tsx +2 -2
- package/src/charts/primitives/ChartTooltip.tsx +1 -1
- package/src/cli/index.ts +1070 -99
- package/src/cli/init-sync.test.ts +19 -3
- package/src/cli/tokens-css.test.ts +27 -14
- package/src/cli/toolchain-wiring.test.ts +694 -0
- package/src/dashboard/delta-badge.tsx +3 -3
- package/src/dashboard/stat-card.tsx +1 -1
- package/src/data/virtual-list.tsx +8 -8
- package/src/guard/check-source.test.ts +663 -11
- package/src/guard/index.ts +727 -94
- package/src/guard/types.ts +29 -4
- package/src/register.ts +32 -0
- package/src/shell/app-mobile-nav.module.css +44 -11
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -1
- package/src/shell/app-mobile-nav.test.tsx +34 -0
- package/src/shell/app-mobile-nav.tsx +16 -5
- package/src/surfaces.ts +1 -1
- package/src/theme/density-relations.test.ts +8 -5
- package/src/theme/spacing.test.ts +2 -0
- package/src/theme-toggle/index.tsx +2 -2
- package/src/tokens/index.ts +5 -3
- package/src/tokens/palette.ts +24 -18
- package/src/vite-color-mix.ts +2 -2
package/src/guard/types.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guard types — Mantine-free, dependency-free (zero imports beyond TS types).
|
|
3
3
|
*
|
|
4
|
-
* GuardKind is the closed set of
|
|
4
|
+
* GuardKind is the closed set of 25 violation kinds the theme guard can emit.
|
|
5
5
|
* Finding is the structured result per violation, replacing the old `Violation` shape.
|
|
6
6
|
* GuardConfig is the per-run configuration that drives checkSource.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/** The
|
|
9
|
+
/** The 25 theme-guard violation kinds. */
|
|
10
10
|
export type GuardKind =
|
|
11
11
|
| 'raw-hex'
|
|
12
12
|
| 'raw-color-fn'
|
|
@@ -28,6 +28,11 @@ export type GuardKind =
|
|
|
28
28
|
| 'raw-form-control'
|
|
29
29
|
| 'sub-16-input-font'
|
|
30
30
|
| 'raw-font-family'
|
|
31
|
+
| 'theme-allow-unscoped'
|
|
32
|
+
| 'surface-shadow-override'
|
|
33
|
+
| 'css-raw-surface'
|
|
34
|
+
| 'inline-font-size'
|
|
35
|
+
| 'hidden-inline-style'
|
|
31
36
|
|
|
32
37
|
/**
|
|
33
38
|
* How hard a finding lands. `error` fails the build; `warn` reports and passes.
|
|
@@ -151,10 +156,30 @@ export type GuardConfig = {
|
|
|
151
156
|
*/
|
|
152
157
|
readonly sub16InputFont: boolean
|
|
153
158
|
/**
|
|
154
|
-
* Allow-comment policy
|
|
155
|
-
*
|
|
159
|
+
* Allow-comment policy. The token that opens an exception annotation inside a COMMENT, on the
|
|
160
|
+
* finding's own line or the line directly above it (matching the oxlint plugin's placement, and
|
|
161
|
+
* surviving an `oxfmt` reflow that moves the comment off the offending line). Default
|
|
162
|
+
* `'theme-allow'`.
|
|
163
|
+
*
|
|
164
|
+
* The accountable form is `theme-allow <rule-id>[, <rule-id>] — <reason>`: the rule ids scope the
|
|
165
|
+
* exception to those kinds only, and the reason says why. A `theme-allow` missing either half
|
|
166
|
+
* still suppresses (so an upgrade never breaks a build over it) but reports
|
|
167
|
+
* {@link GuardKind} `theme-allow-unscoped` on its own line.
|
|
156
168
|
*/
|
|
157
169
|
readonly allowComment: string
|
|
170
|
+
/**
|
|
171
|
+
* Which kinds apply to this consumer. `'mantine'` (default) is the full framework guard.
|
|
172
|
+
*
|
|
173
|
+
* `'tokens-only'` is for a consumer that installs the TOKEN LAYER and nothing else — no
|
|
174
|
+
* `@mantine/core`, often no React (`basalt-ui tokens:css` + `basalt-ui/tokens`). Every kind whose
|
|
175
|
+
* remedy is a Mantine component or prop is disabled there, because "use `TextInput` from
|
|
176
|
+
* `@mantine/core`" is not advice, it is a dependency the consumer deliberately does not have.
|
|
177
|
+
* The color/typography kinds stay on — those are the tokens, which is the whole surface they DO
|
|
178
|
+
* consume. See `TOKENS_ONLY_DISABLED_KINDS` in `./index` for the exact set.
|
|
179
|
+
*
|
|
180
|
+
* Optional so a caller built before this field existed keeps compiling; absent means `'mantine'`.
|
|
181
|
+
*/
|
|
182
|
+
readonly profile?: 'mantine' | 'tokens-only'
|
|
158
183
|
/**
|
|
159
184
|
* Per-rule, per-path exemptions — complements whole-file `exempt` (BasaltConfig, which skips
|
|
160
185
|
* ALL rules for a file) and code-level `appliesTo` (hardcoded per-kind path scoping, e.g.
|
package/src/register.ts
CHANGED
|
@@ -156,3 +156,35 @@ export namespace StandardSchemaV1 {
|
|
|
156
156
|
T['~standard']['types']
|
|
157
157
|
>['output']
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
// ── Ambient app globals ───────────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
declare global {
|
|
163
|
+
/**
|
|
164
|
+
* The app's version string, injected as a build-time constant by basalt's own Vite preset —
|
|
165
|
+
* `basaltViteConfig` defines it from `BUILD_VERSION`, falling back to the app package.json
|
|
166
|
+
* `version` (see `../vite`, the `define` block).
|
|
167
|
+
*
|
|
168
|
+
* Declared HERE rather than beside the define because a consumer's `src/**` never loads
|
|
169
|
+
* `basalt-ui/vite`'s types (that subpath is imported from `vite.config.ts`, which most apps keep
|
|
170
|
+
* in a separate tsconfig project). This file is re-exported from the root barrel, so an app that
|
|
171
|
+
* imports from `'basalt-ui'` at all has it in scope with no `/// <reference>` and no
|
|
172
|
+
* `src/vite-env.d.ts` entry — which is the point: basalt shipped the define and no declaration,
|
|
173
|
+
* so every consumer hand-wrote this block, one of them needing an `oxlint-disable` in a file that
|
|
174
|
+
* existed only because basalt asked for it.
|
|
175
|
+
*
|
|
176
|
+
* A consumer importing ONLY a subpath (`basalt-ui/tokens`, `basalt-ui/charts`) does not load this
|
|
177
|
+
* file and does not get the global. That gap is the same set as the applicability gap: reaching
|
|
178
|
+
* for the root barrel is what `BasaltProvider` requires, and an app that never does is not on
|
|
179
|
+
* basalt's Vite preset either, so there is no `__APP_VERSION__` to declare.
|
|
180
|
+
*
|
|
181
|
+
* Precondition: only defined when your Vite config actually goes through `basaltViteConfig` (or
|
|
182
|
+
* you define it yourself). An app that hand-rolls its Vite config and reads this gets a
|
|
183
|
+
* ReferenceError at runtime — TypeScript is trusting the preset, not proving it.
|
|
184
|
+
*/
|
|
185
|
+
// The dangle IS the API: this declares the build-time constant basalt's own Vite preset defines,
|
|
186
|
+
// so the name is not ours to change. The SHIPPED preset allows it (configs/oxlint.json); this
|
|
187
|
+
// repo's own root .oxlintrc.json does not, hence the local suppression.
|
|
188
|
+
// oxlint-disable-next-line no-underscore-dangle
|
|
189
|
+
const __APP_VERSION__: string
|
|
190
|
+
}
|
|
@@ -46,9 +46,17 @@
|
|
|
46
46
|
display: inline-flex;
|
|
47
47
|
align-items: center;
|
|
48
48
|
justify-content: center;
|
|
49
|
-
/*
|
|
50
|
-
*
|
|
51
|
-
|
|
49
|
+
/* This span IS the active pill, so its box may not depend on what it contains: shipping no icon
|
|
50
|
+
* dependency is a supported configuration, and an empty span collapsed the pill to a 24x4px
|
|
51
|
+
* dash. The floor is the icon box PLUS its inset — everything is border-box under Mantine's
|
|
52
|
+
* reset — so the pill renders identically with and without a glyph, at every density. */
|
|
53
|
+
min-width: calc(
|
|
54
|
+
var(--vx-space-mobile-nav-icon-size) + 2 * var(--vx-space-mobile-nav-tab-inset-x)
|
|
55
|
+
);
|
|
56
|
+
min-height: calc(
|
|
57
|
+
var(--vx-space-mobile-nav-icon-size) + 2 * var(--vx-space-mobile-nav-tab-inset-y)
|
|
58
|
+
);
|
|
59
|
+
padding: var(--vx-space-mobile-nav-tab-inset-y) var(--vx-space-mobile-nav-tab-inset-x);
|
|
52
60
|
border-radius: var(--vx-radius-ctrl);
|
|
53
61
|
transition: transform 140ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
54
62
|
}
|
|
@@ -170,6 +178,27 @@
|
|
|
170
178
|
border-top-right-radius: var(--mantine-radius-lg);
|
|
171
179
|
/* A Drawer is NOT an AppShell section, so Mantine adds no inset of its own here. */
|
|
172
180
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
181
|
+
|
|
182
|
+
/*
|
|
183
|
+
* THE `--drawer-size-auto` TRAP — do not "fix" this by restoring `size="auto"` on the Drawer.
|
|
184
|
+
* `getSize('auto', 'drawer-size')` (Mantine's `get-size.mjs`) resolves ANY non-numeric size to
|
|
185
|
+
* `var(--drawer-size-auto)`, and `@mantine/core` defines that custom property NOWHERE — grepped
|
|
186
|
+
* the full shipped `styles.css`. On a bottom Drawer that dead var flows straight into
|
|
187
|
+
* `--drawer-height`, so `size="auto"` was never wired to anything; the sheet rendered at
|
|
188
|
+
* `calc(100% - offset*2)`, i.e. the full viewport, no matter how little content it held. That is
|
|
189
|
+
* why this component no longer passes a `size` prop at all — it would resolve to *some* value,
|
|
190
|
+
* but this rule below overrides Mantine's `height`/`max-height` regardless of what that value is.
|
|
191
|
+
*
|
|
192
|
+
* This works with NO `!important`: `packages/basalt-ui/styles.css` wraps every Mantine rule in
|
|
193
|
+
* `@layer mantine` (verified against the installed `styles.layer.css`), and this CSS module is
|
|
194
|
+
* unlayered — an unlayered rule always wins over a layered one, independent of specificity or
|
|
195
|
+
* source order. `height: auto` lets the sheet hug its content; `max-height` is a hard ceiling on
|
|
196
|
+
* the whole sheet (header + body), sized with headroom ABOVE the `ScrollArea.Autosize`'s
|
|
197
|
+
* `mah="62dvh"` in the JSX — that autosize cap is what actually decides when the sheet starts
|
|
198
|
+
* scrolling internally, so the two never fight over which one owns "the maximum".
|
|
199
|
+
*/
|
|
200
|
+
height: auto;
|
|
201
|
+
max-height: min(70dvh, 100%);
|
|
173
202
|
}
|
|
174
203
|
|
|
175
204
|
/* Sheet title = a micro-label (docs/DESIGN-SPEC.md §3), matching the sidebar's `.sectionLabel`. */
|
|
@@ -182,14 +211,18 @@
|
|
|
182
211
|
color: var(--vx-faint);
|
|
183
212
|
}
|
|
184
213
|
|
|
185
|
-
/*
|
|
186
|
-
*
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
214
|
+
/*
|
|
215
|
+
* Mantine's `ModalBase` header forces a `min-height` of `calc(3.75rem * var(--mantine-scale))`
|
|
216
|
+
* (60px) — sized for a real dialog title, not a 10px micro-label, and a big share of "unnecessarily
|
|
217
|
+
* big". `--vx-space-mobile-nav-row-height` pins it to the same 44px touch-target floor as every
|
|
218
|
+
* other row in this bar instead. The header's close button IS the dismiss affordance now — no
|
|
219
|
+
* separate grabber: there was never swipe-to-dismiss behind it (Mantine ships no drag primitive),
|
|
220
|
+
* so a plain decorative bar bought nothing an explicit, focusable close control doesn't already
|
|
221
|
+
* give for free, including to keyboard/screen-reader users the grabber never reached.
|
|
222
|
+
*/
|
|
223
|
+
.sheetHeader {
|
|
224
|
+
min-height: var(--vx-space-mobile-nav-row-height);
|
|
225
|
+
padding: 0 var(--mantine-spacing-md);
|
|
193
226
|
}
|
|
194
227
|
|
|
195
228
|
/* Large tap rows inside the sheet. NavLink rows reuse the sidebar's neutral active fill.
|
|
@@ -366,4 +366,38 @@ describe('MobileNav', () => {
|
|
|
366
366
|
expect(dropdown.querySelectorAll('.mantine-Menu-divider')).toHaveLength(0)
|
|
367
367
|
expect(dropdown.querySelectorAll('.mantine-Menu-item')).toHaveLength(1)
|
|
368
368
|
})
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Regression guard for the mobile-sheet-content-height fix. Neither half is pixel-assertable
|
|
372
|
+
* under happy-dom (no layout, and CSS-module class hashes are unavailable under `bun test` per
|
|
373
|
+
* the file header) — so this pins the two DOM-observable facts instead: the sheet is no longer
|
|
374
|
+
* wired through the dead `size="auto"` → `--drawer-size-auto` custom property (see `.sheet` in
|
|
375
|
+
* the CSS module for why that was never wired to anything), and the redundant grabber is gone
|
|
376
|
+
* now that the header's own close button is the sheet's sole dismiss affordance.
|
|
377
|
+
*/
|
|
378
|
+
test('11. the sheet drops the no-op size prop and the redundant grabber', async () => {
|
|
379
|
+
renderBar([
|
|
380
|
+
{
|
|
381
|
+
label: 'Reports',
|
|
382
|
+
mobile: { tab: true },
|
|
383
|
+
items: Array.from({ length: 9 }, (_, i) => item(`row${i + 1}`)),
|
|
384
|
+
},
|
|
385
|
+
{ label: 'Main', items: [item('home')] },
|
|
386
|
+
])
|
|
387
|
+
|
|
388
|
+
fireEvent.click(screen.getByLabelText('Reports'))
|
|
389
|
+
await waitFor(() => expect(drawer()).not.toBeNull())
|
|
390
|
+
|
|
391
|
+
// `size="auto"` resolves to `var(--drawer-size-auto)`, a custom property @mantine/core defines
|
|
392
|
+
// nowhere — its presence anywhere in the tree would be the old bug reappearing.
|
|
393
|
+
expect(document.querySelector('[style*="drawer-size-auto"]')).toBeNull()
|
|
394
|
+
|
|
395
|
+
// The header (title + Mantine's own close button) is the ONLY dismiss affordance now — the
|
|
396
|
+
// grabber div that used to precede the scroll area is gone, so the body wraps exactly the
|
|
397
|
+
// `ScrollArea.Autosize` root and nothing else.
|
|
398
|
+
const header = document.querySelector('.mantine-Drawer-header')
|
|
399
|
+
expect(header).not.toBeNull()
|
|
400
|
+
expect(header?.querySelector('.mantine-Drawer-close')).not.toBeNull()
|
|
401
|
+
expect(document.querySelector('.mantine-Drawer-body')?.children).toHaveLength(1)
|
|
402
|
+
})
|
|
369
403
|
})
|
|
@@ -38,7 +38,7 @@ import classes from './app-mobile-nav.module.css'
|
|
|
38
38
|
* bottom-anchored menu puts it UNDER the footer, off-screen — `menuMax` is what guarantees it fits
|
|
39
39
|
* above the bar (§2.2). `shift.padding` is a viewport inset in px, not a layout spacing token.
|
|
40
40
|
*/
|
|
41
|
-
const MENU_MIDDLEWARES = { flip: false, shift: { padding: 8 } } // theme-allow
|
|
41
|
+
const MENU_MIDDLEWARES = { flip: false, shift: { padding: 8 } } // theme-allow inline-spacing — a floating-ui viewport inset, not layout spacing
|
|
42
42
|
|
|
43
43
|
/** A slot that navigates on tap. */
|
|
44
44
|
type LinkSlot = Extract<MobileNavSlot, { kind: 'link' }>
|
|
@@ -428,18 +428,29 @@ export function MobileNav({
|
|
|
428
428
|
opened={openKey === slot.key}
|
|
429
429
|
onClose={close}
|
|
430
430
|
position="bottom"
|
|
431
|
-
size="auto"
|
|
431
|
+
// NO `size` prop — `size="auto"` used to be here, but it is a no-op on a bottom Drawer in
|
|
432
|
+
// Mantine 9.3.0 (see the `.sheet` rule in the CSS module for the full trap). The sheet's
|
|
433
|
+
// actual height/max-height comes entirely from that unlayered CSS rule, which overrides
|
|
434
|
+
// Mantine's own regardless of what `size` resolves to.
|
|
432
435
|
padding="md"
|
|
436
|
+
// `title` is both the sheet's accessible name (Mantine wires `aria-labelledby` to it
|
|
437
|
+
// automatically) and what makes the header render at all — dropping it would need an
|
|
438
|
+
// explicit `aria-label` on the Drawer instead. `classNames.header` slims Mantine's 60px
|
|
439
|
+
// title bar down to this bar's touch-target row height; its close button is the sheet's
|
|
440
|
+
// ONLY dismiss affordance now (see `.sheetHeader` in the CSS module for why the grabber
|
|
441
|
+
// that used to sit here is gone rather than the header).
|
|
433
442
|
title={slot.label}
|
|
434
|
-
classNames={{
|
|
443
|
+
classNames={{
|
|
444
|
+
content: classes.sheet,
|
|
445
|
+
title: classes.sheetTitle,
|
|
446
|
+
header: classes.sheetHeader,
|
|
447
|
+
}}
|
|
435
448
|
transitionProps={{
|
|
436
449
|
transition: 'slide-up',
|
|
437
450
|
duration: reduceMotion ? 0 : 220,
|
|
438
451
|
timingFunction: 'cubic-bezier(0.32, 0.72, 0, 1)',
|
|
439
452
|
}}
|
|
440
453
|
>
|
|
441
|
-
{/* The dismiss affordance. No swipe-to-dismiss — see `.grabber` in the CSS module. */}
|
|
442
|
-
<div className={classes.grabber} aria-hidden />
|
|
443
454
|
<ScrollArea.Autosize mah="62dvh" type="scroll">
|
|
444
455
|
<Stack gap="xs">
|
|
445
456
|
{slot.groups.map((group) => (
|
package/src/surfaces.ts
CHANGED
|
@@ -467,7 +467,7 @@ export const SURFACES = {
|
|
|
467
467
|
kind: 'tooling',
|
|
468
468
|
layer: 'non-js-asset',
|
|
469
469
|
description:
|
|
470
|
-
'Prebuilt --vx-* token stylesheet — the default buildPaletteCss() output as a plain file, for a consumer
|
|
470
|
+
'Prebuilt --vx-* token stylesheet — the default buildPaletteCss() output as a plain file, for a consumer that has a bundler to resolve the subpath but no React and no Mantine; with no bundler, emit a file you own via `basalt-ui tokens:css`',
|
|
471
471
|
forbiddenImports: [],
|
|
472
472
|
},
|
|
473
473
|
'./configs/*': {
|
|
@@ -266,17 +266,20 @@ describe("Fix 7 — the knob's resolution is honest at every notch", () => {
|
|
|
266
266
|
expect(minRatio).toBeLessThan(0.7)
|
|
267
267
|
})
|
|
268
268
|
|
|
269
|
-
test('frozen-vs-baseline ceiling: fewer than
|
|
270
|
-
// Measured under the 0.1/±3 law: worst case is
|
|
271
|
-
// 0.06/±5 law.
|
|
272
|
-
//
|
|
269
|
+
test('frozen-vs-baseline ceiling: fewer than 27 of the ~129 values equal their level-0 value at any non-zero level', () => {
|
|
270
|
+
// Measured under the 0.1/±3 law: worst case is 25 (at level −1) — down from 43 under the old
|
|
271
|
+
// 0.06/±5 law. It is 25 rather than 24 because `mobileNavTabInsetY` (base 2) joined the set:
|
|
272
|
+
// a base that small rounds back to itself at ±1 and ±2 under any multiplier this law can
|
|
273
|
+
// produce, exactly as `mobileNavTabGap` (base 3) already did. That is the rounding law, not a
|
|
274
|
+
// resolution regression — the ceiling of 27 keeps the same margin over the measured worst case
|
|
275
|
+
// while still catching a future step reduction that reintroduces the real dead zone.
|
|
273
276
|
const baseline = flattenSpacing(0)
|
|
274
277
|
const keys = Object.keys(baseline)
|
|
275
278
|
for (const level of ALL_LEVELS) {
|
|
276
279
|
if (level === 0) continue
|
|
277
280
|
const values = flattenSpacing(level)
|
|
278
281
|
const frozen = keys.filter((k) => values[k] === baseline[k]).length
|
|
279
|
-
expect(frozen).toBeLessThan(
|
|
282
|
+
expect(frozen).toBeLessThan(27)
|
|
280
283
|
}
|
|
281
284
|
})
|
|
282
285
|
|
|
@@ -357,6 +357,8 @@ const SPACE_STEP_SWEEP: ReadonlyArray<
|
|
|
357
357
|
// `app-mobile-nav.tsx` hands `mobileNavMenuWidth` to `<Menu width={…}>`.
|
|
358
358
|
['mobileNavBarHeight', 56, null],
|
|
359
359
|
['mobileNavIconSize', 24, 'space-mobile-nav-icon-size'],
|
|
360
|
+
['mobileNavTabInsetY', 2, 'space-mobile-nav-tab-inset-y'],
|
|
361
|
+
['mobileNavTabInsetX', 12, 'space-mobile-nav-tab-inset-x'],
|
|
360
362
|
['mobileNavRowHeight', 44, 'space-mobile-nav-row-height'],
|
|
361
363
|
['mobileNavMenuWidth', 232, null],
|
|
362
364
|
['agentRailInsetX', 10, 'space-agent-rail-inset-x'],
|
|
@@ -84,7 +84,7 @@ function SchemeGlyph({ dark, reduceMotion }: { dark: boolean; reduceMotion: bool
|
|
|
84
84
|
{dark ? (
|
|
85
85
|
<motion.span
|
|
86
86
|
key="moon"
|
|
87
|
-
style={{ display: 'inline-flex' }} // theme-allow
|
|
87
|
+
style={{ display: 'inline-flex' }} // theme-allow inline-display — a framer-motion glyph wrapper, not a Mantine layout node
|
|
88
88
|
initial={{ opacity: 0, scale: 0.6, rotate: 30 }}
|
|
89
89
|
animate={{ opacity: 1, scale: 1, rotate: 0 }}
|
|
90
90
|
exit={{ opacity: 0, scale: 0.6, rotate: -30 }}
|
|
@@ -95,7 +95,7 @@ function SchemeGlyph({ dark, reduceMotion }: { dark: boolean; reduceMotion: bool
|
|
|
95
95
|
) : (
|
|
96
96
|
<motion.span
|
|
97
97
|
key="sun"
|
|
98
|
-
style={{ display: 'inline-flex' }} // theme-allow
|
|
98
|
+
style={{ display: 'inline-flex' }} // theme-allow inline-display — a framer-motion glyph wrapper, not a Mantine layout node
|
|
99
99
|
initial={{ opacity: 0, scale: 0.6, rotate: -30 }}
|
|
100
100
|
animate={{ opacity: 1, scale: 1, rotate: 0 }}
|
|
101
101
|
exit={{ opacity: 0, scale: 0.6, rotate: 30 }}
|
package/src/tokens/index.ts
CHANGED
|
@@ -533,6 +533,8 @@ function spaceDecls(space: SpaceValues): string[] {
|
|
|
533
533
|
decl('space-app-header-mobile-actions-height', `${space.step.appHeaderMobileActionsHeight}px`),
|
|
534
534
|
decl('space-mobile-nav-tab-gap', `${space.step.mobileNavTabGap}px`),
|
|
535
535
|
decl('space-mobile-nav-icon-size', `${space.step.mobileNavIconSize}px`),
|
|
536
|
+
decl('space-mobile-nav-tab-inset-y', `${space.step.mobileNavTabInsetY}px`),
|
|
537
|
+
decl('space-mobile-nav-tab-inset-x', `${space.step.mobileNavTabInsetX}px`),
|
|
536
538
|
decl('space-mobile-nav-row-height', `${space.step.mobileNavRowHeight}px`),
|
|
537
539
|
decl('space-agent-rail-inset-x', `${space.step.agentRailInsetX}px`),
|
|
538
540
|
decl('space-agent-part-gap-top', `${space.step.agentPartGapTop}px`),
|
|
@@ -758,13 +760,13 @@ export type BuildPaletteOpts = {
|
|
|
758
760
|
*/
|
|
759
761
|
mediaFallback?: boolean
|
|
760
762
|
/**
|
|
761
|
-
* How much of the SPACING set to emit. Default: `'all'` (every one of the
|
|
763
|
+
* How much of the SPACING set to emit. Default: `'all'` (every one of the 108 `--vx-space-*`
|
|
762
764
|
* declarations the framework's own components read).
|
|
763
765
|
*
|
|
764
766
|
* `'core'` keeps the 9 generic anchors — the stack rhythm, the two control heights, the row
|
|
765
|
-
* insets — and drops the
|
|
767
|
+
* insets — and drops the 99 named for a specific basalt component
|
|
766
768
|
* (`--vx-space-agent-transcript-inset`, `--vx-space-toc-sub-indent`, …), taking the emitted set
|
|
767
|
-
* from
|
|
769
|
+
* from 202 variables to 103. Nothing outside basalt's own React components reads those, so a
|
|
768
770
|
* framework-free consumer is carrying them for nothing.
|
|
769
771
|
*
|
|
770
772
|
* This is a SPACING filter only: color, radius, shadow, type and status variables are identical
|
package/src/tokens/palette.ts
CHANGED
|
@@ -97,12 +97,12 @@ export const p = (fam: readonly string[], light = 2, dark = 3): ColorPair => ({
|
|
|
97
97
|
*/
|
|
98
98
|
export const SHADOW = {
|
|
99
99
|
card: {
|
|
100
|
-
light: '0 1px 2px rgba(28,25,23,0.05), 0 0 0 1px var(--vx-surface-hairline)',
|
|
101
|
-
dark: '0 1px 3px rgba(0,0,0,0.4), inset 0 0 0 1px color-mix(in srgb, #ffffff 4%, transparent)',
|
|
100
|
+
light: '0 1px 2px rgba(28, 25, 23, 0.05), 0 0 0 1px var(--vx-surface-hairline)',
|
|
101
|
+
dark: '0 1px 3px rgba(0, 0, 0, 0.4), inset 0 0 0 1px color-mix(in srgb, #ffffff 4%, transparent)',
|
|
102
102
|
},
|
|
103
103
|
ctrl: {
|
|
104
|
-
light: '0 1px 2px rgba(28,25,23,0.12)',
|
|
105
|
-
dark: '0 1px 2px rgba(0,0,0,0.35)',
|
|
104
|
+
light: '0 1px 2px rgba(28, 25, 23, 0.12)',
|
|
105
|
+
dark: '0 1px 2px rgba(0, 0, 0, 0.35)',
|
|
106
106
|
},
|
|
107
107
|
/**
|
|
108
108
|
* Interactive-control depth — the ONE value every raised Button/ActionIcon/Input variant shares
|
|
@@ -132,16 +132,16 @@ export const SHADOW = {
|
|
|
132
132
|
* ever needs more, raise the DROP. Do not reintroduce an edge; both shapes have been tried.
|
|
133
133
|
*/
|
|
134
134
|
raised: {
|
|
135
|
-
light: '0 1px 2px rgba(28,25,23,0.10)',
|
|
136
|
-
dark: '0 1px 2px rgba(0,0,0,0.35), inset 0 0 0 1px color-mix(in srgb, #ffffff 8%, transparent)',
|
|
135
|
+
light: '0 1px 2px rgba(28, 25, 23, 0.10)',
|
|
136
|
+
dark: '0 1px 2px rgba(0, 0, 0, 0.35), inset 0 0 0 1px color-mix(in srgb, #ffffff 8%, transparent)',
|
|
137
137
|
},
|
|
138
138
|
// Floating-layer elevation (menus, popovers, tooltips, modals, drawers) — a REAL drop shadow, a
|
|
139
139
|
// step above `card`, so detached surfaces read as lifted off the page (`docs/DESIGN-SPEC.md` §5).
|
|
140
140
|
// Unlike `card`, no embedded ring: floating surfaces carry a real 1px `--vx-surface-border` so
|
|
141
141
|
// Mantine's rotated-square arrow inherits a proper edge.
|
|
142
142
|
overlay: {
|
|
143
|
-
light: '0 8px 24px rgba(28,25,23,0.10)',
|
|
144
|
-
dark: '0 8px 24px rgba(0,0,0,0.5)',
|
|
143
|
+
light: '0 8px 24px rgba(28, 25, 23, 0.10)',
|
|
144
|
+
dark: '0 8px 24px rgba(0, 0, 0, 0.5)',
|
|
145
145
|
},
|
|
146
146
|
} as const
|
|
147
147
|
|
|
@@ -613,8 +613,14 @@ const SPACE_STEP_BASE = {
|
|
|
613
613
|
* `env(safe-area-inset-bottom)` to it. JS-consumed (AppShell `footer.height`), no `--vx-*` var,
|
|
614
614
|
* same shape as `appShellHeaderHeight`. Floored at 48px in `deriveSpacing`. */
|
|
615
615
|
mobileNavBarHeight: 56,
|
|
616
|
-
/** Slot icon box.
|
|
616
|
+
/** Slot icon box. Also the MIN box of the icon slot itself, so the active pill keeps its shape
|
|
617
|
+
* when a destination ships no icon (an icon-less consumer is a supported configuration). */
|
|
617
618
|
mobileNavIconSize: 24,
|
|
619
|
+
/** Active-pill inset around the slot icon, vertical. Sub-scale on its own, but it is one half of
|
|
620
|
+
* a pair with `mobileNavTabInsetX` — a step keeps the pill's shape tracking density. */
|
|
621
|
+
mobileNavTabInsetY: 2,
|
|
622
|
+
/** Active-pill inset around the slot icon, horizontal. */
|
|
623
|
+
mobileNavTabInsetX: 12,
|
|
618
624
|
/** Menu/sheet row height. Floored at 44px in `deriveSpacing` (Apple HIG 44pt / WCAG 2.5.5 AAA). */
|
|
619
625
|
mobileNavRowHeight: 44,
|
|
620
626
|
/** More/group `<Menu width={…}>`. JS-consumed, no var — same rationale as
|
|
@@ -1059,22 +1065,22 @@ function buildPaletteDataUncached(config: DeriveConfig) {
|
|
|
1059
1065
|
const NEUTRAL = {
|
|
1060
1066
|
line: { light: BP.gray[0], dark: BP.gray[4] }, // primary line
|
|
1061
1067
|
line2: { light: BP.gray[1], dark: BP.gray[2] }, // secondary line
|
|
1062
|
-
axis: { light: inkRgba(inkLight, 0.6), dark: 'rgba(255,255,255,0.6)' },
|
|
1063
|
-
axisStroke: { light: inkRgba(inkLight, 0.12), dark: 'rgba(255,255,255,0.12)' },
|
|
1064
|
-
grid: { light: inkRgba(inkLight, 0.07), dark: 'rgba(255,255,255,0.06)' },
|
|
1065
|
-
crosshair: { light: inkRgba(inkLight, 0.32), dark: 'rgba(255,255,255,0.42)' },
|
|
1068
|
+
axis: { light: inkRgba(inkLight, 0.6), dark: 'rgba(255, 255, 255, 0.6)' },
|
|
1069
|
+
axisStroke: { light: inkRgba(inkLight, 0.12), dark: 'rgba(255, 255, 255, 0.12)' },
|
|
1070
|
+
grid: { light: inkRgba(inkLight, 0.07), dark: 'rgba(255, 255, 255, 0.06)' },
|
|
1071
|
+
crosshair: { light: inkRgba(inkLight, 0.32), dark: 'rgba(255, 255, 255, 0.42)' },
|
|
1066
1072
|
dotStroke: { light: BP.white, dark: BP.darkGray[1] }, // matches chart-area bg
|
|
1067
|
-
tooltipBg: { light: BP.white, dark: 'rgba(39,39,42,0.96)' },
|
|
1068
|
-
tooltipText: { light: inkRgba(inkLight, 0.88), dark: 'rgba(255,255,255,0.88)' },
|
|
1069
|
-
tooltipMuted: { light: inkRgba(inkLight, 0.5), dark: 'rgba(255,255,255,0.5)' },
|
|
1073
|
+
tooltipBg: { light: BP.white, dark: 'rgba(39, 39, 42, 0.96)' },
|
|
1074
|
+
tooltipText: { light: inkRgba(inkLight, 0.88), dark: 'rgba(255, 255, 255, 0.88)' },
|
|
1075
|
+
tooltipMuted: { light: inkRgba(inkLight, 0.5), dark: 'rgba(255, 255, 255, 0.5)' },
|
|
1070
1076
|
tooltipBorder: { light: `1px solid ${BP.lightGray[1]}`, dark: 'none' },
|
|
1071
1077
|
tooltipShadow: {
|
|
1072
1078
|
light: `0 2px 8px ${inkRgba(inkLight, 0.1)}`,
|
|
1073
|
-
dark: '0 2px 8px rgba(0,0,0,0.35)',
|
|
1079
|
+
dark: '0 2px 8px rgba(0, 0, 0, 0.35)',
|
|
1074
1080
|
},
|
|
1075
1081
|
// Chart legend text — same DERIVED-ink treatment as axis/grid/tooltip* above (light keys off
|
|
1076
1082
|
// `inkLight`; dark stays flat white like every other neutral chart-chrome pair here).
|
|
1077
|
-
legendText: { light: inkRgba(inkLight, 0.82), dark: 'rgba(255,255,255,0.92)' },
|
|
1083
|
+
legendText: { light: inkRgba(inkLight, 0.82), dark: 'rgba(255, 255, 255, 0.92)' },
|
|
1078
1084
|
// Base neutral for hairlines / muted / DIMMED text / overlays — apply opacity via alpha().
|
|
1079
1085
|
// Binds to --mantine-color-dimmed, so it is THE dimmed-text lever. Equal to `INK.muted` (the
|
|
1080
1086
|
// spec's "muted (secondary text)" row) — kept as its own pair for the pre-existing `--vx-neutral`
|
package/src/vite-color-mix.ts
CHANGED
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
* what a color parser must never do.
|
|
29
29
|
*/
|
|
30
30
|
const NAMED_COLORS: Record<string, string> = {
|
|
31
|
-
white: '#ffffff', // theme-allow — CSS keyword definition, not a design color
|
|
32
|
-
black: '#000000', // theme-allow — CSS keyword definition, not a design color
|
|
31
|
+
white: '#ffffff', // theme-allow raw-hex — the CSS keyword's definition, not a design color
|
|
32
|
+
black: '#000000', // theme-allow raw-hex — the CSS keyword's definition, not a design color
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function normalizeHex(hex: string): string {
|