basalt-ui 1.34.0 → 1.35.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 +22 -0
- package/MIGRATING.md +24 -12
- package/dist/cli/check-theme.d.ts.map +1 -1
- package/dist/cli/check-theme.js +4 -1
- package/dist/cli/doctor.d.ts +5 -10
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +4 -0
- package/dist/cli/index.d.ts +18 -24
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +10 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +16 -6
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +19 -10
- package/dist/shell/app-sidebar-account.d.ts.map +1 -1
- package/dist/shell/app-sidebar-account.js +1 -0
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +11 -6
- package/dist/shell/app-sidebar.module.css +39 -0
- package/dist/shell/app-sidebar.module.css.d.ts +3 -0
- package/dist/shell/index.d.ts +12 -5
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/nav-types.d.ts +19 -3
- package/dist/shell/nav-types.d.ts.map +1 -1
- package/dist/shell/sidebar-block-model.d.ts +3 -3
- package/dist/shell/sidebar-block-model.d.ts.map +1 -1
- package/dist/shell/sidebar-block-model.js +6 -2
- package/dist/shell/sidebar-blocks.d.ts +2 -1
- package/dist/shell/sidebar-blocks.d.ts.map +1 -1
- package/dist/shell/sidebar-blocks.js +13 -5
- package/dist/surfaces.js +2 -2
- package/dist/tokens/index.js +1 -1
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/cli/check-theme.ts +4 -1
- package/src/cli/doctor.ts +9 -10
- package/src/cli/index.ts +26 -24
- package/src/guard/index.ts +34 -18
- package/src/shell/app-mobile-nav.tsx +34 -15
- package/src/shell/app-sidebar-account.tsx +3 -0
- package/src/shell/app-sidebar.module.css +39 -0
- package/src/shell/app-sidebar.module.css.d.ts +3 -0
- package/src/shell/app-sidebar.tsx +13 -6
- package/src/shell/index.tsx +12 -5
- package/src/shell/nav-types.ts +19 -3
- package/src/shell/sidebar-block-model.ts +13 -5
- package/src/shell/sidebar-blocks.tsx +30 -5
- package/src/surfaces.ts +2 -2
- package/src/tokens/index.ts +1 -1
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
SidebarBlock,
|
|
34
34
|
SidebarBlockItem,
|
|
35
35
|
SidebarItem,
|
|
36
|
+
SidebarCustomBlock,
|
|
36
37
|
SidebarListBlock,
|
|
37
38
|
SidebarProgressBlock,
|
|
38
39
|
} from './nav-types'
|
|
@@ -127,6 +128,14 @@ type ActionRow = {
|
|
|
127
128
|
onClick?: ((e: MouseEvent<HTMLElement>) => void) | undefined
|
|
128
129
|
/** Mirrors `SettingsMenuItem.active` — a trailing check + `aria-current` in every row form. */
|
|
129
130
|
active?: boolean
|
|
131
|
+
/** Mirrors `SettingsMenuItem.current` — `'page'` swaps the check for the nav active style. */
|
|
132
|
+
current?: 'selection' | 'page'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `aria-current` for an action row — `"page"` for a destination, `"true"` for a chosen value. */
|
|
136
|
+
function rowCurrent(row: ActionRow): 'page' | 'true' | undefined {
|
|
137
|
+
if (!row.active) return undefined
|
|
138
|
+
return row.current === 'page' ? 'page' : 'true'
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
/**
|
|
@@ -198,6 +207,7 @@ function settingsRows(items: SettingsMenuItem[] | undefined): ActionRow[] {
|
|
|
198
207
|
icon: item.icon,
|
|
199
208
|
onClick: item.onClick,
|
|
200
209
|
...(item.active !== undefined && { active: item.active }),
|
|
210
|
+
...(item.current !== undefined && { current: item.current }),
|
|
201
211
|
}))
|
|
202
212
|
}
|
|
203
213
|
|
|
@@ -248,10 +258,10 @@ const menuActionRow = (row: ActionRow, sectionStart = false) => (
|
|
|
248
258
|
key={row.key}
|
|
249
259
|
className={sectionStart ? `${classes.menuItem} ${classes.menuSectionStart}` : classes.menuItem}
|
|
250
260
|
leftSection={row.icon}
|
|
251
|
-
rightSection={row.active ? <IconCheck /> : undefined}
|
|
261
|
+
rightSection={row.active && row.current !== 'page' ? <IconCheck /> : undefined}
|
|
252
262
|
{...(row.danger ? { color: 'red' } : {})}
|
|
253
263
|
onClick={(event: MouseEvent<HTMLElement>) => row.onClick?.(event)}
|
|
254
|
-
aria-current={row
|
|
264
|
+
aria-current={rowCurrent(row)}
|
|
255
265
|
>
|
|
256
266
|
{row.label}
|
|
257
267
|
</Menu.Item>
|
|
@@ -353,12 +363,13 @@ export function MobileNav({
|
|
|
353
363
|
* applies — the count that picked `menu` vs `sheet` and the rows that render must agree.
|
|
354
364
|
*/
|
|
355
365
|
const moreBlocks = (blocks ?? []).filter(
|
|
356
|
-
(block): block is SidebarListBlock | SidebarProgressBlock =>
|
|
357
|
-
block.kind !== '
|
|
358
|
-
|
|
359
|
-
|
|
366
|
+
(block): block is SidebarListBlock | SidebarProgressBlock | SidebarCustomBlock =>
|
|
367
|
+
sidebarBlockMobile(block) === 'more' && (block.kind !== 'list' || block.items.length > 0),
|
|
368
|
+
)
|
|
369
|
+
// The blocks that open a nested sheet — a list's rows, or a custom block's node.
|
|
370
|
+
const sheetBlocks = moreBlocks.filter(
|
|
371
|
+
(block): block is SidebarListBlock | SidebarCustomBlock => block.kind !== 'progress',
|
|
360
372
|
)
|
|
361
|
-
const listBlocks = moreBlocks.filter((block): block is SidebarListBlock => block.kind === 'list')
|
|
362
373
|
|
|
363
374
|
/**
|
|
364
375
|
* Whether the More surface's UNLABELLED tail (blocks + account + settings) opens its own band —
|
|
@@ -369,10 +380,10 @@ export function MobileNav({
|
|
|
369
380
|
const tailRule = (slot: SurfaceSlot): boolean =>
|
|
370
381
|
moreBlocks.length + extraRows.length > 0 && slotHasRows(slot)
|
|
371
382
|
|
|
372
|
-
/** A list block raises its nested sheet; a progress block just fires its own handler. */
|
|
383
|
+
/** A list or custom block raises its nested sheet; a progress block just fires its own handler. */
|
|
373
384
|
const activateBlock = (block: (typeof moreBlocks)[number]) => {
|
|
374
385
|
close()
|
|
375
|
-
if (block.kind
|
|
386
|
+
if (block.kind !== 'progress') {
|
|
376
387
|
// ONE FRAME, deliberately. `close()` starts the Menu's `returnFocus` (it restores focus to
|
|
377
388
|
// the More tab) while the nested Drawer traps focus on mount — in the same handler the two
|
|
378
389
|
// race, and focus lands wherever the loser wrote last, which is how a tap on a block row
|
|
@@ -614,13 +625,14 @@ export function MobileNav({
|
|
|
614
625
|
classNames={{ root: classes.row }}
|
|
615
626
|
label={row.label}
|
|
616
627
|
leftSection={row.icon}
|
|
617
|
-
rightSection={row.active ? <IconCheck /> : undefined}
|
|
628
|
+
rightSection={row.active && row.current !== 'page' ? <IconCheck /> : undefined}
|
|
618
629
|
{...(row.danger ? { color: 'red' } : {})}
|
|
619
630
|
onClick={(event: MouseEvent<HTMLElement>) => {
|
|
620
631
|
row.onClick?.(event)
|
|
621
632
|
close()
|
|
622
633
|
}}
|
|
623
|
-
|
|
634
|
+
active={row.active === true && row.current === 'page'}
|
|
635
|
+
aria-current={rowCurrent(row)}
|
|
624
636
|
/>
|
|
625
637
|
)
|
|
626
638
|
|
|
@@ -773,7 +785,7 @@ export function MobileNav({
|
|
|
773
785
|
* exit transition would play against a blank sheet; a closed Mantine Drawer renders nothing, so N
|
|
774
786
|
* instances cost N nothings.
|
|
775
787
|
*/
|
|
776
|
-
const blockSheet = (block: SidebarListBlock) => (
|
|
788
|
+
const blockSheet = (block: SidebarListBlock | SidebarCustomBlock) => (
|
|
777
789
|
<Drawer
|
|
778
790
|
key={`block-${block.key}`}
|
|
779
791
|
opened={openBlockKey === block.key}
|
|
@@ -799,7 +811,11 @@ export function MobileNav({
|
|
|
799
811
|
}}
|
|
800
812
|
>
|
|
801
813
|
<ScrollArea.Autosize mah="62dvh" type="scroll" offsetScrollbars="present" scrollbarSize={8}>
|
|
802
|
-
|
|
814
|
+
{block.kind === 'list' ? (
|
|
815
|
+
<Stack gap={1}>{block.items.map(blockItemRow)}</Stack>
|
|
816
|
+
) : (
|
|
817
|
+
block.node
|
|
818
|
+
)}
|
|
803
819
|
</ScrollArea.Autosize>
|
|
804
820
|
</Drawer>
|
|
805
821
|
)
|
|
@@ -815,13 +831,16 @@ export function MobileNav({
|
|
|
815
831
|
if (slot.kind === 'menu') return menuSlot(slot)
|
|
816
832
|
return sheetSlot(slot)
|
|
817
833
|
})}
|
|
818
|
-
{
|
|
834
|
+
{sheetBlocks.map(blockSheet)}
|
|
819
835
|
</nav>
|
|
820
836
|
)
|
|
821
837
|
}
|
|
822
838
|
|
|
823
839
|
/** `Awaiting action · 3` — the row's whole job is to say how much is behind it. */
|
|
824
|
-
function blockRowLabel(
|
|
840
|
+
function blockRowLabel(
|
|
841
|
+
block: SidebarListBlock | SidebarProgressBlock | SidebarCustomBlock,
|
|
842
|
+
): string {
|
|
843
|
+
if (block.kind === 'custom') return block.label ?? ''
|
|
825
844
|
return block.kind === 'list'
|
|
826
845
|
? `${block.label} · ${block.count ?? block.items.length}`
|
|
827
846
|
: `${block.label} · ${block.value} of ${block.total}`
|
|
@@ -101,6 +101,9 @@ export function SidebarAccount(props: BasaltAccountProps & BasaltProps) {
|
|
|
101
101
|
return (
|
|
102
102
|
<UnstyledButton
|
|
103
103
|
className={cx(classes.accountRow, className)}
|
|
104
|
+
// The collapsed rail hides `.accountText` (display:none), which drops "Sign in" from the
|
|
105
|
+
// accessibility tree — the label keeps the button nameable with only the glyph showing.
|
|
106
|
+
aria-label="Sign in"
|
|
104
107
|
onClick={actions?.onSignIn}
|
|
105
108
|
{...(style !== undefined && { style })}
|
|
106
109
|
>
|
|
@@ -249,6 +249,21 @@
|
|
|
249
249
|
color: var(--vx-ink);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/* A settings entry that IS the current page (`SettingsMenuItem.current: 'page'`) — the nav row's
|
|
253
|
+
* active treatment (theme/nav-link.module.css): accent fill, weight 600, accent icon. */
|
|
254
|
+
.footerBtn[aria-current='page'],
|
|
255
|
+
.settingsMenuItem[aria-current='page'] {
|
|
256
|
+
background-color: color-mix(in srgb, var(--vx-accent) 12%, transparent);
|
|
257
|
+
color: var(--vx-ink);
|
|
258
|
+
font-weight: 600;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.footerBtn[aria-current='page'] > svg,
|
|
262
|
+
.footerBtn[aria-current='page'] .footerIconSlot > svg,
|
|
263
|
+
.settingsMenuItem[aria-current='page'] [data-position='left'] {
|
|
264
|
+
color: var(--vx-accent);
|
|
265
|
+
}
|
|
266
|
+
|
|
252
267
|
.footerText {
|
|
253
268
|
flex: 1;
|
|
254
269
|
text-align: left;
|
|
@@ -399,6 +414,25 @@
|
|
|
399
414
|
color: var(--vx-ink);
|
|
400
415
|
}
|
|
401
416
|
|
|
417
|
+
/* A custom block's rail trigger (`rail: 'icon'`) — a footer-row-sized icon target. */
|
|
418
|
+
.blockRailTrigger {
|
|
419
|
+
display: flex;
|
|
420
|
+
align-items: center;
|
|
421
|
+
justify-content: center;
|
|
422
|
+
padding: var(--vx-space-row-inset-y) var(--vx-space-row-inset-x);
|
|
423
|
+
border-radius: var(--vx-radius-ctrl);
|
|
424
|
+
color: var(--vx-faint);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.blockRailTrigger:hover {
|
|
428
|
+
background-color: color-mix(in srgb, var(--vx-ink) 6%, transparent);
|
|
429
|
+
color: var(--vx-ink);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.blockRailPopover {
|
|
433
|
+
min-width: 220px;
|
|
434
|
+
}
|
|
435
|
+
|
|
402
436
|
.blockIcon {
|
|
403
437
|
position: relative;
|
|
404
438
|
display: flex;
|
|
@@ -717,6 +751,11 @@
|
|
|
717
751
|
display: none;
|
|
718
752
|
}
|
|
719
753
|
|
|
754
|
+
.root[data-collapsed] .block[data-rail='icon'] {
|
|
755
|
+
display: flex;
|
|
756
|
+
justify-content: center;
|
|
757
|
+
}
|
|
758
|
+
|
|
720
759
|
.root[data-collapsed] .block[data-rail='dot'] .blockHeader {
|
|
721
760
|
justify-content: center;
|
|
722
761
|
padding: 0;
|
|
@@ -42,5 +42,8 @@ declare const classes: {
|
|
|
42
42
|
readonly footerLinks: string
|
|
43
43
|
readonly footerIconSlot: string
|
|
44
44
|
readonly footerVersion: string
|
|
45
|
+
readonly settingsMenuItem: string
|
|
46
|
+
readonly blockRailTrigger: string
|
|
47
|
+
readonly blockRailPopover: string
|
|
45
48
|
}
|
|
46
49
|
export default classes
|
|
@@ -183,6 +183,12 @@ function hasActiveDescendant(item: SidebarItem): boolean {
|
|
|
183
183
|
return item.children?.some((c) => hasActiveDescendant(c)) ?? false
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/** `aria-current` for a settings entry: `"page"` for a destination, `"true"` for a chosen value. */
|
|
187
|
+
function settingsCurrent(entry: SettingsMenuItem): 'page' | 'true' | undefined {
|
|
188
|
+
if (!entry.active) return undefined
|
|
189
|
+
return entry.current === 'page' ? 'page' : 'true'
|
|
190
|
+
}
|
|
191
|
+
|
|
186
192
|
/**
|
|
187
193
|
* Renders a nav link body without the Tooltip/Box wrapper.
|
|
188
194
|
*
|
|
@@ -488,7 +494,7 @@ export function AppSidebar(props: AppSidebarProps) {
|
|
|
488
494
|
className={classes.footerBtn}
|
|
489
495
|
onClick={entry.onClick}
|
|
490
496
|
aria-label={entry.label}
|
|
491
|
-
aria-current={entry
|
|
497
|
+
aria-current={settingsCurrent(entry)}
|
|
492
498
|
>
|
|
493
499
|
{/* A FIXED slot, so rows align on one icon column whether or not each ships an icon —
|
|
494
500
|
and the gear fallback is functional, not decorative: in the collapsed rail the label
|
|
@@ -501,7 +507,7 @@ export function AppSidebar(props: AppSidebarProps) {
|
|
|
501
507
|
</Text>
|
|
502
508
|
{/* The rail ring rides the FIRST row only — one progress block is one mark. */}
|
|
503
509
|
{index === 0 ? ringMark : null}
|
|
504
|
-
{entry.active ? <IconCheck /> : null}
|
|
510
|
+
{entry.active && entry.current !== 'page' ? <IconCheck /> : null}
|
|
505
511
|
</UnstyledButton>
|
|
506
512
|
))}
|
|
507
513
|
{versionLabel !== undefined && (
|
|
@@ -526,10 +532,11 @@ export function AppSidebar(props: AppSidebarProps) {
|
|
|
526
532
|
{settingsItems.map((entry) => (
|
|
527
533
|
<Menu.Item
|
|
528
534
|
key={entry.key}
|
|
535
|
+
className={classes.settingsMenuItem}
|
|
529
536
|
leftSection={entry.icon}
|
|
530
|
-
rightSection={entry.active ? <IconCheck /> : undefined}
|
|
537
|
+
rightSection={entry.active && entry.current !== 'page' ? <IconCheck /> : undefined}
|
|
531
538
|
onClick={entry.onClick}
|
|
532
|
-
aria-current={entry
|
|
539
|
+
aria-current={settingsCurrent(entry)}
|
|
533
540
|
>
|
|
534
541
|
{entry.label}
|
|
535
542
|
</Menu.Item>
|
|
@@ -576,7 +583,7 @@ export function AppSidebar(props: AppSidebarProps) {
|
|
|
576
583
|
<NavSection key={section.label} section={section} collapsed={collapsed} />
|
|
577
584
|
))}
|
|
578
585
|
{navBlocks.map((block) => (
|
|
579
|
-
<SidebarBlockView key={block.key} block={block} />
|
|
586
|
+
<SidebarBlockView key={block.key} block={block} collapsed={collapsed} />
|
|
580
587
|
))}
|
|
581
588
|
</Stack>
|
|
582
589
|
</ScrollArea>
|
|
@@ -584,7 +591,7 @@ export function AppSidebar(props: AppSidebarProps) {
|
|
|
584
591
|
{(bottomBlocks.length > 0 || settingsRow !== null || account !== undefined) && (
|
|
585
592
|
<Stack gap={0} className={classes.footer}>
|
|
586
593
|
{bottomBlocks.map((block) => (
|
|
587
|
-
<SidebarBlockView key={block.key} block={block} />
|
|
594
|
+
<SidebarBlockView key={block.key} block={block} collapsed={collapsed} />
|
|
588
595
|
))}
|
|
589
596
|
{settingsRow}
|
|
590
597
|
{account !== undefined && (
|
package/src/shell/index.tsx
CHANGED
|
@@ -130,10 +130,16 @@ export type SettingsMenuItem = {
|
|
|
130
130
|
label: string
|
|
131
131
|
icon?: ReactNode
|
|
132
132
|
onClick?: (e: MouseEvent) => void
|
|
133
|
-
/** This entry is the current
|
|
134
|
-
*
|
|
135
|
-
* More-sheet row) and the row carries `aria-current="true"`. */
|
|
133
|
+
/** This entry is the current one (e.g. the active theme radio, the open dev tool, the open
|
|
134
|
+
* settings page) — how it shows is `current`. */
|
|
136
135
|
active?: boolean
|
|
136
|
+
/**
|
|
137
|
+
* What `active` means. `'selection'` (default) is a chosen value: a trailing check glyph in every
|
|
138
|
+
* form (flat footer row, gear `Menu.Item`, the mobile More-sheet row) and `aria-current="true"`.
|
|
139
|
+
* `'page'` is a navigation destination: no check, the nav-item active style, and
|
|
140
|
+
* `aria-current="page"` — what a router-backed settings entry wants.
|
|
141
|
+
*/
|
|
142
|
+
current?: 'selection' | 'page'
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
export type BasaltShellProps = BasaltProps & {
|
|
@@ -167,8 +173,9 @@ export type BasaltShellProps = BasaltProps & {
|
|
|
167
173
|
* Because they are data, basalt owns the projections the two `ReactNode` slots could not express:
|
|
168
174
|
* a `list` with a `count` becomes a dot on its icon in the collapsed rail, a `progress` block
|
|
169
175
|
* becomes a ring on the settings row, and a block with `mobile: 'more'` becomes one More-sheet
|
|
170
|
-
* row opening a nested sheet of its items. A `kind: 'custom'` block is desktop-only, exactly
|
|
171
|
-
* `sidebarNavExtra` was
|
|
176
|
+
* row opening a nested sheet of its items. A `kind: 'custom'` block is desktop-only and hidden in the rail, exactly
|
|
177
|
+
* as `sidebarNavExtra` was, unless it opts in with `icon` + `label` (a rail icon opening it in a
|
|
178
|
+
* popover) and `mobile: 'more'` (a More row opening it in a sheet).
|
|
172
179
|
*/
|
|
173
180
|
sidebarBlocks?: SidebarBlock[]
|
|
174
181
|
/**
|
package/src/shell/nav-types.ts
CHANGED
|
@@ -139,9 +139,10 @@ export type SidebarProgressBlock = {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
142
|
+
* Arbitrary consumer content (a tree, a filter panel) that no set of rows can express. Replaced
|
|
143
|
+
* `BasaltShellProps.sidebarNavExtra`. By default desktop-only and hidden on the collapsed rail,
|
|
144
|
+
* exactly as that prop was; `icon` + `label` opt it into the rail (as an icon opening the node in a
|
|
145
|
+
* popover) and `mobile: 'more'` into the More sheet.
|
|
145
146
|
*/
|
|
146
147
|
export type SidebarCustomBlock = {
|
|
147
148
|
kind: 'custom'
|
|
@@ -149,6 +150,21 @@ export type SidebarCustomBlock = {
|
|
|
149
150
|
node: ReactNode
|
|
150
151
|
/** @default 'nav' */
|
|
151
152
|
placement?: 'nav' | 'bottom'
|
|
153
|
+
/** The rail icon. Needs `label` too: an icon-only trigger must be nameable. */
|
|
154
|
+
icon?: ReactNode
|
|
155
|
+
/** The rail icon's accessible name, the popover heading and the More row / sheet title. */
|
|
156
|
+
label?: string
|
|
157
|
+
/**
|
|
158
|
+
* Collapsed-rail projection: `'icon'` swaps the node for `icon`, which opens it in a popover.
|
|
159
|
+
* @default 'icon' when both `icon` and `label` are set, `'hidden'` otherwise
|
|
160
|
+
*/
|
|
161
|
+
rail?: 'icon' | 'hidden'
|
|
162
|
+
/**
|
|
163
|
+
* Below `sm` there is no sidebar: `'more'` makes one More row, labelled `label`, that opens the
|
|
164
|
+
* node in a sheet. Needs `label`.
|
|
165
|
+
* @default 'hidden'
|
|
166
|
+
*/
|
|
167
|
+
mobile?: 'more' | 'hidden'
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
/**
|
|
@@ -26,11 +26,17 @@ export function sidebarBlockPlacement(block: SidebarBlock): 'nav' | 'bottom' {
|
|
|
26
26
|
/**
|
|
27
27
|
* The collapsed-rail projection. A list block earns a dot only when it carries a `count` — a rail
|
|
28
28
|
* icon with no number behind it is a dot that means nothing — and a progress block a ring on the
|
|
29
|
-
* settings row. A `custom` block has
|
|
30
|
-
*
|
|
29
|
+
* settings row. A `custom` block has a rail form only when it opts in (see below), which is
|
|
30
|
+
* `sidebarNavExtra`'s old hidden-in-the-rail behaviour otherwise.
|
|
31
31
|
*/
|
|
32
|
-
export function sidebarBlockRail(block: SidebarBlock): 'dot' | 'ring' | 'hidden' {
|
|
33
|
-
|
|
32
|
+
export function sidebarBlockRail(block: SidebarBlock): 'dot' | 'ring' | 'icon' | 'hidden' {
|
|
33
|
+
// A `custom` block is arbitrary content with no icon-column form of its own, so it is hidden
|
|
34
|
+
// unless the consumer supplied the icon AND the name that make a rail trigger. Like a list's dot,
|
|
35
|
+
// that overrides an explicit `rail: 'icon'` without them.
|
|
36
|
+
if (block.kind === 'custom') {
|
|
37
|
+
if (block.icon === undefined || block.label === undefined) return 'hidden'
|
|
38
|
+
return block.rail ?? 'icon'
|
|
39
|
+
}
|
|
34
40
|
if (block.kind === 'progress') return block.rail ?? 'ring'
|
|
35
41
|
// A dot has to sit ON something. With no `icon` there is no rail node to badge, and with no
|
|
36
42
|
// `count` there is no number behind the dot — either way the block leaves the rail rather than
|
|
@@ -47,7 +53,9 @@ export function sidebarBlockRail(block: SidebarBlock): 'dot' | 'ring' | 'hidden'
|
|
|
47
53
|
* threshold is measured against.
|
|
48
54
|
*/
|
|
49
55
|
export function sidebarBlockMobile(block: SidebarBlock): 'more' | 'hidden' {
|
|
50
|
-
|
|
56
|
+
// A custom block's More row is named by `label`; without one there is nothing to name it.
|
|
57
|
+
if (block.kind === 'custom')
|
|
58
|
+
return block.mobile === 'more' && block.label !== undefined ? 'more' : 'hidden'
|
|
51
59
|
if (block.kind === 'progress') return block.mobile ?? 'hidden'
|
|
52
60
|
return block.mobile ?? 'more'
|
|
53
61
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* module's class. A second CSS module would hash `.block*` into a different name than the rail
|
|
13
13
|
* rules could ever select.
|
|
14
14
|
*/
|
|
15
|
-
import { Collapse, NavLink, Progress, Text, UnstyledButton } from '@mantine/core'
|
|
15
|
+
import { Collapse, NavLink, Popover, Progress, Text, UnstyledButton } from '@mantine/core'
|
|
16
16
|
import { useState } from 'react'
|
|
17
17
|
import type { ReactNode } from 'react'
|
|
18
18
|
import type {
|
|
@@ -280,10 +280,33 @@ function ProgressBlock({ block }: { block: SidebarProgressBlock }) {
|
|
|
280
280
|
)
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
/**
|
|
284
|
-
|
|
283
|
+
/**
|
|
284
|
+
* A `kind: 'custom'` block — consumer content. Without `icon` + `label` it is hidden on the rail
|
|
285
|
+
* exactly as `navExtra` was (CSS only, the node stays mounted). With them the rail swaps the node
|
|
286
|
+
* for its icon, which opens the node in a popover: that swap is JS-gated on `collapsed`, because
|
|
287
|
+
* the trigger is a different element and nothing in the node's own subtree could express it. The
|
|
288
|
+
* popover mounts only while open, so the inline node is never duplicated.
|
|
289
|
+
*/
|
|
290
|
+
function CustomBlock({ block, collapsed }: { block: SidebarCustomBlock; collapsed: boolean }) {
|
|
291
|
+
const rail = sidebarBlockRail(block)
|
|
292
|
+
if (collapsed && rail === 'icon') {
|
|
293
|
+
return (
|
|
294
|
+
<div className={classes.block} data-rail="icon">
|
|
295
|
+
<Popover position="right-start" withArrow={false} offset={4} withinPortal zIndex={400}>
|
|
296
|
+
<Popover.Target>
|
|
297
|
+
<UnstyledButton className={classes.blockRailTrigger} aria-label={block.label}>
|
|
298
|
+
<IconSlot>{block.icon}</IconSlot>
|
|
299
|
+
</UnstyledButton>
|
|
300
|
+
</Popover.Target>
|
|
301
|
+
<Popover.Dropdown p="xs" className={classes.blockRailPopover}>
|
|
302
|
+
{block.node}
|
|
303
|
+
</Popover.Dropdown>
|
|
304
|
+
</Popover>
|
|
305
|
+
</div>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
285
308
|
return (
|
|
286
|
-
<div className={classes.block} data-rail=
|
|
309
|
+
<div className={classes.block} data-rail={rail}>
|
|
287
310
|
{block.node}
|
|
288
311
|
</div>
|
|
289
312
|
)
|
|
@@ -292,10 +315,12 @@ function CustomBlock({ block }: { block: SidebarCustomBlock }) {
|
|
|
292
315
|
/** One block, whichever kind it is. `AppSidebar` places it; this decides nothing but the paint. */
|
|
293
316
|
export function SidebarBlockView({
|
|
294
317
|
block,
|
|
318
|
+
collapsed = false,
|
|
295
319
|
}: {
|
|
296
320
|
block: SidebarListBlock | SidebarProgressBlock | SidebarCustomBlock
|
|
321
|
+
collapsed?: boolean
|
|
297
322
|
}) {
|
|
298
323
|
if (block.kind === 'list') return <ListBlock block={block} />
|
|
299
324
|
if (block.kind === 'progress') return <ProgressBlock block={block} />
|
|
300
|
-
return <CustomBlock block={block} />
|
|
325
|
+
return <CustomBlock block={block} collapsed={collapsed} />
|
|
301
326
|
}
|
package/src/surfaces.ts
CHANGED
|
@@ -564,8 +564,8 @@ export const SURFACES = {
|
|
|
564
564
|
layer: 'mantine-coupled',
|
|
565
565
|
rule: 'controls',
|
|
566
566
|
skill: ['basalt-design'],
|
|
567
|
-
// The text lane of law C1 — the plugin's `control-outside-home` seen through a
|
|
568
|
-
//
|
|
567
|
+
// The text lane of law C1 — the plugin's `control-outside-home` seen through a host-tag nesting
|
|
568
|
+
// count, since a regex scan has no AST. Same law, same wave, same promotion.
|
|
569
569
|
guardKinds: ['raw-selection-control'],
|
|
570
570
|
// The control tier itself (laws C1/C3/C5/C9). `hand-rolled-filter`, `control-size-literal` and
|
|
571
571
|
// `responsive-twin` all ship `error`; the two C1 placement rules are the grace entries in
|
package/src/tokens/index.ts
CHANGED
|
@@ -824,7 +824,7 @@ function warnConsumerVxName(name: string): void {
|
|
|
824
824
|
if (!BASALT_VX_FAMILIES.includes(family) || warnedVxNames.has(name)) return
|
|
825
825
|
warnedVxNames.add(name)
|
|
826
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.
|
|
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.36.0.`,
|
|
828
828
|
)
|
|
829
829
|
}
|
|
830
830
|
|