basalt-ui 1.33.0 → 1.34.1

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 (83) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +33 -0
  3. package/MIGRATING.md +115 -15
  4. package/README.md +3 -0
  5. package/agent/skills/basalt-app/SKILL.md +5 -3
  6. package/configs/oxlint-basalt.json +33 -0
  7. package/configs/oxlint-plugin.js +21 -6
  8. package/configs/oxlint-plugin.test.ts +98 -10
  9. package/configs/oxlint.json +2 -30
  10. package/dist/cli/check-theme.d.ts.map +1 -1
  11. package/dist/cli/check-theme.js +40 -36
  12. package/dist/cli/doctor.d.ts +5 -10
  13. package/dist/cli/doctor.d.ts.map +1 -1
  14. package/dist/cli/doctor.js +7 -5
  15. package/dist/cli/guard-hook.d.ts.map +1 -1
  16. package/dist/cli/guard-hook.js +6 -34
  17. package/dist/cli/index.d.ts +45 -83
  18. package/dist/cli/index.d.ts.map +1 -1
  19. package/dist/cli/index.js +73 -19
  20. package/dist/cli/sync.d.ts.map +1 -1
  21. package/dist/cli/sync.js +7 -16
  22. package/dist/common/vx-families.d.ts +7 -0
  23. package/dist/common/vx-families.d.ts.map +1 -0
  24. package/dist/common/vx-families.js +32 -0
  25. package/dist/common/vx-family.d.ts +11 -0
  26. package/dist/common/vx-family.d.ts.map +1 -0
  27. package/dist/common/vx-family.js +12 -0
  28. package/dist/guard/index.d.ts +2 -2
  29. package/dist/guard/index.d.ts.map +1 -1
  30. package/dist/guard/index.js +11 -13
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +0 -2
  34. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  35. package/dist/shell/app-mobile-nav.js +2 -1
  36. package/dist/shell/app-mobile-nav.module.css +12 -0
  37. package/dist/shell/app-sidebar.d.ts.map +1 -1
  38. package/dist/shell/app-sidebar.js +32 -36
  39. package/dist/shell/app-sidebar.module.css +6 -0
  40. package/dist/shell/index.d.ts +0 -1
  41. package/dist/shell/index.d.ts.map +1 -1
  42. package/dist/shell/index.js +0 -2
  43. package/dist/shell/nav-glyph.d.ts +14 -0
  44. package/dist/shell/nav-glyph.d.ts.map +1 -0
  45. package/dist/shell/nav-glyph.js +21 -0
  46. package/dist/shell/nav-types.d.ts +1 -0
  47. package/dist/shell/nav-types.d.ts.map +1 -1
  48. package/dist/shell/sidebar-search.d.ts.map +1 -1
  49. package/dist/shell/sidebar-search.js +1 -3
  50. package/dist/surfaces.d.ts +2 -2
  51. package/dist/surfaces.js +2 -2
  52. package/dist/tokens/index.d.ts +1 -1
  53. package/dist/tokens/index.d.ts.map +1 -1
  54. package/dist/tokens/index.js +17 -1
  55. package/dist/tokens/size-classes.d.ts +12 -0
  56. package/dist/tokens/size-classes.d.ts.map +1 -1
  57. package/dist/tokens/size-classes.js +11 -5
  58. package/llms.txt +4 -4
  59. package/package.json +1 -1
  60. package/src/cli/check-theme.ts +63 -37
  61. package/src/cli/doctor.ts +12 -15
  62. package/src/cli/guard-hook.ts +8 -35
  63. package/src/cli/index.ts +126 -113
  64. package/src/cli/sync.ts +7 -16
  65. package/src/common/vx-families.ts +36 -0
  66. package/src/common/vx-family.ts +22 -0
  67. package/src/guard/index.ts +22 -25
  68. package/src/index.ts +0 -2
  69. package/src/shell/app-mobile-nav.module.css +12 -0
  70. package/src/shell/app-mobile-nav.tsx +8 -1
  71. package/src/shell/app-sidebar.module.css +6 -0
  72. package/src/shell/app-sidebar.tsx +52 -50
  73. package/src/shell/index.tsx +0 -1
  74. package/src/shell/nav-glyph.tsx +38 -0
  75. package/src/shell/nav-types.ts +1 -0
  76. package/src/shell/sidebar-search.tsx +1 -5
  77. package/src/surfaces.ts +2 -2
  78. package/src/tokens/index.ts +28 -1
  79. package/src/tokens/size-classes.ts +18 -5
  80. package/dist/shell/use-breakpoint.d.ts +0 -10
  81. package/dist/shell/use-breakpoint.d.ts.map +0 -1
  82. package/dist/shell/use-breakpoint.js +0 -11
  83. package/src/shell/use-breakpoint.ts +0 -37
