@estiva-app/ui 0.53.1 → 0.54.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/NavItem.d.ts +14 -1
- package/dist/NavItem.d.ts.map +1 -1
- package/dist/NavTree.d.ts +93 -13
- package/dist/NavTree.d.ts.map +1 -1
- package/dist/UnreadDot.d.ts +12 -0
- package/dist/UnreadDot.d.ts.map +1 -0
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/index.js.map +2 -2
- package/dist/eslint/no-native-title.d.ts +1 -1
- package/dist/eslint/no-native-title.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +215 -141
- package/dist/index.js.map +4 -4
- package/dist/looks.d.ts +1 -1
- package/dist/looks.d.ts.map +1 -1
- package/package.json +1 -1
- package/registry.json +154 -11
- package/src/NavItem.mdx +14 -1
- package/src/NavItem.stories.tsx +15 -0
- package/src/NavItem.test.tsx +31 -0
- package/src/NavItem.tsx +49 -12
- package/src/NavTree.mdx +25 -0
- package/src/NavTree.stories.tsx +57 -1
- package/src/NavTree.test.tsx +78 -1
- package/src/NavTree.tsx +175 -40
- package/src/SectionLabel.stories.tsx +2 -3
- package/src/UnreadDot.mdx +57 -0
- package/src/UnreadDot.stories.tsx +22 -0
- package/src/UnreadDot.test.tsx +17 -0
- package/src/UnreadDot.tsx +17 -0
- package/src/eslint/no-native-title.ts +1 -1
- package/src/index.ts +2 -1
- package/src/looks.ts +2 -1
- package/src/registry/registry.test.ts +5 -5
- package/stories/Choosing.mdx +1 -0
- package/tailwind-preset.js +1 -0
- package/tokens.css +5 -0
package/dist/looks.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ 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
|
-
export declare const SIDEBAR_ROW_CLASSES = "rounded-
|
|
37
|
+
export declare const SIDEBAR_ROW_CLASSES = "rounded-lg transition-colors";
|
|
38
38
|
export declare const SIDEBAR_ROW_TEXT_CLASSES = "text-text-secondary hover:text-text-primary";
|
|
39
39
|
/**
|
|
40
40
|
* A panel that slides open and shut on Base UI's Collapsible: Base UI measures
|
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;
|
|
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"}
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"kind": "package",
|
|
5
5
|
"repo": "estiva-ui",
|
|
6
6
|
"package": "@estiva-app/ui",
|
|
7
|
-
"packageVersion": "0.
|
|
8
|
-
"exports":
|
|
9
|
-
"typeExports":
|
|
7
|
+
"packageVersion": "0.54.0",
|
|
8
|
+
"exports": 122,
|
|
9
|
+
"typeExports": 129,
|
|
10
10
|
"files": null
|
|
11
11
|
},
|
|
12
12
|
"storybook": {
|
|
@@ -3657,6 +3657,18 @@
|
|
|
3657
3657
|
"required": false,
|
|
3658
3658
|
"note": "Shown only while the row is pointed at or focused, left of the count: a neutral `Chip`, typically."
|
|
3659
3659
|
},
|
|
3660
|
+
{
|
|
3661
|
+
"name": "unread",
|
|
3662
|
+
"takes": "true/false",
|
|
3663
|
+
"required": false,
|
|
3664
|
+
"note": "Something new here: brighter words, the label in medium, and the `UnreadDot` at the right."
|
|
3665
|
+
},
|
|
3666
|
+
{
|
|
3667
|
+
"name": "urgent",
|
|
3668
|
+
"takes": "true/false",
|
|
3669
|
+
"required": false,
|
|
3670
|
+
"note": "With `unread`: the warning badge in the dot's place."
|
|
3671
|
+
},
|
|
3660
3672
|
{
|
|
3661
3673
|
"name": "children",
|
|
3662
3674
|
"takes": "anything",
|
|
@@ -3702,8 +3714,9 @@
|
|
|
3702
3714
|
"docPage": "src/NavItem.mdx",
|
|
3703
3715
|
"app": null,
|
|
3704
3716
|
"looks": [
|
|
3705
|
-
"text-body-2 rounded-
|
|
3706
|
-
"text-body-2 rounded-
|
|
3717
|
+
"text-body-2 rounded-lg transition-colors bg-bg-nav-active text-text-primary",
|
|
3718
|
+
"text-body-2 rounded-lg transition-colors text-text-primary hover:bg-bg-hover",
|
|
3719
|
+
"text-body-2 rounded-lg transition-colors text-text-secondary hover:text-text-primary hover:bg-bg-hover",
|
|
3707
3720
|
"text-center font-mono text-caption tabular-nums text-text-muted",
|
|
3708
3721
|
"text-center font-mono text-caption tabular-nums text-text-muted transition-opacity",
|
|
3709
3722
|
"overflow-hidden transition-[height] duration-150 ease-out motion-reduce:transition-none"
|
|
@@ -3724,11 +3737,23 @@
|
|
|
3724
3737
|
"required": false,
|
|
3725
3738
|
"note": "The quiet label over the groups. Absent, none is drawn."
|
|
3726
3739
|
},
|
|
3740
|
+
{
|
|
3741
|
+
"name": "titleActions",
|
|
3742
|
+
"takes": "SectionAction[]",
|
|
3743
|
+
"required": false,
|
|
3744
|
+
"note": "Beside the label, always shown: \"New\", typically."
|
|
3745
|
+
},
|
|
3727
3746
|
{
|
|
3728
3747
|
"name": "groups",
|
|
3729
3748
|
"takes": "NavTreeGroup[] | null",
|
|
3730
|
-
"required":
|
|
3731
|
-
"note": "`null` while the first read is on its way: a skeleton in the
|
|
3749
|
+
"required": false,
|
|
3750
|
+
"note": "`null` while the first read is on its way: a skeleton in the groups' place."
|
|
3751
|
+
},
|
|
3752
|
+
{
|
|
3753
|
+
"name": "children",
|
|
3754
|
+
"takes": "anything",
|
|
3755
|
+
"required": false,
|
|
3756
|
+
"note": "The groups, drawn by the app: `NavTreeSection`s with `NavItem` rows — for an app whose groups and rows each read their own data."
|
|
3732
3757
|
},
|
|
3733
3758
|
{
|
|
3734
3759
|
"name": "selected",
|
|
@@ -3745,8 +3770,8 @@
|
|
|
3745
3770
|
{
|
|
3746
3771
|
"name": "emptyMessage",
|
|
3747
3772
|
"takes": "text",
|
|
3748
|
-
"required":
|
|
3749
|
-
"note": "What to say when
|
|
3773
|
+
"required": false,
|
|
3774
|
+
"note": "What to say when `groups` is empty. The words are the caller's; absent, nothing is said."
|
|
3750
3775
|
},
|
|
3751
3776
|
{
|
|
3752
3777
|
"name": "storageKey",
|
|
@@ -3765,6 +3790,98 @@
|
|
|
3765
3790
|
"app": null,
|
|
3766
3791
|
"looks": []
|
|
3767
3792
|
},
|
|
3793
|
+
{
|
|
3794
|
+
"name": "NavTreeSection",
|
|
3795
|
+
"repo": "estiva-ui",
|
|
3796
|
+
"kind": "component",
|
|
3797
|
+
"importPath": "@estiva-app/ui",
|
|
3798
|
+
"sourceFile": "src/NavTree.tsx",
|
|
3799
|
+
"purpose": "One group of a `NavTree`, for an app that draws its own: the standard heading folding its rows, lightly indented under its title, with the group's line, skeleton, actions and `onIntent`.",
|
|
3800
|
+
"purposeFrom": "comment",
|
|
3801
|
+
"props": [
|
|
3802
|
+
{
|
|
3803
|
+
"name": "title",
|
|
3804
|
+
"takes": "text",
|
|
3805
|
+
"required": true,
|
|
3806
|
+
"note": null
|
|
3807
|
+
},
|
|
3808
|
+
{
|
|
3809
|
+
"name": "id",
|
|
3810
|
+
"takes": "text",
|
|
3811
|
+
"required": false,
|
|
3812
|
+
"note": "Marks the group's box as `data-nav-group`, for tests and photo runs to find it."
|
|
3813
|
+
},
|
|
3814
|
+
{
|
|
3815
|
+
"name": "message",
|
|
3816
|
+
"takes": "text",
|
|
3817
|
+
"required": false,
|
|
3818
|
+
"note": "Said in the rows' place, level with them."
|
|
3819
|
+
},
|
|
3820
|
+
{
|
|
3821
|
+
"name": "trailing",
|
|
3822
|
+
"takes": "anything",
|
|
3823
|
+
"required": false,
|
|
3824
|
+
"note": "Beside the title, always shown: an `UnreadDot`, typically."
|
|
3825
|
+
},
|
|
3826
|
+
{
|
|
3827
|
+
"name": "actions",
|
|
3828
|
+
"takes": "SectionAction[]",
|
|
3829
|
+
"required": false,
|
|
3830
|
+
"note": "Beside the title, shown on hover or focus: \"Open\", typically."
|
|
3831
|
+
},
|
|
3832
|
+
{
|
|
3833
|
+
"name": "defaultOpen",
|
|
3834
|
+
"takes": "true/false",
|
|
3835
|
+
"required": false,
|
|
3836
|
+
"note": "Open unless told otherwise."
|
|
3837
|
+
},
|
|
3838
|
+
{
|
|
3839
|
+
"name": "open",
|
|
3840
|
+
"takes": "true/false",
|
|
3841
|
+
"required": false,
|
|
3842
|
+
"note": "The caller owns whether the group is open."
|
|
3843
|
+
},
|
|
3844
|
+
{
|
|
3845
|
+
"name": "onOpenChange",
|
|
3846
|
+
"takes": "a handler",
|
|
3847
|
+
"required": false,
|
|
3848
|
+
"note": null
|
|
3849
|
+
},
|
|
3850
|
+
{
|
|
3851
|
+
"name": "onIntent",
|
|
3852
|
+
"takes": "a handler",
|
|
3853
|
+
"required": false,
|
|
3854
|
+
"note": "The pointer rests on the group, or the keyboard reaches it: read its rows now."
|
|
3855
|
+
},
|
|
3856
|
+
{
|
|
3857
|
+
"name": "loading",
|
|
3858
|
+
"takes": "true/false",
|
|
3859
|
+
"required": false,
|
|
3860
|
+
"note": "Its rows are being read: a skeleton in their place."
|
|
3861
|
+
},
|
|
3862
|
+
{
|
|
3863
|
+
"name": "storageKey",
|
|
3864
|
+
"takes": "text",
|
|
3865
|
+
"required": false,
|
|
3866
|
+
"note": "Remember it open or closed in this browser, under this key."
|
|
3867
|
+
},
|
|
3868
|
+
{
|
|
3869
|
+
"name": "children",
|
|
3870
|
+
"takes": "anything",
|
|
3871
|
+
"required": false,
|
|
3872
|
+
"note": "The rows: `NavItem`s."
|
|
3873
|
+
}
|
|
3874
|
+
],
|
|
3875
|
+
"variants": [],
|
|
3876
|
+
"ownsBehaviours": [],
|
|
3877
|
+
"status": "stable",
|
|
3878
|
+
"migrationStage": null,
|
|
3879
|
+
"docsId": "frame-navtree--docs",
|
|
3880
|
+
"storyId": "frame-navtree--default",
|
|
3881
|
+
"docPage": null,
|
|
3882
|
+
"app": null,
|
|
3883
|
+
"looks": []
|
|
3884
|
+
},
|
|
3768
3885
|
{
|
|
3769
3886
|
"name": "normalizeHref",
|
|
3770
3887
|
"repo": "estiva-ui",
|
|
@@ -4979,8 +5096,8 @@
|
|
|
4979
5096
|
"app": null,
|
|
4980
5097
|
"looks": [
|
|
4981
5098
|
"transition-transform duration-150 text-text-secondary rotate-90",
|
|
4982
|
-
"rounded-
|
|
4983
|
-
"rounded-
|
|
5099
|
+
"rounded-lg transition-colors text-text-secondary hover:text-text-primary",
|
|
5100
|
+
"rounded-lg transition-colors text-text-secondary hover:text-text-primary hover:bg-bg-hover",
|
|
4984
5101
|
"-mr-1 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100",
|
|
4985
5102
|
"opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100"
|
|
4986
5103
|
]
|
|
@@ -6472,6 +6589,32 @@
|
|
|
6472
6589
|
"app": null,
|
|
6473
6590
|
"looks": []
|
|
6474
6591
|
},
|
|
6592
|
+
{
|
|
6593
|
+
"name": "UnreadDot",
|
|
6594
|
+
"repo": "estiva-ui",
|
|
6595
|
+
"kind": "component",
|
|
6596
|
+
"importPath": "@estiva-app/ui",
|
|
6597
|
+
"sourceFile": "src/UnreadDot.tsx",
|
|
6598
|
+
"purpose": "The accent dot that says \"something new here\": a 6px disc centred in a 24px slot, so it sits exactly where a control would and the row does not shift when one replaces the other.",
|
|
6599
|
+
"purposeFrom": "page",
|
|
6600
|
+
"props": [
|
|
6601
|
+
{
|
|
6602
|
+
"name": "className",
|
|
6603
|
+
"takes": "text",
|
|
6604
|
+
"required": false,
|
|
6605
|
+
"note": null
|
|
6606
|
+
}
|
|
6607
|
+
],
|
|
6608
|
+
"variants": [],
|
|
6609
|
+
"ownsBehaviours": [],
|
|
6610
|
+
"status": "stable",
|
|
6611
|
+
"migrationStage": null,
|
|
6612
|
+
"docsId": "primitives-unreaddot--docs",
|
|
6613
|
+
"storyId": "primitives-unreaddot--default",
|
|
6614
|
+
"docPage": "src/UnreadDot.mdx",
|
|
6615
|
+
"app": null,
|
|
6616
|
+
"looks": []
|
|
6617
|
+
},
|
|
6475
6618
|
{
|
|
6476
6619
|
"name": "UrgentPersonMention",
|
|
6477
6620
|
"repo": "estiva-ui",
|
package/src/NavItem.mdx
CHANGED
|
@@ -41,6 +41,13 @@ count — a muted mono number whose tooltip says what it counts.
|
|
|
41
41
|
|
|
42
42
|
<Canvas of={NavItemStories.WithHintLongLabel} />
|
|
43
43
|
|
|
44
|
+
- Something new on the row is `unread`: its words and icon brighten, the
|
|
45
|
+
label is set in medium, and the **UnreadDot** sits at the right.
|
|
46
|
+
`urgent` with it draws the warning badge in the dot's place. Say "unread"
|
|
47
|
+
in the row's own accessible name too: a dot reads as nothing.
|
|
48
|
+
|
|
49
|
+
<Canvas of={NavItemStories.Unread} />
|
|
50
|
+
|
|
44
51
|
## When not
|
|
45
52
|
|
|
46
53
|
- An icon rail's tile → **RailItem**.
|
|
@@ -73,9 +80,14 @@ import { NavItem } from '@estiva-app/ui'
|
|
|
73
80
|
`Hide what is under {label}` or `Show what is under {label}`.
|
|
74
81
|
- `hint` takes room only while it shows, so a label at rest keeps its full
|
|
75
82
|
width.
|
|
83
|
+
- The ⋮ lands on what is at the right: the count, or the dot. With nothing
|
|
84
|
+
there it keeps no room at rest, so the label has the row's whole width,
|
|
85
|
+
and takes its 24px slot only while it shows.
|
|
86
|
+
- `active` is `bg-nav-active`, the current row in a sidebar, which each theme
|
|
87
|
+
sets for itself.
|
|
76
88
|
|
|
77
89
|
- A row is 32px tall, 8px in on each side, 8px between the icon and the label,
|
|
78
|
-
the label 14px regular,
|
|
90
|
+
the label 14px regular (medium when unread), 8px corners as a **MenuItem**'s.
|
|
79
91
|
|
|
80
92
|
## What it owns
|
|
81
93
|
|
|
@@ -89,6 +101,7 @@ It does these by itself. Write none of them around it.
|
|
|
89
101
|
| Turns its icon into the arrow that folds `children` while pointed at or focused, and slides them | a positioned `IconButton` over the icon, open state, `group-hover` reveals, an indented box | |
|
|
90
102
|
| Tells assistive technology whether its rows are open | `aria-expanded`, `aria-controls` | |
|
|
91
103
|
| Shows `hint` only while pointed at or focused, and makes the label give way | a positioned chip over the row, `group-hover` reveals | |
|
|
104
|
+
| Draws the dot or the badge for `unread` and `urgent`, and brightens the row | an **UnreadDot** placed by hand, a weight or colour passed in | |
|
|
92
105
|
|
|
93
106
|
## Keys
|
|
94
107
|
|
package/src/NavItem.stories.tsx
CHANGED
|
@@ -94,3 +94,18 @@ export const WithRowsUnderClosed: Story = {
|
|
|
94
94
|
</NavItem>
|
|
95
95
|
),
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
/** Something new: the words brighten, the label is medium, and the dot sits at the right. */
|
|
99
|
+
export const Unread: Story = {
|
|
100
|
+
args: { icon: placeholder, unread: true },
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Urgent and unread: the warning badge in the dot's place. */
|
|
104
|
+
export const UnreadUrgent: Story = {
|
|
105
|
+
args: { icon: placeholder, unread: true, urgent: true },
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Unread, with a menu: point at it and the ⋮ lands on the dot. */
|
|
109
|
+
export const UnreadWithMenu: Story = {
|
|
110
|
+
args: { icon: placeholder, unread: true, menu: MENU },
|
|
111
|
+
}
|
package/src/NavItem.test.tsx
CHANGED
|
@@ -80,3 +80,34 @@ describe('NavItem with a hint', () => {
|
|
|
80
80
|
expect(container.firstElementChild?.tagName).toBe('A')
|
|
81
81
|
})
|
|
82
82
|
})
|
|
83
|
+
|
|
84
|
+
describe('NavItem unread', () => {
|
|
85
|
+
it('draws the dot, and sets the label in medium', () => {
|
|
86
|
+
const { container } = render(<NavItem href="#" label="Item one" unread />)
|
|
87
|
+
expect(container.querySelector('[data-unread]')).not.toBeNull()
|
|
88
|
+
expect(screen.getByText('Item one').className).toContain('font-medium')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('urgent draws the badge in the dot’s place', () => {
|
|
92
|
+
const { container } = render(<NavItem href="#" label="Item one" unread urgent />)
|
|
93
|
+
expect(container.querySelector('[data-urgent]')).not.toBeNull()
|
|
94
|
+
expect(container.querySelector('[data-unread]')).toBeNull()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('urgent without unread draws nothing', () => {
|
|
98
|
+
const { container } = render(<NavItem href="#" label="Item one" urgent />)
|
|
99
|
+
expect(container.querySelector('[data-urgent], [data-unread]')).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('with a menu, the ⋮ sits in the 24px slot over the dot', () => {
|
|
103
|
+
render(<NavItem href="#" label="Item one" unread menu={<span>Action</span>} />)
|
|
104
|
+
const menu = screen.getByRole('button', { name: 'More options for Item one' }).closest('[data-nav-menu]')
|
|
105
|
+
expect(menu?.className).toContain('right-2')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('with a count and a menu, the ⋮ lands on the count', () => {
|
|
109
|
+
render(<NavItem href="#" label="Item one" count={3} menu={<span>Action</span>} />)
|
|
110
|
+
const menu = screen.getByRole('button', { name: 'More options for Item one' }).closest('[data-nav-menu]')
|
|
111
|
+
expect(menu?.className).toContain('right-1')
|
|
112
|
+
})
|
|
113
|
+
})
|
package/src/NavItem.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useId, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react'
|
|
2
2
|
import { Collapsible } from '@base-ui/react/collapsible'
|
|
3
|
-
import { IconChevronRight, IconDotsVertical } from '@tabler/icons-react'
|
|
3
|
+
import { IconAlertSquareRounded, IconChevronRight, IconDotsVertical } from '@tabler/icons-react'
|
|
4
4
|
import { cn } from './cn'
|
|
5
5
|
import { COLLAPSIBLE_PANEL_CLASSES, SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_TEXT_CLASSES } from './looks'
|
|
6
6
|
import { IconButton } from './IconButton'
|
|
7
7
|
import { Menu } from './Menu'
|
|
8
|
+
import { UnreadDot } from './UnreadDot'
|
|
8
9
|
import { WithTooltip } from './Tooltip'
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -34,6 +35,15 @@ import { WithTooltip } from './Tooltip'
|
|
|
34
35
|
* `hint` is shown only while the row is pointed at or focused, left of the
|
|
35
36
|
* count — a neutral `Chip`, typically. It takes its own room while it shows,
|
|
36
37
|
* so the label ends with "…" before it instead of running under it.
|
|
38
|
+
*
|
|
39
|
+
* `unread` brightens the row's words and icon, sets the label in medium, and
|
|
40
|
+
* draws the `UnreadDot` at the right; `urgent` with it draws the warning badge
|
|
41
|
+
* in the dot's place (Katerina, 2 October: Peek's row, moved in). The ⋮ lands
|
|
42
|
+
* on whatever is in that place, and with nothing there it takes no room until
|
|
43
|
+
* the row is pointed at, so a label at rest keeps the row's whole width.
|
|
44
|
+
*
|
|
45
|
+
* Active is `bg-nav-active`, the current row in a sidebar, which each theme
|
|
46
|
+
* sets: the selected tab's neutral fill in most, Signal's blue in Signal.
|
|
37
47
|
*/
|
|
38
48
|
export interface NavItemProps extends Omit<ComponentPropsWithoutRef<'a'>, 'href' | 'children'> {
|
|
39
49
|
label: string
|
|
@@ -51,6 +61,10 @@ export interface NavItemProps extends Omit<ComponentPropsWithoutRef<'a'>, 'href'
|
|
|
51
61
|
menuLabel?: string
|
|
52
62
|
/** Shown only while the row is pointed at or focused, left of the count: a neutral `Chip`, typically. */
|
|
53
63
|
hint?: ReactNode
|
|
64
|
+
/** Something new here: brighter words, the label in medium, and the `UnreadDot` at the right. Say so in the row's own words for assistive tech too. */
|
|
65
|
+
unread?: boolean
|
|
66
|
+
/** With `unread`: the warning badge in the dot's place. */
|
|
67
|
+
urgent?: boolean
|
|
54
68
|
/** Rows under this one, folding under it: `NavItem`s. While the row is pointed at or focused, its icon becomes the arrow. */
|
|
55
69
|
children?: ReactNode
|
|
56
70
|
/** With `children`: open unless told otherwise. */
|
|
@@ -71,6 +85,8 @@ export function NavItem({
|
|
|
71
85
|
menu,
|
|
72
86
|
menuLabel,
|
|
73
87
|
hint,
|
|
88
|
+
unread = false,
|
|
89
|
+
urgent = false,
|
|
74
90
|
children,
|
|
75
91
|
defaultOpen = true,
|
|
76
92
|
open: openProp,
|
|
@@ -89,6 +105,10 @@ export function NavItem({
|
|
|
89
105
|
// The row draws beside it — the ⋮, the arrow — so the pointer can leave the
|
|
90
106
|
// link for them while the row still reads as the one it is on.
|
|
91
107
|
const besides = menu != null || hasRows
|
|
108
|
+
// What sits at the right at rest, after the count: the dot, or the badge.
|
|
109
|
+
const mark = unread ? (urgent ? <UrgentMark /> : <UnreadDot />) : null
|
|
110
|
+
// Under the ⋮ while it shows: the count and the mark step aside for it.
|
|
111
|
+
const stepsAside = menu != null && 'transition-opacity group-hover/nav:opacity-0 group-focus-within/nav:opacity-0 group-has-[[data-nav-menu]_[aria-expanded=true]]/nav:opacity-0'
|
|
92
112
|
|
|
93
113
|
const row = (
|
|
94
114
|
<a
|
|
@@ -100,7 +120,7 @@ export function NavItem({
|
|
|
100
120
|
// shrink-0 family; Katerina, 2026-09-02).
|
|
101
121
|
'flex h-8 min-w-0 shrink-0 items-center gap-2 px-2 text-body-2',
|
|
102
122
|
SIDEBAR_ROW_CLASSES,
|
|
103
|
-
active ? 'bg-bg-active text-text-primary' : cn(SIDEBAR_ROW_TEXT_CLASSES, 'hover:bg-bg-hover'),
|
|
123
|
+
active ? 'bg-bg-nav-active text-text-primary' : cn(unread ? 'text-text-primary' : SIDEBAR_ROW_TEXT_CLASSES, 'hover:bg-bg-hover'),
|
|
104
124
|
// With a menu the ⋮ sits beside the link, so the row keeps its hover
|
|
105
125
|
// look while the ⋮ is pointed at and while its menu is open. The same
|
|
106
126
|
// for the arrow.
|
|
@@ -121,7 +141,7 @@ export function NavItem({
|
|
|
121
141
|
{icon}
|
|
122
142
|
</span>
|
|
123
143
|
)}
|
|
124
|
-
<span className=
|
|
144
|
+
<span className={cn('flex-1 truncate', unread && 'font-medium')}>{label}</span>
|
|
125
145
|
{hint != null && (
|
|
126
146
|
// Takes room only while it shows, so the label gives way to it.
|
|
127
147
|
<span className="hidden shrink-0 items-center group-hover/nav:flex group-focus-within/nav:flex">{hint}</span>
|
|
@@ -135,17 +155,21 @@ export function NavItem({
|
|
|
135
155
|
<span
|
|
136
156
|
className={cn(
|
|
137
157
|
'min-w-4 shrink-0 text-center font-mono text-caption tabular-nums text-text-muted',
|
|
138
|
-
// With a menu, the ⋮ takes this place while it shows.
|
|
139
|
-
|
|
158
|
+
// With a menu and no mark after it, the ⋮ takes this place while it shows.
|
|
159
|
+
!mark && stepsAside,
|
|
140
160
|
)}
|
|
141
161
|
>
|
|
142
162
|
{count}
|
|
143
163
|
</span>
|
|
144
164
|
</WithTooltip>
|
|
145
|
-
) :
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
165
|
+
) : null}
|
|
166
|
+
{mark ? (
|
|
167
|
+
<span className={cn('flex shrink-0', stepsAside)}>{mark}</span>
|
|
168
|
+
) : menu != null && !count ? (
|
|
169
|
+
// Nothing at the right, but a menu: no room is kept at rest, so the
|
|
170
|
+
// label has the row's whole width; while the ⋮ shows, its 24px slot
|
|
171
|
+
// 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-within/nav:block group-has-[[data-nav-menu]_[aria-expanded=true]]/nav:block" />
|
|
149
173
|
) : null}
|
|
150
174
|
</a>
|
|
151
175
|
)
|
|
@@ -170,9 +194,11 @@ export function NavItem({
|
|
|
170
194
|
</div>
|
|
171
195
|
)}
|
|
172
196
|
{menu != null && (
|
|
173
|
-
// `right-1`: the 24px button's 16px icon then ends 8px
|
|
174
|
-
// edge, where the count ends (px-2), so the ⋮ lands on
|
|
175
|
-
|
|
197
|
+
// On a count, `right-1`: the 24px button's 16px icon then ends 8px
|
|
198
|
+
// from the row's edge, where the count ends (px-2), so the ⋮ lands on
|
|
199
|
+
// the number. On the mark or the empty slot, `right-2`: the button
|
|
200
|
+
// sits in the 24px slot itself.
|
|
201
|
+
<div data-nav-menu className={cn('absolute inset-y-0 flex items-center', count && !mark ? 'right-1' : 'right-2', 'opacity-0 transition-opacity focus-within:opacity-100 group-hover/nav:opacity-100 has-[[aria-expanded=true]]:opacity-100')}>
|
|
176
202
|
<Menu
|
|
177
203
|
align="right"
|
|
178
204
|
trigger={
|
|
@@ -204,3 +230,14 @@ export function NavItem({
|
|
|
204
230
|
</Collapsible.Root>
|
|
205
231
|
)
|
|
206
232
|
}
|
|
233
|
+
|
|
234
|
+
/** Urgent and unread: a 12px warning mark on its tint, in the dot's 24px slot. */
|
|
235
|
+
function UrgentMark() {
|
|
236
|
+
return (
|
|
237
|
+
<div className="flex h-6 w-6 shrink-0 items-center justify-center" data-urgent>
|
|
238
|
+
<div className="flex items-center rounded-full bg-warning-muted p-0.5 signal:shadow-glow-warning">
|
|
239
|
+
<IconAlertSquareRounded size={12} stroke={2.5} className="text-warning-default" />
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
)
|
|
243
|
+
}
|
package/src/NavTree.mdx
CHANGED
|
@@ -27,6 +27,30 @@ rows to any depth.
|
|
|
27
27
|
|
|
28
28
|
<Canvas of={NavTreeStories.Loading} />
|
|
29
29
|
|
|
30
|
+
- `titleActions` sit beside the label, always shown ("New"); a group's
|
|
31
|
+
`actions` beside its title on hover ("Open"), its `trailing` always (an
|
|
32
|
+
**UnreadDot**). A row's `unread` and `urgent` are **NavItem**'s.
|
|
33
|
+
|
|
34
|
+
<Canvas of={NavTreeStories.WithActionsAndUnread} />
|
|
35
|
+
|
|
36
|
+
- Rows can be read as they open. A group's `nodes={null}` is a skeleton in
|
|
37
|
+
its rows' place, and its `message` a line in their place ("Nothing in this
|
|
38
|
+
yet."). A row with `hasChildren` and no `children` has its arrow, a
|
|
39
|
+
skeleton under it once open, and `onOpenChange` says when to read them;
|
|
40
|
+
`childrenMessage` is the line under it. `onIntent` on a group says the
|
|
41
|
+
pointer rests on it or the keyboard reached it: read it then, so opening
|
|
42
|
+
it draws rows.
|
|
43
|
+
|
|
44
|
+
<Canvas of={NavTreeStories.ReadAsItOpens} />
|
|
45
|
+
|
|
46
|
+
- An app whose groups and rows each read their own data draws the groups
|
|
47
|
+
itself: leave out `groups` and pass **NavTreeSection**s as children, each
|
|
48
|
+
with **NavItem** rows. A `NavTreeSection` is one group exactly as `groups`
|
|
49
|
+
draws it — the heading, the light indent, `message`, `loading`, `trailing`,
|
|
50
|
+
`actions`, `onIntent`.
|
|
51
|
+
|
|
52
|
+
<Canvas of={NavTreeStories.AppDrawnGroups} />
|
|
53
|
+
|
|
30
54
|
## When not
|
|
31
55
|
|
|
32
56
|
- A flat list of places → **NavItem** rows in the **Sidebar**.
|
|
@@ -68,6 +92,7 @@ It does these by itself. Write none of them around it.
|
|
|
68
92
|
| Draws every level from one list, rows under rows | a recursive render of **NavItem**s and **CollapsibleSection**s | |
|
|
69
93
|
| Opens the group that holds the current row | a search through the list for the current row | |
|
|
70
94
|
| Says it is loading, or empty, in the rows' place | a skeleton or an **EmptyState** placed by hand | |
|
|
95
|
+
| Says when the pointer rests on a group, 150ms, and not while it sweeps past | a timer on pointer enter and leave | |
|
|
71
96
|
|
|
72
97
|
## Keys
|
|
73
98
|
|
package/src/NavTree.stories.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
import { IconSquareRounded } from '@tabler/icons-react'
|
|
3
3
|
import { useState } from 'react'
|
|
4
|
+
import { IconArrowUpRight, IconPlus } from '@tabler/icons-react'
|
|
4
5
|
import { Chip } from './Chip'
|
|
6
|
+
import { UnreadDot } from './UnreadDot'
|
|
5
7
|
import { NavItem } from './NavItem'
|
|
6
|
-
import { NavTree, type NavTreeGroup } from './NavTree'
|
|
8
|
+
import { NavTree, NavTreeSection, type NavTreeGroup } from './NavTree'
|
|
7
9
|
import { Sidebar } from './Sidebar'
|
|
8
10
|
|
|
9
11
|
const placeholder = <IconSquareRounded size={16} stroke={1.5} />
|
|
@@ -82,3 +84,57 @@ export const Empty: Story = { args: { groups: [] } }
|
|
|
82
84
|
|
|
83
85
|
/** Close a group and reload the page: still closed. This browser remembers each group, under the key. */
|
|
84
86
|
export const Remembered: Story = { args: { storageKey: 'estiva-ui.stories.nav-tree' } }
|
|
87
|
+
|
|
88
|
+
/** Actions beside the label and on each group, a group's dot, and unread rows — one urgent. */
|
|
89
|
+
export const WithActionsAndUnread: Story = {
|
|
90
|
+
args: {
|
|
91
|
+
titleActions: [{ icon: <IconPlus size={16} stroke={1.5} />, tooltip: 'New', onClick: () => {} }],
|
|
92
|
+
groups: [
|
|
93
|
+
{ id: 'u1', title: 'Group one', trailing: <UnreadDot />, actions: [{ icon: <IconArrowUpRight size={16} stroke={1.5} />, tooltip: 'Open', onClick: () => {} }], defaultOpen: true, nodes: [
|
|
94
|
+
{ id: 'u1a', label: 'Item two', href: '#', icon: placeholder, unread: true },
|
|
95
|
+
{ id: 'u1b', label: 'Item three', href: '#', icon: placeholder, unread: true, urgent: true },
|
|
96
|
+
{ id: 'u1c', label: 'Item four', href: '#', icon: placeholder },
|
|
97
|
+
] },
|
|
98
|
+
{ id: 'u2', title: 'Group two', actions: [{ icon: <IconArrowUpRight size={16} stroke={1.5} />, tooltip: 'Open', onClick: () => {} }], nodes: [{ id: 'u2a', label: 'Item five', href: '#', icon: placeholder }] },
|
|
99
|
+
],
|
|
100
|
+
selected: 'u1c',
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Read as it opens: a group still being read, a group with nothing in it, a row whose rows are read when it opens, and one that could not be read. */
|
|
105
|
+
export const ReadAsItOpens: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
groups: [
|
|
108
|
+
{ id: 'l1', title: 'Being read', defaultOpen: true, nodes: null },
|
|
109
|
+
{ id: 'l2', title: 'Empty', defaultOpen: true, nodes: [], message: 'Nothing in this yet.' },
|
|
110
|
+
{ id: 'l3', title: 'Rows read on open', defaultOpen: true, nodes: [
|
|
111
|
+
{ id: 'l3a', label: 'Item with rows not read yet', href: '#', icon: placeholder, hasChildren: true, defaultOpen: false },
|
|
112
|
+
{ id: 'l3b', label: 'Item whose rows failed', href: '#', icon: placeholder, hasChildren: true, childrenMessage: 'This could not be read.' },
|
|
113
|
+
] },
|
|
114
|
+
],
|
|
115
|
+
selected: undefined,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Groups the app draws itself, as `NavTree`'s children: each a `NavTreeSection` with `NavItem` rows — for groups and rows that each read their own data. */
|
|
120
|
+
export const AppDrawnGroups: Story = {
|
|
121
|
+
args: { groups: undefined },
|
|
122
|
+
render: function Render(args) {
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex h-screen bg-bg-base">
|
|
125
|
+
<Sidebar>
|
|
126
|
+
<NavTree title={args.title}>
|
|
127
|
+
<NavTreeSection title="Group one" trailing={<UnreadDot />}>
|
|
128
|
+
<NavItem href="#" label="Item one" icon={placeholder} unread />
|
|
129
|
+
<NavItem href="#" label="Item two" icon={placeholder} active>
|
|
130
|
+
<NavItem href="#" label="Item three" icon={placeholder} />
|
|
131
|
+
</NavItem>
|
|
132
|
+
</NavTreeSection>
|
|
133
|
+
<NavTreeSection title="Group two" loading />
|
|
134
|
+
<NavTreeSection title="Group three" message="Nothing in this yet." />
|
|
135
|
+
</NavTree>
|
|
136
|
+
</Sidebar>
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
},
|
|
140
|
+
}
|