@estiva-app/ui 0.45.0 → 0.46.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/dist/CollapsibleSection.d.ts +7 -1
- package/dist/CollapsibleSection.d.ts.map +1 -1
- package/dist/InlineChip.d.ts +1 -1
- package/dist/NavItem.d.ts +11 -1
- package/dist/NavItem.d.ts.map +1 -1
- package/dist/SectionHeader.d.ts +9 -1
- package/dist/SectionHeader.d.ts.map +1 -1
- package/dist/eslint/index.js +2 -1
- package/dist/eslint/index.js.map +2 -2
- package/dist/eslint/no-restyled-part.d.ts.map +1 -1
- package/dist/index.js +76 -37
- package/dist/index.js.map +2 -2
- package/dist/looks.d.ts +3 -0
- package/dist/looks.d.ts.map +1 -1
- package/package.json +1 -1
- package/registry.json +51 -9
- package/src/Chip.tsx +1 -1
- package/src/CollapsibleSection.mdx +5 -0
- package/src/CollapsibleSection.stories.tsx +5 -0
- package/src/CollapsibleSection.tsx +10 -3
- package/src/CommandPalette.tsx +1 -1
- package/src/InlineChip.test.tsx +1 -1
- package/src/InlineChip.tsx +1 -1
- package/src/NavItem.mdx +9 -0
- package/src/NavItem.stories.tsx +23 -0
- package/src/NavItem.tsx +56 -6
- package/src/Reaction.tsx +1 -1
- package/src/SectionHeader.mdx +9 -0
- package/src/SectionHeader.stories.tsx +15 -0
- package/src/SectionHeader.tsx +28 -7
- package/src/eslint/no-restyled-part.ts +2 -1
- package/src/looks.ts +4 -0
- package/tailwind-preset.js +1 -0
- package/tokens.css +19 -4
package/dist/looks.d.ts
CHANGED
|
@@ -33,4 +33,7 @@ export declare const FLOATING_SURFACE_CLASSES = "rounded-lg border border-border
|
|
|
33
33
|
export declare const CHIP_TEXT_CLASSES = "text-chip signal:font-mono signal:text-small signal:font-semibold signal:tabular-nums";
|
|
34
34
|
/** Words typed straight into an input with no box of its own: SearchInput and ChipInput. */
|
|
35
35
|
export declare const BARE_INPUT_CLASSES = "bg-transparent text-text-primary placeholder:text-text-muted outline-none";
|
|
36
|
+
/** A sidebar row's shape and its words at rest, brightening under the pointer: NavItem, and SectionHeader's `row` (a folder among rows). */
|
|
37
|
+
export declare const SIDEBAR_ROW_CLASSES = "rounded-md transition-colors";
|
|
38
|
+
export declare const SIDEBAR_ROW_TEXT_CLASSES = "text-text-secondary hover:text-text-primary";
|
|
36
39
|
//# sourceMappingURL=looks.d.ts.map
|
package/dist/looks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"looks.d.ts","sourceRoot":"","sources":["../src/looks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,2MAA2M,CAAA;AAEzO,wFAAwF;AACxF,eAAO,MAAM,kBAAkB,kDAAkD,CAAA;AAEjF,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,iKAAiK,CAAA;AAEpM,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,4DAA4D,CAAA;AAEjG,kJAAkJ;AAClJ,eAAO,MAAM,kBAAkB;;;CAGrB,CAAA;AAEV,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,uJAAuJ,CAAA;AAEvL,mEAAmE;AACnE,eAAO,MAAM,wBAAwB,qEAAqE,CAAA;AAE1G,yDAAyD;AACzD,eAAO,MAAM,iBAAiB,0FAA0F,CAAA;AAExH,4FAA4F;AAC5F,eAAO,MAAM,kBAAkB,8EAA8E,CAAA"}
|
|
1
|
+
{"version":3,"file":"looks.d.ts","sourceRoot":"","sources":["../src/looks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,2MAA2M,CAAA;AAEzO,wFAAwF;AACxF,eAAO,MAAM,kBAAkB,kDAAkD,CAAA;AAEjF,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,iKAAiK,CAAA;AAEpM,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,4DAA4D,CAAA;AAEjG,kJAAkJ;AAClJ,eAAO,MAAM,kBAAkB;;;CAGrB,CAAA;AAEV,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,uJAAuJ,CAAA;AAEvL,mEAAmE;AACnE,eAAO,MAAM,wBAAwB,qEAAqE,CAAA;AAE1G,yDAAyD;AACzD,eAAO,MAAM,iBAAiB,0FAA0F,CAAA;AAExH,4FAA4F;AAC5F,eAAO,MAAM,kBAAkB,8EAA8E,CAAA;AAE7G,4IAA4I;AAC5I,eAAO,MAAM,mBAAmB,iCAAiC,CAAA;AACjE,eAAO,MAAM,wBAAwB,gDAAgD,CAAA"}
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"kind": "package",
|
|
5
5
|
"repo": "estiva-ui",
|
|
6
6
|
"package": "@estiva-app/ui",
|
|
7
|
-
"packageVersion": "0.
|
|
7
|
+
"packageVersion": "0.46.0",
|
|
8
8
|
"exports": 100,
|
|
9
9
|
"typeExports": 109,
|
|
10
10
|
"files": null
|
|
@@ -832,14 +832,14 @@
|
|
|
832
832
|
"docPage": "src/Chip.mdx",
|
|
833
833
|
"app": null,
|
|
834
834
|
"looks": [
|
|
835
|
-
"bg-accent-muted text-accent-
|
|
835
|
+
"bg-accent-muted text-accent-text signal:border signal:border-accent-outline",
|
|
836
836
|
"bg-info-muted text-info-default signal:border signal:border-info-outline",
|
|
837
837
|
"bg-warning-muted text-warning-default signal:border signal:border-warning-outline signal:shadow-glow-warning",
|
|
838
838
|
"bg-success-muted text-success-default signal:border signal:border-success-outline",
|
|
839
839
|
"bg-error-muted text-error-default signal:border signal:border-error-outline",
|
|
840
840
|
"overflow-x-clip text-ellipsis whitespace-nowrap signal:tracking-wide text-chip signal:font-mono signal:text-small signal:font-semibold signal:tabular-nums",
|
|
841
841
|
"overflow-x-clip text-ellipsis whitespace-nowrap signal:tracking-wide",
|
|
842
|
-
"rounded-full bg-accent-muted text-accent-
|
|
842
|
+
"rounded-full bg-accent-muted text-accent-text signal:border signal:border-accent-outline",
|
|
843
843
|
"rounded-full bg-info-muted text-info-default signal:border signal:border-info-outline",
|
|
844
844
|
"rounded-full bg-warning-muted text-warning-default signal:border signal:border-warning-outline signal:shadow-glow-warning",
|
|
845
845
|
"rounded-full bg-success-muted text-success-default signal:border signal:border-success-outline",
|
|
@@ -1045,6 +1045,12 @@
|
|
|
1045
1045
|
"required": false,
|
|
1046
1046
|
"note": null
|
|
1047
1047
|
},
|
|
1048
|
+
{
|
|
1049
|
+
"name": "look",
|
|
1050
|
+
"takes": "heading | row",
|
|
1051
|
+
"required": false,
|
|
1052
|
+
"note": "`heading` (default): the title is a section heading."
|
|
1053
|
+
},
|
|
1048
1054
|
{
|
|
1049
1055
|
"name": "children",
|
|
1050
1056
|
"takes": "anything",
|
|
@@ -1071,6 +1077,13 @@
|
|
|
1071
1077
|
"hover",
|
|
1072
1078
|
"always"
|
|
1073
1079
|
]
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
"prop": "look",
|
|
1083
|
+
"values": [
|
|
1084
|
+
"heading",
|
|
1085
|
+
"row"
|
|
1086
|
+
]
|
|
1074
1087
|
}
|
|
1075
1088
|
],
|
|
1076
1089
|
"ownsBehaviours": [
|
|
@@ -3449,6 +3462,18 @@
|
|
|
3449
3462
|
"required": false,
|
|
3450
3463
|
"note": "16px, stroke 1.5."
|
|
3451
3464
|
},
|
|
3465
|
+
{
|
|
3466
|
+
"name": "menu",
|
|
3467
|
+
"takes": "anything",
|
|
3468
|
+
"required": false,
|
|
3469
|
+
"note": "The row's \"More options\" menu: `MenuItem`s."
|
|
3470
|
+
},
|
|
3471
|
+
{
|
|
3472
|
+
"name": "menuLabel",
|
|
3473
|
+
"takes": "text",
|
|
3474
|
+
"required": false,
|
|
3475
|
+
"note": "The ⋮'s accessible name and tooltip. Default \"More options for {label}\"."
|
|
3476
|
+
},
|
|
3452
3477
|
{
|
|
3453
3478
|
"name": "className",
|
|
3454
3479
|
"takes": "text",
|
|
@@ -3465,9 +3490,10 @@
|
|
|
3465
3490
|
"docPage": "src/NavItem.mdx",
|
|
3466
3491
|
"app": null,
|
|
3467
3492
|
"looks": [
|
|
3468
|
-
"
|
|
3469
|
-
"
|
|
3470
|
-
"text-center font-mono text-caption tabular-nums text-text-muted"
|
|
3493
|
+
"text-body-2 rounded-md transition-colors bg-bg-active text-text-primary",
|
|
3494
|
+
"text-body-2 rounded-md transition-colors text-text-secondary hover:text-text-primary hover:bg-bg-hover",
|
|
3495
|
+
"text-center font-mono text-caption tabular-nums text-text-muted",
|
|
3496
|
+
"text-center font-mono text-caption tabular-nums text-text-muted transition-opacity"
|
|
3471
3497
|
]
|
|
3472
3498
|
},
|
|
3473
3499
|
{
|
|
@@ -4186,9 +4212,9 @@
|
|
|
4186
4212
|
"docPage": "src/Reaction.mdx",
|
|
4187
4213
|
"app": null,
|
|
4188
4214
|
"looks": [
|
|
4189
|
-
"rounded-full border transition-colors disabled:cursor-not-allowed disabled:opacity-50 border-accent-primary bg-accent-muted text-accent-
|
|
4215
|
+
"rounded-full border transition-colors disabled:cursor-not-allowed disabled:opacity-50 border-accent-primary bg-accent-muted text-accent-text hover:border-accent-hover ship:text-text-primary",
|
|
4190
4216
|
"rounded-full border transition-colors disabled:cursor-not-allowed disabled:opacity-50 border-border-default bg-bg-inset text-text-primary hover:border-border-strong hover:bg-bg-hover",
|
|
4191
|
-
"border-accent-primary bg-accent-muted text-accent-
|
|
4217
|
+
"border-accent-primary bg-accent-muted text-accent-text hover:border-accent-hover ship:text-text-primary",
|
|
4192
4218
|
"border-border-default bg-bg-inset text-text-primary hover:border-border-strong hover:bg-bg-hover",
|
|
4193
4219
|
"text-chip signal:font-mono signal:text-small signal:font-semibold signal:tabular-nums"
|
|
4194
4220
|
]
|
|
@@ -4518,6 +4544,12 @@
|
|
|
4518
4544
|
"required": false,
|
|
4519
4545
|
"note": "`fill` lights the row under the pointer when it does something (a toggle, actions); `none` keeps it still — a heading whose actions are always shown, where the buttons light up on their own (UIG-14, Katerina, 19 September)."
|
|
4520
4546
|
},
|
|
4547
|
+
{
|
|
4548
|
+
"name": "look",
|
|
4549
|
+
"takes": "heading | quiet | row",
|
|
4550
|
+
"required": false,
|
|
4551
|
+
"note": "`heading` (default): a section's title, the `SectionLabel`."
|
|
4552
|
+
},
|
|
4521
4553
|
{
|
|
4522
4554
|
"name": "render",
|
|
4523
4555
|
"takes": "useRender.RenderProp",
|
|
@@ -4545,6 +4577,14 @@
|
|
|
4545
4577
|
"fill",
|
|
4546
4578
|
"none"
|
|
4547
4579
|
]
|
|
4580
|
+
},
|
|
4581
|
+
{
|
|
4582
|
+
"prop": "look",
|
|
4583
|
+
"values": [
|
|
4584
|
+
"heading",
|
|
4585
|
+
"quiet",
|
|
4586
|
+
"row"
|
|
4587
|
+
]
|
|
4548
4588
|
}
|
|
4549
4589
|
],
|
|
4550
4590
|
"ownsBehaviours": [],
|
|
@@ -4555,7 +4595,9 @@
|
|
|
4555
4595
|
"docPage": "src/SectionHeader.mdx",
|
|
4556
4596
|
"app": null,
|
|
4557
4597
|
"looks": [
|
|
4558
|
-
"
|
|
4598
|
+
"transition-transform duration-150 text-text-secondary rotate-90",
|
|
4599
|
+
"rounded-md transition-colors text-text-secondary hover:text-text-primary",
|
|
4600
|
+
"rounded-md transition-colors text-text-secondary hover:text-text-primary hover:bg-bg-hover",
|
|
4559
4601
|
"-mr-1 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100",
|
|
4560
4602
|
"opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100"
|
|
4561
4603
|
]
|
package/src/Chip.tsx
CHANGED
|
@@ -33,7 +33,7 @@ export interface ChipProps {
|
|
|
33
33
|
|
|
34
34
|
const typeStyles: Record<ChipType, string> = {
|
|
35
35
|
neutral: 'bg-bg-inset text-text-primary',
|
|
36
|
-
brand: 'bg-accent-muted text-accent-
|
|
36
|
+
brand: 'bg-accent-muted text-accent-text signal:border signal:border-accent-outline',
|
|
37
37
|
info: 'bg-info-muted text-info-default signal:border signal:border-info-outline',
|
|
38
38
|
warning: 'bg-warning-muted text-warning-default signal:border signal:border-warning-outline signal:shadow-glow-warning',
|
|
39
39
|
success: 'bg-success-muted text-success-default signal:border signal:border-success-outline',
|
|
@@ -22,6 +22,11 @@ toggle, and the rows under it, sliding shut and open.
|
|
|
22
22
|
- `open` + `onOpenChange` when the app owns the state.
|
|
23
23
|
- `actions` for add, sort, filter beside the title — SectionHeader's,
|
|
24
24
|
revealed on hover or focus.
|
|
25
|
+
- `look="row"` when the section is a row among rows — a folder in a
|
|
26
|
+
sidebar, under a quiet label: the title at a row's weight, and its rows
|
|
27
|
+
indented under it, past the chevron.
|
|
28
|
+
|
|
29
|
+
<Canvas of={CollapsibleSectionStories.AsARow} />
|
|
25
30
|
|
|
26
31
|
In a sidebar — the Sidebar's own Composed canvas, which is where that
|
|
27
32
|
composition lives:
|
|
@@ -42,3 +42,8 @@ export const WithTrailing: Story = {
|
|
|
42
42
|
/** Close it and reload the page: still closed. This browser remembers, under the key. */
|
|
43
43
|
export const Remembered: Story = { args: { storageKey: 'estiva-ui.stories.collapsible-section' } }
|
|
44
44
|
|
|
45
|
+
|
|
46
|
+
/** `look="row"`: a fold that is a row among rows — a folder in a sidebar. The title at a row's weight, the rows indented under it, past the chevron. */
|
|
47
|
+
export const AsARow: Story = {
|
|
48
|
+
args: { title: 'Folder', look: 'row' },
|
|
49
|
+
}
|
|
@@ -36,6 +36,12 @@ export interface CollapsibleSectionProps {
|
|
|
36
36
|
/** Beside the title, revealed on hover or focus — `SectionHeader`'s. */
|
|
37
37
|
actions?: SectionAction[]
|
|
38
38
|
showActions?: 'hover' | 'always'
|
|
39
|
+
/**
|
|
40
|
+
* `heading` (default): the title is a section heading. `row`: the fold is a
|
|
41
|
+
* row among rows — a folder in a sidebar — and its rows sit indented under
|
|
42
|
+
* its title, past the chevron (`SectionHeader`'s `look`).
|
|
43
|
+
*/
|
|
44
|
+
look?: 'heading' | 'row'
|
|
39
45
|
/** The rows. */
|
|
40
46
|
children: ReactNode
|
|
41
47
|
/** On the section's box: `mt-2` between groups, `shrink-0` in a scrolling column. */
|
|
@@ -66,7 +72,7 @@ function writeStored(key: string | undefined, open: boolean) {
|
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
export function CollapsibleSection({ title, defaultOpen = true, open: openProp, onOpenChange, storageKey, trailing, actions, showActions, children, className, contentClassName }: CollapsibleSectionProps) {
|
|
75
|
+
export function CollapsibleSection({ title, defaultOpen = true, open: openProp, onOpenChange, storageKey, trailing, actions, showActions, look = 'heading', children, className, contentClassName }: CollapsibleSectionProps) {
|
|
70
76
|
const [openState, setOpenState] = useState(() => readStored(storageKey) ?? defaultOpen)
|
|
71
77
|
const open = openProp ?? openState
|
|
72
78
|
const setOpen = (next: boolean) => {
|
|
@@ -76,7 +82,7 @@ export function CollapsibleSection({ title, defaultOpen = true, open: openProp,
|
|
|
76
82
|
}
|
|
77
83
|
return (
|
|
78
84
|
<Collapsible.Root open={open} onOpenChange={setOpen} className={cn('flex flex-col', className)}>
|
|
79
|
-
<SectionHeader title={title} chevron isExpanded={open} trailing={trailing} actions={actions} showActions={showActions} className="shrink-0" render={<Collapsible.Trigger />} />
|
|
85
|
+
<SectionHeader title={title} chevron isExpanded={open} trailing={trailing} actions={actions} showActions={showActions} look={look} className="shrink-0" render={<Collapsible.Trigger />} />
|
|
80
86
|
{/* The slide: Base UI measures the panel and writes its height to a
|
|
81
87
|
variable — `auto` again once the slide ends, so rows that arrive
|
|
82
88
|
later are not clipped — and the panel is 0 high on its opening frame
|
|
@@ -87,7 +93,8 @@ export function CollapsibleSection({ title, defaultOpen = true, open: openProp,
|
|
|
87
93
|
hiddenUntilFound
|
|
88
94
|
className="h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-150 ease-out motion-reduce:transition-none data-[starting-style]:h-0 data-[ending-style]:h-0"
|
|
89
95
|
>
|
|
90
|
-
|
|
96
|
+
{/* A row's rows start under its title: the 16px chevron and its 8px gap (pl-6). */}
|
|
97
|
+
<div className={cn('flex flex-col', look === 'row' && 'pl-6', contentClassName)}>{children}</div>
|
|
91
98
|
</Collapsible.Panel>
|
|
92
99
|
</Collapsible.Root>
|
|
93
100
|
)
|
package/src/CommandPalette.tsx
CHANGED
|
@@ -639,7 +639,7 @@ export function CommandPaletteAnswer({ children, note }: CommandPaletteAnswerPro
|
|
|
639
639
|
before "[1]" would stand between them as a gap. */}
|
|
640
640
|
{paragraph.split(/\s*(\[\d+\])/).map((part, j) =>
|
|
641
641
|
/^\[\d+\]$/.test(part) ? (
|
|
642
|
-
<sup key={j} className="ml-0.5 font-mono text-small text-accent-
|
|
642
|
+
<sup key={j} className="ml-0.5 font-mono text-small text-accent-text">
|
|
643
643
|
{part.slice(1, -1)}
|
|
644
644
|
</sup>
|
|
645
645
|
) : (
|
package/src/InlineChip.test.tsx
CHANGED
|
@@ -52,7 +52,7 @@ describe('InlineChip', () => {
|
|
|
52
52
|
expect(neutral).toContain('text-text-primary')
|
|
53
53
|
const person = inlineChipClassName('person').split(' ')
|
|
54
54
|
expect(person).toContain('text-body-2')
|
|
55
|
-
expect(person).toContain('text-accent-
|
|
55
|
+
expect(person).toContain('text-accent-text')
|
|
56
56
|
const urgent = inlineChipClassName('urgent').split(' ')
|
|
57
57
|
expect(urgent).toContain('text-warning-default')
|
|
58
58
|
// quiet is smaller: its caption size replaces the body size, and its colour survives beside it
|
package/src/InlineChip.tsx
CHANGED
|
@@ -33,7 +33,7 @@ export const INLINE_CHIP_TONE_CLASSES = {
|
|
|
33
33
|
/** A thing or a place — anything that is not a person. */
|
|
34
34
|
neutral: 'bg-bg-active text-text-primary',
|
|
35
35
|
/** A person. */
|
|
36
|
-
person: 'bg-accent-muted text-accent-
|
|
36
|
+
person: 'bg-accent-muted text-accent-text',
|
|
37
37
|
/** A person, called urgently. */
|
|
38
38
|
urgent: 'bg-warning-muted text-warning-default',
|
|
39
39
|
/**
|
package/src/NavItem.mdx
CHANGED
|
@@ -22,6 +22,12 @@ count — a muted mono number whose tooltip says what it counts.
|
|
|
22
22
|
|
|
23
23
|
<Canvas of={NavItemStories.ZeroDrawsNothing} />
|
|
24
24
|
|
|
25
|
+
- A row with things to do to it — move it, rename it — takes them as
|
|
26
|
+
`menu`: pass `MenuItem`s, and the row draws the ⋮, shows it while the row
|
|
27
|
+
is pointed at or focused, and moves the count aside for it.
|
|
28
|
+
|
|
29
|
+
<Canvas of={NavItemStories.WithMenu} />
|
|
30
|
+
|
|
25
31
|
## When not
|
|
26
32
|
|
|
27
33
|
- An icon rail's tile → **RailItem**.
|
|
@@ -47,6 +53,8 @@ import { NavItem } from '@estiva-app/ui'
|
|
|
47
53
|
`active` from the location, intercept the click, navigate — every
|
|
48
54
|
anchor prop passes through.
|
|
49
55
|
- The label truncates; the count never gives up its width.
|
|
56
|
+
- `menu` is the items only: `menu={<MenuItem label="Move to Folder…" onClick={move} />}`.
|
|
57
|
+
The ⋮'s name is `More options for {label}` unless `menuLabel` says otherwise.
|
|
50
58
|
|
|
51
59
|
- A row is 32px tall, 8px in on each side, 8px between the icon and the label,
|
|
52
60
|
the label 14px regular, 6px corners.
|
|
@@ -59,6 +67,7 @@ It does these by itself. Write none of them around it.
|
|
|
59
67
|
|---|---|---|
|
|
60
68
|
| Marks the current place for assistive technology when `active` | `aria-current` | |
|
|
61
69
|
| Keeps its 32px in a scrolling column | `shrink-0` | |
|
|
70
|
+
| Shows a ⋮ over the count while pointed at, focused or open, and keeps the row's hover look under it | a positioned `IconButton` + `Menu` over the row, `group-hover` reveals | |
|
|
62
71
|
|
|
63
72
|
## Props
|
|
64
73
|
|
package/src/NavItem.stories.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
import { IconSquareRounded } from '@tabler/icons-react'
|
|
3
|
+
import { MenuItem } from './Menu'
|
|
3
4
|
import { NavItem } from './NavItem'
|
|
4
5
|
|
|
5
6
|
const placeholder = <IconSquareRounded size={16} stroke={1.5} />
|
|
@@ -37,3 +38,25 @@ export const ZeroDrawsNothing: Story = {
|
|
|
37
38
|
export const LongLabelTruncates: Story = {
|
|
38
39
|
args: { label: 'An item whose label runs much longer than the column has room for', count: 7, countLabel: '7 open' },
|
|
39
40
|
}
|
|
41
|
+
|
|
42
|
+
const MENU = (
|
|
43
|
+
<>
|
|
44
|
+
<MenuItem label="Action one" onClick={() => {}} />
|
|
45
|
+
<MenuItem label="Action two" onClick={() => {}} />
|
|
46
|
+
</>
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
/** A row with a menu: point at it (or Tab to it) and a ⋮ takes the count's place; the count comes back when you leave. */
|
|
50
|
+
export const WithMenu: Story = {
|
|
51
|
+
args: { icon: placeholder, count: 6, countLabel: '6 open', menu: MENU },
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The active row keeps its fill; the ⋮ sits on it the same way. */
|
|
55
|
+
export const WithMenuActive: Story = {
|
|
56
|
+
args: { icon: placeholder, count: 6, countLabel: '6 open', menu: MENU, active: true },
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** No count: the ⋮ still appears in the same place, and a long label stops short of it. */
|
|
60
|
+
export const WithMenuNoCount: Story = {
|
|
61
|
+
args: { label: 'An item whose label runs much longer than the column has room for', icon: placeholder, menu: MENU },
|
|
62
|
+
}
|
package/src/NavItem.tsx
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactNode } from 'react'
|
|
2
|
+
import { IconDotsVertical } from '@tabler/icons-react'
|
|
2
3
|
import { cn } from './cn'
|
|
4
|
+
import { SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_TEXT_CLASSES } from './looks'
|
|
5
|
+
import { IconButton } from './IconButton'
|
|
6
|
+
import { Menu } from './Menu'
|
|
3
7
|
import { WithTooltip } from './Tooltip'
|
|
4
8
|
|
|
5
9
|
/**
|
|
@@ -12,6 +16,12 @@ import { WithTooltip } from './Tooltip'
|
|
|
12
16
|
* count draws its zero (see Tabs) — both rulings stand, deliberately.
|
|
13
17
|
*
|
|
14
18
|
* Active is the same fill as a selected tab — `bg-active`, neutral.
|
|
19
|
+
*
|
|
20
|
+
* `menu` gives the row a "More options" menu (UIG-44, Katerina, 30
|
|
21
|
+
* September): a ⋮ that shows while the row is pointed at, focused or its
|
|
22
|
+
* menu is open, in the count's place — the count steps aside rather than
|
|
23
|
+
* sitting under it. The ⋮ is beside the link, not inside it (a button inside
|
|
24
|
+
* an anchor is not a button), and follows it in the Tab order.
|
|
15
25
|
*/
|
|
16
26
|
export interface NavItemProps extends Omit<ComponentPropsWithoutRef<'a'>, 'href'> {
|
|
17
27
|
label: string
|
|
@@ -23,12 +33,15 @@ export interface NavItemProps extends Omit<ComponentPropsWithoutRef<'a'>, 'href'
|
|
|
23
33
|
active?: boolean
|
|
24
34
|
/** 16px, stroke 1.5. */
|
|
25
35
|
icon?: ReactNode
|
|
36
|
+
/** The row's "More options" menu: `MenuItem`s. Absent, the row has no menu and is exactly the plain row. */
|
|
37
|
+
menu?: ReactNode
|
|
38
|
+
/** The ⋮'s accessible name and tooltip. Default "More options for {label}". */
|
|
39
|
+
menuLabel?: string
|
|
26
40
|
className?: string
|
|
27
41
|
}
|
|
28
42
|
|
|
29
|
-
export function NavItem({ label, href, count, countLabel, active = false, icon, className, ...props }: NavItemProps) {
|
|
30
|
-
|
|
31
|
-
// @estiva-escape(no-copied-look): N5 (Katerina, 24 September: record only): a row that fills on hover, as PersonTrigger's row also does
|
|
43
|
+
export function NavItem({ label, href, count, countLabel, active = false, icon, menu, menuLabel, className, ...props }: NavItemProps) {
|
|
44
|
+
const row = (
|
|
32
45
|
<a
|
|
33
46
|
href={href}
|
|
34
47
|
aria-current={active ? 'page' : undefined}
|
|
@@ -36,8 +49,12 @@ export function NavItem({ label, href, count, countLabel, active = false, icon,
|
|
|
36
49
|
// shrink-0: the row keeps its 32px inside an overflowing flex column
|
|
37
50
|
// — without it the list compresses instead of scrolling (the missing-
|
|
38
51
|
// shrink-0 family; Katerina, 2026-09-02).
|
|
39
|
-
'flex h-8 min-w-0 shrink-0 items-center gap-2
|
|
40
|
-
|
|
52
|
+
'flex h-8 min-w-0 shrink-0 items-center gap-2 px-2 text-body-2',
|
|
53
|
+
SIDEBAR_ROW_CLASSES,
|
|
54
|
+
active ? 'bg-bg-active text-text-primary' : cn(SIDEBAR_ROW_TEXT_CLASSES, 'hover:bg-bg-hover'),
|
|
55
|
+
// With a menu the ⋮ sits beside the link, so the row keeps its hover
|
|
56
|
+
// look while the ⋮ is pointed at and while its menu is open.
|
|
57
|
+
menu != null && !active && 'group-hover/nav:bg-bg-hover group-hover/nav:text-text-primary group-has-[[aria-expanded=true]]/nav:bg-bg-hover group-has-[[aria-expanded=true]]/nav:text-text-primary',
|
|
41
58
|
className,
|
|
42
59
|
)}
|
|
43
60
|
{...props}
|
|
@@ -50,9 +67,42 @@ export function NavItem({ label, href, count, countLabel, active = false, icon,
|
|
|
50
67
|
here sits on the same axis as a SectionHeader's action above it
|
|
51
68
|
— right edges alone left a digit 4px off a 16px icon (Katerina,
|
|
52
69
|
2026-09-09). A three-digit count grows the box leftwards. */}
|
|
53
|
-
<span
|
|
70
|
+
<span
|
|
71
|
+
className={cn(
|
|
72
|
+
'min-w-4 shrink-0 text-center font-mono text-caption tabular-nums text-text-muted',
|
|
73
|
+
// With a menu, the ⋮ takes this place while it shows.
|
|
74
|
+
menu != null && 'transition-opacity group-hover/nav:opacity-0 group-focus-within/nav:opacity-0 group-has-[[aria-expanded=true]]/nav:opacity-0',
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{count}
|
|
78
|
+
</span>
|
|
54
79
|
</WithTooltip>
|
|
80
|
+
) : menu != null ? (
|
|
81
|
+
// No count, but a menu: the count's place is kept free, so a long
|
|
82
|
+
// label truncates before the ⋮ instead of running under it.
|
|
83
|
+
<span aria-hidden="true" className="min-w-4 shrink-0" />
|
|
55
84
|
) : null}
|
|
56
85
|
</a>
|
|
57
86
|
)
|
|
87
|
+
if (menu == null) return row
|
|
88
|
+
const name = menuLabel ?? `More options for ${label}`
|
|
89
|
+
return (
|
|
90
|
+
<div className="group/nav relative shrink-0">
|
|
91
|
+
{row}
|
|
92
|
+
{/* `right-1`: the 24px button's 16px icon then ends 8px from the row's
|
|
93
|
+
edge, where the count ends (px-2), so the ⋮ lands on the number. */}
|
|
94
|
+
<div className="absolute inset-y-0 right-1 flex items-center opacity-0 transition-opacity focus-within:opacity-100 group-hover/nav:opacity-100 has-[[aria-expanded=true]]:opacity-100">
|
|
95
|
+
<Menu
|
|
96
|
+
align="right"
|
|
97
|
+
trigger={
|
|
98
|
+
<IconButton tooltip="More options" aria-label={name}>
|
|
99
|
+
<IconDotsVertical size={16} stroke={1.5} />
|
|
100
|
+
</IconButton>
|
|
101
|
+
}
|
|
102
|
+
>
|
|
103
|
+
{menu}
|
|
104
|
+
</Menu>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
58
108
|
}
|
package/src/Reaction.tsx
CHANGED
|
@@ -81,7 +81,7 @@ export function Reaction({ emoji, count, pressed = false, className, ...props }:
|
|
|
81
81
|
(Katerina, 2026-09-08). The number reads in the text colour
|
|
82
82
|
there; the accent keeps the edge and the fill. Signal keeps
|
|
83
83
|
Peek's blue-on-wash, which reads. */
|
|
84
|
-
'border-accent-primary bg-accent-muted text-accent-
|
|
84
|
+
'border-accent-primary bg-accent-muted text-accent-text hover:border-accent-hover ship:text-text-primary'
|
|
85
85
|
: 'border-border-default bg-bg-inset text-text-primary hover:border-border-strong hover:bg-bg-hover',
|
|
86
86
|
className,
|
|
87
87
|
)}
|
package/src/SectionHeader.mdx
CHANGED
|
@@ -24,6 +24,15 @@ title the toggle — that is where a section folds.
|
|
|
24
24
|
|
|
25
25
|
<Canvas of={SectionHeaderStories.PersistentActions} />
|
|
26
26
|
|
|
27
|
+
- `look="quiet"`: a small grey label over a list whose rows are rows, not
|
|
28
|
+
headings — "Folders" over the folders — with its action held on screen.
|
|
29
|
+
It never lights up under the pointer; its button does.
|
|
30
|
+
|
|
31
|
+
<Canvas of={SectionHeaderStories.Quiet} />
|
|
32
|
+
|
|
33
|
+
- `look="row"`: the header is a row among rows — a folder in a sidebar. Use
|
|
34
|
+
it through **CollapsibleSection**'s `look="row"`.
|
|
35
|
+
|
|
27
36
|
## When not
|
|
28
37
|
|
|
29
38
|
- A section that opens and closes → **CollapsibleSection**: the header,
|
|
@@ -56,3 +56,18 @@ export const WithTrailing: Story = {
|
|
|
56
56
|
actions: [{ icon: <IconPlus size={16} stroke={1.5} />, tooltip: 'Add', onClick: () => {} }],
|
|
57
57
|
},
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/** `look="quiet"`: a small grey label over a list, with its action always on screen — "Folders" over the folders. The row never lights up; the button does. */
|
|
61
|
+
export const Quiet: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
title: 'Folders',
|
|
64
|
+
look: 'quiet',
|
|
65
|
+
showActions: 'always',
|
|
66
|
+
actions: [{ icon: <IconPlus size={16} stroke={1.5} />, tooltip: 'New folder', onClick: () => {} }],
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `look="row"`: a row among rows — the chevron and the title at a sidebar row's size and colour, brightening under the pointer. CollapsibleSection's `look="row"` draws this. */
|
|
71
|
+
export const Row: Story = {
|
|
72
|
+
args: { title: 'Folder', look: 'row', chevron: true, isExpanded: true, onToggle: () => {} },
|
|
73
|
+
}
|
package/src/SectionHeader.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react'
|
|
|
2
2
|
import { IconChevronRight } from '@tabler/icons-react'
|
|
3
3
|
import { useRender } from '@base-ui/react/use-render'
|
|
4
4
|
import { cn } from './cn'
|
|
5
|
+
import { SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_TEXT_CLASSES } from './looks'
|
|
5
6
|
import { IconButton } from './IconButton'
|
|
6
7
|
import { SectionLabel } from './SectionLabel'
|
|
7
8
|
|
|
@@ -65,6 +66,14 @@ export interface SectionHeaderProps {
|
|
|
65
66
|
* where the buttons light up on their own (UIG-14, Katerina, 19 September).
|
|
66
67
|
*/
|
|
67
68
|
hover?: 'fill' | 'none'
|
|
69
|
+
/**
|
|
70
|
+
* `heading` (default): a section's title, the `SectionLabel`. `quiet`: a
|
|
71
|
+
* small grey label over a list (12px, muted) — "Folders" over the folders,
|
|
72
|
+
* which never reads as one of them. `row`: a row among rows — the chevron
|
|
73
|
+
* and the title at a row's weight and colour, as a sidebar row is (Katerina,
|
|
74
|
+
* 30 September: Leaf's folders, in Ship).
|
|
75
|
+
*/
|
|
76
|
+
look?: 'heading' | 'quiet' | 'row'
|
|
68
77
|
/**
|
|
69
78
|
* What the title renders as, in Base UI's manner. A plain button with
|
|
70
79
|
* `onToggle` by default; `CollapsibleSection` hands in `Collapsible.Trigger`.
|
|
@@ -73,24 +82,32 @@ export interface SectionHeaderProps {
|
|
|
73
82
|
className?: string
|
|
74
83
|
}
|
|
75
84
|
|
|
76
|
-
export function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, trailing, actions, showActions = 'hover', hover = 'fill', render, className }: SectionHeaderProps) {
|
|
85
|
+
export function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, trailing, actions, showActions = 'hover', hover = 'fill', look = 'heading', render, className }: SectionHeaderProps) {
|
|
86
|
+
const row = look === 'row'
|
|
77
87
|
const titleElement = useRender({
|
|
78
88
|
render: render ?? (chevron ? <button type="button" onClick={onToggle} aria-expanded={isExpanded} /> : <span />),
|
|
79
89
|
props: {
|
|
80
90
|
// `text-left`: a button centres its text. `h-full` and `flex-1`: the
|
|
81
91
|
// whole row up to the actions is the hit target, as it was when the
|
|
82
92
|
// row itself carried the click.
|
|
83
|
-
className: 'flex h-full min-w-0 flex-1 items-center gap-
|
|
93
|
+
className: cn('flex h-full min-w-0 flex-1 items-center text-left', row ? 'gap-2' : 'gap-1'),
|
|
84
94
|
children: (
|
|
85
95
|
<>
|
|
86
96
|
{chevron && (
|
|
87
97
|
<IconChevronRight
|
|
88
|
-
size={12}
|
|
98
|
+
size={row ? 16 : 12}
|
|
89
99
|
stroke={1.5}
|
|
90
|
-
className={cn('shrink-0
|
|
100
|
+
className={cn('shrink-0 transition-transform duration-150', !row && 'text-text-secondary', isExpanded && 'rotate-90')}
|
|
91
101
|
/>
|
|
92
102
|
)}
|
|
93
|
-
|
|
103
|
+
{look === 'quiet' ? (
|
|
104
|
+
<span className="min-w-0 truncate text-caption text-text-muted">{title}</span>
|
|
105
|
+
) : row ? (
|
|
106
|
+
// A row's words: NavItem's size and colour, brightening under the pointer with the row.
|
|
107
|
+
<span className="min-w-0 flex-1 truncate text-body-2">{title}</span>
|
|
108
|
+
) : (
|
|
109
|
+
<SectionLabel>{title}</SectionLabel>
|
|
110
|
+
)}
|
|
94
111
|
</>
|
|
95
112
|
),
|
|
96
113
|
},
|
|
@@ -99,11 +116,15 @@ export function SectionHeader({ title, chevron = false, isExpanded = true, onTog
|
|
|
99
116
|
return (
|
|
100
117
|
<div
|
|
101
118
|
className={cn(
|
|
102
|
-
'group flex h-[32px] shrink-0 items-center gap-1
|
|
119
|
+
'group flex h-[32px] shrink-0 items-center gap-1 px-2',
|
|
120
|
+
!row && 'transition-colors',
|
|
121
|
+
// A row is NavItem's shape and colour: 6px corners, secondary words that brighten on hover.
|
|
122
|
+
row ? cn(SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_TEXT_CLASSES) : 'rounded-lg',
|
|
103
123
|
// The fill says "this does something": a row with a toggle or actions
|
|
104
124
|
// lights up, a fixed heading over rows does not (2026-09-09, the
|
|
105
125
|
// Sidebar's fixed group).
|
|
106
|
-
|
|
126
|
+
// A quiet label never lights up: it is a label; its button does.
|
|
127
|
+
hover === 'fill' && look !== 'quiet' && (chevron || (actions && actions.length > 0)) && 'hover:bg-bg-hover',
|
|
107
128
|
className,
|
|
108
129
|
)}
|
|
109
130
|
>
|
|
@@ -125,6 +125,7 @@ export const PART_LOOK_PROPS: Record<string, string[]> = {
|
|
|
125
125
|
Card: ['fill', 'hover', 'attention', 'selected', 'active', 'hovered', 'quietUntilHover', 'unreadable', 'clip'],
|
|
126
126
|
Checkbox: ['row'],
|
|
127
127
|
Chip: ['type'],
|
|
128
|
+
CollapsibleSection: ['look'],
|
|
128
129
|
Divider: ['tone', 'orientation'],
|
|
129
130
|
EmptyState: ['scope'],
|
|
130
131
|
FieldLine: ['tone'],
|
|
@@ -142,7 +143,7 @@ export const PART_LOOK_PROPS: Record<string, string[]> = {
|
|
|
142
143
|
Reaction: ['pressed'],
|
|
143
144
|
ReactionPicker: ['surface'],
|
|
144
145
|
RichText: ['size'],
|
|
145
|
-
SectionHeader: ['hover'],
|
|
146
|
+
SectionHeader: ['hover', 'look'],
|
|
146
147
|
SectionLabel: ['tone', 'truncate'],
|
|
147
148
|
Select: ['size'],
|
|
148
149
|
Tabs: ['size'],
|
package/src/looks.ts
CHANGED
|
@@ -42,3 +42,7 @@ export const CHIP_TEXT_CLASSES = 'text-chip signal:font-mono signal:text-small s
|
|
|
42
42
|
|
|
43
43
|
/** Words typed straight into an input with no box of its own: SearchInput and ChipInput. */
|
|
44
44
|
export const BARE_INPUT_CLASSES = 'bg-transparent text-text-primary placeholder:text-text-muted outline-none'
|
|
45
|
+
|
|
46
|
+
/** A sidebar row's shape and its words at rest, brightening under the pointer: NavItem, and SectionHeader's `row` (a folder among rows). */
|
|
47
|
+
export const SIDEBAR_ROW_CLASSES = 'rounded-md transition-colors'
|
|
48
|
+
export const SIDEBAR_ROW_TEXT_CLASSES = 'text-text-secondary hover:text-text-primary'
|
package/tailwind-preset.js
CHANGED
|
@@ -175,6 +175,7 @@ export default {
|
|
|
175
175
|
'accent-primary': 'var(--accent-primary)',
|
|
176
176
|
'accent-hover': 'var(--accent-hover)',
|
|
177
177
|
'accent-muted': 'var(--accent-muted)',
|
|
178
|
+
'accent-text': 'var(--accent-text)',
|
|
178
179
|
// semantic
|
|
179
180
|
'info-default': 'var(--info-default)',
|
|
180
181
|
'info-muted': 'var(--info-muted)',
|