basalt-ui 1.29.2 → 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 +9 -0
- package/MIGRATING.md +693 -368
- 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/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/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,7 @@ import { dirname, resolve } from 'node:path'
|
|
|
14
14
|
// eslint-disable-next-line -- the plugin is plain JS; the ledger + id set are named exports beside it
|
|
15
15
|
import basaltPlugin, {
|
|
16
16
|
CTL_THEME_TAGS,
|
|
17
|
+
WAIVER_ID_ALIASES,
|
|
17
18
|
DEPRECATED_EXPORTS,
|
|
18
19
|
KNOWN_RULE_IDS,
|
|
19
20
|
PLUGIN_RULE_ADVISORY,
|
|
@@ -21,7 +22,11 @@ import basaltPlugin, {
|
|
|
21
22
|
RETIRED_RULE_IDS,
|
|
22
23
|
} from './oxlint-plugin.js'
|
|
23
24
|
import { CTL_THEME } from '../src/theme/ctl-theme'
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
GUARD_RULES,
|
|
27
|
+
PLUGIN_RULE_IDS,
|
|
28
|
+
WAIVER_ID_ALIASES as GUARD_WAIVER_ID_ALIASES,
|
|
29
|
+
} from '../src/guard/index.ts'
|
|
25
30
|
import { PLUGIN_RULE_ID_LIST, SURFACES } from '../src/surfaces.ts'
|
|
26
31
|
|
|
27
32
|
const PLUGIN_PATH = resolve(import.meta.dirname, 'oxlint-plugin.js')
|
|
@@ -1547,6 +1552,37 @@ describe('KNOWN_RULE_IDS', () => {
|
|
|
1547
1552
|
})
|
|
1548
1553
|
})
|
|
1549
1554
|
|
|
1555
|
+
// ── one law, two rule ids ──────────────────────────────────────────────────────────────────────
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* `basalt/control-outside-home` and the guard's `raw-selection-control` are the SAME law read by
|
|
1559
|
+
* two engines — the guard's own message says so — and a waiver naming either used to silence only
|
|
1560
|
+
* the lane whose id it spelled. The working annotation was `theme-allow raw-selection-control
|
|
1561
|
+
* control-outside-home`: two ids, undocumented, one silent debug cycle per consumer.
|
|
1562
|
+
*/
|
|
1563
|
+
describe('WAIVER_ID_ALIASES', () => {
|
|
1564
|
+
it('is identical in both lanes', () => {
|
|
1565
|
+
expect(WAIVER_ID_ALIASES).toEqual(GUARD_WAIVER_ID_ALIASES)
|
|
1566
|
+
})
|
|
1567
|
+
|
|
1568
|
+
it('names only ids both registries know', () => {
|
|
1569
|
+
for (const [id, aliases] of Object.entries(WAIVER_ID_ALIASES)) {
|
|
1570
|
+
expect([id, KNOWN_RULE_IDS.has(id)]).toEqual([id, true])
|
|
1571
|
+
for (const alias of aliases) expect([alias, KNOWN_RULE_IDS.has(alias)]).toEqual([alias, true])
|
|
1572
|
+
}
|
|
1573
|
+
})
|
|
1574
|
+
|
|
1575
|
+
it('waives the plugin rule under the TEXT lane’s id', () => {
|
|
1576
|
+
const { code, rules } = run(
|
|
1577
|
+
`import { Select } from '@mantine/core'\nexport const C = () => (\n <div>\n` +
|
|
1578
|
+
` {/* theme-allow raw-selection-control — the Tauri header has no shell */}\n` +
|
|
1579
|
+
` <Select data={[]} />\n </div>\n)\n`,
|
|
1580
|
+
)
|
|
1581
|
+
expect(code).toBe(0)
|
|
1582
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1583
|
+
})
|
|
1584
|
+
})
|
|
1585
|
+
|
|
1550
1586
|
// ── theme-allow fails closed on a typo ─────────────────────────────────────────────────────────
|
|
1551
1587
|
|
|
1552
1588
|
describe('theme-allow with an unrecognized rule id', () => {
|
|
@@ -1605,6 +1641,23 @@ const MANTINE_IMPORT = `import { Button, ScrollArea, SegmentedControl, Select }
|
|
|
1605
1641
|
const BASALT_IMPORT = `import { ChartCard, PageBar, Section, SettingsSection } from 'basalt-ui'\n`
|
|
1606
1642
|
|
|
1607
1643
|
describe('basalt/hand-rolled-filter', () => {
|
|
1644
|
+
/**
|
|
1645
|
+
* The deliberate consequence of teaching the slot resolver to read an object spread: a slot IS a
|
|
1646
|
+
* slot in either spelling, so a raw Mantine control conditionally spread into one moves from
|
|
1647
|
+
* `control-outside-home` (warn, and the WRONG verdict — it does have a home) to this rule
|
|
1648
|
+
* (error, and the right one). One home model or none; a spread that is a home for the rule that
|
|
1649
|
+
* exempts and not for the rule that polices is the silent half-application this wave removes.
|
|
1650
|
+
*/
|
|
1651
|
+
it('flags a raw Select handed to a slot written as an object spread', () => {
|
|
1652
|
+
const { code, rules } = run(
|
|
1653
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = ({ cond }) => (\n` +
|
|
1654
|
+
` <Section {...(cond && { filters: <Select data={[]} /> })}>body</Section>\n)\n`,
|
|
1655
|
+
)
|
|
1656
|
+
expect(code).toBe(1)
|
|
1657
|
+
expect(rules).toContain('hand-rolled-filter')
|
|
1658
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1659
|
+
})
|
|
1660
|
+
|
|
1608
1661
|
it('flags a raw Mantine Select handed to a PageBar filters slot', () => {
|
|
1609
1662
|
const { code, rules } = run(
|
|
1610
1663
|
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <PageBar filters={<Select data={[]} />} />\n`,
|
|
@@ -2208,9 +2261,81 @@ describe('basalt/bound-control-outside-home', () => {
|
|
|
2208
2261
|
)
|
|
2209
2262
|
expect(rules).toContain('bound-control-outside-home')
|
|
2210
2263
|
})
|
|
2264
|
+
|
|
2265
|
+
/**
|
|
2266
|
+
* A slot written as an OBJECT SPREAD is the same home as a slot written as a JSXAttribute — and
|
|
2267
|
+
* `exactOptionalPropertyTypes` FORCES the spread, because `tabs={cond ? <X/> : undefined}` is not
|
|
2268
|
+
* assignable to an optional prop. Every control rule resolved homes off JSXAttribute nodes alone,
|
|
2269
|
+
* so a strict consumer's conditional slot was a false positive on a rule scheduled to go `error`.
|
|
2270
|
+
*/
|
|
2271
|
+
describe('a slot written as an object spread', () => {
|
|
2272
|
+
it.each([
|
|
2273
|
+
'{...(cond && { tabs: <SelectFilter field={f} /> })}',
|
|
2274
|
+
'{...(cond ? { tabs: <SelectFilter field={f} /> } : {})}',
|
|
2275
|
+
'{...{ filters: <SelectFilter field={f} /> }}',
|
|
2276
|
+
])('does NOT flag %s on a Section', (spread) => {
|
|
2277
|
+
const { code, rules } = run(
|
|
2278
|
+
`${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = ({ cond }) => (\n <Section ${spread}>body</Section>\n)\n`,
|
|
2279
|
+
)
|
|
2280
|
+
expect(code).toBe(0)
|
|
2281
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2282
|
+
})
|
|
2283
|
+
|
|
2284
|
+
it('does NOT flag a hoisted binding handed to a spread slot', () => {
|
|
2285
|
+
const { rules } = run(
|
|
2286
|
+
`${ASIDE_IMPORT}${CONTROLS_IMPORT}const tabs = <SelectFilter field={f} />\n` +
|
|
2287
|
+
`export const C = ({ cond }) => <Section {...(cond && { tabs })}>body</Section>\n`,
|
|
2288
|
+
)
|
|
2289
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2290
|
+
})
|
|
2291
|
+
|
|
2292
|
+
// Deliberately narrow: only a Property reaching a JSXSpreadAttribute through object / `&&` /
|
|
2293
|
+
// `?:` nodes is claimed. A non-slot key, and an object built anywhere else, are not homes.
|
|
2294
|
+
it('still flags a non-slot key in the same spread', () => {
|
|
2295
|
+
const { rules } = run(
|
|
2296
|
+
`${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = ({ cond }) => (\n` +
|
|
2297
|
+
` <Section {...(cond && { footer: <SelectFilter field={f} /> })}>body</Section>\n)\n`,
|
|
2298
|
+
)
|
|
2299
|
+
expect(rules).toContain('bound-control-outside-home')
|
|
2300
|
+
})
|
|
2301
|
+
|
|
2302
|
+
it('still flags a slot-shaped object built inside a .map() callback', () => {
|
|
2303
|
+
const { rules } = run(
|
|
2304
|
+
`${CONTROLS_IMPORT}export const C = () => [1].map((k) => ({ tabs: <SelectFilter field={f} /> }))\n`,
|
|
2305
|
+
)
|
|
2306
|
+
expect(rules).toContain('bound-control-outside-home')
|
|
2307
|
+
})
|
|
2308
|
+
|
|
2309
|
+
it('still flags one on a tag that is not a slot owner', () => {
|
|
2310
|
+
const { rules } = run(
|
|
2311
|
+
`import { Stack } from '@mantine/core'\n${CONTROLS_IMPORT}export const C = ({ cond }) => (\n` +
|
|
2312
|
+
` <Stack {...(cond && { tabs: <SelectFilter field={f} /> })} />\n)\n`,
|
|
2313
|
+
)
|
|
2314
|
+
expect(rules).toContain('bound-control-outside-home')
|
|
2315
|
+
})
|
|
2316
|
+
})
|
|
2317
|
+
|
|
2318
|
+
it('names PageBar as the reachable home for a shell-less app', () => {
|
|
2319
|
+
const { output } = run(
|
|
2320
|
+
`import { Stack } from '@mantine/core'\n${CONTROLS_IMPORT}` +
|
|
2321
|
+
`export const C = () => (\n <Stack>\n <SelectFilter field={f} />\n </Stack>\n)\n`,
|
|
2322
|
+
)
|
|
2323
|
+
expect(output).toContain('PageBar` needs no `BasaltShell`')
|
|
2324
|
+
})
|
|
2211
2325
|
})
|
|
2212
2326
|
|
|
2213
2327
|
describe('basalt/control-size-literal', () => {
|
|
2328
|
+
// Same widening as `hand-rolled-filter` above, for the same reason: a slot is a slot in either
|
|
2329
|
+
// spelling, and the tier the message promises is mounted by the slot however it was written.
|
|
2330
|
+
it('flags a size prop inside a slot written as an object spread', () => {
|
|
2331
|
+
const { code, rules } = run(
|
|
2332
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = ({ cond }) => (\n` +
|
|
2333
|
+
` <Section {...(cond && { actions: <Button size="xs">Go</Button> })}>body</Section>\n)\n`,
|
|
2334
|
+
)
|
|
2335
|
+
expect(code).toBe(1)
|
|
2336
|
+
expect(rules).toContain('control-size-literal')
|
|
2337
|
+
})
|
|
2338
|
+
|
|
2214
2339
|
it.each(['size="xs"', 'w={200}', 'fullWidth', 'visibleFrom="sm"', 'hiddenFrom="sm"'])(
|
|
2215
2340
|
'flags %s on an element inside a home slot',
|
|
2216
2341
|
(prop) => {
|
|
@@ -2554,6 +2679,43 @@ describe('basalt/in-body-page-title', () => {
|
|
|
2554
2679
|
)
|
|
2555
2680
|
expect(rules).not.toContain('in-body-page-title')
|
|
2556
2681
|
})
|
|
2682
|
+
|
|
2683
|
+
// The narrowing that made C8 satisfiable — the law is "one NAME per page", and a name is
|
|
2684
|
+
// written in words. rb hit both of these shapes 24 times across 12 files with no legal fix.
|
|
2685
|
+
it('does NOT flag a nav-less detail route rendering the document title', () => {
|
|
2686
|
+
const { code, rules } = run(
|
|
2687
|
+
`export const C = ({ knot }) => <Title order={1}>{knot.title}</Title>\n`,
|
|
2688
|
+
)
|
|
2689
|
+
expect(code).toBe(0)
|
|
2690
|
+
expect(rules).not.toContain('in-body-page-title')
|
|
2691
|
+
})
|
|
2692
|
+
|
|
2693
|
+
it("does NOT flag a card's hero VALUE at order={2}", () => {
|
|
2694
|
+
const { rules } = run(`export const C = ({ g }) => <Title order={2}>{RANK[g.rank]}</Title>\n`)
|
|
2695
|
+
expect(rules).not.toContain('in-body-page-title')
|
|
2696
|
+
})
|
|
2697
|
+
|
|
2698
|
+
// The escape is the EXPRESSION, not the braces: a literal laundered through `{…}` is still a
|
|
2699
|
+
// static page name, and so is a substitution-free template.
|
|
2700
|
+
it.each([`{'Users'}`, '{`Users`}'])('still flags a laundered literal %s', (child) => {
|
|
2701
|
+
const { rules } = run(`export const C = () => <Title order={1}>${child}</Title>\n`)
|
|
2702
|
+
expect(rules).toContain('in-body-page-title')
|
|
2703
|
+
})
|
|
2704
|
+
|
|
2705
|
+
it('still flags a name with a value interpolated into it', () => {
|
|
2706
|
+
const { rules } = run(`export const C = ({ n }) => <Title order={1}>Users {n}</Title>\n`)
|
|
2707
|
+
expect(rules).toContain('in-body-page-title')
|
|
2708
|
+
})
|
|
2709
|
+
|
|
2710
|
+
it('still flags a name nested one element deep', () => {
|
|
2711
|
+
const { rules } = run(`export const C = () => <Title order={1}><span>Users</span></Title>\n`)
|
|
2712
|
+
expect(rules).toContain('in-body-page-title')
|
|
2713
|
+
})
|
|
2714
|
+
|
|
2715
|
+
it('names PageTitle as the shell-less answer', () => {
|
|
2716
|
+
const { output } = run(`export const C = () => <Title order={1}>Page</Title>\n`)
|
|
2717
|
+
expect(output).toContain('<PageTitle>')
|
|
2718
|
+
})
|
|
2557
2719
|
})
|
|
2558
2720
|
|
|
2559
2721
|
describe('basalt/responsive-twin', () => {
|
|
@@ -3170,16 +3332,31 @@ describe('basalt/forms-field-key', () => {
|
|
|
3170
3332
|
expect(rules).toContain('forms-field-key')
|
|
3171
3333
|
})
|
|
3172
3334
|
|
|
3173
|
-
// The second message:
|
|
3174
|
-
//
|
|
3335
|
+
// The second message: reported whether or not the element carries a key — the remedy is the
|
|
3336
|
+
// rename, never an added attribute.
|
|
3175
3337
|
it('flags a spread field() even when the element already has a key', () => {
|
|
3176
|
-
const { code, rules
|
|
3338
|
+
const { code, rules } = run(
|
|
3177
3339
|
`import { field } from 'basalt-ui/forms'\n` +
|
|
3178
3340
|
`export const C = () => <TextInput key="e" {...field(form, 'email')} />\n`,
|
|
3179
3341
|
)
|
|
3180
3342
|
expect(code).toBe(1)
|
|
3181
3343
|
expect(rules).toContain('forms-field-key')
|
|
3182
|
-
|
|
3344
|
+
})
|
|
3345
|
+
|
|
3346
|
+
/**
|
|
3347
|
+
* The message has to state the version it is RUNNING at, not the one the deprecation was
|
|
3348
|
+
* scheduled in. It used to say the alias "still bundles `key` INTO the returned object" and "is
|
|
3349
|
+
* removed in 1.29.0" while running AT 1.29.x, where `field` no longer exists at all — a consumer
|
|
3350
|
+
* reading it concluded nothing was broken, on a call site that does not compile.
|
|
3351
|
+
*/
|
|
3352
|
+
it('says `field` no longer exists, not that it is merely deprecated', () => {
|
|
3353
|
+
const { output } = run(
|
|
3354
|
+
`import { field } from 'basalt-ui/forms'\n` +
|
|
3355
|
+
`export const C = () => <TextInput {...field(form, 'email')} />\n`,
|
|
3356
|
+
)
|
|
3357
|
+
expect(output).toContain('NO LONGER EXISTS')
|
|
3358
|
+
expect(output).toContain('does not compile')
|
|
3359
|
+
expect(output).not.toContain('still bundles')
|
|
3183
3360
|
})
|
|
3184
3361
|
|
|
3185
3362
|
it('honours a theme-allow naming the rule', () => {
|
package/configs/oxlint.json
CHANGED
|
@@ -34,17 +34,17 @@
|
|
|
34
34
|
"basalt/raw-scroll-container": "error",
|
|
35
35
|
"basalt/hand-rolled-filter": "error",
|
|
36
36
|
"basalt/control-outside-home": "warn",
|
|
37
|
-
"basalt/bound-control-outside-home": "
|
|
37
|
+
"basalt/bound-control-outside-home": "error",
|
|
38
38
|
"basalt/control-size-literal": "error",
|
|
39
39
|
"basalt/page-bar-budget": "error",
|
|
40
40
|
"basalt/in-body-page-title": "error",
|
|
41
41
|
"basalt/responsive-twin": "error",
|
|
42
42
|
"basalt/search-literal-link": "error",
|
|
43
43
|
"basalt/use-search-from-literal": "error",
|
|
44
|
-
"basalt/provider-above-router": "
|
|
45
|
-
"basalt/query-fn-unwrap": "
|
|
44
|
+
"basalt/provider-above-router": "error",
|
|
45
|
+
"basalt/query-fn-unwrap": "error",
|
|
46
46
|
"basalt/deprecated-export": "warn",
|
|
47
|
-
"basalt/forms-field-key": "
|
|
47
|
+
"basalt/forms-field-key": "error",
|
|
48
48
|
"basalt/visx-boundary": "error",
|
|
49
49
|
"basalt/visx-tooltip": "error",
|
|
50
50
|
"basalt/agent-resume-guard": "error",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChartCard.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAGtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAEhE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"ChartCard.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAGtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAEhE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AA0ChD;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,cAAc,GAAG,WAAW,GACtC,eAAe,CAAC,aAAa,CAAC,GAAG;IAC/B,mGAAmG;IACnG,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,gGAAgG;IAChG,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;8FAC0F;IAC1F,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;kEAC8D;IAC9D,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+FAA+F;IAC/F,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,8FAA8F;IAC9F,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;sDACkD;IAClD,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;oGACgG;IAChG,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,CAAA;AAEH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,KAAK,EACL,WAAW,EACX,aAAa,EACb,KAAK,EACL,OAAO,EACP,KAAK,EACL,iBAAuB,EACvB,WAAW,EACX,SAAS,EACT,UAAU,EACV,KAAK,EACL,QAAQ,GACT,EAAE,cAAc,+BAgEhB"}
|
|
@@ -17,10 +17,10 @@ const bodyClipStyle = {
|
|
|
17
17
|
borderBottomLeftRadius: VX.radiusCard,
|
|
18
18
|
borderBottomRightRadius: VX.radiusCard,
|
|
19
19
|
overflow: "hidden",
|
|
20
|
-
padding: "2px var(--
|
|
20
|
+
padding: "2px var(--vx-space-card-inset-x, 0.8125rem) var(--vx-space-card-inset-y, 0.6875rem)"
|
|
21
21
|
};
|
|
22
22
|
const headerWrapStyle = {
|
|
23
|
-
padding: "var(--
|
|
23
|
+
padding: "var(--vx-space-card-inset-y, 0.6875rem) var(--vx-space-card-inset-x, 0.8125rem) 4px"
|
|
24
24
|
};
|
|
25
25
|
function ChartCard({
|
|
26
26
|
title,
|
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { BasaltProps } from '../../common/props.js';
|
|
3
3
|
import type { ChartLegendConfig, LegendPlacement, SeriesStyle } from '../series.js';
|
|
4
|
+
import type { ResponsiveChartHeight } from './chart-frame-layout.js';
|
|
4
5
|
import type { ChartState } from './ChartPending.js';
|
|
5
6
|
/** Re-exported for `ChartFrame.test.tsx` and any other consumer that previously reached these
|
|
6
7
|
* through `ChartFrame` — the layout math itself lives in `./chart-frame-layout` (pure, DOM-free). */
|
|
7
8
|
export { legendEntryCap, resolvePlotRect, resolveChartTier } from './chart-frame-layout.js';
|
|
9
|
+
export type { ResponsiveChartHeight } from './chart-frame-layout.js';
|
|
8
10
|
/** Legend configuration for {@link ChartFrame}. Omit entirely (or pass `{}`) for the default
|
|
9
11
|
* bottom-placed legend; pass `false` only for the sparkline exemption. */
|
|
10
12
|
export type ChartFrameLegend = {
|
|
11
13
|
/** Default 'bottom'. */
|
|
12
14
|
placement?: LegendPlacement;
|
|
13
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Wrap cap → "+N more" rollup at high cardinality.
|
|
17
|
+
*
|
|
18
|
+
* An explicit value WINS OUTRIGHT — including over the phone tier's own two-row default
|
|
19
|
+
* (`chartTierMetrics`). It used to be `Math.min`'d against the tier, which made the documented
|
|
20
|
+
* per-chart opt-out ("opt back out with an explicit `legend.maxRows`") false in the one direction
|
|
21
|
+
* anybody needs it: a consumer's 380px inspector panel is "a phone" on a 1440px desktop, and a
|
|
22
|
+
* six-series legend rolled up to `+4 more` with no way to say no. Under `fill` the measured
|
|
23
|
+
* entry cap can still roll up FURTHER — that is a floor on the plot, not a tier default
|
|
24
|
+
* ({@link legendEntryCap}).
|
|
25
|
+
*/
|
|
14
26
|
maxRows?: number;
|
|
15
27
|
/** Visually separate role: series | overlay | reference. */
|
|
16
28
|
groups?: boolean;
|
|
@@ -28,8 +40,16 @@ export type ChartFrameLegend = {
|
|
|
28
40
|
export type ChartFrameProps = BasaltProps & {
|
|
29
41
|
/** Series identity — drives the derived legend. Pass the SAME array the kind draws + tooltips from. */
|
|
30
42
|
series: readonly SeriesStyle[];
|
|
31
|
-
/**
|
|
32
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Height in pixels, or one per size step. Used when neither `aspectRatio` nor `fill` is set.
|
|
45
|
+
* Default 240.
|
|
46
|
+
*
|
|
47
|
+
* `{ base: 180, md: 260 }` is the declarative escape from a page of fixed-height charts: the
|
|
48
|
+
* steps are compared against the frame's own MEASURED width, not the viewport, so a chart squeezed
|
|
49
|
+
* by a `PageAside` gets the short height on a desktop too. A plain number is unchanged and stays
|
|
50
|
+
* supported ({@link resolveFrameHeight}).
|
|
51
|
+
*/
|
|
52
|
+
height?: number | ResponsiveChartHeight;
|
|
33
53
|
/** height = Math.round(containerWidth / aspectRatio). Ignored when `fill` is set. */
|
|
34
54
|
aspectRatio?: number;
|
|
35
55
|
/** Fill the parent flex/grid cell's measured height instead of a fixed/derived one. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChartFrame.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartFrame.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAIrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"ChartFrame.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartFrame.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAIrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAQhF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAIjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAEhD;qGACqG;AACrG,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACxF,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAKjE;0EAC0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wBAAwB;IACxB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG;IAC1C,uGAAuG;IACvG,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;IAC9B;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,qBAAqB,CAAA;IACvC,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,gBAAgB,GAAG,KAAK,CAAA;IACjC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;oDACgD;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,SAAS,CAAA;CACxC,CAAA;AAED,0FAA0F;AAC1F,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC5B,CAAA;AAcD;oFACoF;AACpF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CAC1C,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,iBAAiB,GAAG,KAAK,GAAG,SAAS,EAC7C,KAAK,CAAC,EAAE,qBAAqB,EAC7B,WAAW,CAAC,EAAE,MAAM,GACnB,gBAAgB,GAAG,KAAK,CAU1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,MAAM,EACN,WAAW,EACX,IAAY,EACZ,QAA4B,EAC5B,OAAO,EACP,MAAW,EACX,SAAS,EACT,SAAiB,EACjB,KAAK,EACL,SAAS,EACT,KAAK,EACL,QAAQ,GACT,EAAE,eAAe,GAAG,SAAS,CA6G7B"}
|
|
@@ -4,9 +4,10 @@ import { VX } from "../../tokens/index.js";
|
|
|
4
4
|
import { useChartSize } from "../hooks/useChartSize.js";
|
|
5
5
|
import { deriveLegend } from "../series.js";
|
|
6
6
|
import {
|
|
7
|
-
chartTierMetrics,
|
|
8
7
|
legendEntryCap,
|
|
9
8
|
resolveChartTier,
|
|
9
|
+
resolveFrameHeight,
|
|
10
|
+
resolveLegendMaxRows,
|
|
10
11
|
resolvePlotRect
|
|
11
12
|
} from "./chart-frame-layout.js";
|
|
12
13
|
import { ChartTierProvider } from "./chart-tier.js";
|
|
@@ -67,7 +68,7 @@ function ChartFrame({
|
|
|
67
68
|
const resolvedState = resolveChartState({ ...state !== void 0 && { state }, isPending });
|
|
68
69
|
const legendVisible = legend !== false && resolvedState === null;
|
|
69
70
|
const tier = resolveChartTier(containerW);
|
|
70
|
-
const resolvedHeight = fill ? containerH : aspectRatio !== void 0 ? Math.round(containerW / aspectRatio) : height ?? DEFAULT_HEIGHT;
|
|
71
|
+
const resolvedHeight = fill ? containerH : aspectRatio !== void 0 ? Math.round(containerW / aspectRatio) : resolveFrameHeight(height ?? DEFAULT_HEIGHT, containerW);
|
|
71
72
|
const sideLegendWidth = legendVisible && vertical ? legendW : 0;
|
|
72
73
|
const topBottomLegendHeight = legendVisible && !vertical ? legendH : 0;
|
|
73
74
|
const plot = resolvePlotRect({
|
|
@@ -79,8 +80,7 @@ function ChartFrame({
|
|
|
79
80
|
});
|
|
80
81
|
const legendItems = legend === false ? [] : deriveLegend(series);
|
|
81
82
|
const togglable = legend !== false && (legend.toggle ?? legendItems.length > 1);
|
|
82
|
-
const
|
|
83
|
-
const callerMaxRows = legend === false ? void 0 : legend.maxRows === void 0 ? tierMaxRows : tierMaxRows === void 0 ? legend.maxRows : Math.min(legend.maxRows, tierMaxRows);
|
|
83
|
+
const callerMaxRows = legend === false ? void 0 : resolveLegendMaxRows({ callerMaxRows: legend.maxRows, tier });
|
|
84
84
|
const maxRows = legend === false ? void 0 : fill && !vertical ? legendEntryCap({
|
|
85
85
|
items: legendItems,
|
|
86
86
|
containerW,
|
|
@@ -24,6 +24,42 @@ export type ChartTier = 'phone' | 'desktop';
|
|
|
24
24
|
* later, and `resolvePlotRect` already treats an unmeasured width as the first-frame case.
|
|
25
25
|
*/
|
|
26
26
|
export declare function resolveChartTier(containerW: number): ChartTier;
|
|
27
|
+
/**
|
|
28
|
+
* A chart height stated per size step instead of as one number — the declarative escape from "every
|
|
29
|
+
* chart on this page is 260px tall on a 375px phone too".
|
|
30
|
+
*
|
|
31
|
+
* `base` is required and is the height a box narrower than every named step gets; each further key
|
|
32
|
+
* is the height from that step UP. Keys are Mantine's breakpoint names so a consumer writes the
|
|
33
|
+
* vocabulary they already write, and nothing more than `sm`/`md`/`lg` is offered: `xs`/`xl` would
|
|
34
|
+
* be two more rungs nobody has asked a chart to have.
|
|
35
|
+
*
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <ChartFrame series={series} height={{ base: 180, md: 260 }}>{…}</ChartFrame>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export type ResponsiveChartHeight = {
|
|
41
|
+
/** Below the narrowest STATED step — and NOT the first-frame value; see {@link resolveFrameHeight}. */
|
|
42
|
+
base: number;
|
|
43
|
+
/** From 768px of MEASURED container width up. */
|
|
44
|
+
sm?: number;
|
|
45
|
+
/** From 992px up. */
|
|
46
|
+
md?: number;
|
|
47
|
+
/** From 1200px up. */
|
|
48
|
+
lg?: number;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a `height` prop — a plain number, or a {@link ResponsiveChartHeight} — against the
|
|
52
|
+
* measured container width.
|
|
53
|
+
*
|
|
54
|
+
* A plain number passes through untouched: this is a WIDENING of the prop, not a replacement, and
|
|
55
|
+
* every existing `height={240}` must keep meaning exactly 240.
|
|
56
|
+
*
|
|
57
|
+
* An UNMEASURED box (`containerW <= 0` — SSR, or before the `ResizeObserver`'s first callback)
|
|
58
|
+
* resolves to the LARGEST stated step, matching {@link resolveChartTier}'s first-frame rule: the
|
|
59
|
+
* first paint must not be phone-shaped chrome that grows one frame later. Falling to `base` there
|
|
60
|
+
* would make every SSR'd chart short and then jump.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveFrameHeight(height: number | ResponsiveChartHeight, containerW: number): number;
|
|
27
63
|
/** Every size a chart's chrome resolves per tier. One object so a new tier-sensitive size is added
|
|
28
64
|
* in one place and read by name, rather than each primitive branching on the tier itself. */
|
|
29
65
|
export type ChartTierMetrics = {
|
|
@@ -46,6 +82,25 @@ export type ChartTierMetrics = {
|
|
|
46
82
|
};
|
|
47
83
|
/** The resolved sizes for one tier. Frozen module constants — never a new object per render. */
|
|
48
84
|
export declare function chartTierMetrics(tier: ChartTier): ChartTierMetrics;
|
|
85
|
+
/**
|
|
86
|
+
* Which legend cap applies: the caller's, or the tier's own default.
|
|
87
|
+
*
|
|
88
|
+
* **An explicit `callerMaxRows` wins OUTRIGHT.** This used to be `Math.min(caller, tier)`, which
|
|
89
|
+
* made the documented per-chart opt-out ("opt back out with an explicit `legend.maxRows`") false in
|
|
90
|
+
* the one direction anybody needs it. The tier's `PHONE_LEGEND_MAX_ROWS` is a DEFAULT for charts
|
|
91
|
+
* that said nothing — a chart that names a number has already answered the question the default
|
|
92
|
+
* exists to answer, and it matters because the tier keys on the measured box: a 380px inspector
|
|
93
|
+
* panel on a 1440px desktop is "a phone", so a six-series legend rolled up to two rows + `+4 more`
|
|
94
|
+
* with no way to say no. `margin` and `xLabelRotate` always had that escape; the legend did not.
|
|
95
|
+
*
|
|
96
|
+
* A `fill` frame's measured {@link legendEntryCap} still applies ON TOP of whatever wins here — it
|
|
97
|
+
* is a floor on the PLOT, not a tier default, so a chart that would otherwise render no plot at all
|
|
98
|
+
* still rolls up.
|
|
99
|
+
*/
|
|
100
|
+
export declare function resolveLegendMaxRows(input: {
|
|
101
|
+
callerMaxRows?: number | undefined;
|
|
102
|
+
tier: ChartTier;
|
|
103
|
+
}): number | undefined;
|
|
49
104
|
/**
|
|
50
105
|
* Resolve the plot rect from the measured box and the measured legend band. Pure and exported so
|
|
51
106
|
* the two floors below are testable without a `ResizeObserver`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart-frame-layout.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/chart-frame-layout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAUhD;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,CAAA;AAE3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAE9D;AAqBD;6FAC6F;AAC7F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2FAA2F;IAC3F,IAAI,EAAE,SAAS,CAAA;IACf;uFACmF;IACnF,QAAQ,EAAE,MAAM,CAAA;IAChB,kCAAkC;IAClC,cAAc,EAAE,MAAM,CAAA;IACtB,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,uCAAuC;IACvC,eAAe,EAAE,MAAM,CAAA;IACvB;yDACqD;IACrD,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,6EAA6E;IAC7E,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AA6BD,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,gBAAgB,CAElE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,qBAAqB,EAAE,MAAM,CAAA;CAC9B,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAOpC;AAmCD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE;IACpC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,GAAG,MAAM,GAAG,SAAS,CAerB"}
|
|
1
|
+
{"version":3,"file":"chart-frame-layout.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/chart-frame-layout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAUhD;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,CAAA;AAE3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAE9D;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,uGAAuG;IACvG,IAAI,EAAE,MAAM,CAAA;IACZ,iDAAiD;IACjD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,qBAAqB;IACrB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,sBAAsB;IACtB,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ,CAAA;AAiBD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,GAAG,qBAAqB,EACtC,UAAU,EAAE,MAAM,GACjB,MAAM,CAQR;AAqBD;6FAC6F;AAC7F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2FAA2F;IAC3F,IAAI,EAAE,SAAS,CAAA;IACf;uFACmF;IACnF,QAAQ,EAAE,MAAM,CAAA;IAChB,kCAAkC;IAClC,cAAc,EAAE,MAAM,CAAA;IACtB,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,uCAAuC;IACvC,eAAe,EAAE,MAAM,CAAA;IACvB;yDACqD;IACrD,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,6EAA6E;IAC7E,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AA6BD,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,gBAAgB,CAElE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,IAAI,EAAE,SAAS,CAAA;CAChB,GAAG,MAAM,GAAG,SAAS,CAErB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,qBAAqB,EAAE,MAAM,CAAA;CAC9B,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAOpC;AAmCD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE;IACpC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,GAAG,MAAM,GAAG,SAAS,CAerB"}
|
|
@@ -6,6 +6,17 @@ const LEGEND_SWATCH_W = 24;
|
|
|
6
6
|
function resolveChartTier(containerW) {
|
|
7
7
|
return containerW > 0 && containerW < VX.phoneChartWidth ? "phone" : "desktop";
|
|
8
8
|
}
|
|
9
|
+
const HEIGHT_STEP_WIDTH = { sm: 768, md: 992, lg: 1200 };
|
|
10
|
+
const HEIGHT_STEPS = ["lg", "md", "sm"];
|
|
11
|
+
function resolveFrameHeight(height, containerW) {
|
|
12
|
+
if (typeof height === "number") return height;
|
|
13
|
+
const widest = HEIGHT_STEPS.find((step2) => height[step2] !== void 0);
|
|
14
|
+
if (containerW <= 0) return widest === void 0 ? height.base : height[widest] ?? height.base;
|
|
15
|
+
const step = HEIGHT_STEPS.find(
|
|
16
|
+
(name) => containerW >= HEIGHT_STEP_WIDTH[name] && height[name] !== void 0
|
|
17
|
+
);
|
|
18
|
+
return step === void 0 ? height.base : height[step] ?? height.base;
|
|
19
|
+
}
|
|
9
20
|
const PHONE_MARGIN_SCALE = 0.75;
|
|
10
21
|
const PHONE_DOT_R = Math.max(VX.dotR - 1, 2);
|
|
11
22
|
const PHONE_LEGEND_MAX_ROWS = 2;
|
|
@@ -38,6 +49,9 @@ const PHONE_METRICS = {
|
|
|
38
49
|
function chartTierMetrics(tier) {
|
|
39
50
|
return tier === "phone" ? PHONE_METRICS : DESKTOP_METRICS;
|
|
40
51
|
}
|
|
52
|
+
function resolveLegendMaxRows(input) {
|
|
53
|
+
return input.callerMaxRows ?? chartTierMetrics(input.tier).legendMaxRows;
|
|
54
|
+
}
|
|
41
55
|
function resolvePlotRect(input) {
|
|
42
56
|
const { containerW, resolvedHeight, minWidth, sideLegendWidth, topBottomLegendHeight } = input;
|
|
43
57
|
return {
|
|
@@ -85,5 +99,7 @@ export {
|
|
|
85
99
|
chartTierMetrics,
|
|
86
100
|
legendEntryCap,
|
|
87
101
|
resolveChartTier,
|
|
102
|
+
resolveFrameHeight,
|
|
103
|
+
resolveLegendMaxRows,
|
|
88
104
|
resolvePlotRect
|
|
89
105
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check-theme.d.ts","sourceRoot":"","sources":["../../src/cli/check-theme.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"check-theme.d.ts","sourceRoot":"","sources":["../../src/cli/check-theme.ts"],"names":[],"mappings":"AA+BA;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,aAAa,GAAE,MAAsB,EACrC,KAAK,GAAE,SAAS,MAAM,EAAO,GAC5B,MAAM,CAuKR"}
|
package/dist/cli/check-theme.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
conflictingProfileFlags,
|
|
13
13
|
declaredProfile,
|
|
14
14
|
DEFAULT_ROOTS,
|
|
15
|
+
hasBasaltKey,
|
|
15
16
|
readBasaltConfig,
|
|
16
17
|
resolveExemptRules,
|
|
17
18
|
resolveProjectDir,
|
|
@@ -69,6 +70,11 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
|
|
|
69
70
|
findings.push(...checkSource(readFileSync(resolve(cwd, rel), "utf8"), rel, guardCfg));
|
|
70
71
|
}
|
|
71
72
|
if (scanned.length === 0) {
|
|
73
|
+
if (!hasBasaltKey(cwd)) {
|
|
74
|
+
console.error(
|
|
75
|
+
`\u2716 basalt-ui check-theme: ${cwd}/package.json has no "basalt" key \u2014 very likely not the package you meant to scan (BASALT_CWD, else the invocation cwd; nothing is inferred). At a repo root? Give the command \`root: <pkg>/\` (lefthook) or \`working-directory: <pkg>\` (Actions), or set BASALT_CWD=<pkg>. If this directory IS it, run \`basalt-ui init\`.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
72
78
|
if (cfg.roots === void 0) {
|
|
73
79
|
console.error(
|
|
74
80
|
`\u2716 basalt-ui check-theme: 0 files scanned \u2014 no "basalt.roots" configured in package.json, and the built-in default roots (${DEFAULT_ROOTS.join(", ")}) matched zero files. Set "basalt": { "roots": [...] } in package.json to point at your source directories.`
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -15,16 +15,22 @@ export type DoctorResult = {
|
|
|
15
15
|
* Check a consumer repo's basalt integration and print a pass/warn report.
|
|
16
16
|
*
|
|
17
17
|
* Narrowed to what a single-directory-scoped run can actually verify — no workspace-glob walking,
|
|
18
|
-
* no ascend/descend project discovery (see {@link resolveProjectDir}).
|
|
18
|
+
* no ascend/descend project discovery (see {@link resolveProjectDir}). Six checks, numbered in
|
|
19
19
|
* the order they run and print:
|
|
20
20
|
*
|
|
21
21
|
* Hard failures (exit non-zero):
|
|
22
22
|
* 1. `.basalt/manifest.json` exists (`basalt-ui init` was run).
|
|
23
|
-
* 2. `
|
|
23
|
+
* 2. `install-parity`: basalt-ui resolves, doctor prints WHERE and at what version, and that
|
|
24
|
+
* version agrees with the manifest's `basaltVersion`.
|
|
25
|
+
* 3. `guard-scan`: `check-theme` would scan more than zero files, sharing `scannableFiles` so the
|
|
26
|
+
* two cannot disagree. Both RESTORED in 1.30.0 after four minors gone with no MIGRATING row —
|
|
27
|
+
* README § `doctor` carries what their absence cost (a no-op CI gate, a green guard scanning
|
|
28
|
+
* nothing, and two basalt-WRITTEN docs describing output the CLI no longer produced).
|
|
29
|
+
* 4. `oxlint-preset` ("rules in sync"): the consumer's `.oxlintrc.json` extends the shipped
|
|
24
30
|
* preset AND that path resolves. `init` keeps an existing config, so the framework's whole
|
|
25
31
|
* lint half can be silently off — one repo carried six real violations invisibly across five
|
|
26
32
|
* minors this way.
|
|
27
|
-
*
|
|
33
|
+
* 6. `ai-major-parity`: within THIS package.json, `dependencies`/`devDependencies`/
|
|
28
34
|
* `peerDependencies` agree on the `ai` package's major. `basalt/ai-sdk-major` (the oxlint
|
|
29
35
|
* plugin rule) already catches this per linted FILE against its nearest package.json; this
|
|
30
36
|
* check exists for the shape a lint run cannot see — a package whose own manifest asks its
|
|
@@ -34,7 +40,7 @@ export type DoctorResult = {
|
|
|
34
40
|
* already agree warns that the exemption is stale.
|
|
35
41
|
*
|
|
36
42
|
* Warning (non-fatal):
|
|
37
|
-
*
|
|
43
|
+
* 5. `lefthook-preset` ("lefthook wired"): a pre-commit command runs `check-theme`, read from
|
|
38
44
|
* `lefthook dump --format json` (resolves `extends`/`include`/`root:` the way lefthook itself
|
|
39
45
|
* does — see {@link inspectLefthookGate}). No lefthook config at all is a pass (a consumer may
|
|
40
46
|
* gate elsewhere); a config that resolves but wires no guard command warns; a config lefthook
|
package/dist/cli/doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AA6BA,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,YAAY,EAAE,MAAM,CAAA;IACpB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AA8DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,MAAM,CAAC,aAAa,GAAE,MAAsB,EAAE,KAAK,GAAE,MAAM,EAAO,GAAG,MAAM,CAsO1F"}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { relative, resolve } from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
MANIFEST_PATH,
|
|
5
5
|
basaltBinCommand,
|
|
@@ -12,7 +12,10 @@ import {
|
|
|
12
12
|
parseJsonc,
|
|
13
13
|
readBasaltConfig,
|
|
14
14
|
readIfExists,
|
|
15
|
+
readManifest,
|
|
15
16
|
resolveProjectDir,
|
|
17
|
+
resolveRoots,
|
|
18
|
+
scannableFiles,
|
|
16
19
|
shippedAssetPath
|
|
17
20
|
} from "./index.js";
|
|
18
21
|
import { findManifestAbove, parentInstallAdvice } from "./sync.js";
|
|
@@ -93,6 +96,30 @@ basalt-ui doctor \u2014 ${cwd}
|
|
|
93
96
|
fail(`${MANIFEST_PATH} missing \u2014 run \`basalt-ui init\` to scaffold the consumer repo`);
|
|
94
97
|
}
|
|
95
98
|
const install = findBasaltInstall(cwd);
|
|
99
|
+
const manifestVersion = profile === "tokens-only" ? void 0 : readManifest(cwd).basaltVersion;
|
|
100
|
+
const at = install.dir === null ? "" : `${relative(cwd, install.dir)} (${install.version})`;
|
|
101
|
+
if (install.dir === null) {
|
|
102
|
+
fail(
|
|
103
|
+
"install-parity: basalt-ui does not resolve here, above, or in a sibling workspace package."
|
|
104
|
+
);
|
|
105
|
+
} else if (manifestVersion === void 0 || install.version === manifestVersion) {
|
|
106
|
+
const matches = manifestVersion === void 0 ? "" : " and matches the manifest's basaltVersion";
|
|
107
|
+
pass(`basalt-ui resolves at ${at}${matches}`);
|
|
108
|
+
} else {
|
|
109
|
+
fail(
|
|
110
|
+
`install-parity: basalt-ui resolves at ${at}, but ${MANIFEST_PATH} was last written by ${manifestVersion} \u2014 the managed layer is from a different release than the code. Run \`basalt-ui sync\` (or \`bun install\`, if the lockfile is what is stale).`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const scanned = profile === "tokens-only" ? null : scannableFiles(cwd, cfg).length;
|
|
114
|
+
if (scanned === null) {
|
|
115
|
+
pass("guard-scan: n/a \u2014 a tokens-only consumer runs the token lane, not the file walk");
|
|
116
|
+
} else if (scanned === 0) {
|
|
117
|
+
fail(
|
|
118
|
+
"guard-scan: `check-theme` would scan ZERO files here \u2014 either `basalt.roots` points at paths that do not exist, or this is not the package the config lives in (BASALT_CWD, else the cwd; nothing is inferred)."
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
pass(`guard-scan: check-theme covers ${scanned} file(s) under ${resolveRoots(cfg).join(", ")}`);
|
|
122
|
+
}
|
|
96
123
|
if (profile === "tokens-only") {
|
|
97
124
|
pass("oxlint-preset: n/a \u2014 the shipped preset is a Mantine/React preset");
|
|
98
125
|
} else {
|
package/dist/cli/help.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAA;CAAE,CAAC,CAkC1F,CAAA;AAED;yCACyC;AACzC,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAWhF;AAED,0GAA0G;AAC1G,eAAO,MAAM,KAAK,QAoC0E,CAAA"}
|