basalt-ui 2.0.4 → 2.1.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/src/cli/index.ts CHANGED
@@ -148,23 +148,11 @@ function sanitizeSeverity(cfg: BasaltConfig): BasaltConfig {
148
148
  /** Does `cwd`'s own `package.json` carry a `basalt` key? — "misconfigured roots" vs "wrong
149
149
  * directory", the failure a root-invoked hook hits since 1.29.0's single resolver. Never a walk. */
150
150
  export function hasBasaltKey(cwd: string): boolean {
151
- const raw = readIfExists(resolve(cwd, 'package.json'))
152
- try {
153
- return raw !== null && (JSON.parse(raw) as { basalt?: unknown }).basalt !== undefined
154
- } catch {
155
- return false
156
- }
151
+ return readPackageJson<{ basalt?: unknown }>(cwd)?.basalt !== undefined
157
152
  }
158
153
 
159
154
  export function readBasaltConfig(cwd: string): BasaltConfig {
160
- try {
161
- const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf8')) as {
162
- basalt?: BasaltConfig
163
- }
164
- return sanitizeSeverity(pkg.basalt ?? {})
165
- } catch {
166
- return {}
167
- }
155
+ return sanitizeSeverity(readPackageJson<{ basalt?: BasaltConfig }>(cwd)?.basalt ?? {})
168
156
  }
169
157
 
170
158
  /**
@@ -237,13 +225,10 @@ const UNRESOLVED_INSTALL: BasaltInstall = { dir: null, version: null, how: null
237
225
  /** `<dir>/node_modules/basalt-ui` when it holds a readable package.json, else null. */
238
226
  function basaltInstallAt(dir: string): { dir: string; version: string | null } | null {
239
227
  const installDir = resolve(dir, 'node_modules', 'basalt-ui')
240
- const raw = readIfExists(resolve(installDir, 'package.json'))
241
- if (raw === null) return null
242
- try {
243
- const pkg = JSON.parse(raw) as { version?: string }
244
- return { dir: installDir, version: pkg.version ?? null }
245
- } catch {
246
- return { dir: installDir, version: null }
228
+ if (readIfExists(resolve(installDir, 'package.json')) === null) return null
229
+ return {
230
+ dir: installDir,
231
+ version: readPackageJson<{ version?: string }>(installDir)?.version ?? null,
247
232
  }
248
233
  }
249
234
 
@@ -500,31 +485,30 @@ export function readIfExists(abs: string): string | null {
500
485
  }
501
486
  }
502
487
 
488
+ /** `<dir>/package.json` parsed as `T`; null when absent or not JSON. */
489
+ export function readPackageJson<T>(dir: string): T | null {
490
+ const raw = readIfExists(resolve(dir, 'package.json'))
491
+ try {
492
+ return raw === null ? null : (JSON.parse(raw) as T)
493
+ } catch {
494
+ return null
495
+ }
496
+ }
497
+
503
498
  /** Read a shipped source asset from the package root; null if the sibling hasn't authored it yet. */
504
499
  export function readSource(pkgRoot: string, source: string): string | null {
505
500
  return readIfExists(resolve(pkgRoot, source))
506
501
  }
507
502
 
508
503
  function readPackageName(cwd: string): string {
509
- try {
510
- const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf8')) as { name?: string }
511
- if (pkg.name && pkg.name.length > 0) return pkg.name
512
- } catch {
513
- // fall through to dir name
514
- }
504
+ const name = readPackageJson<{ name?: string }>(cwd)?.name
505
+ if (name && name.length > 0) return name
515
506
  return cwd.split('/').filter(Boolean).pop() ?? 'app'
516
507
  }
517
508
 
518
509
  /** The framework's own published version, read from the package root package.json. */
519
510
  export function readFrameworkVersion(pkgRoot: string): string {
520
- try {
521
- const pkg = JSON.parse(readFileSync(resolve(pkgRoot, 'package.json'), 'utf8')) as {
522
- version?: string
523
- }
524
- return pkg.version ?? '0.0.0'
525
- } catch {
526
- return '0.0.0'
527
- }
511
+ return readPackageJson<{ version?: string }>(pkgRoot)?.version ?? '0.0.0'
528
512
  }
529
513
 
530
514
  /**
@@ -733,20 +717,18 @@ export function resolveAppDir(root: string): AppResolution {
733
717
 
734
718
  /** True when `pkgName` appears in the consumer's dependencies, devDependencies, or peerDependencies. */
