@estiva-app/ui 0.22.0 → 0.24.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/README.md +21 -0
- package/dist/AppShell.d.ts.map +1 -1
- package/dist/AttachmentCard.d.ts.map +1 -1
- package/dist/Banner.d.ts +20 -2
- package/dist/Banner.d.ts.map +1 -1
- package/dist/ContainerHeader.d.ts +25 -0
- package/dist/ContainerHeader.d.ts.map +1 -0
- package/dist/EmptyState.d.ts +10 -1
- package/dist/EmptyState.d.ts.map +1 -1
- package/dist/Form.d.ts.map +1 -1
- package/dist/IconButton.d.ts +9 -1
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/PreviewCard.d.ts +9 -2
- package/dist/PreviewCard.d.ts.map +1 -1
- package/dist/SectionHeader.d.ts +7 -1
- package/dist/SectionHeader.d.ts.map +1 -1
- package/dist/Select.d.ts +11 -3
- package/dist/Select.d.ts.map +1 -1
- package/dist/Tooltip.d.ts +13 -1
- package/dist/Tooltip.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/gates/app-checks.d.ts.map +1 -1
- package/dist/gates/{chunk-ZGJ2J5NU.js → chunk-EA33NP5B.js} +275 -11
- package/dist/gates/chunk-EA33NP5B.js.map +7 -0
- package/dist/gates/{chunk-AUXD4GCY.js → chunk-GTQZEHPC.js} +15 -1
- package/dist/gates/chunk-GTQZEHPC.js.map +7 -0
- package/dist/gates/cli.js +1 -1
- package/dist/gates/create-app.d.ts +2 -0
- package/dist/gates/create-app.d.ts.map +1 -1
- package/dist/gates/create-app.js +1 -1
- package/dist/gates/index.js +25 -3
- package/dist/gates/index.js.map +2 -2
- package/dist/gates/status.d.ts +2 -0
- package/dist/gates/status.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +196 -156
- package/dist/index.js.map +4 -4
- package/dist/registry/app-DQI6WLHR.js +10 -0
- package/dist/registry/app.d.ts +21 -0
- package/dist/registry/app.d.ts.map +1 -0
- package/dist/registry/build-LGFCCOLR.js +27 -0
- package/dist/registry/build-LGFCCOLR.js.map +7 -0
- package/dist/registry/build.d.ts +68 -2
- package/dist/registry/build.d.ts.map +1 -1
- package/dist/registry/{chunk-MRSBS5OP.js → chunk-E4JNV7PC.js} +68 -12
- package/dist/registry/chunk-E4JNV7PC.js.map +7 -0
- package/dist/registry/{chunk-QDYGB3QN.js → chunk-NJN4MQAM.js} +60 -15
- package/dist/registry/chunk-NJN4MQAM.js.map +7 -0
- package/dist/registry/{chunk-IJNCYVH4.js → chunk-W2B2G7OE.js} +34 -9
- package/dist/registry/chunk-W2B2G7OE.js.map +7 -0
- package/dist/registry/chunk-WMFF3MPP.js +648 -0
- package/dist/registry/chunk-WMFF3MPP.js.map +7 -0
- package/dist/registry/cli.js +81 -26
- package/dist/registry/cli.js.map +3 -3
- package/dist/registry/find.d.ts +12 -1
- package/dist/registry/find.d.ts.map +1 -1
- package/dist/registry/index.d.ts +5 -2
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +11 -3
- package/dist/registry/schema.d.ts +83 -6
- package/dist/registry/schema.d.ts.map +1 -1
- package/package.json +2 -2
- package/registry.json +283 -95
- package/src/AppShell.mdx +10 -0
- package/src/AppShell.test.tsx +16 -0
- package/src/AppShell.tsx +4 -1
- package/src/AttachmentCard.mdx +9 -0
- package/src/AttachmentCard.test.tsx +10 -0
- package/src/AttachmentCard.tsx +8 -5
- package/src/Avatar.mdx +13 -3
- package/src/AvatarGroup.mdx +4 -0
- package/src/Banner.mdx +30 -7
- package/src/Banner.stories.tsx +22 -0
- package/src/Banner.test.tsx +35 -0
- package/src/Banner.tsx +34 -9
- package/src/Breadcrumb.mdx +10 -0
- package/src/Button.mdx +14 -3
- package/src/Card.mdx +9 -0
- package/src/Checkbox.mdx +10 -0
- package/src/Chip.mdx +4 -0
- package/src/ChipInput.mdx +12 -0
- package/src/CollapsibleSection.mdx +9 -0
- package/src/CommandPalette.mdx +14 -2
- package/src/ConfirmDialog.mdx +11 -0
- package/src/ContainerHeader.mdx +60 -0
- package/src/ContainerHeader.stories.tsx +62 -0
- package/src/ContainerHeader.test.tsx +47 -0
- package/src/ContainerHeader.tsx +45 -0
- package/src/DialogShell.mdx +14 -0
- package/src/Divider.mdx +9 -1
- package/src/EditableText.mdx +11 -0
- package/src/EmptyState.mdx +27 -4
- package/src/EmptyState.stories.tsx +6 -1
- package/src/EmptyState.test.tsx +18 -1
- package/src/EmptyState.tsx +14 -1
- package/src/Field.mdx +10 -1
- package/src/FieldLine.mdx +9 -1
- package/src/FilePicker.mdx +8 -0
- package/src/Form.mdx +10 -0
- package/src/Form.test.tsx +26 -0
- package/src/Form.tsx +7 -0
- package/src/IconButton.mdx +16 -2
- package/src/IconButton.stories.tsx +3 -0
- package/src/IconButton.test.tsx +25 -0
- package/src/IconButton.tsx +61 -46
- package/src/IdentityMenu.mdx +9 -0
- package/src/InlineChip.mdx +8 -0
- package/src/Kbd.mdx +4 -0
- package/src/Link.mdx +10 -0
- package/src/Menu.mdx +16 -1
- package/src/MenuItem.mdx +12 -2
- package/src/NavItem.mdx +8 -0
- package/src/Person.mdx +4 -0
- package/src/PersonTrigger.mdx +9 -0
- package/src/Popover.mdx +21 -0
- package/src/PreviewCard.mdx +19 -4
- package/src/PreviewCard.tsx +11 -4
- package/src/ProgressBar.mdx +8 -0
- package/src/Property.mdx +4 -0
- package/src/Rail.mdx +9 -1
- package/src/RailItem.mdx +8 -0
- package/src/Reaction.mdx +9 -0
- package/src/ReactionPicker.mdx +8 -0
- package/src/ScrollArea.mdx +13 -2
- package/src/SearchInput.mdx +9 -0
- package/src/SectionHeader.mdx +13 -0
- package/src/SectionHeader.stories.tsx +9 -0
- package/src/SectionHeader.test.tsx +9 -0
- package/src/SectionHeader.tsx +8 -2
- package/src/SectionLabel.mdx +4 -0
- package/src/Select.mdx +21 -2
- package/src/Select.stories.tsx +4 -1
- package/src/Select.test.tsx +17 -0
- package/src/Select.tsx +34 -13
- package/src/Sidebar.mdx +8 -0
- package/src/Skeleton.mdx +4 -0
- package/src/Tabs.mdx +12 -1
- package/src/TextInput.mdx +8 -0
- package/src/Textarea.mdx +8 -0
- package/src/Toast.mdx +12 -1
- package/src/Toolbar.mdx +11 -0
- package/src/Tooltip.mdx +26 -2
- package/src/Tooltip.stories.tsx +26 -0
- package/src/Tooltip.test.tsx +29 -0
- package/src/Tooltip.tsx +17 -4
- package/src/TopBar.mdx +4 -0
- package/src/eslint/no-rebuilt-behaviour.ts +1 -1
- package/src/eslint/no-restyled-part.ts +1 -0
- package/src/gates/app-checks.ts +23 -1
- package/src/gates/create-app.test.ts +77 -6
- package/src/gates/create-app.ts +288 -15
- package/src/gates/status.ts +16 -0
- package/src/index.ts +1 -0
- package/src/pages.test.ts +142 -0
- package/src/registry/app.test.ts +562 -0
- package/src/registry/app.ts +854 -0
- package/src/registry/build.ts +114 -25
- package/src/registry/cli.ts +105 -42
- package/src/registry/find.ts +45 -8
- package/src/registry/index.ts +8 -1
- package/src/registry/registry.test.ts +5 -5
- package/src/registry/schema.ts +163 -12
- package/tailwind-preset.js +22 -1
- package/dist/gates/chunk-AUXD4GCY.js.map +0 -7
- package/dist/gates/chunk-ZGJ2J5NU.js.map +0 -7
- package/dist/registry/build-GOVLABI6.js +0 -13
- package/dist/registry/chunk-IJNCYVH4.js.map +0 -7
- package/dist/registry/chunk-MRSBS5OP.js.map +0 -7
- package/dist/registry/chunk-QDYGB3QN.js.map +0 -7
- /package/dist/registry/{build-GOVLABI6.js.map → app-DQI6WLHR.js.map} +0 -0
package/src/AppShell.mdx
CHANGED
|
@@ -75,6 +75,16 @@ import { AppShell, Banner, IdentityMenu, Rail, RailItem } from '@estiva-app/ui'
|
|
|
75
75
|
- In the floating manner the card owns `overflow-hidden` — your content
|
|
76
76
|
manages its own scrolling inside it.
|
|
77
77
|
- Desktop only for now — no narrow-screen behaviour is provided.
|
|
78
|
+
- In the Signal theme the floating frame draws the control-room dot grid on
|
|
79
|
+
its ground, behind the card. Nothing to add: the preset makes it.
|
|
80
|
+
|
|
81
|
+
## What it owns
|
|
82
|
+
|
|
83
|
+
It does these by itself. Write none of them around it.
|
|
84
|
+
|
|
85
|
+
| It does | So you never write | Refused when written by hand |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| Scrolls the page's content column, in a ScrollArea | `overflow-auto` on the page or on `main` | |
|
|
78
88
|
|
|
79
89
|
## Props
|
|
80
90
|
|
package/src/AppShell.test.tsx
CHANGED
|
@@ -36,4 +36,20 @@ describe('AppShell', () => {
|
|
|
36
36
|
expect(main.className).toContain('overflow-hidden')
|
|
37
37
|
expect(main.closest('[style*="overflow: scroll"]')).toBeNull()
|
|
38
38
|
})
|
|
39
|
+
|
|
40
|
+
it('carries the Signal canvas on the floating frame, and only there', () => {
|
|
41
|
+
const { container, unmount } = render(
|
|
42
|
+
<AppShell variant="floating" nav={<nav>Nav</nav>}>
|
|
43
|
+
<p>Page</p>
|
|
44
|
+
</AppShell>,
|
|
45
|
+
)
|
|
46
|
+
expect((container.firstElementChild as HTMLElement).className).toContain('signal-canvas')
|
|
47
|
+
unmount()
|
|
48
|
+
const solid = render(
|
|
49
|
+
<AppShell nav={<nav>Nav</nav>}>
|
|
50
|
+
<p>Page</p>
|
|
51
|
+
</AppShell>,
|
|
52
|
+
)
|
|
53
|
+
expect(solid.container.querySelector('.signal-canvas')).toBeNull()
|
|
54
|
+
})
|
|
39
55
|
})
|
package/src/AppShell.tsx
CHANGED
|
@@ -45,7 +45,10 @@ export function AppShell({ variant = 'solid', menu, logo, search, identity, bann
|
|
|
45
45
|
|
|
46
46
|
if (variant === 'floating') {
|
|
47
47
|
return (
|
|
48
|
-
|
|
48
|
+
// `signal-canvas`: in the Signal theme, the preset draws the control-room dot
|
|
49
|
+
// grid behind everything in the frame (Peek's, moved here with the frame:
|
|
50
|
+
// UIG-14, Katerina, 19 September — the canvas is the theme's, not an app's).
|
|
51
|
+
<div className="signal-canvas relative h-full min-h-0 overflow-hidden bg-bg-base">
|
|
49
52
|
{bar}
|
|
50
53
|
<div className="flex h-full pb-4 pr-4 pt-[52px]">
|
|
51
54
|
{nav}
|
package/src/AttachmentCard.mdx
CHANGED
|
@@ -57,6 +57,15 @@ import { AttachmentCard } from '@estiva-app/ui'
|
|
|
57
57
|
on Base UI's `Button` like InputChip's ✕ — not an `IconButton`, the 24px
|
|
58
58
|
square that fills on hover.
|
|
59
59
|
|
|
60
|
+
## What it owns
|
|
61
|
+
|
|
62
|
+
It does these by itself. Write none of them around it.
|
|
63
|
+
|
|
64
|
+
| It does | So you never write | Refused when written by hand |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| Stops the clicks on its picture, its download and its remove control, so they never also click the card or row around it | `stopPropagation` | |
|
|
67
|
+
| Shows the full name on hover when the name is cut off | a tooltip of your own | |
|
|
68
|
+
|
|
60
69
|
## Props
|
|
61
70
|
|
|
62
71
|
<Controls of={AttachmentCardStories.Document} />
|
|
@@ -169,4 +169,14 @@ describe('AttachmentCard', () => {
|
|
|
169
169
|
expect(onRemove).toHaveBeenCalledTimes(1)
|
|
170
170
|
expect(outside).not.toHaveBeenCalled()
|
|
171
171
|
})
|
|
172
|
+
|
|
173
|
+
it('the ✕ shows when the keyboard reaches it, not only on hover (C4)', async () => {
|
|
174
|
+
render(<AttachmentCard pending name="a.png" size={1} onRemove={() => {}} />)
|
|
175
|
+
const remove = screen.getByRole('button', { name: 'Remove a.png' })
|
|
176
|
+
// Hidden at rest; each of these reveals it. jsdom draws nothing, so the classes are the contract.
|
|
177
|
+
expect(remove.className).toContain('opacity-0')
|
|
178
|
+
for (const reveal of ['group-hover:opacity-100', 'group-focus-within:opacity-100', 'focus-visible:opacity-100']) expect(remove.className).toContain(reveal)
|
|
179
|
+
await userEvent.tab()
|
|
180
|
+
expect(document.activeElement).toBe(remove)
|
|
181
|
+
})
|
|
172
182
|
})
|
package/src/AttachmentCard.tsx
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { Card } from './Card'
|
|
21
21
|
import { IconButton } from './IconButton'
|
|
22
22
|
import { Link } from './Link'
|
|
23
|
-
import { WithTooltip } from './Tooltip'
|
|
23
|
+
import { WithTooltip, type WithTooltipProps } from './Tooltip'
|
|
24
24
|
import { cn } from './cn'
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -98,7 +98,7 @@ function TypeIcon({ name }: { name: string }) {
|
|
|
98
98
|
* the fallback face can fit and then not, in a line whose box never changes. The wrapper is `flex-col` so the
|
|
99
99
|
* text stretches across it, as wide as it is without one.
|
|
100
100
|
*/
|
|
101
|
-
function Truncating({ text, hint, className, wrapperClassName }: { text: string; hint?: string; className: string; wrapperClassName?: string }) {
|
|
101
|
+
function Truncating({ text, hint, className, wrapperClassName, placement }: { text: string; hint?: string; className: string; wrapperClassName?: string; placement?: WithTooltipProps['placement'] }) {
|
|
102
102
|
const ref = useRef<HTMLSpanElement>(null)
|
|
103
103
|
const [cut, setCut] = useState(false)
|
|
104
104
|
useLayoutEffect(() => {
|
|
@@ -127,7 +127,7 @@ function Truncating({ text, hint, className, wrapperClassName }: { text: string;
|
|
|
127
127
|
)
|
|
128
128
|
if (!hint && !cut) return line
|
|
129
129
|
return (
|
|
130
|
-
<WithTooltip label={hint ?? text} wrapperClassName={cn('min-w-0 flex-col', wrapperClassName)}>
|
|
130
|
+
<WithTooltip label={hint ?? text} placement={placement} wrapperClassName={cn('min-w-0 flex-col', wrapperClassName)}>
|
|
131
131
|
{line}
|
|
132
132
|
</WithTooltip>
|
|
133
133
|
)
|
|
@@ -206,7 +206,8 @@ export function AttachmentCard({
|
|
|
206
206
|
)}
|
|
207
207
|
</div>
|
|
208
208
|
<div className="flex flex-col gap-[1px] min-w-0">
|
|
209
|
-
|
|
209
|
+
{/* Below, not above: above, a cut name's tooltip covered the ✕ on the corner (UIG-14, C4). */}
|
|
210
|
+
<Truncating text={name} className={NAME_CLASSES} placement={onRemove ? 'bottom' : undefined} />
|
|
210
211
|
<Truncating
|
|
211
212
|
text={(failed || warning ? note : state === 'uploading' ? (note ?? 'Uploading…') : (note ?? sizeText)) ?? ''}
|
|
212
213
|
hint={noteHint}
|
|
@@ -219,7 +220,9 @@ export function AttachmentCard({
|
|
|
219
220
|
<BaseButton
|
|
220
221
|
type="button"
|
|
221
222
|
aria-label={`Remove ${name}`}
|
|
222
|
-
|
|
223
|
+
// Shows with the card's hover and with keyboard focus, as Download does: hover-only left it invisible to
|
|
224
|
+
// the keyboard that had reached it (UIG-14, C4, Katerina 19 September).
|
|
225
|
+
className="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-bg-elevated border border-border-strong flex items-center justify-center text-text-secondary hover:text-text-primary opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 transition-opacity"
|
|
223
226
|
onClick={(event) => {
|
|
224
227
|
event.stopPropagation()
|
|
225
228
|
onRemove()
|
package/src/Avatar.mdx
CHANGED
|
@@ -13,13 +13,15 @@ theirs, or — when there is no name to take initials from — a silhouette.
|
|
|
13
13
|
## When
|
|
14
14
|
|
|
15
15
|
- Anywhere a person appears at a glance: a row, a card, a picker, a byline.
|
|
16
|
-
- The scale is 16 · 24 · 32 · 36 (default)
|
|
17
|
-
inventing sizes.
|
|
16
|
+
- The scale is 16 · 18 · 20 · 24 · 32 · 36 (default): 18 in a group's reply
|
|
17
|
+
row, 20 in a **Person**. Pick from it rather than inventing sizes.
|
|
18
18
|
|
|
19
19
|
## When not
|
|
20
20
|
|
|
21
21
|
- Several people at once → **AvatarGroup**.
|
|
22
|
-
- The face beside
|
|
22
|
+
- The face beside a name, with nothing else on the line → **Person**. A byline
|
|
23
|
+
that sets the name its own way — bold, with a time — is an Avatar beside its
|
|
24
|
+
own text.
|
|
23
25
|
- The face as a button that opens a menu → **PersonTrigger**.
|
|
24
26
|
|
|
25
27
|
## How
|
|
@@ -66,6 +68,14 @@ a stack of members — and the tile becomes one image with one name.
|
|
|
66
68
|
|
|
67
69
|
<Canvas of={AvatarStories.Palette} />
|
|
68
70
|
|
|
71
|
+
## What it owns
|
|
72
|
+
|
|
73
|
+
It does these by itself. Write none of them around it.
|
|
74
|
+
|
|
75
|
+
| It does | So you never write | Refused when written by hand |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Shows initials from the name when there is no picture, or when it fails to load | an `onError`, or initials of your own | |
|
|
78
|
+
|
|
69
79
|
## Props
|
|
70
80
|
|
|
71
81
|
<Controls of={AvatarStories.Initials} />
|
package/src/AvatarGroup.mdx
CHANGED
|
@@ -34,6 +34,10 @@ import { AvatarGroup } from '@estiva-app/ui'
|
|
|
34
34
|
- The ring rides on the Avatar itself (one box); don't wrap each face in a
|
|
35
35
|
clipping window — a window minus a border crops the initials.
|
|
36
36
|
|
|
37
|
+
## What it owns
|
|
38
|
+
|
|
39
|
+
Nothing. It only draws. Clicks and keys are yours.
|
|
40
|
+
|
|
37
41
|
## Props
|
|
38
42
|
|
|
39
43
|
<Controls of={AvatarGroupStories.ThreeMembers} />
|
package/src/Banner.mdx
CHANGED
|
@@ -12,8 +12,8 @@ line, one of four tones, gone when there is nothing to say.
|
|
|
12
12
|
|
|
13
13
|
## When
|
|
14
14
|
|
|
15
|
-
- The
|
|
16
|
-
|
|
15
|
+
- The state of the whole surface: the reason the last read failed, a read-only
|
|
16
|
+
notice.
|
|
17
17
|
- **`ok`** confirms, **`info`** informs, **`warning`** cautions,
|
|
18
18
|
**`error`** reports a failure — and only `error` announces itself
|
|
19
19
|
(`role="alert"`); the others are polite (`role="status"`).
|
|
@@ -22,14 +22,15 @@ line, one of four tones, gone when there is nothing to say.
|
|
|
22
22
|
|
|
23
23
|
## When not
|
|
24
24
|
|
|
25
|
-
- Feedback about one action, briefly → **Toast**.
|
|
25
|
+
- Feedback about one action, briefly → **Toast**. A failure is the exception:
|
|
26
|
+
it stays, in the Banner, until the person acts on it.
|
|
26
27
|
- Something that floats over the page and must **not** fade → still
|
|
27
28
|
**Toast**, with `durationMs: 0` and an action. A banner is the strip
|
|
28
29
|
under the header at the app's full width; a floating pill is not one, even
|
|
29
30
|
a permanent one (Katerina, D21).
|
|
30
31
|
- A standing property of one thing → **Chip** on that thing.
|
|
31
|
-
-
|
|
32
|
-
|
|
32
|
+
- More than one action, or a second line of text — that is a different
|
|
33
|
+
component (not in the package yet).
|
|
33
34
|
|
|
34
35
|
## How
|
|
35
36
|
|
|
@@ -41,13 +42,35 @@ import { Banner } from '@estiva-app/ui'
|
|
|
41
42
|
|
|
42
43
|
- Render it only while there is something to say — an empty strip is not
|
|
43
44
|
a state.
|
|
44
|
-
- One line of
|
|
45
|
-
|
|
45
|
+
- One line of text, `body-2`. If it needs a **second line**, it has outgrown
|
|
46
|
+
this component.
|
|
47
|
+
- `icon` puts a 16px icon before the line and `action` one small muted Button
|
|
48
|
+
at the end of it, both in the tone (Katerina, 2026-09-18: Peek's strip above
|
|
49
|
+
the composer became a Banner, `info`). With either, the banner is one line
|
|
50
|
+
and the text truncates. One action at most.
|
|
51
|
+
|
|
52
|
+
<Canvas of={BannerStories.WithIconAndAction} />
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<Banner tone="info" icon={<IconUserPlus size={16} stroke={1.5} />} action={{ label: 'Join', onClick: join }}>
|
|
56
|
+
You are not in <span className="font-medium">{title}</span> yet
|
|
57
|
+
</Banner>
|
|
58
|
+
```
|
|
59
|
+
|
|
46
60
|
- `onDismiss` adds an ✕ at the right-hand end (Katerina, D21). Use it where
|
|
47
61
|
the reader decides the notice is done with, rather than the app. The row
|
|
48
62
|
is 40px rather than 36px with it, because the button is taller than the
|
|
49
63
|
line of text; without it, nothing about the strip changes.
|
|
50
64
|
|
|
65
|
+
## What it owns
|
|
66
|
+
|
|
67
|
+
It does these by itself. Write none of them around it.
|
|
68
|
+
|
|
69
|
+
| It does | So you never write | Refused when written by hand |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| Says what it is to assistive technology: an alert for an error, a status for the rest | `role="alert"` or `role="status"` | ✓ |
|
|
72
|
+
| Draws its own ✕ when you pass `onDismiss`, and its one small action when you pass `action` | a close button or an action button of your own | |
|
|
73
|
+
|
|
51
74
|
## Props
|
|
52
75
|
|
|
53
76
|
<Controls of={BannerStories.Ok} />
|
package/src/Banner.stories.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { IconPencilMinus, IconUserPlus } from '@tabler/icons-react'
|
|
2
3
|
import { Banner } from './Banner'
|
|
3
4
|
|
|
4
5
|
const meta = {
|
|
@@ -34,6 +35,27 @@ export const Dismissible: Story = {
|
|
|
34
35
|
args: { tone: 'warning', children: 'This workspace is read-only until sign-in.', onDismiss: () => {} },
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* With an icon and one action (Katerina, 2026-09-18). One line: the text
|
|
40
|
+
* truncates, and the Button stays at the end.
|
|
41
|
+
*/
|
|
42
|
+
export const WithIconAndAction: Story = {
|
|
43
|
+
parameters: { controls: { disable: true } },
|
|
44
|
+
render: () => (
|
|
45
|
+
<div className="flex w-full max-w-xl flex-col gap-2">
|
|
46
|
+
<Banner tone="info" icon={<IconPencilMinus size={16} stroke={1.5} />} action={{ label: 'Invite people', onClick: () => {} }}>
|
|
47
|
+
This is the start of the conversation in <span className="font-medium">Project Alpha</span>
|
|
48
|
+
</Banner>
|
|
49
|
+
<Banner tone="info" icon={<IconPencilMinus size={16} stroke={1.5} />}>
|
|
50
|
+
This is the start of your conversation with <span className="font-medium">Sam Lee</span>
|
|
51
|
+
</Banner>
|
|
52
|
+
<Banner tone="info" icon={<IconUserPlus size={16} stroke={1.5} />} action={{ label: 'Join', onClick: () => {} }}>
|
|
53
|
+
You are not in <span className="font-medium">Project Alpha</span> yet — join to take part in the conversation
|
|
54
|
+
</Banner>
|
|
55
|
+
</div>
|
|
56
|
+
),
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
/** All four tones, stacked. */
|
|
38
60
|
export const AllTones: Story = {
|
|
39
61
|
parameters: { controls: { disable: true } },
|
package/src/Banner.test.tsx
CHANGED
|
@@ -57,4 +57,39 @@ describe('Banner', () => {
|
|
|
57
57
|
)
|
|
58
58
|
expect(container.firstElementChild!.getAttribute('role')).toBe('alert')
|
|
59
59
|
})
|
|
60
|
+
|
|
61
|
+
it('puts an icon before the line and one small muted action after it, on one line (Katerina, 2026-09-18)', async () => {
|
|
62
|
+
const onClick = vi.fn()
|
|
63
|
+
const { container } = render(
|
|
64
|
+
<Banner tone="info" icon={<svg data-testid="icon" />} action={{ label: 'Join', onClick }}>
|
|
65
|
+
You are not in <b>Alpha</b> yet
|
|
66
|
+
</Banner>,
|
|
67
|
+
)
|
|
68
|
+
const row = container.firstElementChild!
|
|
69
|
+
expect(row.getAttribute('role')).toBe('status')
|
|
70
|
+
expect(row.firstElementChild!.querySelector('[data-testid="icon"]')).not.toBeNull()
|
|
71
|
+
const line = screen.getByText(/You are not in/)
|
|
72
|
+
expect(line.className).toContain('truncate')
|
|
73
|
+
const button = screen.getByRole('button', { name: 'Join' })
|
|
74
|
+
expect(line.nextElementSibling).toBe(button)
|
|
75
|
+
await userEvent.click(button)
|
|
76
|
+
expect(onClick).toHaveBeenCalledOnce()
|
|
77
|
+
// No dismiss unless asked for.
|
|
78
|
+
expect(screen.queryByRole('button', { name: 'Dismiss' })).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('an icon alone still keeps the line on one line, and a dismiss alone still wraps', () => {
|
|
82
|
+
const { rerender } = render(
|
|
83
|
+
<Banner tone="info" icon={<svg />}>
|
|
84
|
+
One line
|
|
85
|
+
</Banner>,
|
|
86
|
+
)
|
|
87
|
+
expect(screen.getByText('One line').className).toContain('truncate')
|
|
88
|
+
rerender(
|
|
89
|
+
<Banner tone="info" onDismiss={() => {}}>
|
|
90
|
+
One line
|
|
91
|
+
</Banner>,
|
|
92
|
+
)
|
|
93
|
+
expect(screen.getByText('One line').className).not.toContain('truncate')
|
|
94
|
+
})
|
|
60
95
|
})
|
package/src/Banner.tsx
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
1
2
|
import { IconX } from '@tabler/icons-react'
|
|
3
|
+
import { Button } from './Button'
|
|
2
4
|
import { cn } from './cn'
|
|
3
5
|
import { IconButton } from './IconButton'
|
|
4
6
|
|
|
@@ -13,6 +15,11 @@ import { IconButton } from './IconButton'
|
|
|
13
15
|
* An `error` announces itself (`role="alert"`); the other tones are
|
|
14
16
|
* polite (`role="status"`).
|
|
15
17
|
*
|
|
18
|
+
* `icon` and `action` put a 16px icon before the line and one small muted
|
|
19
|
+
* Button after it (Katerina, 2026-09-18, UIG-13: Peek's composer strip became
|
|
20
|
+
* a Banner in the `info` tone). With either, the banner is one line and the
|
|
21
|
+
* text truncates.
|
|
22
|
+
*
|
|
16
23
|
* `onDismiss` adds an `✕` at the right-hand end (Katerina, D21,
|
|
17
24
|
* 2026-09-07). Without it the strip is exactly what it was — a banner an
|
|
18
25
|
* app removes by not rendering it. With it the row is 40px rather than
|
|
@@ -28,7 +35,16 @@ export type BannerTone = 'ok' | 'error' | 'info' | 'warning'
|
|
|
28
35
|
|
|
29
36
|
export interface BannerProps {
|
|
30
37
|
tone: BannerTone
|
|
31
|
-
|
|
38
|
+
/** The one line. Text, or text with a name in `font-medium`. */
|
|
39
|
+
children: ReactNode
|
|
40
|
+
/**
|
|
41
|
+
* A 16px icon before the line, in the tone's colour (Katerina, 2026-09-18:
|
|
42
|
+
* Peek's composer strip became a Banner). With an icon or an action the
|
|
43
|
+
* banner is one line, and the text truncates.
|
|
44
|
+
*/
|
|
45
|
+
icon?: ReactNode
|
|
46
|
+
/** The one thing to do: the package's Button, muted and small, at the end of the line. */
|
|
47
|
+
action?: { label: string; onClick?: () => void }
|
|
32
48
|
/** Adds an `✕` at the right-hand end. Absent: the app removes the banner. */
|
|
33
49
|
onDismiss?: () => void
|
|
34
50
|
/** The dismiss button's accessible name. Defaults to "Dismiss". */
|
|
@@ -43,25 +59,34 @@ const TONE_STYLES: Record<BannerTone, string> = {
|
|
|
43
59
|
warning: 'bg-warning-muted text-warning-default',
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
export function Banner({ tone, children, onDismiss, dismissLabel = 'Dismiss', className }: BannerProps) {
|
|
62
|
+
export function Banner({ tone, children, icon, action, onDismiss, dismissLabel = 'Dismiss', className }: BannerProps) {
|
|
47
63
|
const role = tone === 'error' ? 'alert' : 'status'
|
|
48
|
-
//
|
|
49
|
-
// the height it had.
|
|
50
|
-
if (!onDismiss) {
|
|
64
|
+
// Nothing beside the line, no row: every banner that predates the props keeps
|
|
65
|
+
// the DOM and the height it had.
|
|
66
|
+
if (!onDismiss && !icon && !action) {
|
|
51
67
|
return (
|
|
52
68
|
<div role={role} className={cn('px-4 py-2 text-body-2', TONE_STYLES[tone], className)}>
|
|
53
69
|
{children}
|
|
54
70
|
</div>
|
|
55
71
|
)
|
|
56
72
|
}
|
|
73
|
+
const oneLine = Boolean(icon || action)
|
|
57
74
|
return (
|
|
58
75
|
<div role={role} className={cn('flex items-center gap-3 px-4 py-2 text-body-2', TONE_STYLES[tone], className)}>
|
|
59
|
-
<span className="
|
|
76
|
+
{icon && <span className="flex shrink-0">{icon}</span>}
|
|
77
|
+
<span className={cn('min-w-0 flex-1', oneLine && 'truncate')}>{children}</span>
|
|
78
|
+
{action && (
|
|
79
|
+
<Button variant="muted" size="small" className="shrink-0" onClick={action.onClick}>
|
|
80
|
+
{action.label}
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
60
83
|
{/* `current` so the ✕ takes the tone's colour rather than the
|
|
61
84
|
muted grey an IconButton wears on a neutral surface. */}
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
85
|
+
{onDismiss && (
|
|
86
|
+
<IconButton variant="current" aria-label={dismissLabel} onClick={onDismiss} className="-mr-1 shrink-0">
|
|
87
|
+
<IconX size={16} stroke={1.5} />
|
|
88
|
+
</IconButton>
|
|
89
|
+
)}
|
|
65
90
|
</div>
|
|
66
91
|
)
|
|
67
92
|
}
|
package/src/Breadcrumb.mdx
CHANGED
|
@@ -56,6 +56,16 @@ import { Breadcrumb } from '@estiva-app/ui'
|
|
|
56
56
|
`min-w-0` container so it can truncate at all.
|
|
57
57
|
- The separators are `aria-hidden` — the trail reads as its places.
|
|
58
58
|
|
|
59
|
+
## What it owns
|
|
60
|
+
|
|
61
|
+
It does these by itself. Write none of them around it.
|
|
62
|
+
|
|
63
|
+
| It does | So you never write | Refused when written by hand |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| Shortens a trail that does not fit, and shows a cut crumb in full on hover | truncation code, or a tooltip | |
|
|
66
|
+
| Names itself as a navigation, and marks the last crumb as the current page | `<nav>`, `aria-label` or `aria-current` | |
|
|
67
|
+
| Hides the separators from screen readers, so the trail reads as its places | `aria-hidden` on each separator | |
|
|
68
|
+
|
|
59
69
|
## Props
|
|
60
70
|
|
|
61
71
|
<Controls of={BreadcrumbStories.OnAnItem} />
|
package/src/Button.mdx
CHANGED
|
@@ -27,12 +27,13 @@ The push-button for an action: a verb, clicked once — "Create", "Save",
|
|
|
27
27
|
## When not
|
|
28
28
|
|
|
29
29
|
- An icon with no label → **IconButton**.
|
|
30
|
-
- Going somewhere rather than doing something → **Tabs
|
|
31
|
-
or a
|
|
30
|
+
- Going somewhere rather than doing something → **Link**; **Tabs** or
|
|
31
|
+
**Breadcrumb** when it is a view or a trail.
|
|
32
32
|
- Choosing a value → **Select**. Acting from a popover list → **MenuItem**.
|
|
33
33
|
- A control the person may never be allowed to use is **absent, not
|
|
34
34
|
disabled**. `disabled` is for *momentarily* unavailable — and it says why,
|
|
35
|
-
with `disabledReason` ("Available after sign-in").
|
|
35
|
+
with `disabledReason` ("Available after sign-in"). A send button waiting for
|
|
36
|
+
its fields needs no reason.
|
|
36
37
|
|
|
37
38
|
## How
|
|
38
39
|
|
|
@@ -68,6 +69,16 @@ import { IconPlus } from '@tabler/icons-react'
|
|
|
68
69
|
flex and grid, and position; a colour, a text size, a border, a corner or a
|
|
69
70
|
shadow is refused by the lint (`estiva/no-restyled-part`, UIG-9).
|
|
70
71
|
|
|
72
|
+
## What it owns
|
|
73
|
+
|
|
74
|
+
It does these by itself. Write none of them around it.
|
|
75
|
+
|
|
76
|
+
| It does | So you never write | Refused when written by hand |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Says what it is to assistive technology: a button | `role="button"` | ✓ |
|
|
79
|
+
| Is reachable with Tab, also when it is disabled with a reason | a `tabIndex` | ✓ |
|
|
80
|
+
| Presses on Enter and Space, and does nothing while disabled | a key handler | |
|
|
81
|
+
|
|
71
82
|
## Keys
|
|
72
83
|
|
|
73
84
|
| Key | Does |
|
package/src/Card.mdx
CHANGED
|
@@ -76,6 +76,15 @@ import { Card } from '@estiva-app/ui'
|
|
|
76
76
|
shadow is refused by the lint (`estiva/no-restyled-part`, UIG-9). What is inside the card is yours: its padding and its words go on
|
|
77
77
|
your own element inside.
|
|
78
78
|
|
|
79
|
+
## What it owns
|
|
80
|
+
|
|
81
|
+
It does these by itself. Write none of them around it.
|
|
82
|
+
|
|
83
|
+
| It does | So you never write | Refused when written by hand |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| With `href`, becomes a link: Link's anchor, with a real address | an anchor around the card | |
|
|
86
|
+
| Without `href`, shows it can be clicked when you pass `onClick` | `cursor-pointer` | |
|
|
87
|
+
|
|
79
88
|
## Props
|
|
80
89
|
|
|
81
90
|
<Controls of={CardStories.Surface} />
|
package/src/Checkbox.mdx
CHANGED
|
@@ -79,6 +79,16 @@ import { Checkbox } from '@estiva-app/ui'
|
|
|
79
79
|
- It does not move when it toggles: the tick is always in the box, hidden
|
|
80
80
|
when unchecked, so both states hang on a line of text the same way.
|
|
81
81
|
|
|
82
|
+
## What it owns
|
|
83
|
+
|
|
84
|
+
It does these by itself. Write none of them around it.
|
|
85
|
+
|
|
86
|
+
| It does | So you never write | Refused when written by hand |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| Says what it is to assistive technology: a checkbox, ticked or not | `role="checkbox"` or `aria-checked` | ✓ |
|
|
89
|
+
| Toggles on Space and on a click; Enter is left to the form | a click or key handler | |
|
|
90
|
+
| Is one Tab stop, in a `row` too | a `tabIndex` on the row | |
|
|
91
|
+
|
|
82
92
|
## Keys
|
|
83
93
|
|
|
84
94
|
| Key | Does |
|
package/src/Chip.mdx
CHANGED
|
@@ -38,6 +38,10 @@ import { Chip } from '@estiva-app/ui'
|
|
|
38
38
|
it never rides the text baseline.
|
|
39
39
|
- A count alone works too — `label="3"`, no icon.
|
|
40
40
|
|
|
41
|
+
## What it owns
|
|
42
|
+
|
|
43
|
+
Nothing. It only draws. Clicks and keys are yours.
|
|
44
|
+
|
|
41
45
|
## Props
|
|
42
46
|
|
|
43
47
|
<Controls of={ChipStories.Neutral} />
|
package/src/ChipInput.mdx
CHANGED
|
@@ -74,6 +74,18 @@ import { ChipInput } from '@estiva-app/ui'
|
|
|
74
74
|
|
|
75
75
|
<Canvas of={ChipInputStories.ALongLabelCut} />
|
|
76
76
|
|
|
77
|
+
## What it owns
|
|
78
|
+
|
|
79
|
+
It does these by itself. Write none of them around it.
|
|
80
|
+
|
|
81
|
+
| It does | So you never write | Refused when written by hand |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| Floats on top of the page: its suggestions | a portal, or a `z-index` to win | ✓ |
|
|
84
|
+
| Moves through its items with the arrow keys, through the suggestions | an arrow-key handler | ✓ |
|
|
85
|
+
| Says what it is to assistive technology: a combobox and its options | `role="combobox"`, `listbox` or `option` | ✓ |
|
|
86
|
+
| Filters as you type; Enter adds the highlighted entry, Backspace on an empty query removes the last chip | a filter, or key handlers | |
|
|
87
|
+
| Escape clears the query first, and only then lets the surface around it act | an Escape handler | |
|
|
88
|
+
|
|
77
89
|
## Keys
|
|
78
90
|
|
|
79
91
|
| Input | What happens |
|
|
@@ -60,6 +60,15 @@ import { CollapsibleSection, NavItem } from '@estiva-app/ui'
|
|
|
60
60
|
with `open`.
|
|
61
61
|
- A browser that blocks storage still gets a working section; it forgets.
|
|
62
62
|
|
|
63
|
+
## What it owns
|
|
64
|
+
|
|
65
|
+
It does these by itself. Write none of them around it.
|
|
66
|
+
|
|
67
|
+
| It does | So you never write | Refused when written by hand |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| Opens and closes from its title, on a click, Enter or Space | open state, or a key handler | |
|
|
70
|
+
| Tells assistive technology whether it is open | `aria-expanded` | |
|
|
71
|
+
|
|
63
72
|
## Keys
|
|
64
73
|
|
|
65
74
|
| Key | Does |
|
package/src/CommandPalette.mdx
CHANGED
|
@@ -49,8 +49,8 @@ import { CommandPalette, CommandPaletteSearch } from '@estiva-app/ui'
|
|
|
49
49
|
onQueryChange={setQuery}
|
|
50
50
|
placeholder="Search, or choose a place"
|
|
51
51
|
groups={[
|
|
52
|
-
{ label: 'Recent', rows: [{ id: 'item-one', label: 'Item one', icon: <IconSquareRounded size={16} stroke={1.5} />, onSelect:
|
|
53
|
-
{ label: 'Places', rows: [{ id: 'place-one', label: 'Place one', icon
|
|
52
|
+
{ label: 'Recent', rows: [{ id: 'item-one', label: 'Item one', icon: <IconSquareRounded size={16} stroke={1.5} />, onSelect: openItem, onForget: forget }] },
|
|
53
|
+
{ label: 'Places', rows: [{ id: 'place-one', label: 'Place one', icon: <IconSquareRounded size={16} stroke={1.5} />, onSelect: goIn, onGoIn: goIn }] },
|
|
54
54
|
]}
|
|
55
55
|
/>
|
|
56
56
|
</CommandPalette>
|
|
@@ -102,6 +102,18 @@ import { CommandPalette, CommandPaletteSearch } from '@estiva-app/ui'
|
|
|
102
102
|
- **Focus never falls out of a level.** When the element that had it goes — a
|
|
103
103
|
row picked, a level swapped — focus goes to the level's first control.
|
|
104
104
|
|
|
105
|
+
## What it owns
|
|
106
|
+
|
|
107
|
+
It does these by itself. Write none of them around it.
|
|
108
|
+
|
|
109
|
+
| It does | So you never write | Refused when written by hand |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| Floats on top of the page | a portal, or a `z-index` to win | ✓ |
|
|
112
|
+
| Holds focus inside while open, and gives it back to whatever opened it | focus code, or a Tab handler | ✓ |
|
|
113
|
+
| Moves through its items with the arrow keys, through the rows, stopping at the ends | an arrow-key handler | ✓ |
|
|
114
|
+
| Says what it is to assistive technology: a list of options you search | `role="combobox"`, `listbox` or `option` | ✓ |
|
|
115
|
+
| Closes on Esc or a click outside; Esc closes an open list first | close handlers | |
|
|
116
|
+
|
|
105
117
|
## Keys
|
|
106
118
|
|
|
107
119
|
| Where | Input | What happens |
|
package/src/ConfirmDialog.mdx
CHANGED
|
@@ -53,6 +53,17 @@ import { ConfirmDialog } from '@estiva-app/ui'
|
|
|
53
53
|
- Focus is trapped in the card and returns to whatever opened it, as for
|
|
54
54
|
every dialog since stage 3.
|
|
55
55
|
|
|
56
|
+
## What it owns
|
|
57
|
+
|
|
58
|
+
It does these by itself. Write none of them around it.
|
|
59
|
+
|
|
60
|
+
| It does | So you never write | Refused when written by hand |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Floats on top of the page | a portal, or a `z-index` to win | ✓ |
|
|
63
|
+
| Says what it is to assistive technology: a dialog that must be answered | `role="alertdialog"` | ✓ |
|
|
64
|
+
| Keeps focus inside while open, as DialogShell does, and gives it back to whatever opened it | focus code | |
|
|
65
|
+
| Ignores a press outside, because the question has to be answered; Escape cancels | a backdrop handler | |
|
|
66
|
+
|
|
56
67
|
## Keys
|
|
57
68
|
|
|
58
69
|
Different from `DialogShell`'s in one row, and it is the row that matters.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks'
|
|
2
|
+
import * as ContainerHeaderStories from './ContainerHeader.stories'
|
|
3
|
+
|
|
4
|
+
<Meta of={ContainerHeaderStories} />
|
|
5
|
+
|
|
6
|
+
# ContainerHeader
|
|
7
|
+
|
|
8
|
+
The bar across the top of a column — a list, a thread, a side panel: its
|
|
9
|
+
title, and the buttons that act on the whole column at the right edge. 48px,
|
|
10
|
+
with a hairline under it. Peek's ContainerHeader, moved into the package as it
|
|
11
|
+
looks (Katerina, 2026-09-18).
|
|
12
|
+
|
|
13
|
+
<Canvas of={ContainerHeaderStories.WithActions} />
|
|
14
|
+
|
|
15
|
+
## When
|
|
16
|
+
|
|
17
|
+
- The top of a column in a frame: the list beside the conversation, the
|
|
18
|
+
thread, a side panel. One per column.
|
|
19
|
+
- The column's own actions — new, sort, close — as IconButtons with tooltips
|
|
20
|
+
in `actions`.
|
|
21
|
+
|
|
22
|
+
## When not
|
|
23
|
+
|
|
24
|
+
- One section inside a column → **SectionHeader**, the 32px row with a
|
|
25
|
+
SectionLabel.
|
|
26
|
+
- The top of the whole app → **TopBar**.
|
|
27
|
+
- A dialog's title → **DialogShell** draws its own.
|
|
28
|
+
|
|
29
|
+
## How
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { ContainerHeader, IconButton } from '@estiva-app/ui'
|
|
33
|
+
|
|
34
|
+
<ContainerHeader
|
|
35
|
+
title="All items"
|
|
36
|
+
chevron
|
|
37
|
+
actions={
|
|
38
|
+
<IconButton tooltip="New item" aria-label="New item" onClick={openNew}>
|
|
39
|
+
<IconEdit size={16} stroke={1.5} />
|
|
40
|
+
</IconButton>
|
|
41
|
+
}
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- A string title is one line in `body-2-strong` and keeps its width. Anything
|
|
46
|
+
else — an EditableText, a title over a caption — takes the room left and is
|
|
47
|
+
drawn as given.
|
|
48
|
+
|
|
49
|
+
<Canvas of={ContainerHeaderStories.WithRichTitle} />
|
|
50
|
+
|
|
51
|
+
- Only offer a button that does something: a button with nothing behind it is
|
|
52
|
+
a promise the column cannot keep.
|
|
53
|
+
|
|
54
|
+
## What it owns
|
|
55
|
+
|
|
56
|
+
Nothing. It only draws. Clicks and keys are yours.
|
|
57
|
+
|
|
58
|
+
## Props
|
|
59
|
+
|
|
60
|
+
<Controls of={ContainerHeaderStories.Default} />
|