basalt-ui 1.32.1 → 1.34.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/AGENTS.md +1 -1
- package/CHANGELOG.md +65 -0
- package/MIGRATING.md +218 -25
- package/README.md +3 -0
- package/agent/rules/basalt-state.md +2 -2
- package/agent/rules/basalt-tokens.md +1 -1
- package/agent/skills/basalt-app/SKILL.md +5 -3
- package/configs/oxlint-basalt.json +33 -0
- package/configs/oxlint-plugin.js +89 -67
- package/configs/oxlint-plugin.test.ts +224 -19
- package/configs/oxlint.json +2 -30
- package/dist/agent/coalesce.d.ts.map +1 -1
- package/dist/agent/coalesce.js +1 -2
- package/dist/agent/merge.d.ts.map +1 -1
- package/dist/agent/merge.js +1 -2
- package/dist/agent-chat/relative-time.d.ts +9 -0
- package/dist/agent-chat/relative-time.d.ts.map +1 -1
- package/dist/agent-chat/relative-time.js +1 -2
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +1 -2
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +3 -3
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +1 -2
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +1 -2
- package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -1
- package/dist/cli/audit-allows.d.ts +1 -1
- package/dist/cli/audit-allows.d.ts.map +1 -1
- package/dist/cli/audit-allows.js +4 -4
- package/dist/cli/check-theme.d.ts.map +1 -1
- package/dist/cli/check-theme.js +38 -38
- package/dist/cli/config.d.ts +197 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +4 -6
- package/dist/cli/guard-hook.d.ts.map +1 -1
- package/dist/cli/guard-hook.js +7 -41
- package/dist/cli/index.d.ts +31 -257
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +64 -37
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +7 -16
- package/dist/commands/define-commands.d.ts.map +1 -1
- package/dist/commands/define-commands.js +2 -2
- package/dist/commands/define-overlays.d.ts.map +1 -1
- package/dist/commands/define-overlays.js +1 -2
- package/dist/common/index.d.ts +0 -1
- package/dist/common/index.d.ts.map +1 -1
- package/dist/common/index.js +0 -2
- package/dist/common/validate.d.ts +2 -2
- package/dist/common/validate.d.ts.map +1 -1
- package/dist/common/validate.js +2 -3
- package/dist/common/vx-families.d.ts +7 -0
- package/dist/common/vx-families.d.ts.map +1 -0
- package/dist/common/vx-families.js +32 -0
- package/dist/common/vx-family.d.ts +11 -0
- package/dist/common/vx-family.d.ts.map +1 -0
- package/dist/common/vx-family.js +12 -0
- package/dist/content/article-card.d.ts.map +1 -1
- package/dist/content/article-card.js +4 -1
- package/dist/controls/enum-filter.d.ts.map +1 -1
- package/dist/controls/enum-filter.js +3 -13
- package/dist/controls/filter-pill.d.ts +8 -0
- package/dist/controls/filter-pill.d.ts.map +1 -1
- package/dist/controls/filter-pill.js +15 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -1
- package/dist/controls/multi-select-filter.js +3 -13
- package/dist/controls/number-filter.d.ts +3 -0
- package/dist/controls/number-filter.d.ts.map +1 -1
- package/dist/controls/number-filter.js +12 -4
- package/dist/controls/range-filter.d.ts.map +1 -1
- package/dist/controls/range-filter.js +1 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +2 -3
- package/dist/guard/index.d.ts +17 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +62 -23
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/guard/vx-names.d.ts +3 -0
- package/dist/guard/vx-names.d.ts.map +1 -0
- package/dist/guard/vx-names.js +257 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/notifications/mount-guard.d.ts.map +1 -1
- package/dist/notifications/mount-guard.js +1 -2
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +2 -3
- package/dist/provider/lab-theme.d.ts +5 -4
- package/dist/provider/lab-theme.d.ts.map +1 -1
- package/dist/provider/lab-theme.js +1 -3
- package/dist/router-tanstack/search-store.d.ts +18 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-store.js +33 -1
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +2 -1
- package/dist/shell/app-mobile-nav.module.css +12 -0
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +32 -36
- package/dist/shell/app-sidebar.module.css +6 -0
- package/dist/shell/index.d.ts +0 -1
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +0 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +3 -4
- package/dist/shell/nav-glyph.d.ts +14 -0
- package/dist/shell/nav-glyph.d.ts.map +1 -0
- package/dist/shell/nav-glyph.js +21 -0
- package/dist/shell/nav-types.d.ts +1 -0
- package/dist/shell/nav-types.d.ts.map +1 -1
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +1 -2
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +1 -3
- package/dist/surfaces.d.ts +3 -3
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +4 -2
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +24 -2
- package/dist/tokens/size-classes.d.ts +25 -0
- package/dist/tokens/size-classes.d.ts.map +1 -1
- package/dist/tokens/size-classes.js +14 -0
- package/llms.txt +4 -4
- package/package.json +1 -1
- package/src/agent/coalesce.ts +1 -2
- package/src/agent/merge.ts +1 -2
- package/src/agent-chat/relative-time.ts +1 -2
- package/src/agent-chat/thread-message.tsx +2 -3
- package/src/charts/kinds/BandStrip.tsx +7 -3
- package/src/charts/kinds/Bars.tsx +1 -2
- package/src/charts/kinds/StackedArea.tsx +4 -2
- package/src/charts/primitives/legend-disclosure.tsx +1 -0
- package/src/cli/audit-allows.ts +10 -9
- package/src/cli/check-theme.ts +61 -39
- package/src/cli/config.ts +209 -0
- package/src/cli/doctor.ts +5 -7
- package/src/cli/guard-hook.ts +9 -42
- package/src/cli/index.ts +101 -294
- package/src/cli/sync.ts +7 -16
- package/src/commands/define-commands.ts +2 -2
- package/src/commands/define-overlays.ts +1 -2
- package/src/common/index.ts +0 -1
- package/src/common/validate.ts +4 -5
- package/src/common/vx-families.ts +36 -0
- package/src/common/vx-family.ts +22 -0
- package/src/content/article-card.tsx +1 -0
- package/src/controls/enum-filter.tsx +3 -13
- package/src/controls/filter-pill.tsx +20 -0
- package/src/controls/multi-select-filter.tsx +3 -13
- package/src/controls/number-filter.tsx +24 -3
- package/src/controls/range-filter.tsx +1 -2
- package/src/data/data-table.tsx +2 -3
- package/src/guard/index.ts +134 -47
- package/src/guard/types.ts +3 -2
- package/src/guard/vx-names.ts +257 -0
- package/src/index.ts +0 -2
- package/src/notifications/mount-guard.ts +1 -2
- package/src/provider/index.tsx +2 -3
- package/src/provider/lab-theme.ts +28 -21
- package/src/router-tanstack/search-store.ts +73 -1
- package/src/shell/app-mobile-nav.module.css +12 -0
- package/src/shell/app-mobile-nav.tsx +8 -1
- package/src/shell/app-sidebar.module.css +6 -0
- package/src/shell/app-sidebar.tsx +52 -50
- package/src/shell/index.tsx +0 -1
- package/src/shell/mobile-nav-model.ts +3 -5
- package/src/shell/nav-glyph.tsx +38 -0
- package/src/shell/nav-types.ts +1 -0
- package/src/shell/page-bar.tsx +1 -4
- package/src/shell/sidebar-search.tsx +1 -5
- package/src/surfaces.ts +4 -2
- package/src/tokens/index.ts +34 -2
- package/src/tokens/size-classes.ts +27 -0
- package/dist/common/is-dev.d.ts +0 -3
- package/dist/common/is-dev.d.ts.map +0 -1
- package/dist/common/is-dev.js +0 -6
- package/dist/shell/use-breakpoint.d.ts +0 -10
- package/dist/shell/use-breakpoint.d.ts.map +0 -1
- package/dist/shell/use-breakpoint.js +0 -11
- package/src/common/is-dev.ts +0 -4
- package/src/shell/use-breakpoint.ts +0 -37
|
@@ -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({
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
402
|
-
|
|
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
|
|
package/src/shell/index.tsx
CHANGED
|
@@ -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
|
|
|
@@ -87,8 +87,6 @@ export function blockRowCount(blocks: readonly SidebarBlock[] | undefined): numb
|
|
|
87
87
|
}).length
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
const DEV = process.env['NODE_ENV'] !== 'production'
|
|
91
|
-
|
|
92
90
|
/**
|
|
93
91
|
* §2.3 rule 1. A section's `mobile: false` wins over every item inside it; otherwise the item's own
|
|
94
92
|
* field decides, with the shorthand booleans normalized and `undefined` meaning "reachable, but not
|
|
@@ -351,7 +349,7 @@ function buildCandidates(
|
|
|
351
349
|
/** Rule 11 — `mobile: 'tab'` on a disabled destination is a configuration mistake, not a slot. */
|
|
352
350
|
const tabbable = (item: SidebarItem): boolean => {
|
|
353
351
|
if (!item.disabled) return true
|
|
354
|
-
if (
|
|
352
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
355
353
|
console.warn(
|
|
356
354
|
`[basalt] MobileNav: "${item.key}" asks for a bar slot but is disabled — a disabled destination never becomes a tab. It still renders, disabled, in the More surface.`,
|
|
357
355
|
)
|
|
@@ -373,7 +371,7 @@ function buildCandidates(
|
|
|
373
371
|
candidates.push(sectionCandidate(section, menuMax))
|
|
374
372
|
continue
|
|
375
373
|
}
|
|
376
|
-
if (
|
|
374
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
377
375
|
console.warn(
|
|
378
376
|
`[basalt] MobileNav: mobileNav.tabs names "${id}", which is neither a destination key nor a visible section label — ignoring it.`,
|
|
379
377
|
)
|
|
@@ -464,7 +462,7 @@ export function projectMobileNav(
|
|
|
464
462
|
const limit = needsMore ? maxTabs - 1 : maxTabs
|
|
465
463
|
const kept = candidates.slice(0, limit)
|
|
466
464
|
const dropped = candidates.slice(limit)
|
|
467
|
-
if (
|
|
465
|
+
if (process.env.NODE_ENV !== 'production' && dropped.length > 0) {
|
|
468
466
|
// Rule 13 wants the NAMES, not a tally: "2 candidates dropped" tells a consumer their bar is
|
|
469
467
|
// wrong, while the keys tell them which `mobile: 'tab'` to move or which section to reorder.
|
|
470
468
|
console.warn(
|
|
@@ -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
|
+
}
|
package/src/shell/nav-types.ts
CHANGED
|
@@ -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
|
package/src/shell/page-bar.tsx
CHANGED
|
@@ -46,9 +46,6 @@ import asideClasses from './page-aside.module.css'
|
|
|
46
46
|
/** The custom property row 2's measured height is published on. */
|
|
47
47
|
export const PAGE_BAR_HEIGHT_VAR = '--basalt-page-bar-h'
|
|
48
48
|
|
|
49
|
-
/** Dev-only diagnostics, constant-folded out of a production bundle — see {@link useAsidePanelSlot}. */
|
|
50
|
-
const DEV = process.env['NODE_ENV'] !== 'production'
|
|
51
|
-
|
|
52
49
|
/**
|
|
53
50
|
* What a `PageAside` publishes when it projects itself into row 2 below `sm`
|
|
54
51
|
* (`docs/ASIDE-SPEC.md` §0 "Desktop and mobile are one declaration"). Metadata only — the aside's
|
|
@@ -236,7 +233,7 @@ export function useAsidePanelSlot(): {
|
|
|
236
233
|
setSettled(true)
|
|
237
234
|
}, [])
|
|
238
235
|
useEffect(() => {
|
|
239
|
-
if (
|
|
236
|
+
if (process.env.NODE_ENV === 'production' || !settled || !inShell || panelHost) return
|
|
240
237
|
console.warn(
|
|
241
238
|
"[basalt] PageAside: this page's PageBar renders no row 2 (no `tabs`, no `filters`, no `filtersEnd`), so below `sm` there is no pill to open the aside — it renders in flow at the bottom of the page instead. Give the bar a row 2, or expect the in-flow form on a phone.",
|
|
242
239
|
)
|
|
@@ -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
|
|
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',
|
|
@@ -355,6 +355,8 @@ export const SURFACES = {
|
|
|
355
355
|
'inline-font-size',
|
|
356
356
|
'css-raw-surface',
|
|
357
357
|
'surface-shadow-override',
|
|
358
|
+
// A `var(--vx-*)` naming a token basalt no longer (or never) emits.
|
|
359
|
+
'unknown-vx-token',
|
|
358
360
|
// Not a token kind — the kind that judges the ANNOTATION rather than the code. It lands here
|
|
359
361
|
// because `agent/rules/basalt-tokens.md` is where the `theme-allow` grammar is documented
|
|
360
362
|
// (said once, on purpose), so this is the header that must name its enforcement.
|
|
@@ -660,7 +662,7 @@ export const SURFACES = {
|
|
|
660
662
|
kind: 'tooling',
|
|
661
663
|
layer: 'non-js-asset',
|
|
662
664
|
description:
|
|
663
|
-
'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',
|
|
664
666
|
forbiddenImports: [],
|
|
665
667
|
},
|
|
666
668
|
'./llms.txt': {
|
package/src/tokens/index.ts
CHANGED
|
@@ -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
|
|
@@ -54,7 +56,13 @@ export type { SpaceValues } from './palette'
|
|
|
54
56
|
|
|
55
57
|
// The two responsive tables — MIGRATING recipes and the raw-breakpoint guard messages send
|
|
56
58
|
// consumers here for them; `migrating-imports.test.ts` pins that every documented import resolves.
|
|
57
|
-
export {
|
|
59
|
+
export {
|
|
60
|
+
CONTAINER_CLASSES,
|
|
61
|
+
CONTAINER_GRID_BREAKPOINTS,
|
|
62
|
+
CONTAINER_KEYS,
|
|
63
|
+
resolveContainerClass,
|
|
64
|
+
SIZE_CLASSES,
|
|
65
|
+
} from './size-classes'
|
|
58
66
|
export type { ContainerClass } from './size-classes'
|
|
59
67
|
|
|
60
68
|
/** A per-theme color pair: a hue keeps its identity but shifts shade across schemes. */
|
|
@@ -800,6 +808,26 @@ function frameworkDerived(data: PaletteData, only: 'core' | 'all', legacyAliases
|
|
|
800
808
|
return legacyAliases ? `${canonical}\n${frameworkDerivedAliases(data)}` : canonical
|
|
801
809
|
}
|
|
802
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
|
+
|
|
803
831
|
/**
|
|
804
832
|
* Wrap a map of `{ name: ColorPair }` into `var(--vx-<prefix><name>)` token refs — the runtime
|
|
805
833
|
* companion to `buildPaletteCss`. Lets a consumer define its own series and read stable refs.
|
|
@@ -812,7 +840,11 @@ export function seriesTokens<const T extends SeriesMap>(
|
|
|
812
840
|
prefix = '',
|
|
813
841
|
): { [K in keyof T]: string } {
|
|
814
842
|
const out = {} as { [K in keyof T]: string }
|
|
815
|
-
for (const key of Object.keys(map) as (keyof T)[])
|
|
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
|
+
}
|
|
816
848
|
return out
|
|
817
849
|
}
|
|
818
850
|
|
|
@@ -14,6 +14,33 @@ 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
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `CONTAINER_CLASSES` as the five-key map `<Grid type="container" breakpoints={…}>` takes (Mantine
|
|
32
|
+
* types it `Record<MantineSize, string>`), so `Grid.Col` keys resolve onto the grid's own width:
|
|
33
|
+
* `xs` opens compact, `sm` regular, `md`/`lg`/`xl` wide. `basalt/raw-breakpoint` trusts this import
|
|
34
|
+
* by name (from `basalt-ui/tokens`) — any other imported map is unreadable to it and flagged.
|
|
35
|
+
*/
|
|
36
|
+
export const CONTAINER_GRID_BREAKPOINTS = {
|
|
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,
|
|
42
|
+
} as const
|
|
43
|
+
|
|
17
44
|
/** The class a MEASURED inline size falls in (`px` is a min-width boundary, so 240 is `compact`). */
|
|
18
45
|
export function resolveContainerClass(px: number): ContainerClass {
|
|
19
46
|
if (px >= CONTAINER_CLASSES.wide) return 'wide'
|
package/dist/common/is-dev.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"is-dev.d.ts","sourceRoot":"","sources":["../../src/common/is-dev.ts"],"names":[],"mappings":"AAAA,kHAAkH;AAClH,wBAAgB,KAAK,IAAI,OAAO,CAE/B"}
|
package/dist/common/is-dev.js
DELETED
|
@@ -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
|
-
};
|
package/src/common/is-dev.ts
DELETED
|
@@ -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
|
-
}
|