package/src/index.ts CHANGED
@@ -85,8 +85,6 @@ export {
85
85
  PageAside,
86
86
  type PageAsideProps,
87
87
  type PageAsideSlot,
88
- useBreakpoint,
89
- type BreakpointName,
90
88
  useSizeClass,
91
89
  type SizeClass,
92
90
  PageTitle,
@@ -69,6 +69,18 @@
69
69
  height: var(--vx-space-mobile-nav-icon-size);
70
70
  }
71
71
 
72
+ /* The first-letter fallback for an icon-less slot (`nav-glyph.tsx`) sits in the same icon-size box
73
+ * an svg gets, so the pill's height does not depend on the glyph's line box. */
74
+ .tabIcon > [data-nav-glyph] {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ width: var(--vx-space-mobile-nav-icon-size);
79
+ height: var(--vx-space-mobile-nav-icon-size);
80
+ line-height: 1;
81
+ font-weight: 600;
82
+ }
83
+
72
84
  .tab:active .tabIcon {
73
85
  transform: scale(0.9);
74
86
  }
@@ -39,6 +39,7 @@ import type {
39
39
  import type { BasaltAccountProps } from './account-types'
40
40
  import type { SettingsMenuItem } from './index'
41
41
  import { NavCountBadge } from './nav-count-badge'
42
+ import { hasIcon, initialGlyph } from './nav-glyph'
42
43
  import { SidebarBlockToneDot } from './sidebar-blocks'
43
44
  import { sidebarBlockMobile } from './sidebar-block-model'
44
45
  import { useBasaltSpacing } from '../theme'
@@ -456,7 +457,13 @@ export function MobileNav({
456
457
  const tabInner = (slot: MobileNavSlot) => (
457
458
  <>
458
459
  <span className={classes.tabIcon}>
459
- {slot.icon ?? (slot.kind !== 'link' && slot.isMore ? <IconMore /> : null)}
460
+ {hasIcon(slot.icon) ? (
461
+ slot.icon
462
+ ) : slot.kind !== 'link' && slot.isMore ? (
463
+ <IconMore />
464
+ ) : (
465
+ initialGlyph(slot.short)
466
+ )}
460
467
  {hasCount(slot) ? <span className={classes.tabDot} aria-hidden /> : null}
461
468
  </span>
462
469
  <Text className={classes.label}>{slot.short}</Text>
@@ -47,6 +47,12 @@
47
47
  margin-inline-end: var(--vx-space-nav-icon-gap);
48
48
  }
49
49
 
50
+ /* The first-letter fallback for an icon-less row (`nav-glyph.tsx`) — a glyph, not a text run. */
51
+ .link :global(.mantine-NavLink-section[data-position='left']) > [data-nav-glyph] {
52
+ line-height: 1;
53
+ font-weight: 600;
54
+ }
55
+
50
56
  /* Inactive rows: muted text + faint icon. Active rows keep the theme's ink text (via `--nl-color`)
51
57
  * untouched — this selector only ever matches when `data-active` is absent. `aria-current` is no
52
58
  * longer part of the exclusion: `use-nav.ts` is the sole authority on active, stamping exactly one
@@ -39,6 +39,7 @@ import { assertRequiredProps } from '../common/validate'
39
39
  import type { BrandConfig, SettingsMenuItem } from './index'
40
40
  import type { SidebarBlock, SidebarItem, SidebarSection } from './nav-types'
41
41
  import { NavCountBadge } from './nav-count-badge'
42
+ import { iconOrInitial } from './nav-glyph'
42
43
  import { SidebarAccount } from './app-sidebar-account'
43
44
  import type { AccountMenuItem, BasaltAccountProps } from './account-types'
44
45
  import { SidebarSearch } from './sidebar-search'
@@ -190,12 +191,24 @@ function hasActiveDescendant(item: SidebarItem): boolean {
190
191
  * element. That is what replaced the old render callback — a callback had to re-derive basalt's
191
192
  * own styling to look right, and every consumer got it subtly wrong.
192
193
  */
193
- function NavLinkBody({ item, active }: { item: SidebarItem; active: boolean }) {
194
+ function NavLinkBody({
195
+ item,
196
+ active,
197
+ collapsed = false,
198
+ }: {
199
+ item: SidebarItem
200
+ active: boolean
201
+ collapsed?: boolean
202
+ }) {
194
203
  const Anchor = item.Anchor
195
204
  const shared = {
196
205
  classNames: { root: classes.link },
197
206
  label: item.label,
198
- leftSection: item.icon,
207
+ leftSection: iconOrInitial(item.icon, item.label),
208
+ // Only the collapsed rail hides the label (CSS), leaving the icon alone — name the link there.
209
+ // Expanded, the visible text IS the name and carries the count badge; an `aria-label` would
210
+ // override both and drop the count ("Inbox 5" → "Inbox").
211
+ ...(collapsed && { 'aria-label': item.label }),
199
212
  rightSection: item.badge ?? (item.count ? <NavCountBadge count={item.count} /> : undefined),
200
213
  active,
201
214
  // Basalt is the one that stamps `aria-current`, not the theme and not a router `<Link>`'s own
@@ -215,6 +228,32 @@ function NavLinkBody({ item, active }: { item: SidebarItem; active: boolean }) {
215
228
  return <NavLink component="a" {...(item.href !== undefined && { href: item.href })} {...shared} />
216
229
  }
217
230
 
231
+ /**
232
+ * One child row, shared by the inline child list and the hover popover. Never in the rail (the
233
+ * inline list is hidden there and the popover shows labels), so it carries no `aria-label`.
234
+ */
235
+ function ChildRow({ child }: { child: SidebarItem }) {
236
+ if (!child.disabled) {
237
+ return (
238
+ <Box>
239
+ <NavLinkBody item={child} active={Boolean(child.active)} />
240
+ </Box>
241
+ )
242
+ }
243
+ return (
244
+ <Tooltip label="Coming soon" position="right" withArrow>
245
+ <Box>
246
+ <NavLink
247
+ classNames={{ root: classes.link }}
248
+ label={child.label}
249
+ leftSection={iconOrInitial(child.icon, child.label)}
250
+ data-disabled
251
+ />
252
+ </Box>
253
+ </Tooltip>
254
+ )
255
+ }
256
+
218
257
  /**
219
258
  * Group label — a micro-label (docs/DESIGN-SPEC.md §3: mono, uppercase, tracked, faint). Typography
220
259
  * lives entirely in `.sectionLabel` (app-sidebar.module.css), not Mantine `Text` props, since the
@@ -304,7 +343,8 @@ function NavItemRow({ item, collapsed }: { item: SidebarItem; collapsed: boolean
304
343
  <NavLink
305
344
  classNames={{ root: classes.link }}
306
345
  label={item.label}
307
- leftSection={item.icon}
346
+ leftSection={iconOrInitial(item.icon, item.label)}
347
+ {...(collapsed && { 'aria-label': item.label })}
308
348
  data-disabled
309
349
  />
310
350
  </Box>
@@ -316,7 +356,7 @@ function NavItemRow({ item, collapsed }: { item: SidebarItem; collapsed: boolean
316
356
  return (
317
357
  <Tooltip key={item.key} label={item.label} position="right" withArrow disabled={!collapsed}>
318
358
  <Box className={classes.navItem}>
319
- <NavLinkBody item={item} active={active} />
359
+ <NavLinkBody item={item} active={active} collapsed={collapsed} />
320
360
  </Box>
321
361
  </Tooltip>
322
362
  )
@@ -331,32 +371,13 @@ function NavItemRow({ item, collapsed }: { item: SidebarItem; collapsed: boolean
331
371
  <Box key={item.key}>
332
372
  <Tooltip label={item.label} position="right" withArrow disabled={!collapsed}>
333
373
  <Box className={classes.navItem}>
334
- <NavLinkBody item={item} active={active} />
374
+ <NavLinkBody item={item} active={active} collapsed={collapsed} />
335
375
  </Box>
336
376
  </Tooltip>
337
377
  <Stack gap={0} className={classes.childList}>
338
- {item.children!.map((child) => {
339
- if (child.disabled) {
340
- return (
341
- <Tooltip key={child.key} label="Coming soon" position="right" withArrow>
342
- <Box>
343
- <NavLink
344
- classNames={{ root: classes.link }}
345
- label={child.label}
346
- leftSection={child.icon}
347
- data-disabled
348
- />
349
- </Box>
350
- </Tooltip>
351
- )
352
- }
353
- const childActive = Boolean(child.active)
354
- return (
355
- <Box key={child.key}>
356
- <NavLinkBody item={child} active={childActive} />
357
- </Box>
358
- )
359
- })}
378
+ {item.children!.map((child) => (
379
+ <ChildRow key={child.key} child={child} />
380
+ ))}
360
381
  </Stack>
361
382
  </Box>
362
383
  )
@@ -383,7 +404,7 @@ function NavItemRow({ item, collapsed }: { item: SidebarItem; collapsed: boolean
383
404
  onFocus={scheduleOpen}
384
405
  onBlur={scheduleClose}
385
406
  >
386
- <NavLinkBody item={item} active={active} />
407
+ <NavLinkBody item={item} active={active} collapsed={collapsed} />
387
408
  </Box>
388
409
  </Tooltip>
389
410
  </Popover.Target>
@@ -397,28 +418,9 @@ function NavItemRow({ item, collapsed }: { item: SidebarItem; collapsed: boolean
397
418
  onBlur={scheduleClose}
398
419
  >
399
420
  <Stack gap={0}>
400
- {item.children!.map((child) => {
401
- if (child.disabled) {
402
- return (
403
- <Tooltip key={child.key} label="Coming soon" position="right" withArrow>
404
- <Box>
405
- <NavLink
406
- classNames={{ root: classes.link }}
407
- label={child.label}
408
- leftSection={child.icon}
409
- data-disabled
410
- />
411
- </Box>
412
- </Tooltip>
413
- )
414
- }
415
- const childActive = Boolean(child.active)
416
- return (
417
- <Box key={child.key}>
418
- <NavLinkBody item={child} active={childActive} />
419
- </Box>
420
- )
421
- })}
421
+ {item.children!.map((child) => (
422
+ <ChildRow key={child.key} child={child} />
423
+ ))}
422
424
  </Stack>
423
425
  </Popover.Dropdown>
424
426
 
@@ -84,7 +84,6 @@ export {
84
84
  export { AppBreadcrumbs, type AppBreadcrumbsProps } from './app-breadcrumbs'
85
85
  export { PageBar, type PageBarProps, type PageBarSlot } from './page-bar'
86
86
  export { PageAside, type PageAsideProps, type PageAsideSlot } from './page-aside'
87
- export { useBreakpoint, type BreakpointName } from './use-breakpoint'
88
87
  export { useSizeClass, type SizeClass } from './use-size-class'
89
88
  export { PageTitle, type PageTitleProps, type PageTitleSlot } from './page-title'
90
89
 
@@ -0,0 +1,38 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ /** `Intl.Segmenter` is in every supported runtime; the code-point split is the belt-and-braces fallback. */
4
+ const segmenter =
5
+ typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
6
+ ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
7
+ : undefined
8
+
9
+ /** The first grapheme of a label — the fallback body for an item or action shipping no icon. */
10
+ export function initial(label: string): string {
11
+ const first = segmenter
12
+ ? segmenter.segment(label)[Symbol.iterator]().next().value?.segment
13
+ : [...label][0]
14
+ return first?.toUpperCase() ?? '?'
15
+ }
16
+
17
+ /** False for everything React renders as nothing — `cond && <Icon />` yields `false`, not `null`. */
18
+ export function hasIcon(icon: ReactNode): boolean {
19
+ return icon !== null && icon !== undefined && typeof icon !== 'boolean' && icon !== ''
20
+ }
21
+
22
+ /**
23
+ * The label's first grapheme as a decorative glyph. The collapsed rail and the landscape tab bar
24
+ * show the icon ALONE, so an absent icon would render a nameless, empty target. `aria-hidden`: the
25
+ * accessible name comes from the label, never the glyph.
26
+ */
27
+ export function initialGlyph(label: string): ReactNode {
28
+ return (
29
+ <span aria-hidden data-nav-glyph>
30
+ {initial(label)}
31
+ </span>
32
+ )
33
+ }
34
+
35
+ /** A nav item's icon, or its label's first-letter glyph when it ships none. */
36
+ export function iconOrInitial(icon: ReactNode, label: string): ReactNode {
37
+ return hasIcon(icon) ? icon : initialGlyph(label)
38
+ }
@@ -31,6 +31,7 @@ export type SidebarItem = {
31
31
  short?: string
32
32
  /** Mobile placement. `true` ≡ `'tab'`, `false` ≡ `'hidden'`. @default 'more' */
33
33
  mobile?: boolean | NavMobilePlacement
34
+ /** The collapsed rail and landscape tab bar show the icon alone; `null` falls back to the label's first letter. */
34
35
  icon: ReactNode
35
36
  /**
36
37
  * The router seam. basalt renders every pixel of chrome (desktop row, 56px slot, 44px sheet
@@ -13,6 +13,7 @@ import { OverflowMenu } from '../controls/actions'
13
13
  import type { BarAction } from '../controls/actions'
14
14
  import { cx } from '../common/props'
15
15
  import type { BasaltProps } from '../common/props'
16
+ import { initial } from './nav-glyph'
16
17
  import classes from './sidebar-search.module.css'
17
18
 
18
19
  /**
@@ -55,11 +56,6 @@ function IconSearch() {
55
56
  )
56
57
  }
57
58
 
58
- /** The first grapheme of a label — the fallback body for an action shipping no icon. */
59
- function initial(label: string): string {
60
- return [...label][0]?.toUpperCase() ?? '?'
61
- }
62
-
63
59
  /**
64
60
  * One trailing action as an icon-only button. `size="icon"` is EXPLICIT here, not inherited: the
65
61
  * search row is chrome, not a control home, so no `CtlSlot` wraps it and nothing else would resolve
package/src/surfaces.ts CHANGED
@@ -299,7 +299,7 @@ export const SURFACES = {
299
299
  "no second cssVariablesResolver — don't hand-build createTheme or re-add the resolver basalt already installs",
300
300
  ],
301
301
  description:
302
- 'BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, PageBar, PageAside, useSizeClass (useBreakpoint on notice), NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, WidgetHeader, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone), ConnectivityProvider/useConnectivity/ConnectivityIndicator (auto-mounted, C1: absorbed from the dropped ./connectivity), createBasaltQueryClient/unwrap/BasaltQueryDevtools (C1: absorbed from the dropped ./query), BasaltDevDock (C5: lazy Router/Query devtools + theme lab in one dock)',
302
+ 'BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, PageBar, PageAside, useSizeClass, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, WidgetHeader, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone), ConnectivityProvider/useConnectivity/ConnectivityIndicator (auto-mounted, C1: absorbed from the dropped ./connectivity), createBasaltQueryClient/unwrap/BasaltQueryDevtools (C1: absorbed from the dropped ./query), BasaltDevDock (C5: lazy Router/Query devtools + theme lab in one dock)',
303
303
  optionalPeers: [
304
304
  'react-markdown',
305
305
  'remark-gfm',
@@ -662,7 +662,7 @@ export const SURFACES = {
662
662
  kind: 'tooling',
663
663
  layer: 'non-js-asset',
664
664
  description:
665
- 'Raw toolchain presets for consumer extends: oxlint.json, oxfmt.json, tsconfig.*.json, lefthook.yml, check.yml',
665
+ 'Raw toolchain presets for consumer extends: oxlint.json (or the guards-only oxlint-basalt.json), oxfmt.json, tsconfig.*.json, lefthook.yml, check.yml',
666
666
  forbiddenImports: [],
667
667
  },
668
668
  './llms.txt': {
@@ -22,6 +22,8 @@ import {
22
22
  import type { PaletteData, RadiusValues, SpaceValues } from './palette'
23
23
  import { SIZE_CLASSES, sizeClassMaxEm } from './size-classes'
24
24
  import { HIT_COARSE, HIT_FINE } from '../common/hit-floor'
25
+ import { BASALT_VX_FAMILIES } from '../common/vx-families'
26
+ import { vxFamilyOf } from '../common/vx-family'
25
27
 
26
28
  // The raw hue families + pair-picker — the building blocks a consumer's series module composes
27
29
  // (`hrv: p(BP.blue)`). The doctrine sends every consumer here, so they are public surface, not
@@ -57,6 +59,7 @@ export type { SpaceValues } from './palette'
57
59
  export {
58
60
  CONTAINER_CLASSES,
59
61
  CONTAINER_GRID_BREAKPOINTS,
62
+ CONTAINER_KEYS,
60
63
  resolveContainerClass,
61
64
  SIZE_CLASSES,
62
65
  } from './size-classes'
@@ -805,6 +808,26 @@ function frameworkDerived(data: PaletteData, only: 'core' | 'all', legacyAliases
805
808
  return legacyAliases ? `${canonical}\n${frameworkDerivedAliases(data)}` : canonical
806
809
  }
807
810
 
811
+ const warnedVxNames = new Set<string>()
812
+
813
+ /**
814
+ * Warn, once per name, on a consumer series/group name inside a basalt-owned `--vx-*` family
815
+ * (`surface`, `space`, `ink`, …): `check-theme`'s `unknown-vx-token` judges every name there against
816
+ * what basalt emits, which is sound only while no consumer REF points there. A WARNING for the
817
+ * minor that kind is graced in (1.34.x), dev builds only; `grace.test.ts` pins the message's
818
+ * version to the ledger's `promote` and requires a throw once the kind promotes.
819
+ * The families are the guard's own generated `BASALT_VX_FAMILIES`. Checked on the refs, not in
820
+ * `buildPaletteCss`, which runs on every first paint.
821
+ */
822
+ function warnConsumerVxName(name: string): void {
823
+ const family = vxFamilyOf(name)
824
+ if (!BASALT_VX_FAMILIES.includes(family) || warnedVxNames.has(name)) return
825
+ warnedVxNames.add(name)
826
+ console.warn(
827
+ `[basalt] ${name} is inside basalt's own --vx-${family} family — name your series group something basalt does not own (groupTokens('app', …)). Throws from 1.35.0.`,
828
+ )
829
+ }
830
+
808
831
  /**
809
832
  * Wrap a map of `{ name: ColorPair }` into `var(--vx-<prefix><name>)` token refs — the runtime
810
833
  * companion to `buildPaletteCss`. Lets a consumer define its own series and read stable refs.
@@ -817,7 +840,11 @@ export function seriesTokens<const T extends SeriesMap>(
817
840
  prefix = '',
818
841
  ): { [K in keyof T]: string } {
819
842
  const out = {} as { [K in keyof T]: string }
820
- for (const key of Object.keys(map) as (keyof T)[]) out[key] = `var(--vx-${prefix}${String(key)})`
843
+ for (const key of Object.keys(map) as (keyof T)[]) {
844
+ // Literal read at the branch, so a production bundle drops the check (common/dev-gate.test.ts).
845
+ if (process.env.NODE_ENV !== 'production') warnConsumerVxName(`--vx-${prefix}${String(key)}`)
846
+ out[key] = `var(--vx-${prefix}${String(key)})`
847
+ }
821
848
  return out
822
849
  }
823
850
 
@@ -14,6 +14,19 @@ export const CONTAINER_CLASSES = { micro: 0, compact: 240, regular: 480, wide: 8
14
14
 
15
15
  export type ContainerClass = keyof typeof CONTAINER_CLASSES
16
16
 
17
+ /**
18
+ * `CONTAINER_CLASSES` as the CSS lengths `<SimpleGrid type="container">` keys take — Mantine writes
19
+ * each key verbatim into `@container simple-grid (min-width: <key>)`, so a theme name (`sm`) is a
20
+ * dead query and the key has to be a px length: `cols={{ base: 1, [CONTAINER_KEYS.regular]: 2 }}`.
21
+ * `micro` has no key — `base` is that class. `basalt/raw-breakpoint` judges theme names only, so a
22
+ * computed key is green.
23
+ */
24
+ export const CONTAINER_KEYS = {
25
+ compact: `${CONTAINER_CLASSES.compact}px`,
26
+ regular: `${CONTAINER_CLASSES.regular}px`,
27
+ wide: `${CONTAINER_CLASSES.wide}px`,
28
+ } as const
29
+
17
30
  /**
18
31
  * `CONTAINER_CLASSES` as the five-key map `<Grid type="container" breakpoints={…}>` takes (Mantine
19
32
  * types it `Record<MantineSize, string>`), so `Grid.Col` keys resolve onto the grid's own width:
@@ -21,11 +34,11 @@ export type ContainerClass = keyof typeof CONTAINER_CLASSES
21
34
  * by name (from `basalt-ui/tokens`) — any other imported map is unreadable to it and flagged.
22
35
  */
23
36
  export const CONTAINER_GRID_BREAKPOINTS = {
24
- xs: `${CONTAINER_CLASSES.compact}px`,
25
- sm: `${CONTAINER_CLASSES.regular}px`,
26
- md: `${CONTAINER_CLASSES.wide}px`,
27
- lg: `${CONTAINER_CLASSES.wide}px`,
28
- xl: `${CONTAINER_CLASSES.wide}px`,
37
+ xs: CONTAINER_KEYS.compact,
38
+ sm: CONTAINER_KEYS.regular,
39
+ md: CONTAINER_KEYS.wide,
40
+ lg: CONTAINER_KEYS.wide,
41
+ xl: CONTAINER_KEYS.wide,
29
42
  } as const
30
43
 
31
44
  /** The class a MEASURED inline size falls in (`px` is a min-width boundary, so 240 is `compact`). */
@@ -1,10 +0,0 @@
1
- export type BreakpointName = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
2
- /**
3
- * @deprecated Use `useSizeClass` — removed in 1.33.0 (see `MIGRATING.md`). `theme.breakpoints` is
4
- * now derived: `sm` moved 48em → 52.5em and `md`/`lg`/`xl` all sit at 75em (`xs`/`sm` collapse,
5
- * `md`/`lg`/`xl` collapse).
6
- */
7
- export declare function useBreakpoint(name: BreakpointName, edge?: 'min' | 'max', options?: {
8
- fallback?: boolean;
9
- }): boolean;
10
- //# sourceMappingURL=use-breakpoint.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-breakpoint.d.ts","sourceRoot":"","sources":["../../src/shell/use-breakpoint.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAE7D;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,cAAc,EACpB,IAAI,GAAE,KAAK,GAAG,KAAa,EAC3B,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAC/B,OAAO,CAKT"}
@@ -1,11 +0,0 @@
1
- import { useMantineTheme } from "@mantine/core";
2
- import { useMediaQuery } from "../common/use-media-query.js";
3
- function useBreakpoint(name, edge = "min", options) {
4
- const theme = useMantineTheme();
5
- const bp = theme.breakpoints[name];
6
- const fallback = options?.fallback ?? edge === "max";
7
- return useMediaQuery(`(${edge}-width: ${bp})`, fallback);
8
- }
9
- export {
10
- useBreakpoint
11
- };
@@ -1,37 +0,0 @@
1
- /**
2
- * `useBreakpoint` — DEPRECATED (on notice in `MIGRATING.md`, removed in 1.33.0): use
3
- * `useSizeClass` from `./use-size-class`. One media-query hook over the theme's own breakpoints,
4
- * built on the shared `useMediaQuery` (`common/use-media-query`) — SSR/hydration-safe.
5
- *
6
- * `edge` defaults to `'min'` (`"at least this wide"`); pass `'max'` for `"narrower than"`. The
7
- * `fallback` (what the server, and a shim with no `matchMedia`, sees) defaults to the answer that
8
- * assumes the NARROW side of the query — `false` for `min`, `true` for `max` — since a component
9
- * library cannot know a consumer's real device mix; `options.fallback` overrides it.
10
- *
11
- * @example
12
- * import { useBreakpoint } from 'basalt-ui'
13
- *
14
- * const isDesktop = useBreakpoint('sm') // >= theme.breakpoints.sm
15
- * const isNarrow = !useBreakpoint('sm') // < theme.breakpoints.sm
16
- * const isWide = useBreakpoint('lg') // >= theme.breakpoints.lg
17
- */
18
- import { useMantineTheme } from '@mantine/core'
19
- import { useMediaQuery } from '../common/use-media-query'
20
-
21
- export type BreakpointName = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
22
-
23
- /**
24
- * @deprecated Use `useSizeClass` — removed in 1.33.0 (see `MIGRATING.md`). `theme.breakpoints` is
25
- * now derived: `sm` moved 48em → 52.5em and `md`/`lg`/`xl` all sit at 75em (`xs`/`sm` collapse,
26
- * `md`/`lg`/`xl` collapse).
27
- */
28
- export function useBreakpoint(
29
- name: BreakpointName,
30
- edge: 'min' | 'max' = 'min',
31
- options?: { fallback?: boolean },
32
- ): boolean {
33
- const theme = useMantineTheme()
34
- const bp = theme.breakpoints[name]
35
- const fallback = options?.fallback ?? edge === 'max'
36
- return useMediaQuery(`(${edge}-width: ${bp})`, fallback)
37
- }