basalt-ui 2.0.1 → 2.0.3

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/MIGRATING.md +37 -19
  3. package/configs/check.yml +5 -5
  4. package/dist/cli/config.d.ts +10 -0
  5. package/dist/cli/config.d.ts.map +1 -1
  6. package/dist/cli/config.js +1 -1
  7. package/dist/cli/doctor-upgrade.d.ts +1 -1
  8. package/dist/cli/doctor-upgrade.d.ts.map +1 -1
  9. package/dist/cli/doctor-upgrade.js +36 -1
  10. package/dist/cli/doctor.d.ts.map +1 -1
  11. package/dist/cli/doctor.js +28 -3
  12. package/dist/cli/index.d.ts +28 -0
  13. package/dist/cli/index.d.ts.map +1 -1
  14. package/dist/cli/index.js +60 -7
  15. package/dist/cli/init.d.ts.map +1 -1
  16. package/dist/cli/init.js +7 -1
  17. package/dist/cli/sync.d.ts.map +1 -1
  18. package/dist/cli/sync.js +5 -0
  19. package/dist/provider/index.d.ts +8 -1
  20. package/dist/provider/index.d.ts.map +1 -1
  21. package/dist/provider/index.js +3 -2
  22. package/dist/provider/spotlight-actions.d.ts +3 -2
  23. package/dist/provider/spotlight-actions.d.ts.map +1 -1
  24. package/dist/provider/spotlight-actions.js +5 -3
  25. package/dist/shell/app-sidebar.d.ts.map +1 -1
  26. package/dist/shell/app-sidebar.js +9 -2
  27. package/dist/shell/app-sidebar.module.css +6 -0
  28. package/dist/shell/app-sidebar.module.css.d.ts +1 -0
  29. package/dist/tokens/core.d.ts +12 -11
  30. package/dist/tokens/core.d.ts.map +1 -1
  31. package/dist/tokens/core.js +9 -1
  32. package/llms.txt +2 -2
  33. package/package.json +1 -1
  34. package/src/cli/config.ts +11 -1
  35. package/src/cli/doctor-upgrade.ts +49 -2
  36. package/src/cli/doctor.ts +37 -3
  37. package/src/cli/index.ts +93 -7
  38. package/src/cli/init.ts +10 -1
  39. package/src/cli/sync.ts +6 -0
  40. package/src/provider/index.tsx +10 -2
  41. package/src/provider/spotlight-actions.tsx +8 -3
  42. package/src/shell/app-sidebar.module.css +6 -0
  43. package/src/shell/app-sidebar.module.css.d.ts +1 -0
  44. package/src/shell/app-sidebar.tsx +16 -2
  45. package/src/tokens/core.ts +25 -9
package/src/cli/sync.ts CHANGED
@@ -101,6 +101,12 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
101
101
  return 0
102
102
  }
103
103
 
104
+ const appError = resolvePlacement(cwd).appError
105
+ if (appError !== null) {
106
+ console.error(`✖ basalt-ui sync: ${appError}`)
107
+ return 1
108
+ }
109
+
104
110
  // Refuse BEFORE reconcileRoots, which writes `basalt.roots` into package.json.
