@estiva-app/ui 0.55.0 → 0.56.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/ContainerHeader.d.ts +7 -1
- package/dist/ContainerHeader.d.ts.map +1 -1
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/ListColumn.d.ts +2 -0
- package/dist/ListColumn.d.ts.map +1 -1
- package/dist/Menu.d.ts.map +1 -1
- package/dist/NavItem.d.ts.map +1 -1
- package/dist/NavTree.d.ts +9 -1
- package/dist/NavTree.d.ts.map +1 -1
- package/dist/PersonTrigger.d.ts.map +1 -1
- package/dist/SectionHeader.d.ts +22 -3
- package/dist/SectionHeader.d.ts.map +1 -1
- package/dist/SectionLabel.d.ts.map +1 -1
- package/dist/gates/index.js +1 -1
- package/dist/gates/index.js.map +2 -2
- package/dist/index.js +136 -72
- package/dist/index.js.map +4 -4
- package/dist/looks.d.ts +4 -0
- package/dist/looks.d.ts.map +1 -1
- package/package.json +1 -1
- package/registry.json +93 -22
- package/src/CollapsibleSection.stories.tsx +14 -1
- package/src/CollapsibleSection.tsx +10 -4
- package/src/ContainerHeader.mdx +4 -0
- package/src/ContainerHeader.stories.tsx +14 -0
- package/src/ContainerHeader.test.tsx +8 -0
- package/src/ContainerHeader.tsx +34 -6
- package/src/IconButton.tsx +2 -1
- package/src/ListColumn.tsx +4 -2
- package/src/Menu.tsx +5 -4
- package/src/NavItem.test.tsx +2 -2
- package/src/NavItem.tsx +21 -12
- package/src/NavTree.mdx +16 -1
- package/src/NavTree.stories.tsx +49 -2
- package/src/NavTree.test.tsx +7 -0
- package/src/NavTree.tsx +21 -4
- package/src/PersonTrigger.tsx +6 -5
- package/src/SectionHeader.mdx +6 -0
- package/src/SectionHeader.stories.tsx +18 -0
- package/src/SectionHeader.test.tsx +26 -0
- package/src/SectionHeader.tsx +99 -23
- package/src/SectionLabel.tsx +6 -3
- package/src/Sidebar.tsx +1 -1
- package/src/eslint/no-copied-look.test.ts +2 -2
- package/src/gates/app-checks.ts +1 -1
- package/src/looks.ts +6 -0
package/dist/looks.d.ts
CHANGED
|
@@ -34,6 +34,10 @@ export declare const CHIP_TEXT_CLASSES = "text-chip signal:font-mono signal:text
|
|
|
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
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
|
+
/** A quiet icon control: secondary, brightening on a hover fill — IconButton's `muted`. */
|
|
38
|
+
export declare const MUTED_CONTROL_CLASSES = "text-text-secondary hover:bg-bg-hover hover:text-text-primary";
|
|
39
|
+
/** A control that opens a menu: a hover fill, held while its menu is up — PersonTrigger's row, and a ContainerHeader title with a menu. */
|
|
40
|
+
export declare const MENU_TRIGGER_CLASSES = "cursor-pointer rounded-md transition-colors hover:bg-bg-hover data-[popup-open]:bg-bg-hover";
|
|
37
41
|
export declare const SIDEBAR_ROW_CLASSES = "rounded-lg transition-colors";
|
|
38
42
|
export declare const SIDEBAR_ROW_TEXT_CLASSES = "text-text-secondary hover:text-text-primary";
|
|
39
43
|
/**
|
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;AAE7G,4IAA4I;AAE5I,eAAO,MAAM,mBAAmB,iCAAiC,CAAA;AACjE,eAAO,MAAM,wBAAwB,gDAAgD,CAAA;AAErF;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,kLAAkL,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;AAE5I,2FAA2F;AAC3F,eAAO,MAAM,qBAAqB,kEAAkE,CAAA;AAEpG,2IAA2I;AAC3I,eAAO,MAAM,oBAAoB,gGAAgG,CAAA;AAEjI,eAAO,MAAM,mBAAmB,iCAAiC,CAAA;AACjE,eAAO,MAAM,wBAAwB,gDAAgD,CAAA;AAErF;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,kLAAkL,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.56.0",
|
|
8
8
|
"exports": 130,
|
|
9
9
|
"typeExports": 134,
|
|
10
10
|
"files": null
|
|
@@ -1185,6 +1185,12 @@
|
|
|
1185
1185
|
"required": false,
|
|
1186
1186
|
"note": "Remember open or closed in this browser, under this key."
|
|
1187
1187
|
},
|
|
1188
|
+
{
|
|
1189
|
+
"name": "icon",
|
|
1190
|
+
"takes": "anything",
|
|
1191
|
+
"required": false,
|
|
1192
|
+
"note": "16px, before the title; the arrow takes its place on hover — `SectionHeader`'s."
|
|
1193
|
+
},
|
|
1188
1194
|
{
|
|
1189
1195
|
"name": "trailing",
|
|
1190
1196
|
"takes": "anything",
|
|
@@ -1197,6 +1203,18 @@
|
|
|
1197
1203
|
"required": false,
|
|
1198
1204
|
"note": "Beside the title, revealed on hover or focus — `SectionHeader`'s."
|
|
1199
1205
|
},
|
|
1206
|
+
{
|
|
1207
|
+
"name": "menu",
|
|
1208
|
+
"takes": "anything",
|
|
1209
|
+
"required": false,
|
|
1210
|
+
"note": "Beside the actions, revealed with them: the section's \"More options\" menu, `MenuItem`s — `SectionHeader`'s."
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
"name": "menuLabel",
|
|
1214
|
+
"takes": "text",
|
|
1215
|
+
"required": false,
|
|
1216
|
+
"note": "The ⋮'s accessible name — `SectionHeader`'s."
|
|
1217
|
+
},
|
|
1200
1218
|
{
|
|
1201
1219
|
"name": "showActions",
|
|
1202
1220
|
"takes": "hover | always",
|
|
@@ -1750,6 +1768,12 @@
|
|
|
1750
1768
|
"required": false,
|
|
1751
1769
|
"note": "A chevron after the title, for a title that opens something."
|
|
1752
1770
|
},
|
|
1771
|
+
{
|
|
1772
|
+
"name": "titleMenu",
|
|
1773
|
+
"takes": "anything",
|
|
1774
|
+
"required": false,
|
|
1775
|
+
"note": "The menu the title opens: `MenuItem`s — the column's views (Katerina, 5 October: Peek's Topics column, Topics or Folders)."
|
|
1776
|
+
},
|
|
1753
1777
|
{
|
|
1754
1778
|
"name": "actions",
|
|
1755
1779
|
"takes": "anything",
|
|
@@ -1764,14 +1788,29 @@
|
|
|
1764
1788
|
}
|
|
1765
1789
|
],
|
|
1766
1790
|
"variants": [],
|
|
1767
|
-
"ownsBehaviours": [
|
|
1791
|
+
"ownsBehaviours": [
|
|
1792
|
+
{
|
|
1793
|
+
"id": "base-ui",
|
|
1794
|
+
"behaviour": "Is built on Base UI"
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
"id": "role",
|
|
1798
|
+
"behaviour": "Says what it is to assistive technology"
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
"id": "tab-stop",
|
|
1802
|
+
"behaviour": "Is reachable with Tab"
|
|
1803
|
+
}
|
|
1804
|
+
],
|
|
1768
1805
|
"status": "stable",
|
|
1769
1806
|
"migrationStage": null,
|
|
1770
1807
|
"docsId": "navigation-containerheader--docs",
|
|
1771
|
-
"storyId": "navigation-containerheader--
|
|
1808
|
+
"storyId": "navigation-containerheader--with-title-menu",
|
|
1772
1809
|
"docPage": "src/ContainerHeader.mdx",
|
|
1773
1810
|
"app": null,
|
|
1774
|
-
"looks": [
|
|
1811
|
+
"looks": [
|
|
1812
|
+
"cursor-pointer rounded-md transition-colors hover:bg-bg-hover"
|
|
1813
|
+
]
|
|
1775
1814
|
},
|
|
1776
1815
|
{
|
|
1777
1816
|
"name": "deleteBlock",
|
|
@@ -3320,6 +3359,12 @@
|
|
|
3320
3359
|
"required": false,
|
|
3321
3360
|
"note": "A chevron after the title, as `ContainerHeader` draws it."
|
|
3322
3361
|
},
|
|
3362
|
+
{
|
|
3363
|
+
"name": "titleMenu",
|
|
3364
|
+
"takes": "anything",
|
|
3365
|
+
"required": false,
|
|
3366
|
+
"note": "The menu the title opens — `ContainerHeader`'s: the column's views."
|
|
3367
|
+
},
|
|
3323
3368
|
{
|
|
3324
3369
|
"name": "actions",
|
|
3325
3370
|
"takes": "anything",
|
|
@@ -4018,6 +4063,18 @@
|
|
|
4018
4063
|
"required": false,
|
|
4019
4064
|
"note": "Beside the title, shown on hover or focus: \"Open\", typically."
|
|
4020
4065
|
},
|
|
4066
|
+
{
|
|
4067
|
+
"name": "menu",
|
|
4068
|
+
"takes": "anything",
|
|
4069
|
+
"required": false,
|
|
4070
|
+
"note": "The group's \"More options\" menu, after its actions: `MenuItem`s."
|
|
4071
|
+
},
|
|
4072
|
+
{
|
|
4073
|
+
"name": "icon",
|
|
4074
|
+
"takes": "anything",
|
|
4075
|
+
"required": false,
|
|
4076
|
+
"note": "16px, before the title; the arrow takes its place on hover."
|
|
4077
|
+
},
|
|
4021
4078
|
{
|
|
4022
4079
|
"name": "defaultOpen",
|
|
4023
4080
|
"takes": "true/false",
|
|
@@ -4320,8 +4377,8 @@
|
|
|
4320
4377
|
"docPage": "src/PersonTrigger.mdx",
|
|
4321
4378
|
"app": null,
|
|
4322
4379
|
"looks": [
|
|
4323
|
-
"
|
|
4324
|
-
"
|
|
4380
|
+
"text-body-2 text-text-primary cursor-pointer rounded-md transition-colors hover:bg-bg-hover",
|
|
4381
|
+
"text-body-2 text-text-primary cursor-pointer rounded-md transition-colors hover:bg-bg-hover bg-bg-hover"
|
|
4325
4382
|
]
|
|
4326
4383
|
},
|
|
4327
4384
|
{
|
|
@@ -5197,6 +5254,12 @@
|
|
|
5197
5254
|
"required": false,
|
|
5198
5255
|
"note": "Collapsible: draws the chevron and makes the title a button that toggles."
|
|
5199
5256
|
},
|
|
5257
|
+
{
|
|
5258
|
+
"name": "icon",
|
|
5259
|
+
"takes": "anything",
|
|
5260
|
+
"required": false,
|
|
5261
|
+
"note": "16px, stroke 1.5, before the title — a folder's (Katerina, 5 October: Peek's Folders, the standard heading with a folder icon)."
|
|
5262
|
+
},
|
|
5200
5263
|
{
|
|
5201
5264
|
"name": "isExpanded",
|
|
5202
5265
|
"takes": "true/false",
|
|
@@ -5221,6 +5284,18 @@
|
|
|
5221
5284
|
"required": false,
|
|
5222
5285
|
"note": "Right-aligned, in the order given."
|
|
5223
5286
|
},
|
|
5287
|
+
{
|
|
5288
|
+
"name": "menu",
|
|
5289
|
+
"takes": "anything",
|
|
5290
|
+
"required": false,
|
|
5291
|
+
"note": "The heading's \"More options\" menu: `MenuItem`s — what acts on the whole section, a folder's Rename or Archive (Katerina, 5 October: Peek's Folders page goes, and its folder actions move onto the folder's heading)."
|
|
5292
|
+
},
|
|
5293
|
+
{
|
|
5294
|
+
"name": "menuLabel",
|
|
5295
|
+
"takes": "text",
|
|
5296
|
+
"required": false,
|
|
5297
|
+
"note": "The ⋮'s accessible name. Default \"More options for\" the title."
|
|
5298
|
+
},
|
|
5224
5299
|
{
|
|
5225
5300
|
"name": "showActions",
|
|
5226
5301
|
"takes": "hover | always",
|
|
@@ -5284,11 +5359,11 @@
|
|
|
5284
5359
|
"docPage": "src/SectionHeader.mdx",
|
|
5285
5360
|
"app": null,
|
|
5286
5361
|
"looks": [
|
|
5362
|
+
"opacity-0 transition-[opacity,transform] duration-150 group-hover:opacity-100",
|
|
5363
|
+
"opacity-0 transition-[opacity,transform] duration-150 group-hover:opacity-100 rotate-90",
|
|
5287
5364
|
"transition-transform duration-150 text-text-secondary rotate-90",
|
|
5288
5365
|
"rounded-lg transition-colors text-text-secondary hover:text-text-primary",
|
|
5289
|
-
"rounded-lg transition-colors text-text-secondary hover:text-text-primary hover:bg-bg-hover"
|
|
5290
|
-
"-mr-1 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100",
|
|
5291
|
-
"opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100"
|
|
5366
|
+
"rounded-lg transition-colors text-text-secondary hover:text-text-primary hover:bg-bg-hover"
|
|
5292
5367
|
]
|
|
5293
5368
|
},
|
|
5294
5369
|
{
|
|
@@ -5349,19 +5424,15 @@
|
|
|
5349
5424
|
"docPage": "src/SectionLabel.mdx",
|
|
5350
5425
|
"app": null,
|
|
5351
5426
|
"looks": [
|
|
5352
|
-
"text-h5 leading-3
|
|
5353
|
-
"text-h5 leading-3
|
|
5354
|
-
"text-h5 leading-3
|
|
5355
|
-
"text-h5 leading-3
|
|
5356
|
-
"text-h5 leading-3
|
|
5357
|
-
"text-h5 leading-3
|
|
5358
|
-
"text-h5 leading-3
|
|
5359
|
-
"text-h5 leading-3
|
|
5360
|
-
"text-h5 leading-3
|
|
5361
|
-
"text-h5 leading-3 signal:font-mono signal:text-small signal:uppercase signal:tracking-widest leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest text-text-muted truncate",
|
|
5362
|
-
"text-h5 leading-3 signal:font-mono signal:text-small signal:uppercase signal:tracking-widest leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest text-text-primary",
|
|
5363
|
-
"text-h5 leading-3 signal:font-mono signal:text-small signal:uppercase signal:tracking-widest leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest text-text-primary truncate",
|
|
5364
|
-
"text-h5 leading-3 signal:font-mono signal:text-small signal:uppercase signal:tracking-widest",
|
|
5427
|
+
"text-h5 leading-3 text-text-secondary truncate",
|
|
5428
|
+
"text-h5 leading-3 text-text-muted truncate",
|
|
5429
|
+
"text-h5 leading-3 text-text-primary truncate",
|
|
5430
|
+
"text-h5 leading-3 leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest text-text-secondary",
|
|
5431
|
+
"text-h5 leading-3 leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest text-text-secondary truncate",
|
|
5432
|
+
"text-h5 leading-3 leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest text-text-muted",
|
|
5433
|
+
"text-h5 leading-3 leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest text-text-muted truncate",
|
|
5434
|
+
"text-h5 leading-3 leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest text-text-primary",
|
|
5435
|
+
"text-h5 leading-3 leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest text-text-primary truncate",
|
|
5365
5436
|
"leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest"
|
|
5366
5437
|
]
|
|
5367
5438
|
},
|
|
@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
|
2
2
|
import { IconPlus } from '@tabler/icons-react'
|
|
3
3
|
import { Chip } from './Chip'
|
|
4
4
|
import { CollapsibleSection } from './CollapsibleSection'
|
|
5
|
+
import { MenuItem } from './Menu'
|
|
5
6
|
import { NavItem } from './NavItem'
|
|
6
7
|
|
|
7
8
|
/** A section that opens and closes: the header's title is the toggle, and the rows slide. */
|
|
@@ -9,7 +10,7 @@ const meta = {
|
|
|
9
10
|
title: 'Navigation/CollapsibleSection',
|
|
10
11
|
component: CollapsibleSection,
|
|
11
12
|
decorators: [(Story) => <div className="w-[280px]"><Story /></div>],
|
|
12
|
-
args: { title: 'Section', contentClassName: 'gap-
|
|
13
|
+
args: { title: 'Section', contentClassName: 'gap-0.5', children: null },
|
|
13
14
|
argTypes: { children: { control: false }, actions: { control: false } },
|
|
14
15
|
render: (args) => (
|
|
15
16
|
<CollapsibleSection {...args}>
|
|
@@ -48,6 +49,18 @@ export const AsARow: Story = {
|
|
|
48
49
|
args: { title: 'Folder', look: 'row' },
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/** `menu`: the section's "More options" ⋮, after its actions — a folder's Rename, Archive. Hover the title. */
|
|
53
|
+
export const WithMenu: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
menu: (
|
|
56
|
+
<>
|
|
57
|
+
<MenuItem label="Rename" />
|
|
58
|
+
<MenuItem label="Delete" destructive />
|
|
59
|
+
</>
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
/** `indent`: the heading look, its rows starting under its title, past the chevron — a folder in a sidebar. */
|
|
52
65
|
export const Indented: Story = {
|
|
53
66
|
args: { title: 'Group', indent: true },
|
|
@@ -32,10 +32,16 @@ export interface CollapsibleSectionProps {
|
|
|
32
32
|
onOpenChange?: (open: boolean) => void
|
|
33
33
|
/** Remember open or closed in this browser, under this key. The app prefixes it. */
|
|
34
34
|
storageKey?: string
|
|
35
|
+
/** 16px, before the title; the arrow takes its place on hover — `SectionHeader`'s. */
|
|
36
|
+
icon?: ReactNode
|
|
35
37
|
/** Beside the title and always visible, before the actions — a count. `SectionHeader`'s. */
|
|
36
38
|
trailing?: ReactNode
|
|
37
39
|
/** Beside the title, revealed on hover or focus — `SectionHeader`'s. */
|
|
38
40
|
actions?: SectionAction[]
|
|
41
|
+
/** Beside the actions, revealed with them: the section's "More options" menu, `MenuItem`s — `SectionHeader`'s. */
|
|
42
|
+
menu?: ReactNode
|
|
43
|
+
/** The ⋮'s accessible name — `SectionHeader`'s. */
|
|
44
|
+
menuLabel?: string
|
|
39
45
|
showActions?: 'hover' | 'always'
|
|
40
46
|
/**
|
|
41
47
|
* `heading` (default): the title is a section heading. `row`: the fold is a
|
|
@@ -79,7 +85,7 @@ function writeStored(key: string | undefined, open: boolean) {
|
|
|
79
85
|
}
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
export function CollapsibleSection({ title, defaultOpen = true, open: openProp, onOpenChange, storageKey, trailing, actions, showActions, look = 'heading', indent = false, children, className, contentClassName }: CollapsibleSectionProps) {
|
|
88
|
+
export function CollapsibleSection({ title, defaultOpen = true, open: openProp, onOpenChange, storageKey, icon, trailing, actions, menu, menuLabel, showActions, look = 'heading', indent = false, children, className, contentClassName }: CollapsibleSectionProps) {
|
|
83
89
|
const [openState, setOpenState] = useState(() => readStored(storageKey) ?? defaultOpen)
|
|
84
90
|
const open = openProp ?? openState
|
|
85
91
|
const setOpen = (next: boolean) => {
|
|
@@ -89,7 +95,7 @@ export function CollapsibleSection({ title, defaultOpen = true, open: openProp,
|
|
|
89
95
|
}
|
|
90
96
|
return (
|
|
91
97
|
<Collapsible.Root open={open} onOpenChange={setOpen} className={cn('flex flex-col', className)}>
|
|
92
|
-
<SectionHeader title={title} chevron isExpanded={open} trailing={trailing} actions={actions} showActions={showActions} look={look} className="shrink-0" render={<Collapsible.Trigger />} />
|
|
98
|
+
<SectionHeader title={title} chevron icon={icon} isExpanded={open} trailing={trailing} actions={actions} menu={menu} menuLabel={menuLabel} showActions={showActions} look={look} className="shrink-0" render={<Collapsible.Trigger />} />
|
|
93
99
|
{/* The slide: Base UI measures the panel and writes its height to a
|
|
94
100
|
variable — `auto` again once the slide ends, so rows that arrive
|
|
95
101
|
later are not clipped — and the panel is 0 high on its opening frame
|
|
@@ -101,8 +107,8 @@ export function CollapsibleSection({ title, defaultOpen = true, open: openProp,
|
|
|
101
107
|
className={COLLAPSIBLE_PANEL_CLASSES}
|
|
102
108
|
>
|
|
103
109
|
{/* A row's rows start under its title: the 16px chevron and its 8px gap
|
|
104
|
-
(pl-6). An indented heading's: its
|
|
105
|
-
<div className={cn('flex flex-col', look === 'row'
|
|
110
|
+
(pl-6). An indented heading's: the same — its arrow is a row's now (5 October). */}
|
|
111
|
+
<div className={cn('flex flex-col pt-0.5', (look === 'row' || indent) && 'pl-6', contentClassName)}>{children}</div>
|
|
106
112
|
</Collapsible.Panel>
|
|
107
113
|
</Collapsible.Root>
|
|
108
114
|
)
|
package/src/ContainerHeader.mdx
CHANGED
|
@@ -17,6 +17,10 @@ with a hairline under it.
|
|
|
17
17
|
thread, a side panel. One per column.
|
|
18
18
|
- The column's own actions — new, sort, close — as IconButtons with tooltips
|
|
19
19
|
in `actions`.
|
|
20
|
+
- `titleMenu`: the title opens a menu of the column's views, its chevron
|
|
21
|
+
drawn; the title and the chevron are one button.
|
|
22
|
+
|
|
23
|
+
<Canvas of={ContainerHeaderStories.WithTitleMenu} />
|
|
20
24
|
|
|
21
25
|
## When not
|
|
22
26
|
|
|
@@ -2,6 +2,7 @@ import type { Decorator, Meta, StoryObj } from '@storybook/react-vite'
|
|
|
2
2
|
import { IconEdit, IconSortDescending, IconX } from '@tabler/icons-react'
|
|
3
3
|
import { ContainerHeader } from './ContainerHeader'
|
|
4
4
|
import { IconButton } from './IconButton'
|
|
5
|
+
import { MenuItem } from './Menu'
|
|
5
6
|
import { Card } from './Card'
|
|
6
7
|
|
|
7
8
|
// The top of a column: a surface with a hairline, so the bar's own hairline and height can be seen.
|
|
@@ -18,6 +19,19 @@ const meta = {
|
|
|
18
19
|
export default meta
|
|
19
20
|
type Story = StoryObj<typeof meta>
|
|
20
21
|
|
|
22
|
+
/** A title that opens a menu: the title and its chevron are one button. Click it. */
|
|
23
|
+
export const WithTitleMenu: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
title: 'View one',
|
|
26
|
+
titleMenu: (
|
|
27
|
+
<>
|
|
28
|
+
<MenuItem label="View one" selected />
|
|
29
|
+
<MenuItem label="View two" />
|
|
30
|
+
</>
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
/** A string title, one line, alone. */
|
|
22
36
|
export const Default: Story = {}
|
|
23
37
|
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
9
9
|
import { cleanup, render, screen } from '@testing-library/react'
|
|
10
|
+
import userEvent from '@testing-library/user-event'
|
|
11
|
+
import { MenuItem } from './Menu'
|
|
10
12
|
import { ContainerHeader } from './ContainerHeader'
|
|
11
13
|
|
|
12
14
|
afterEach(cleanup)
|
|
@@ -44,4 +46,10 @@ describe('ContainerHeader', () => {
|
|
|
44
46
|
expect(container.firstElementChild!.children).toHaveLength(2)
|
|
45
47
|
expect(screen.getByRole('button', { name: 'New' }).closest('div')!.className).toContain('gap-1')
|
|
46
48
|
})
|
|
49
|
+
|
|
50
|
+
it('a title with a menu is one button, and opens it', async () => {
|
|
51
|
+
render(<ContainerHeader title="View one" titleMenu={<MenuItem label="View two" />} />)
|
|
52
|
+
await userEvent.click(screen.getByRole('button', { name: 'View one' }))
|
|
53
|
+
expect(await screen.findByRole('menuitem', { name: 'View two' })).not.toBeNull()
|
|
54
|
+
})
|
|
47
55
|
})
|
package/src/ContainerHeader.tsx
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import { IconChevronDown } from '@tabler/icons-react'
|
|
3
|
+
import { Button as BaseButton } from '@base-ui/react/button'
|
|
3
4
|
import { cn } from './cn'
|
|
5
|
+
import { MENU_TRIGGER_CLASSES } from './looks'
|
|
6
|
+
import { Menu } from './Menu'
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* The bar across the top of a column — a list, a thread, a side panel: its
|
|
@@ -20,20 +23,45 @@ export interface ContainerHeaderProps {
|
|
|
20
23
|
title: ReactNode
|
|
21
24
|
/** A chevron after the title, for a title that opens something. */
|
|
22
25
|
chevron?: boolean
|
|
26
|
+
/**
|
|
27
|
+
* The menu the title opens: `MenuItem`s — the column's views (Katerina,
|
|
28
|
+
* 5 October: Peek's Topics column, Topics or Folders). The title and its
|
|
29
|
+
* chevron are then one button, lit under the pointer; the chevron is drawn.
|
|
30
|
+
*/
|
|
31
|
+
titleMenu?: ReactNode
|
|
23
32
|
/** The column's own buttons, at the right edge: IconButtons with tooltips, 4px apart. */
|
|
24
33
|
actions?: ReactNode
|
|
25
34
|
className?: string
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
export function ContainerHeader({ title, chevron = false, actions, className }: ContainerHeaderProps) {
|
|
37
|
+
export function ContainerHeader({ title, chevron = false, titleMenu, actions, className }: ContainerHeaderProps) {
|
|
29
38
|
const text = typeof title === 'string'
|
|
39
|
+
const name = (
|
|
40
|
+
<div className={cn('flex items-center gap-1', text ? 'shrink-0' : 'min-w-0 flex-1')}>
|
|
41
|
+
{text ? <span className="whitespace-nowrap text-body-2-strong text-text-primary">{title}</span> : <div className="min-w-0 flex-1">{title}</div>}
|
|
42
|
+
{(chevron || titleMenu != null) && <IconChevronDown size={12} stroke={1.5} className="shrink-0 text-text-secondary" />}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
30
45
|
return (
|
|
31
|
-
|
|
46
|
+
// With a title menu the bar gives the button 6px of its left padding
|
|
47
|
+
// (pl-3.5 + the button's px-1.5 = pl-5): the words stay where a title's
|
|
48
|
+
// are, and the fill is inside the bar — a negative margin had it clipped
|
|
49
|
+
// (Katerina, 5 October).
|
|
50
|
+
<div className={cn('flex h-12 shrink-0 items-center justify-between overflow-hidden border-b border-border-subtle py-2 pr-4', titleMenu != null ? 'pl-3.5' : 'pl-5', className)}>
|
|
32
51
|
<div className={cn('flex items-center gap-2 overflow-hidden', text ? 'shrink-0' : 'min-w-0 flex-1')}>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
{titleMenu != null ? (
|
|
53
|
+
<Menu
|
|
54
|
+
trigger={
|
|
55
|
+
<BaseButton type="button" className={cn('flex min-w-0 items-center px-1.5 py-1', MENU_TRIGGER_CLASSES)}>
|
|
56
|
+
{name}
|
|
57
|
+
</BaseButton>
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
{titleMenu}
|
|
61
|
+
</Menu>
|
|
62
|
+
) : (
|
|
63
|
+
name
|
|
64
|
+
)}
|
|
37
65
|
</div>
|
|
38
66
|
{actions && (
|
|
39
67
|
<div className="flex shrink-0 items-center justify-end gap-3">
|
package/src/IconButton.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComponentPropsWithRef, ReactNode } from 'react'
|
|
2
2
|
import { Button as BaseButton } from '@base-ui/react/button'
|
|
3
3
|
import { cn } from './cn'
|
|
4
|
+
import { MUTED_CONTROL_CLASSES } from './looks'
|
|
4
5
|
import { useFormBusy } from './formBusy'
|
|
5
6
|
import { TooltipTrigger } from './Tooltip'
|
|
6
7
|
|
|
@@ -82,7 +83,7 @@ export function IconButton({
|
|
|
82
83
|
// stretched and carries no `self-center` — see the note there.
|
|
83
84
|
'flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 self-center cursor-pointer',
|
|
84
85
|
!off && variant === 'primary' && 'bg-accent-primary hover:bg-accent-hover text-text-inverse',
|
|
85
|
-
!off && (variant === 'muted' || variant === 'resolve') &&
|
|
86
|
+
!off && (variant === 'muted' || variant === 'resolve') && MUTED_CONTROL_CLASSES,
|
|
86
87
|
!off && variant === 'outlined' && 'border border-border-default hover:bg-bg-hover text-text-secondary',
|
|
87
88
|
!off && variant === 'current' && 'hover:bg-bg-hover',
|
|
88
89
|
off && variant === 'primary' && 'bg-bg-disabled text-text-disabled',
|
package/src/ListColumn.tsx
CHANGED
|
@@ -32,6 +32,8 @@ export interface ListColumnProps {
|
|
|
32
32
|
title: ReactNode
|
|
33
33
|
/** A chevron after the title, as `ContainerHeader` draws it. */
|
|
34
34
|
chevron?: boolean
|
|
35
|
+
/** The menu the title opens — `ContainerHeader`'s: the column's views. */
|
|
36
|
+
titleMenu?: ReactNode
|
|
35
37
|
/** The column's own actions, at the header's right — a `Toolbar` of `ToolbarButton`s, or one `IconButton`. */
|
|
36
38
|
actions?: ReactNode
|
|
37
39
|
/** A row between the header and the list that stays put while the list scrolls — a field that adds to the list. */
|
|
@@ -44,7 +46,7 @@ export interface ListColumnProps {
|
|
|
44
46
|
className?: string
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
function Column({ title, chevron = false, actions, above, collapsed = false, children, className, broken = false }: ListColumnProps & { broken?: boolean }) {
|
|
49
|
+
function Column({ title, chevron = false, titleMenu, actions, above, collapsed = false, children, className, broken = false }: ListColumnProps & { broken?: boolean }) {
|
|
48
50
|
return (
|
|
49
51
|
<div
|
|
50
52
|
className={cn(
|
|
@@ -55,7 +57,7 @@ function Column({ title, chevron = false, actions, above, collapsed = false, chi
|
|
|
55
57
|
)}
|
|
56
58
|
data-collapsed={collapsed || undefined}
|
|
57
59
|
>
|
|
58
|
-
<ContainerHeader title={title} chevron={chevron} actions={actions} />
|
|
60
|
+
<ContainerHeader title={title} chevron={chevron} titleMenu={titleMenu} actions={actions} />
|
|
59
61
|
{above}
|
|
60
62
|
{broken ? (
|
|
61
63
|
// The message takes the room the list had and centres itself in it (Katerina, 19 September).
|
package/src/Menu.tsx
CHANGED
|
@@ -95,7 +95,8 @@ export function MenuPanel({ children, className, ...props }: MenuPanelProps) {
|
|
|
95
95
|
return (
|
|
96
96
|
<div
|
|
97
97
|
className={cn(
|
|
98
|
-
'
|
|
98
|
+
// 2px between rows, as a list column's and a sidebar's (Katerina, 5 October).
|
|
99
|
+
'flex flex-col gap-0.5 p-2',
|
|
99
100
|
FLOATING_SURFACE_CLASSES,
|
|
100
101
|
/*
|
|
101
102
|
* A divider in a menu runs the width of the rows it separates.
|
|
@@ -248,7 +249,7 @@ export function Menu({ trigger, align = 'left', openOnHover = false, open, onOpe
|
|
|
248
249
|
* sentence was written at 0.12.6 and the prop was not: a `p-1` on
|
|
249
250
|
* `className` added 4px to these 8px instead of replacing them
|
|
250
251
|
* (PLAN Finding 60). The prop exists now. */}
|
|
251
|
-
<ScrollArea viewportClassName="max-h-[var(--available-height)]" contentClassName={cn('flex flex-col p-2', contentClassName)}>
|
|
252
|
+
<ScrollArea viewportClassName="max-h-[var(--available-height)]" contentClassName={cn('flex flex-col gap-0.5 p-2', contentClassName)}>
|
|
252
253
|
<MenuContext.Provider value={{ openOnHover }}>
|
|
253
254
|
<DividerInPanel.Provider value={true}>{children}</DividerInPanel.Provider>
|
|
254
255
|
</MenuContext.Provider>
|
|
@@ -578,14 +579,14 @@ export function MenuSection({ label, children, className }: { label: string; chi
|
|
|
578
579
|
)
|
|
579
580
|
if (!inMenu) {
|
|
580
581
|
return (
|
|
581
|
-
<div className="flex flex-col">
|
|
582
|
+
<div className="flex flex-col gap-0.5">
|
|
582
583
|
{heading}
|
|
583
584
|
{children}
|
|
584
585
|
</div>
|
|
585
586
|
)
|
|
586
587
|
}
|
|
587
588
|
return (
|
|
588
|
-
<BaseMenu.Group className="flex flex-col">
|
|
589
|
+
<BaseMenu.Group className="flex flex-col gap-0.5">
|
|
589
590
|
<BaseMenu.GroupLabel render={heading} />
|
|
590
591
|
{children}
|
|
591
592
|
</BaseMenu.Group>
|
package/src/NavItem.test.tsx
CHANGED
|
@@ -99,10 +99,10 @@ describe('NavItem unread', () => {
|
|
|
99
99
|
expect(container.querySelector('[data-urgent], [data-unread]')).toBeNull()
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
it('with a menu, the ⋮
|
|
102
|
+
it('with a menu over the dot, the ⋮ ends where a heading’s ⋮ ends', () => {
|
|
103
103
|
render(<NavItem href="#" label="Item one" unread menu={<span>Action</span>} />)
|
|
104
104
|
const menu = screen.getByRole('button', { name: 'More options for Item one' }).closest('[data-nav-menu]')
|
|
105
|
-
expect(menu?.className).toContain('right-
|
|
105
|
+
expect(menu?.className).toContain('right-1')
|
|
106
106
|
})
|
|
107
107
|
|
|
108
108
|
it('with a count and a menu, the ⋮ lands on the count', () => {
|
package/src/NavItem.tsx
CHANGED
|
@@ -108,7 +108,11 @@ export function NavItem({
|
|
|
108
108
|
// What sits at the right at rest, after the count: the dot, or the badge.
|
|
109
109
|
const mark = unread ? (urgent ? <UrgentMark /> : <UnreadDot />) : null
|
|
110
110
|
// Under the ⋮ while it shows: the count and the mark step aside for it.
|
|
111
|
-
|
|
111
|
+
// "While it shows" is the pointer on the row or the keyboard in it
|
|
112
|
+
// (`:focus-visible`), never any focus: a clicked row keeps its focus, and
|
|
113
|
+
// with `focus-within` its icon and dot went while the arrow and the ⋮ stayed
|
|
114
|
+
// hidden (Katerina, 5 October).
|
|
115
|
+
const stepsAside = menu != null && 'transition-opacity group-hover/nav:opacity-0 group-has-[:focus-visible]/nav:opacity-0 group-has-[[data-nav-menu]_[aria-expanded=true]]/nav:opacity-0'
|
|
112
116
|
|
|
113
117
|
const row = (
|
|
114
118
|
<a
|
|
@@ -135,7 +139,7 @@ export function NavItem({
|
|
|
135
139
|
className={cn(
|
|
136
140
|
// With rows under it the slot is kept even with no icon: the arrow lands in it.
|
|
137
141
|
'flex min-w-4 shrink-0 items-center',
|
|
138
|
-
hasRows && 'transition-opacity group-hover/nav:opacity-0 group-focus-
|
|
142
|
+
hasRows && 'transition-opacity group-hover/nav:opacity-0 group-has-[:focus-visible]/nav:opacity-0',
|
|
139
143
|
)}
|
|
140
144
|
>
|
|
141
145
|
{icon}
|
|
@@ -144,7 +148,7 @@ export function NavItem({
|
|
|
144
148
|
<span className={cn('flex-1 truncate', unread && 'font-medium')}>{label}</span>
|
|
145
149
|
{hint != null && (
|
|
146
150
|
// Takes room only while it shows, so the label gives way to it.
|
|
147
|
-
<span className="hidden shrink-0 items-center group-hover/nav:flex group-focus-
|
|
151
|
+
<span className="hidden shrink-0 items-center group-hover/nav:flex group-has-[:focus-visible]/nav:flex">{hint}</span>
|
|
148
152
|
)}
|
|
149
153
|
{count ? (
|
|
150
154
|
<WithTooltip label={countLabel ?? `${count} open`}>
|
|
@@ -164,12 +168,15 @@ export function NavItem({
|
|
|
164
168
|
</WithTooltip>
|
|
165
169
|
) : null}
|
|
166
170
|
{mark ? (
|
|
167
|
-
|
|
171
|
+
// `-mr-1`: the 24px slot's dot then sits on the axis of a count and
|
|
172
|
+
// of the ⋮ (8px from the edge to the 16px box), one column with a
|
|
173
|
+
// folder heading's dot and ⋮ (Katerina, 5 October).
|
|
174
|
+
<span className={cn('-mr-1 flex shrink-0', stepsAside)}>{mark}</span>
|
|
168
175
|
) : menu != null && !count ? (
|
|
169
176
|
// Nothing at the right, but a menu: no room is kept at rest, so the
|
|
170
177
|
// label has the row's whole width; while the ⋮ shows, its 24px slot
|
|
171
178
|
// is kept, so a long label ends before it instead of running under it.
|
|
172
|
-
<span aria-hidden="true" className="hidden w-6 shrink-0 group-hover/nav:block group-focus-
|
|
179
|
+
<span aria-hidden="true" className="hidden w-6 shrink-0 group-hover/nav:block group-has-[:focus-visible]/nav:block group-has-[[data-nav-menu]_[aria-expanded=true]]/nav:block" />
|
|
173
180
|
) : null}
|
|
174
181
|
</a>
|
|
175
182
|
)
|
|
@@ -181,7 +188,7 @@ export function NavItem({
|
|
|
181
188
|
{hasRows && (
|
|
182
189
|
// `left-1`: the 24px button's 16px arrow then sits where the icon
|
|
183
190
|
// sits (the row's px-2), so the icon turns into the arrow in place.
|
|
184
|
-
<div className="absolute inset-y-0 left-1 flex items-center opacity-0 transition-opacity
|
|
191
|
+
<div className="absolute inset-y-0 left-1 flex items-center opacity-0 transition-opacity group-hover/nav:opacity-100 group-has-[:focus-visible]/nav:opacity-100">
|
|
185
192
|
<IconButton
|
|
186
193
|
tooltip={open ? 'Collapse' : 'Expand'}
|
|
187
194
|
aria-label={open ? `Hide what is under ${label}` : `Show what is under ${label}`}
|
|
@@ -194,11 +201,13 @@ export function NavItem({
|
|
|
194
201
|
</div>
|
|
195
202
|
)}
|
|
196
203
|
{menu != null && (
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
204
|
+
// `right-1`: the 24px button's 16px icon ends 8px from the row's
|
|
205
|
+
// edge, where a count ends (px-2) — and where a SectionHeader's
|
|
206
|
+
// actions and ⋮ end, so a folder's ⋮ and its rows' ⋮ are one column
|
|
207
|
+
// (Katerina, 5 October: the rows move to the heading, not the
|
|
208
|
+
// heading to the rows). Over a dot or the empty slot it sits 4px
|
|
209
|
+
// right of the slot's centre.
|
|
210
|
+
<div data-nav-menu className={cn('absolute inset-y-0 right-1 flex items-center', 'opacity-0 transition-opacity group-hover/nav:opacity-100 group-has-[:focus-visible]/nav:opacity-100 has-[[aria-expanded=true]]:opacity-100')}>
|
|
202
211
|
<Menu
|
|
203
212
|
align="right"
|
|
204
213
|
trigger={
|
|
@@ -225,7 +234,7 @@ export function NavItem({
|
|
|
225
234
|
className={COLLAPSIBLE_PANEL_CLASSES}
|
|
226
235
|
>
|
|
227
236
|
{/* `pl-6`: past the 16px icon and its 8px gap, so the rows' icons start under this row's label. */}
|
|
228
|
-
<div className="flex flex-col gap-
|
|
237
|
+
<div className="flex flex-col gap-0.5 pt-0.5 pl-6">{children}</div>
|
|
229
238
|
</Collapsible.Panel>
|
|
230
239
|
</Collapsible.Root>
|
|
231
240
|
)
|