735
719
  function hasDependency(cwd: string, pkgName: string): boolean {
736
- try {
737
- const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf8')) as {
738
- dependencies?: Record<string, string>
739
- devDependencies?: Record<string, string>
740
- peerDependencies?: Record<string, string>
741
- }
742
- return Boolean(
743
- pkg.dependencies?.[pkgName] ??
744
- pkg.devDependencies?.[pkgName] ??
745
- pkg.peerDependencies?.[pkgName],
746
- )
747
- } catch {
748
- return false
749
- }
720
+ const pkg =
721
+ readPackageJson<
722
+ Record<
723
+ 'dependencies' | 'devDependencies' | 'peerDependencies',
724
+ Record<string, string> | undefined
725
+ >
726
+ >(cwd)
727
+ return Boolean(
728
+ pkg?.dependencies?.[pkgName] ??
729
+ pkg?.devDependencies?.[pkgName] ??
730
+ pkg?.peerDependencies?.[pkgName],
731
+ )
750
732
  }
751
733
 
752
734
  /**
@@ -1275,14 +1257,8 @@ export function patchPackageJson(
1275
1257
  ): boolean {
1276
1258
  const abs = resolve(cwd, 'package.json')
1277
1259
  const raw = readIfExists(abs)
1278
- if (raw === null) return false
1279
- let pkg: Record<string, unknown>
1280
- try {
1281
- pkg = JSON.parse(raw) as Record<string, unknown>
1282
- } catch {
1283
- return false
1284
- }
1285
- if (!mutate(pkg)) return false
1260
+ const pkg = readPackageJson<Record<string, unknown>>(cwd)
1261
+ if (raw === null || pkg === null || !mutate(pkg)) return false
1286
1262
  const indent = /\n([ \t]+)"/.exec(raw)?.[1] ?? ' '
1287
1263
  writeFileSync(abs, JSON.stringify(pkg, null, indent) + (raw.endsWith('\n') ? '\n' : ''))
1288
1264
  return true
@@ -1689,15 +1665,8 @@ function expandWorkspaceGlobs(cwd: string, patterns: readonly string[]): string[
1689
1665
  * an unrecognized `workspaces` shape.
1690
1666
  */
