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
package/dist/tokens/palette.js
CHANGED
|
@@ -169,8 +169,15 @@ const SPACE_ANCHORS_BASE = {
|
|
|
169
169
|
controlHeightWidget: 24,
|
|
170
170
|
/** The `size="ctl"` Mantine tier's resolved height (`docs/CONTROLS-SPEC.md` §5, C5) —
|
|
171
171
|
* `PageBar`/`Section`/table toolbar/sidebar-block controls. Floored at 28px in
|
|
172
|
-
* {@link deriveSpacing}.
|
|
173
|
-
|
|
172
|
+
* {@link deriveSpacing}.
|
|
173
|
+
*
|
|
174
|
+
* 32, up from 30. At 30 this tier was the ONLY chrome number in the shell that did not agree with
|
|
175
|
+
* its neighbours: the sidebar search trigger is 32 (`sidebarSearchTriggerHeight`), a sidebar block
|
|
176
|
+
* row is 32 (`sidebarBlockRowHeight`), and a NavLink row resolves to ~32 (`2 * rowInsetY +
|
|
177
|
+
* rowLineHeight * text.md`) — so a `size="ctl"` button sitting beside any of them read 2px short
|
|
178
|
+
* and the whole band looked unresolved. 32 lands the entire chrome tier on ONE number, which is
|
|
179
|
+
* also the smallest square that still holds a 16px icon with a 8px surround. */
|
|
180
|
+
controlHeightCtl: 32,
|
|
174
181
|
/** The touch hit-area size (`::before`) every home control expands to below `sm` (C15,
|
|
175
182
|
* `docs/CONTROLS-SPEC.md` §5) — independent of the visible `controlHeightCtl` box, same shape as
|
|
176
183
|
* `mobileNavBarHeight`/`mobileNavRowHeight`'s Apple HIG 44pt / WCAG 2.5.5 floors elsewhere in this
|
|
@@ -194,7 +201,7 @@ const SPACE_STEP_BASE = {
|
|
|
194
201
|
// literal could drift from that header, which is exactly what happened. `SpaceValues.step` still
|
|
195
202
|
// carries the key — see that type's own doc — it is just not part of this BASE table. The
|
|
196
203
|
// `stickyHeaderClearanceMobile` sibling went with the two-row mobile header in 1.26.0: the header
|
|
197
|
-
// is one
|
|
204
|
+
// is one `appShellHeaderHeight` row at every viewport now, so one clearance covers both.
|
|
198
205
|
/** NavLink leftSection icon-to-label gap — shared by `theme/nav-link.module.css` (every render
|
|
199
206
|
* path) and `shell/app-sidebar.module.css`'s own `.link` rule (belt-and-suspenders on the same
|
|
200
207
|
* DOM), and reused verbatim for `.footerBtn`/`.accountRow`'s icon gap (their own doc comments call
|
|
@@ -202,8 +209,14 @@ const SPACE_STEP_BASE = {
|
|
|
202
209
|
navIconGap: 10,
|
|
203
210
|
/** Sidebar region-to-region gap: `.brand`'s bottom padding and `.searchSlot`'s bottom padding (in
|
|
204
211
|
* both the expanded and collapsed rail variants) — the searchSlot comment explicitly says its gap
|
|
205
|
-
* opens the nav region "the same way the brand row's own bottom padding does".
|
|
206
|
-
|
|
212
|
+
* opens the nav region "the same way the brand row's own bottom padding does".
|
|
213
|
+
*
|
|
214
|
+
* 12, up from 10. This is the ONE gap the sidebar spends at all three of its seams (brand → search,
|
|
215
|
+
* search → nav, nav → account), so it is what separates the column's regions from each other; at 10
|
|
216
|
+
* it was indistinguishable from the gaps WITHIN a region (`sidebarSectionLabelGap`, the child-list
|
|
217
|
+
* margins), and the column read as one undifferentiated list of rows. 12 is the first step that
|
|
218
|
+
* reads as a region break without opening a hole. */
|
|
219
|
+
sidebarRegionGap: 12,
|
|
207
220
|
// ── content/prose.module.css ──────────────────────────────────────────────────────────────────
|
|
208
221
|
/** Blockquote rail's own vertical inset. */
|
|
209
222
|
proseQuoteInsetY: 2,
|
|
@@ -354,8 +367,11 @@ const SPACE_STEP_BASE = {
|
|
|
354
367
|
// ── shell/app-sidebar.module.css (beyond the shared/reused anchors above) ────────────────────
|
|
355
368
|
/** Brand row's horizontal inset. */
|
|
356
369
|
sidebarBrandInsetX: 7,
|
|
357
|
-
/** Gap between nav sections.
|
|
358
|
-
|
|
370
|
+
/** Gap between nav sections. Paired with `sidebarSectionLabelGap` below: together they are the
|
|
371
|
+
* section's proximity law — MORE space above a section label than below it, so the label reads as
|
|
372
|
+
* belonging to the list it heads. 16/6, up from 12/2: at 12 above and 2 below the pair was
|
|
373
|
+
* INVERTED, gluing each label to the last row of the section before it. */
|
|
374
|
+
sidebarSectionGap: 16,
|
|
359
375
|
/** Account row's top inset. */
|
|
360
376
|
sidebarAccountInsetTop: 9,
|
|
361
377
|
/** Account row's horizontal inset. */
|
|
@@ -364,8 +380,10 @@ const SPACE_STEP_BASE = {
|
|
|
364
380
|
sidebarAccountInsetBottom: 2,
|
|
365
381
|
/** Identity-initials avatar block's fixed size (width and height). */
|
|
366
382
|
sidebarAvatarSize: 28,
|
|
367
|
-
/** Section-label row's bottom gap (before the first nav row)
|
|
368
|
-
|
|
383
|
+
/** Section-label row's bottom gap (before the first nav row) — the smaller half of the
|
|
384
|
+
* proximity pair documented on `sidebarSectionGap` above. 6, up from 2: 2 was hairline enough that
|
|
385
|
+
* the label looked welded to the first row rather than titling the list. */
|
|
386
|
+
sidebarSectionLabelGap: 6,
|
|
369
387
|
/** Collapsible child-list wrapper's top margin. */
|
|
370
388
|
sidebarChildListGapTop: 2,
|
|
371
389
|
/** Collapsible child-list wrapper's bottom margin. */
|
|
@@ -391,15 +409,22 @@ const SPACE_STEP_BASE = {
|
|
|
391
409
|
sidebarSettingsMenuWidth: 200,
|
|
392
410
|
// ── shell/index.tsx (AppShell dimensions) ──────────────────────────────────────────────
|
|
393
411
|
/** AppShell desktop header bar height — sized to hold one row of `size="md"` controls, so it
|
|
394
|
-
* tracks `controlHeight`.
|
|
395
|
-
*
|
|
396
|
-
* is a vertical rail's WIDTH (governed by icon footprint).
|
|
412
|
+
* tracks `controlHeight`. It no longer coincides with `appShellNavbarRailWidth` below, which is
|
|
413
|
+
* the point that entry's own note was always making: this is a horizontal bar's HEIGHT (governed
|
|
414
|
+
* by control height), that is a vertical rail's WIDTH (governed by icon footprint). Two separate
|
|
415
|
+
* entries, and now visibly so. */
|
|
397
416
|
/** AppShell header bar height — ONE value at every viewport since 1.26.0 (law C14,
|
|
398
417
|
* `docs/CONTROLS-SPEC.md` §2.1). The pre-1.26.0 `appShellHeaderMobileHeight` (97) existed only
|
|
399
418
|
* because the mobile header wrapped to a second, always-reserved page-actions row; `PageBar`
|
|
400
419
|
* folds that row's overflow into a kebab and moves its filters/tabs into the page flow, so the
|
|
401
|
-
* bar is
|
|
402
|
-
|
|
420
|
+
* bar is one row on a phone too and there is no sum to keep in step any more.
|
|
421
|
+
*
|
|
422
|
+
* 44, down from 48. The band holds ONE `ctl`-tier row (32 since this pass), so 48 left 8px of
|
|
423
|
+
* dead air above and below it — the "too much spacing at the top bar" the header reads as. 44
|
|
424
|
+
* keeps 6px of breathing room a side, still clears `touchControlHeight` (36) below `sm` and
|
|
425
|
+
* `controlHeight` (42) for a `size="md"` control, and is the Apple HIG 44pt figure the rest of
|
|
426
|
+
* this table already floors its touch targets at. */
|
|
427
|
+
appShellHeaderHeight: 44,
|
|
403
428
|
/** AppShell navbar width, expanded — ONE entry for both the `base` (mobile, where the navbar is
|
|
404
429
|
* permanently collapsed and unused — see `MobileNav`) and the expanded `sm` value: the same
|
|
405
430
|
* `.root` at full width, genuinely one concept.
|
|
@@ -411,8 +436,9 @@ const SPACE_STEP_BASE = {
|
|
|
411
436
|
* (`sidebarAccountMenuWidth` 220 + the row inset), so the menu no longer overhangs the column it
|
|
412
437
|
* belongs to. */
|
|
413
438
|
appShellNavbarWidth: 256,
|
|
414
|
-
/** AppShell navbar width, collapsed icon rail. Separate from `appShellHeaderHeight` above
|
|
415
|
-
*
|
|
439
|
+
/** AppShell navbar width, collapsed icon rail. Separate from `appShellHeaderHeight` above, which
|
|
440
|
+
* used to share 48 at level 0 and no longer does (see that entry's note) — the two were always
|
|
441
|
+
* independent concepts and the shared literal was only ever hiding it. */
|
|
416
442
|
appShellNavbarRailWidth: 48,
|
|
417
443
|
/** AppShell ASIDE width, unfolded — the right-hand panel region a route claims by rendering
|
|
418
444
|
* `PageAside` (`docs/ASIDE-SPEC.md` §0). ONE entry for every viewport, same reasoning as
|
|
@@ -427,6 +453,36 @@ const SPACE_STEP_BASE = {
|
|
|
427
453
|
* `appShellNavbarRailWidth` (48) on purpose: the navbar rail hosts a full icon nav row and its
|
|
428
454
|
* hover targets, this one hosts exactly one `ctl`-tier icon button and nothing else. */
|
|
429
455
|
appShellAsideRailWidth: 36,
|
|
456
|
+
/** The PAGE GUTTER — `AppShell.Main`'s own horizontal inset, i.e. the distance from the sidebar
|
|
457
|
+
* seam (and from the viewport's right edge) to the first pixel of page content. Unlike its
|
|
458
|
+
* `appShell*` neighbours above this one IS emitted as a `--vx-*` var (see `spaceDecls`'s doc in
|
|
459
|
+
* `tokens/index.ts` for why), because it is the single most visible spacing number the framework
|
|
460
|
+
* ships and a consumer retuning its identity has to be able to reach it.
|
|
461
|
+
*
|
|
462
|
+
* 20, up from the 13 (`SPACE_SCALE.sm`) the main column used to borrow. The sidebar's nav ICON
|
|
463
|
+
* column already sits at x=20 — 10px of `.root` frame inset plus 10px of NavLink inset
|
|
464
|
+
* (`rowInsetX`) — and the brand row claims to align with it while sitting at 13. 20 makes that
|
|
465
|
+
* claim true across the seam: brand mark, nav icons and the page's first column all start on one
|
|
466
|
+
* vertical line, and the content gets a seam-side gutter that reads deliberate instead of
|
|
467
|
+
* incidental. */
|
|
468
|
+
appShellInset: 20,
|
|
469
|
+
/** The page gutter BELOW `sm` — the phone half of `appShellInset` above. A separate entry rather
|
|
470
|
+
* than one responsive value for the same reason `appShellNavbarWidth` is one: there is no sidebar
|
|
471
|
+
* seam to align to on a phone, so the number is answering a different question (how much of a
|
|
472
|
+
* 390px viewport to spend on margin) and must be free to move alone.
|
|
473
|
+
*
|
|
474
|
+
* 8, down from the same borrowed 13. 13 a side spends 26 of 390px — 6.7% of the viewport — on
|
|
475
|
+
* margin before a single card is drawn, which is the "content spacing is too big" a phone reads
|
|
476
|
+
* as. 8 keeps the content off the bezel without competing with the cards' own insets. */
|
|
477
|
+
appShellInsetMobile: 8,
|
|
478
|
+
/** The app header's BREADCRUMB FLOOR — `shell/app-header.module.css`'s `.lead` `min-width`, the
|
|
479
|
+
* width below which the row's one elastic side may not shrink. A step, not the raw `96px` it
|
|
480
|
+
* shipped as: every other dimension of that row (its height, its control tier, its gaps) tracks
|
|
481
|
+
* density, so a frozen floor would grow relatively narrower at every level up — and it is a floor
|
|
482
|
+
* measured in CHARACTERS of the crumb (~13 at level 0), which is exactly the quantity that moves
|
|
483
|
+
* when the type and the row around it move. The full derivation of 96 is on the `.lead` rule
|
|
484
|
+
* itself; this entry only owns the number. */
|
|
485
|
+
appShellHeaderLeadMin: 96,
|
|
430
486
|
// ── shell/app-mobile-nav.module.css ───────────────────────────────────────────────────────────
|
|
431
487
|
/** Tab's icon-to-label gap. */
|
|
432
488
|
mobileNavTabGap: 3,
|
|
@@ -507,11 +563,47 @@ const SPACE_STEP_BASE = {
|
|
|
507
563
|
virtualRowInsetY: 8,
|
|
508
564
|
/** `data/virtual-list.tsx`'s skeleton row horizontal inset. */
|
|
509
565
|
virtualRowInsetX: 12,
|
|
566
|
+
// ── The card/section inset ladder (`tokens/index.ts`'s `spaceMobileDecls` is its ONE home) ────
|
|
567
|
+
// The three numbers a page's CONTENT spends on itself, as opposed to the shell gutter around it
|
|
568
|
+
// (`appShellInset`/`appShellInsetMobile` above). They are grouped, and named, because they step
|
|
569
|
+
// TOGETHER below `sm` — the gutter went 20 -> 8 in the desktop-shell pass while the ladder inside
|
|
570
|
+
// it never moved, so a 390px phone still read "content spacing too big" after the gutter fix.
|
|
571
|
+
//
|
|
572
|
+
// Each value is deliberately the rung of an existing ladder rather than a fresh literal: the card
|
|
573
|
+
// inset IS `SPACE_SCALE.xs`/`.sm` (which is what `StatCard`/`ChartCard` spelled as
|
|
574
|
+
// `--mantine-spacing-xs`/`-sm` before they were named here) and the Section gap IS
|
|
575
|
+
// `SPACE.stackMd`. Naming them buys the ROLE, not a new number — a role the `< sm` override can
|
|
576
|
+
// then re-point without touching the scale every other consumer of `xs`/`sm` reads.
|
|
577
|
+
//
|
|
578
|
+
// There is deliberately NO `*Mobile` sibling for the two that step, unlike `appShellInsetMobile`:
|
|
579
|
+
// that one is a separate entry because a phone gutter answers a different question from a desktop
|
|
580
|
+
// one (how much of 390px to spend on margin, with no sidebar seam to align to). These two answer
|
|
581
|
+
// the SAME question one rung down, so the override reads the neighbouring rung (`scale.xs`,
|
|
582
|
+
// `anchors.stackSm`) and a second literal would only be a place for the ladder to drift apart.
|
|
583
|
+
/** Card inset, VERTICAL — `dashboard/stat-card.tsx` and `charts/primitives/ChartCard.tsx`, which
|
|
584
|
+
* must render identically (a StatCard and a ChartCard sit in the same grid row). Does NOT step
|
|
585
|
+
* below `sm`: the phone is short of WIDTH, and shaving the vertical inset of a card that already
|
|
586
|
+
* hugs its content buys a few pixels at the cost of the card's own breathing room. */
|
|
587
|
+
cardInsetY: 11,
|
|
588
|
+
/** Card inset, HORIZONTAL — same two files, same must-match reason. This IS the half that steps
|
|
589
|
+
* below `sm` (to `SPACE_SCALE.xs`), because horizontal is where a 390px viewport is short. */
|
|
590
|
+
cardInsetX: 13,
|
|
591
|
+
/** `dashboard/section.module.css`'s header→body and body-child gap — a page-level GROUPING
|
|
592
|
+
* rhythm, which is why it reads the stack scale rather than the card-inset one. Steps to
|
|
593
|
+
* `SPACE.stackSm` below `sm`. Distinct from `sidebarSectionGap` above, which is the gap between
|
|
594
|
+
* two NAV sections inside the sidebar column. */
|
|
595
|
+
sectionGap: 12,
|
|
510
596
|
// ── Controls — homes/tiers (`docs/CONTROLS-SPEC.md` §5) ──────────────────────────────────────
|
|
511
597
|
/** `PageBar` row height (row 1 or row 2, each measured independently — the bar's own overall
|
|
512
598
|
* height is the sum published as `--basalt-page-bar-h`, computed by `PageBar` itself, not a
|
|
513
|
-
* static token).
|
|
514
|
-
|
|
599
|
+
* static token).
|
|
600
|
+
*
|
|
601
|
+
* 36, down from 40. Row 1 portals into a shell-owned band directly under the app header
|
|
602
|
+
* (`shell/page-bar.tsx`), so the two stack as visible chrome: at 40 against a 44 header the
|
|
603
|
+
* secondary row all but matched the primary one, and the eye could not tell which band owned the
|
|
604
|
+
* page. 36 puts it a clear step below the header while still holding a 32px `ctl` row with 2px a
|
|
605
|
+
* side. */
|
|
606
|
+
pageBarRowHeight: 36,
|
|
515
607
|
/** `WidgetHeader tier="section"` row height. */
|
|
516
608
|
sectionHeaderHeight: 36,
|
|
517
609
|
/** `WidgetHeader tier="widget"` row height. */
|
package/dist/tokens.css
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
--vx-space-control-height: 2.625rem;
|
|
14
14
|
--vx-space-control-height-tag: 1.25rem;
|
|
15
15
|
--vx-space-control-height-widget: 1.5rem;
|
|
16
|
-
--vx-space-control-height-ctl:
|
|
16
|
+
--vx-space-control-height-ctl: 2rem;
|
|
17
17
|
--vx-space-touch-control-height: 2.25rem;
|
|
18
18
|
--vx-space-stack-xs: 4px;
|
|
19
19
|
--vx-space-stack-sm: 8px;
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
--vx-space-stack-xl: 24px;
|
|
23
23
|
--vx-space-sticky-header-clearance: 12px;
|
|
24
24
|
--vx-space-nav-icon-gap: 10px;
|
|
25
|
-
--vx-space-sidebar-region-gap:
|
|
25
|
+
--vx-space-sidebar-region-gap: 12px;
|
|
26
26
|
--vx-space-prose-quote-inset-y: 2px;
|
|
27
27
|
--vx-space-prose-quote-indent: 12px;
|
|
28
28
|
--vx-space-prose-inline-code-inset-y: 1.5px;
|
|
@@ -85,17 +85,20 @@
|
|
|
85
85
|
--vx-space-settings-row-gap: 16px;
|
|
86
86
|
--vx-space-mermaid-container-inset: 16px;
|
|
87
87
|
--vx-space-sidebar-brand-inset-x: 7px;
|
|
88
|
-
--vx-space-sidebar-section-gap:
|
|
88
|
+
--vx-space-sidebar-section-gap: 16px;
|
|
89
89
|
--vx-space-sidebar-account-inset-top: 9px;
|
|
90
90
|
--vx-space-sidebar-account-inset-x: 7px;
|
|
91
91
|
--vx-space-sidebar-account-inset-bottom: 2px;
|
|
92
92
|
--vx-space-sidebar-avatar-size: 28px;
|
|
93
|
-
--vx-space-sidebar-section-label-gap:
|
|
93
|
+
--vx-space-sidebar-section-label-gap: 6px;
|
|
94
94
|
--vx-space-sidebar-child-list-gap-top: 2px;
|
|
95
95
|
--vx-space-sidebar-child-list-gap-bottom: 3px;
|
|
96
96
|
--vx-space-sidebar-child-list-indent: 15px;
|
|
97
97
|
--vx-space-sidebar-child-row-inset-y: 4px;
|
|
98
98
|
--vx-space-sidebar-child-row-indent: 12px;
|
|
99
|
+
--vx-space-app-shell-inset: 20px;
|
|
100
|
+
--vx-space-app-shell-inset-mobile: 8px;
|
|
101
|
+
--vx-space-app-shell-header-lead-min: 96px;
|
|
99
102
|
--vx-space-mobile-nav-tab-gap: 3px;
|
|
100
103
|
--vx-space-mobile-nav-icon-size: 24px;
|
|
101
104
|
--vx-space-mobile-nav-tab-inset-y: 2px;
|
|
@@ -113,9 +116,12 @@
|
|
|
113
116
|
--vx-space-badge-inset-y: 2px;
|
|
114
117
|
--vx-space-badge-inset-x: 7px;
|
|
115
118
|
--vx-space-stat-card-gap: 8px;
|
|
119
|
+
--vx-space-card-inset-y: 11px;
|
|
120
|
+
--vx-space-card-inset-x: 13px;
|
|
121
|
+
--vx-space-section-gap: 12px;
|
|
116
122
|
--vx-space-virtual-row-inset-y: 8px;
|
|
117
123
|
--vx-space-virtual-row-inset-x: 12px;
|
|
118
|
-
--vx-space-page-bar-row-height: 2.
|
|
124
|
+
--vx-space-page-bar-row-height: 2.25rem;
|
|
119
125
|
--vx-space-section-header-height: 2.25rem;
|
|
120
126
|
--vx-space-widget-header-height: 1.75rem;
|
|
121
127
|
--vx-space-sidebar-block-row-height: 2rem;
|
|
@@ -184,6 +190,12 @@
|
|
|
184
190
|
--vx-badRef: var(--vx-bad-ref);
|
|
185
191
|
--vx-warnRef: var(--vx-warn-ref);
|
|
186
192
|
}
|
|
193
|
+
@media (max-width: 47.99375em) {
|
|
194
|
+
:root {
|
|
195
|
+
--vx-space-card-inset-x: 11px;
|
|
196
|
+
--vx-space-section-gap: 8px;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
187
199
|
:root,
|
|
188
200
|
html[data-mantine-color-scheme='dark'] {
|
|
189
201
|
--vx-status-excellent: #43bf4d;
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# basalt-ui v1.
|
|
1
|
+
# basalt-ui v1.30.0
|
|
2
2
|
# Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
|
|
3
3
|
#
|
|
4
4
|
# basalt-ui is an opinionated framework for Mantine-based React apps.
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
# ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
|
|
20
20
|
|
|
21
21
|
Package: basalt-ui
|
|
22
|
-
Version: 1.
|
|
22
|
+
Version: 1.30.0
|
|
23
23
|
|
|
24
24
|
## basalt-ui
|
|
25
25
|
Import: import { ... } from 'basalt-ui'
|
package/package.json
CHANGED
|
@@ -45,15 +45,20 @@ const bodyClipStyle: CSSProperties = {
|
|
|
45
45
|
borderBottomLeftRadius: VX.radiusCard,
|
|
46
46
|
borderBottomRightRadius: VX.radiusCard,
|
|
47
47
|
overflow: 'hidden',
|
|
48
|
-
padding: '2px var(--
|
|
48
|
+
padding: '2px var(--vx-space-card-inset-x, 0.8125rem) var(--vx-space-card-inset-y, 0.6875rem)',
|
|
49
49
|
}
|
|
50
|
-
// Card inset =
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
50
|
+
// Card inset = the `cardInsetY`/`cardInsetX` role tokens (vertical / horizontal), the SAME two
|
|
51
|
+
// vars `StatCard` reads — a ChartCard and a StatCard in one grid row must not disagree about their
|
|
52
|
+
// inset, which is why the pair is one named role rather than two files each spelling `xs`/`sm`.
|
|
53
|
+
// They used to read `--mantine-spacing-xs`/`-sm`; the `--vx-*` pair carries the same numbers off
|
|
54
|
+
// the same owned scale, is emitted by the same stylesheet at a retuned density, and — unlike the
|
|
55
|
+
// Mantine vars — steps a rung down below `sm` (the mobile inset ladder, `spaceMobileDecls` in
|
|
56
|
+
// `tokens/index.ts`). The rem fallbacks are unchanged and keep a `./charts`-only consumer with
|
|
57
|
+
// neither MantineProvider nor BasaltProvider padded identically. No header/body divider — the
|
|
58
|
+
// title block and the plot read as one continuous surface; a rule here only doubled the card's
|
|
59
|
+
// shadow-embedded ring and made the header feel heavier.
|
|
55
60
|
const headerWrapStyle: CSSProperties = {
|
|
56
|
-
padding: 'var(--
|
|
61
|
+
padding: 'var(--vx-space-card-inset-y, 0.6875rem) var(--vx-space-card-inset-x, 0.8125rem) 4px',
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/**
|
|
@@ -6,11 +6,13 @@ import { useChartSize } from '../hooks/useChartSize'
|
|
|
6
6
|
import { deriveLegend } from '../series'
|
|
7
7
|
import type { ChartLegendConfig, LegendPlacement, SeriesStyle } from '../series'
|
|
8
8
|
import {
|
|
9
|
-
chartTierMetrics,
|
|
10
9
|
legendEntryCap,
|
|
11
10
|
resolveChartTier,
|
|
11
|
+
resolveFrameHeight,
|
|
12
|
+
resolveLegendMaxRows,
|
|
12
13
|
resolvePlotRect,
|
|
13
14
|
} from './chart-frame-layout'
|
|
15
|
+
import type { ResponsiveChartHeight } from './chart-frame-layout'
|
|
14
16
|
import { ChartTierProvider } from './chart-tier'
|
|
15
17
|
import { ChartLegend } from './ChartLegend'
|
|
16
18
|
import { ChartEmpty, ChartError, ChartPending, resolveChartState } from './ChartPending'
|
|
@@ -19,6 +21,7 @@ import type { ChartState } from './ChartPending'
|
|
|
19
21
|
/** Re-exported for `ChartFrame.test.tsx` and any other consumer that previously reached these
|
|
20
22
|
* through `ChartFrame` — the layout math itself lives in `./chart-frame-layout` (pure, DOM-free). */
|
|
21
23
|
export { legendEntryCap, resolvePlotRect, resolveChartTier } from './chart-frame-layout'
|
|
24
|
+
export type { ResponsiveChartHeight } from './chart-frame-layout'
|
|
22
25
|
|
|
23
26
|
const DEFAULT_HEIGHT = 240
|
|
24
27
|
const DEFAULT_MIN_WIDTH = 200
|
|
@@ -28,7 +31,17 @@ const DEFAULT_MIN_WIDTH = 200
|
|
|
28
31
|
export type ChartFrameLegend = {
|
|
29
32
|
/** Default 'bottom'. */
|
|
30
33
|
placement?: LegendPlacement
|
|
31
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Wrap cap → "+N more" rollup at high cardinality.
|
|
36
|
+
*
|
|
37
|
+
* An explicit value WINS OUTRIGHT — including over the phone tier's own two-row default
|
|
38
|
+
* (`chartTierMetrics`). It used to be `Math.min`'d against the tier, which made the documented
|
|
39
|
+
* per-chart opt-out ("opt back out with an explicit `legend.maxRows`") false in the one direction
|
|
40
|
+
* anybody needs it: a consumer's 380px inspector panel is "a phone" on a 1440px desktop, and a
|
|
41
|
+
* six-series legend rolled up to `+4 more` with no way to say no. Under `fill` the measured
|
|
42
|
+
* entry cap can still roll up FURTHER — that is a floor on the plot, not a tier default
|
|
43
|
+
* ({@link legendEntryCap}).
|
|
44
|
+
*/
|
|
32
45
|
maxRows?: number
|
|
33
46
|
/** Visually separate role: series | overlay | reference. */
|
|
34
47
|
groups?: boolean
|
|
@@ -47,8 +60,16 @@ export type ChartFrameLegend = {
|
|
|
47
60
|
export type ChartFrameProps = BasaltProps & {
|
|
48
61
|
/** Series identity — drives the derived legend. Pass the SAME array the kind draws + tooltips from. */
|
|
49
62
|
series: readonly SeriesStyle[]
|
|
50
|
-
/**
|
|
51
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Height in pixels, or one per size step. Used when neither `aspectRatio` nor `fill` is set.
|
|
65
|
+
* Default 240.
|
|
66
|
+
*
|
|
67
|
+
* `{ base: 180, md: 260 }` is the declarative escape from a page of fixed-height charts: the
|
|
68
|
+
* steps are compared against the frame's own MEASURED width, not the viewport, so a chart squeezed
|
|
69
|
+
* by a `PageAside` gets the short height on a desktop too. A plain number is unchanged and stays
|
|
70
|
+
* supported ({@link resolveFrameHeight}).
|
|
71
|
+
*/
|
|
72
|
+
height?: number | ResponsiveChartHeight
|
|
52
73
|
/** height = Math.round(containerWidth / aspectRatio). Ignored when `fill` is set. */
|
|
53
74
|
aspectRatio?: number
|
|
54
75
|
/** Fill the parent flex/grid cell's measured height instead of a fixed/derived one. */
|
|
@@ -206,7 +227,7 @@ export function ChartFrame({
|
|
|
206
227
|
? containerH
|
|
207
228
|
: aspectRatio !== undefined
|
|
208
229
|
? Math.round(containerW / aspectRatio)
|
|
209
|
-
: (height ?? DEFAULT_HEIGHT)
|
|
230
|
+
: resolveFrameHeight(height ?? DEFAULT_HEIGHT, containerW)
|
|
210
231
|
|
|
211
232
|
const sideLegendWidth = legendVisible && vertical ? legendW : 0
|
|
212
233
|
const topBottomLegendHeight = legendVisible && !vertical ? legendH : 0
|
|
@@ -225,17 +246,11 @@ export function ChartFrame({
|
|
|
225
246
|
// A fixed-height frame grows around its legend; a `fill` one cannot, so its legend rolls up
|
|
226
247
|
// instead of eating the plot (see `legendEntryCap`). Top/bottom only — a side legend costs
|
|
227
248
|
// width, not height.
|
|
228
|
-
// The
|
|
229
|
-
// `
|
|
230
|
-
|
|
249
|
+
// The resolved cap — the caller's if they named one, else the tier's DEFAULT, never both
|
|
250
|
+
// `Math.min`'d (`resolveLegendMaxRows` owns that law and says why) — enters the fill measurement
|
|
251
|
+
// as the upper bound, so a phone-width `fill` frame can only ever roll up further, never less.
|
|
231
252
|
const callerMaxRows =
|
|
232
|
-
legend === false
|
|
233
|
-
? undefined
|
|
234
|
-
: legend.maxRows === undefined
|
|
235
|
-
? tierMaxRows
|
|
236
|
-
: tierMaxRows === undefined
|
|
237
|
-
? legend.maxRows
|
|
238
|
-
: Math.min(legend.maxRows, tierMaxRows)
|
|
253
|
+
legend === false ? undefined : resolveLegendMaxRows({ callerMaxRows: legend.maxRows, tier })
|
|
239
254
|
|
|
240
255
|
const maxRows =
|
|
241
256
|
legend === false
|
|
@@ -39,6 +39,70 @@ export function resolveChartTier(containerW: number): ChartTier {
|
|
|
39
39
|
return containerW > 0 && containerW < VX.phoneChartWidth ? 'phone' : 'desktop'
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* A chart height stated per size step instead of as one number — the declarative escape from "every
|
|
44
|
+
* chart on this page is 260px tall on a 375px phone too".
|
|
45
|
+
*
|
|
46
|
+
* `base` is required and is the height a box narrower than every named step gets; each further key
|
|
47
|
+
* is the height from that step UP. Keys are Mantine's breakpoint names so a consumer writes the
|
|
48
|
+
* vocabulary they already write, and nothing more than `sm`/`md`/`lg` is offered: `xs`/`xl` would
|
|
49
|
+
* be two more rungs nobody has asked a chart to have.
|
|
50
|
+
*
|
|
51
|
+
* ```tsx
|
|
52
|
+
* <ChartFrame series={series} height={{ base: 180, md: 260 }}>{…}</ChartFrame>
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export type ResponsiveChartHeight = {
|
|
56
|
+
/** Below the narrowest STATED step — and NOT the first-frame value; see {@link resolveFrameHeight}. */
|
|
57
|
+
base: number
|
|
58
|
+
/** From 768px of MEASURED container width up. */
|
|
59
|
+
sm?: number
|
|
60
|
+
/** From 992px up. */
|
|
61
|
+
md?: number
|
|
62
|
+
/** From 1200px up. */
|
|
63
|
+
lg?: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The measured CONTAINER width, in px, each {@link ResponsiveChartHeight} key takes effect at.
|
|
68
|
+
*
|
|
69
|
+
* The numbers are what Mantine's `sm`/`md`/`lg` breakpoints resolve to at the 16px initial font
|
|
70
|
+
* size, so a consumer's mental model transfers — but they are compared against the element's own
|
|
71
|
+
* MEASURED box, never the viewport, for the reason {@link resolveChartTier} states: a chart in a
|
|
72
|
+
* `PageAside`-squeezed grid cell on a 1440px desktop is as narrow as one on a phone, and the
|
|
73
|
+
* viewport agrees with neither. Keeping them as plain numbers here is also what keeps this file
|
|
74
|
+
* Mantine-free — `theme.breakpoints` is on the coupled side of the boundary.
|
|
75
|
+
*/
|
|
76
|
+
const HEIGHT_STEP_WIDTH = { sm: 768, md: 992, lg: 1200 } as const
|
|
77
|
+
|
|
78
|
+
/** Widest-first, so the first step that fits is the answer. */
|
|
79
|
+
const HEIGHT_STEPS = ['lg', 'md', 'sm'] as const
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a `height` prop — a plain number, or a {@link ResponsiveChartHeight} — against the
|
|
83
|
+
* measured container width.
|
|
84
|
+
*
|
|
85
|
+
* A plain number passes through untouched: this is a WIDENING of the prop, not a replacement, and
|
|
86
|
+
* every existing `height={240}` must keep meaning exactly 240.
|
|
87
|
+
*
|
|
88
|
+
* An UNMEASURED box (`containerW <= 0` — SSR, or before the `ResizeObserver`'s first callback)
|
|
89
|
+
* resolves to the LARGEST stated step, matching {@link resolveChartTier}'s first-frame rule: the
|
|
90
|
+
* first paint must not be phone-shaped chrome that grows one frame later. Falling to `base` there
|
|
91
|
+
* would make every SSR'd chart short and then jump.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveFrameHeight(
|
|
94
|
+
height: number | ResponsiveChartHeight,
|
|
95
|
+
containerW: number,
|
|
96
|
+
): number {
|
|
97
|
+
if (typeof height === 'number') return height
|
|
98
|
+
const widest = HEIGHT_STEPS.find((step) => height[step] !== undefined)
|
|
99
|
+
if (containerW <= 0) return widest === undefined ? height.base : (height[widest] ?? height.base)
|
|
100
|
+
const step = HEIGHT_STEPS.find(
|
|
101
|
+
(name) => containerW >= HEIGHT_STEP_WIDTH[name] && height[name] !== undefined,
|
|
102
|
+
)
|
|
103
|
+
return step === undefined ? height.base : (height[step] ?? height.base)
|
|
104
|
+
}
|
|
105
|
+
|
|
42
106
|
/**
|
|
43
107
|
* How much of `VX.margin` a phone-tier chart keeps as its FLOOR. The measured law is unchanged —
|
|
44
108
|
* a side may still only grow past its floor (`autoMargin`) — this only stops a static token from
|
|
@@ -111,6 +175,28 @@ export function chartTierMetrics(tier: ChartTier): ChartTierMetrics {
|
|
|
111
175
|
return tier === 'phone' ? PHONE_METRICS : DESKTOP_METRICS
|
|
112
176
|
}
|
|
113
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Which legend cap applies: the caller's, or the tier's own default.
|
|
180
|
+
*
|
|
181
|
+
* **An explicit `callerMaxRows` wins OUTRIGHT.** This used to be `Math.min(caller, tier)`, which
|
|
182
|
+
* made the documented per-chart opt-out ("opt back out with an explicit `legend.maxRows`") false in
|
|
183
|
+
* the one direction anybody needs it. The tier's `PHONE_LEGEND_MAX_ROWS` is a DEFAULT for charts
|
|
184
|
+
* that said nothing — a chart that names a number has already answered the question the default
|
|
185
|
+
* exists to answer, and it matters because the tier keys on the measured box: a 380px inspector
|
|
186
|
+
* panel on a 1440px desktop is "a phone", so a six-series legend rolled up to two rows + `+4 more`
|
|
187
|
+
* with no way to say no. `margin` and `xLabelRotate` always had that escape; the legend did not.
|
|
188
|
+
*
|
|
189
|
+
* A `fill` frame's measured {@link legendEntryCap} still applies ON TOP of whatever wins here — it
|
|
190
|
+
* is a floor on the PLOT, not a tier default, so a chart that would otherwise render no plot at all
|
|
191
|
+
* still rolls up.
|
|
192
|
+
*/
|
|
193
|
+
export function resolveLegendMaxRows(input: {
|
|
194
|
+
callerMaxRows?: number | undefined
|
|
195
|
+
tier: ChartTier
|
|
196
|
+
}): number | undefined {
|
|
197
|
+
return input.callerMaxRows ?? chartTierMetrics(input.tier).legendMaxRows
|
|
198
|
+
}
|
|
199
|
+
|
|
114
200
|
/**
|
|
115
201
|
* Resolve the plot rect from the measured box and the measured legend band. Pure and exported so
|
|
116
202
|
* the two floors below are testable without a `ResizeObserver`.
|
package/src/cli/check-theme.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
conflictingProfileFlags,
|
|
20
20
|
declaredProfile,
|
|
21
21
|
DEFAULT_ROOTS,
|
|
22
|
+
hasBasaltKey,
|
|
22
23
|
readBasaltConfig,
|
|
23
24
|
resolveExemptRules,
|
|
24
25
|
resolveProjectDir,
|
|
@@ -96,6 +97,18 @@ export function checkTheme(
|
|
|
96
97
|
// A configured-but-wrong root is never intentional, and silently scanning 0 files under the
|
|
97
98
|
// built-in defaults (argo's pre-migration layout) is the same failure mode for every other
|
|
98
99
|
// consumer — both cases fail loud instead of warn-plus-green.
|
|
100
|
+
// The WRONG-DIRECTORY case leads, and since 1.29.0's single resolver it is the common one:
|
|
101
|
+
// lefthook and GitHub Actions both run at the REPO ROOT while the config lives in a package
|
|
102
|
+
// below it. Only the roots messages used to print, sending a consumer to edit `basalt.roots` in
|
|
103
|
+
// a package.json that has no `basalt` block at all.
|
|
104
|
+
if (!hasBasaltKey(cwd)) {
|
|
105
|
+
console.error(
|
|
106
|
+
`✖ basalt-ui check-theme: ${cwd}/package.json has no "basalt" key — very likely not the ` +
|
|
107
|
+
'package you meant to scan (BASALT_CWD, else the invocation cwd; nothing is inferred). ' +
|
|
108
|
+
'At a repo root? Give the command `root: <pkg>/` (lefthook) or `working-directory: ' +
|
|
109
|
+
'<pkg>` (Actions), or set BASALT_CWD=<pkg>. If this directory IS it, run `basalt-ui init`.',
|
|
110
|
+
)
|
|
111
|
+
}
|
|
99
112
|
if (cfg.roots === undefined) {
|
|
100
113
|
console.error(
|
|
101
114
|
`✖ basalt-ui check-theme: 0 files scanned — no "basalt.roots" configured in package.json, and ` +
|