105
111
  if (!existsSync(resolve(cwd, MANIFEST_PATH))) {
106
112
  const elsewhere = findManifestAbove(cwd)
@@ -41,6 +41,7 @@ import type { Telemetry } from '../telemetry'
41
41
  import type { BasaltDevDockTool } from '../dev-dock'
42
42
  import type { BasaltRouter, BasaltRouterContext } from './create-router'
43
43
  import { useRegistryHotkeys } from './hotkeys'
44
+ import type { FlatNavItem } from '../router-tanstack/nav'
44
45
  import { buildSpotlightActions } from './spotlight-actions'
45
46
 
46
47
  export type BasaltProviderProps = {
@@ -79,6 +80,12 @@ export type BasaltProviderProps = {
79
80
  * can answer. Default `'compact'`. SSR only; an SSR desktop app SHOULD pass it.
80
81
  */
81
82
  sizeClassHint?: SizeClass
83
+ /**
84
+ * Label for the `nav.<id>` Spotlight entries generated from `defineNav`, e.g.
85
+ * `(item) => `Go to ${item.label}``. Default: the destination's own label. A `defineCommands`
86
+ * entry with id `nav.<id>` replaces the generated one outright.
87
+ */
88
+ navCommandLabel?: (item: FlatNavItem) => string
82
89
  } & Omit<MantineProviderProps, 'children' | 'theme' | 'defaultColorScheme' | 'cssVariablesResolver'>
83
90
 
84
91
  // ── Root error boundary ───────────────────────────────────────────────────────────────────────────
@@ -408,6 +415,7 @@ export function BasaltProvider({
408
415
  defaultColorScheme = 'dark',
409
416
  nonce,
410
417
  sizeClassHint = 'compact',
418
+ navCommandLabel,
411
419
  ...rest
412
420
  }: BasaltProviderProps) {
413
421
  useDuplicateProviderGuard()
@@ -432,8 +440,8 @@ export function BasaltProvider({
432
440
  // appears, not frozen at mount — and `detectMac` (inside the builder) never runs during render.
433
441
  const [actions, setActions] = useState<SpotlightActionData[]>([])
434
442
  const refreshActions = useCallback(
435
- () => setActions(buildSpotlightActions(router, setDevTool)),
436
- [router],
443
+ () => setActions(buildSpotlightActions(router, setDevTool, navCommandLabel)),
444
+ [router, navCommandLabel],
437
445
  )
438
446
  useEffect(refreshActions, [refreshActions])
439
447
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The provider's Spotlight list: the `defineCommands` registry, then the `defineNav` destinations
3
- * (via `flattenNav`), then the three dev-dock commands in dev. A snapshot rebuilt each time Spotlight
3
+ * (via `flattenNav`, skipping any id a registered `nav.<id>` command already owns), then the three dev-dock commands in dev. A snapshot rebuilt each time Spotlight
4
4
  * opens, so a command's `when()` is current. Not for use during render (`detectMac` reads `navigator`).
5
5
  */
6
6
  import type { SpotlightActionData } from '@mantine/spotlight'
@@ -8,6 +8,7 @@ import type { AnyRouter } from '@tanstack/react-router'
8
8
  import { getActiveCommands } from '../commands/define-commands'
9
9
  import { detectMac, formatShortcutDisplay } from '../commands/shortcut-format'
10
10
  import { flattenNav, getActiveNav } from '../router-tanstack/nav'
11
+ import type { FlatNavItem } from '../router-tanstack/nav'
11
12
  import type { BasaltDevDockTool } from '../dev-dock'
12
13
  import { runCommandSafely } from './hotkeys'
13
14
 
@@ -20,10 +21,12 @@ const DEV_TOOLS: ReadonlyArray<{ tool: BasaltDevDockTool; label: string }> = [
20
21
  export function buildSpotlightActions(
21
22
  router: AnyRouter,
22
23
  openDevTool: (tool: BasaltDevDockTool) => void,
24
+ navCommandLabel?: (item: FlatNavItem) => string,
23
25
  ): SpotlightActionData[] {
24
26
  const actions: SpotlightActionData[] = []
27
+ const commands = getActiveCommands()
25
28
 
26
- for (const [id, cmd] of Object.entries(getActiveCommands())) {
29
+ for (const [id, cmd] of Object.entries(commands)) {
27
30
  if (cmd.when !== undefined && !cmd.when()) continue
28
31
  actions.push({
29
32
  id,
@@ -42,9 +45,11 @@ export function buildSpotlightActions(
42
45
  if (nav !== undefined) {
43
46
  for (const item of flattenNav(nav)) {
44
47
  if (item.disabled === true) continue
48
+ // A registered `nav.<id>` command owns that destination: the consumer wins, no duplicate.
49
+ if (`nav.${item.id}` in commands) continue
45
50
  actions.push({
46
51
  id: `nav.${item.id}`,
47
- label: item.label,
52
+ label: navCommandLabel?.(item) ?? item.label,
48
53
  group: item.groupLabel,
49
54
  leftSection: item.icon,
50
55
  // `link` is the destination's own `linkOptions`, so a store-backed `search` thunk restores
@@ -472,6 +472,12 @@
472
472
  box-shadow: 0 0 0 2px var(--mantine-color-body);
473
473
  }
474
474
 
475
+ /* Host for a nav row's rail dot: the dot anchors on the icon itself, not the 48px row. */
476
+ .navIconDotHost {
477
+ position: relative;
478
+ display: inline-flex;
479
+ }
480
+
475
481
  .blockItems {
476
482
  display: flex;
477
483
  flex-direction: column;
@@ -29,6 +29,7 @@ declare const classes: {
29
29
  readonly blockHeaderButton: string
30
30
  readonly blockIcon: string
31
31
  readonly blockDot: string
32
+ readonly navIconDotHost: string
32
33
  readonly blockItems: string
33
34
  readonly blockRow: string
34
35
  readonly blockRowStatic: string
@@ -207,14 +207,28 @@ function NavLinkBody({
207
207
  collapsed?: boolean
208
208
  }) {
209
209
  const Anchor = item.Anchor
210
+ const icon = iconOrInitial(item.icon, item.label)
211
+ // The rail hides the right section (where the badge lives), so a badged row keeps a dot on its
212
+ // icon instead — the count rides the accessible name since the number itself has no room.
213
+ const railDot =
214
+ collapsed && ((item.badge !== undefined && item.badge !== null) || Boolean(item.count))
210
215
  const shared = {
211
216
  classNames: { root: classes.link },
212
217
  label: item.label,
213
- leftSection: iconOrInitial(item.icon, item.label),
218
+ leftSection: railDot ? (
219
+ <span className={classes.navIconDotHost}>
220
+ {icon}
221
+ <span className={classes.blockDot} data-basalt-rail-dot aria-hidden />
222
+ </span>
223
+ ) : (
224
+ icon
225
+ ),
214
226
  // Only the collapsed rail hides the label (CSS), leaving the icon alone — name the link there.
215
227
  // Expanded, the visible text IS the name and carries the count badge; an `aria-label` would
216
228
  // override both and drop the count ("Inbox 5" → "Inbox").
217
- ...(collapsed && { 'aria-label': item.label }),
229
+ ...(collapsed && {
230
+ 'aria-label': item.count ? `${item.label}, ${item.count}` : item.label,
231
+ }),
218
232
  rightSection: item.badge ?? (item.count ? <NavCountBadge count={item.count} /> : undefined),
219
233
  active,
220
234
  // Basalt is the one that stamps `aria-current`, not the theme and not a router `<Link>`'s own
@@ -820,15 +820,23 @@ export function defineSeries<const T extends SeriesMap>(map: T): T {
820
820
  /** Runtime key of the {@link SeriesGroup} brand. */
821
821
  const SERIES_GROUP: unique symbol = Symbol('basalt.series-group')
822
822
 
823
- /**
824
- * The brand `groupTokens` puts on its return value. `BuildPaletteOpts.groups` takes these, so the
825
- * `${name}-` key that emits the variables and the one that references them come from one call and
826
- * cannot be spelled two ways.
827
- */
828
- export type SeriesGroup = {
823
+ type GroupPayload = {
829
824
  readonly [SERIES_GROUP]: { readonly prefix: string; readonly map: SeriesMap }
830
825
  }
831
826
 
827
+ declare const GROUP_REF: unique symbol
828
+
829
+ /** A `var(--vx-<name>-<key>)` ref minted by `groupTokens`. The brand lives on the VALUES, so it never shows up in `keyof` the group. */
830
+ export type GroupRef = string & { readonly [GROUP_REF]: true }
831
+
832
+ /**
833
+ * What `groupTokens` returns. `BuildPaletteOpts.groups` takes these, so the `${name}-` key that
834
+ * emits the variables and the one that references them come from one call and cannot be spelled
835
+ * two ways. A bare series map (object values) and a plain string record (unbranded values) are
836
+ * both rejected; the runtime payload sits under the non-enumerable {@link SERIES_GROUP} symbol.
837
+ */
838
+ export type SeriesGroup = { readonly [key: string]: GroupRef }
839
+
832
840
  /**
833
841
  * Build a single namespaced group's token refs (`var(--vx-<name>-<key>)`).
834
842
  *
@@ -838,12 +846,12 @@ export type SeriesGroup = {
838
846
  export function groupTokens<const T extends SeriesMap>(
839
847
  name: string,
840
848
  map: T,
841
- ): { [K in keyof T]: string } & SeriesGroup {
849
+ ): { readonly [K in keyof T]: GroupRef } {
842
850
  const refs = seriesTokens(map, `${name}-`)
843
851
  // Non-enumerable, symbol-keyed: a series key can never collide with the brand, and
844
852
  // `Object.keys(refs)` still lists exactly the series.
845
853
  Object.defineProperty(refs, SERIES_GROUP, { value: { prefix: `${name}-`, map } })
846
- return refs as { [K in keyof T]: string } & SeriesGroup
854
+ return refs as { readonly [K in keyof T]: GroupRef }
847
855
  }
848
856
 
849
857
  /**
@@ -1024,7 +1032,15 @@ export function buildPaletteCss(
1024
1032
  const derived = extraDerived ? `${derivedBlock}\n${extraDerived}` : derivedBlock
1025
1033
  const side = (s: Side): string => {
1026
1034
  const extra = groups
1027
- .map((group) => groupSide(group[SERIES_GROUP].prefix, group[SERIES_GROUP].map, s))
1035
+ .map((group) => {
1036
+ const payload = (group as unknown as Partial<GroupPayload>)[SERIES_GROUP]
1037
+ if (!payload) {
1038
+ throw new Error(
1039
+ 'BuildPaletteOpts.groups: every group must come from groupTokens(name, map)',
1040
+ )
1041
+ }
1042
+ return groupSide(payload.prefix, payload.map, s)
1043
+ })
1028
1044
  .join('\n')
1029
1045
  const framework = frameworkPrimitives(s, data, legacyAliases)
1030
1046
  return extra ? `${framework}\n${extra}` : framework