1691
1667
  function collectWorkspacePackages(cwd: string): WorkspaceWalk {
1692
- const raw = readIfExists(resolve(cwd, 'package.json'))
1693
- if (raw === null) return { packages: [], hasWorkspacesField: false }
1694
-
1695
- let parsed: { workspaces?: unknown; name?: string }
1696
- try {
1697
- parsed = JSON.parse(raw) as { workspaces?: unknown; name?: string }
1698
- } catch {
1699
- return { packages: [], hasWorkspacesField: false }
1700
- }
1668
+ const parsed = readPackageJson<{ workspaces?: unknown; name?: string }>(cwd)
1669
+ if (parsed === null) return { packages: [], hasWorkspacesField: false }
1701
1670
 
1702
1671
  const patterns = isStringArray(parsed.workspaces)
1703
1672
  ? parsed.workspaces
@@ -64,6 +64,18 @@ const LANG_ALIASES: Record<string, string> = {
64
64
  }
65
65
 
66
66
  let highlighterPromise: Promise<HighlighterCore | null> | null = null
67
+ let warnedMissingPeer = false
68
+
69
+ /** Dev-only, once per process: the highlighter peers did not resolve, so code stays unhighlighted. */
70
+ function warnMissingPeer(error: unknown): void {
71
+ if (warnedMissingPeer || process.env['NODE_ENV'] === 'production') return
72
+ warnedMissingPeer = true
73
+ console.warn(
74
+ '[basalt] CodeBlock: syntax highlighting is off — a peer of basalt-ui/content did not load ' +
75
+ `(${error instanceof Error ? error.message : String(error)}). Code renders unhighlighted. ` +
76
+ 'Install them with: bun add shiki @shikijs/langs @shikijs/themes',
77
+ )
78
+ }
67
79
 
68
80
  async function loadHighlighter(): Promise<HighlighterCore | null> {
69
81
  const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] = await Promise.all([
@@ -79,7 +91,10 @@ async function loadHighlighter(): Promise<HighlighterCore | null> {
79
91
 
80
92
  /** Module-level singleton — resolves once; every caller shares the same highlighter instance. */
81
93
  function getHighlighter(): Promise<HighlighterCore | null> {
82
- highlighterPromise ??= loadHighlighter().catch(() => null)
94
+ highlighterPromise ??= loadHighlighter().catch((error: unknown) => {
95
+ warnMissingPeer(error)
96
+ return null
97
+ })
83
98
  return highlighterPromise
84
99
  }
85
100
 
@@ -91,7 +106,8 @@ async function ensureLanguage(highlighter: HighlighterCore, lang: string): Promi
91
106
  try {
92
107
  await highlighter.loadLanguage(loadLang() as LanguageInput)
93
108
  return true
94
- } catch {
109
+ } catch (error) {
110
+ warnMissingPeer(error)
95
111
  return false
96
112
  }
97
113
  }
@@ -103,9 +103,11 @@
103
103
  * Block is breathing room under the band's single `--vx-divider` line — nothing aligns to it.
104
104
  * `stack-md` (12) on a phone, one rung down (`stack-sm`, 8) from `sm` up where the inline gutter
105
105
  * is 16: the page then sits 16 from the seam and 8 under the header, rather than the 20/12 it
106
- * used to.
106
+ * used to. The value rides `--basalt-main-pad-block`, which descendants inherit: the data
107
+ * table's page-sticky offset reads it instead of repeating this module's `sm` switch.
107
108
  */
108
- padding: var(--vx-space-stack-md) var(--app-shell-padding);
109
+ --basalt-main-pad-block: var(--vx-space-stack-md);
110
+ padding: var(--basalt-main-pad-block) var(--app-shell-padding);
109
111
  /* Without this a wide flex/grid child (a bare table, a nowrap control row) sizes Main to ITSELF. */
110
112
  min-width: 0;
111
113
  overflow-y: auto;
@@ -124,7 +126,7 @@
124
126
 
125
127
  @media (min-width: 52.5em) {
126
128
  .main {
127
- padding-block: var(--vx-space-stack-sm);
129
+ --basalt-main-pad-block: var(--vx-space-stack-sm);
128
130
  }
129
131
  }
130
132
 
@@ -327,7 +327,9 @@ function NavSection({ section, collapsed }: { section: SidebarSection; collapsed
327
327
  <SectionLabel>{section.label}</SectionLabel>
328
328
  <IconChevron open={!folded} />
329
329
  </UnstyledButton>
330
- <Collapse expanded={!folded}>
330
+ {/* The rail hides the header (the only way to unfold), so a folded section would strand its
331
+ destinations and their badges: the rail ignores the fold; expanding restores it. */}
332
+ <Collapse expanded={!folded || collapsed}>
331
333
  <Stack gap={1}>{items}</Stack>
332
334
  </Collapse>
333
335
  </div>
@@ -70,20 +70,14 @@
70
70
  * one rung down to `stack-sm` from `sm` up). A sticky `<thead>` resolves against the scrollport's
71
71
  * CONTENT edge, so at `top: 0` it parked that padding below the band and every body row scrolled
72
72
  * through the gap. A negative offset equal to the padding moves the stick line out to the band's
73
- * edge, flush. The two media/token pairs below mirror `.main` there — the layout suite
74
- * (`data-table.layout.test.ts`, invariant 8) fails on drift. A consumer `stickyHeaderOffset` is an
73
+ * edge, flush. The padding is read from `--basalt-main-pad-block`, which `.main` sets and owns
74
+ * (including its `sm` switch), so the two cannot drift; `data-table.layout.test.ts` invariant 8
75
+ * pins both steps. A consumer `stickyHeaderOffset` is an
75
76
  * inline `--table-sticky-header-offset` on the table and beats this class, so it still wins.
76
77
  * Outside a shell no `.mantine-AppShell-main` ancestor matches and the offset stays 0.
77
78
  */
78
79
  :global(.mantine-AppShell-main) .pageSticky {
79
- --table-sticky-header-offset: calc(-1 * var(--vx-space-stack-md));
80
- }
81
-
82
- /* theme-allow raw-media-query — mirrors app-main.module.css's own `sm` switch for Main's block padding; invariant 8 in data-table.layout.test.ts fails on drift */
83
- @media (min-width: 52.5em) {
84
- :global(.mantine-AppShell-main) .pageSticky {
85
- --table-sticky-header-offset: calc(-1 * var(--vx-space-stack-sm));
86
- }
80
+ --table-sticky-header-offset: calc(-1 * var(--basalt-main-pad-block, 0px));
87
81
  }
88
82
 
89
83
  /*