@estiva-app/ui 0.12.9 → 0.13.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/AttachmentCard.d.ts +52 -0
- package/dist/AttachmentCard.d.ts.map +1 -0
- package/dist/Card.d.ts +48 -0
- package/dist/Card.d.ts.map +1 -0
- package/dist/InlineChip.d.ts +59 -0
- package/dist/InlineChip.d.ts.map +1 -0
- package/dist/Link.d.ts +37 -0
- package/dist/Link.d.ts.map +1 -0
- package/dist/Menu.d.ts +12 -2
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Popover.d.ts +17 -2
- package/dist/Popover.d.ts.map +1 -1
- package/dist/ProgressBar.d.ts +32 -0
- package/dist/ProgressBar.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +697 -358
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/src/AttachmentCard.mdx +62 -0
- package/src/AttachmentCard.stories.tsx +92 -0
- package/src/AttachmentCard.test.tsx +172 -0
- package/src/AttachmentCard.tsx +324 -0
- package/src/Card.mdx +68 -0
- package/src/Card.stories.tsx +109 -0
- package/src/Card.test.tsx +106 -0
- package/src/Card.tsx +115 -0
- package/src/EmptyState.mdx +14 -0
- package/src/EmptyState.stories.tsx +28 -7
- package/src/EmptyState.test.tsx +8 -0
- package/src/InlineChip.mdx +56 -0
- package/src/InlineChip.stories.tsx +59 -0
- package/src/InlineChip.test.tsx +81 -0
- package/src/InlineChip.tsx +90 -0
- package/src/Link.mdx +59 -0
- package/src/Link.stories.tsx +102 -0
- package/src/Link.test.tsx +100 -0
- package/src/Link.tsx +58 -0
- package/src/Menu.test.tsx +20 -0
- package/src/Menu.tsx +17 -5
- package/src/Person.stories.tsx +1 -1
- package/src/PersonTrigger.stories.tsx +1 -1
- package/src/Popover.mdx +4 -2
- package/src/Popover.stories.tsx +11 -9
- package/src/Popover.test.tsx +38 -0
- package/src/Popover.tsx +20 -4
- package/src/ProgressBar.mdx +47 -0
- package/src/ProgressBar.stories.tsx +48 -0
- package/src/ProgressBar.test.tsx +62 -0
- package/src/ProgressBar.tsx +56 -0
- package/src/ReactionPicker.mdx +1 -1
- package/src/ReactionPicker.stories.tsx +3 -2
- package/src/Toolbar.stories.tsx +2 -1
- package/src/index.ts +12 -0
- package/stories/Choosing.mdx +5 -0
package/src/Popover.test.tsx
CHANGED
|
@@ -223,3 +223,41 @@ describe('Popover, centred on its anchor', () => {
|
|
|
223
223
|
expect(positioner?.getAttribute('data-align')).toBe('center')
|
|
224
224
|
})
|
|
225
225
|
})
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The padding lives on the scrolling content, so the scrollbar hugs the panel
|
|
229
|
+
* (D63) — and a caller's padding has to land there too. At 0.12.6 a toolbar's
|
|
230
|
+
* `p-1` on `className` was added to the content's `p-2` instead of replacing
|
|
231
|
+
* it, and every toolbar in a Popover grew 8px a side (PLAN Finding 60).
|
|
232
|
+
* Measured in Chrome after the fix: the toolbar 5px inside the panel's edge,
|
|
233
|
+
* as on 0.12.5.
|
|
234
|
+
*/
|
|
235
|
+
describe('Popover, padding', () => {
|
|
236
|
+
// The scrolling content is the box that carries the separator rule.
|
|
237
|
+
const contentOf = (child: HTMLElement) => child.closest('[class*="role=separator"]') as HTMLElement
|
|
238
|
+
|
|
239
|
+
it('pads its content 8px by default', async () => {
|
|
240
|
+
render(
|
|
241
|
+
<Popover trigger={<Button>Open</Button>} ariaLabel="A panel">
|
|
242
|
+
<span>Inside</span>
|
|
243
|
+
</Popover>,
|
|
244
|
+
)
|
|
245
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open' }))
|
|
246
|
+
const content = contentOf(await screen.findByText('Inside'))
|
|
247
|
+
expect(content.className).toContain('p-2')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('takes a caller’s padding on contentClassName, instead of the 8px', async () => {
|
|
251
|
+
render(
|
|
252
|
+
<Popover trigger={<Button>Open</Button>} ariaLabel="A toolbar" className="w-auto" contentClassName="p-1">
|
|
253
|
+
<span>Inside</span>
|
|
254
|
+
</Popover>,
|
|
255
|
+
)
|
|
256
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open' }))
|
|
257
|
+
const content = contentOf(await screen.findByText('Inside'))
|
|
258
|
+
expect(content.className.split(/\s+/)).toContain('p-1')
|
|
259
|
+
expect(content.className.split(/\s+/)).not.toContain('p-2')
|
|
260
|
+
// And the panel carries none of it.
|
|
261
|
+
expect((await screen.findByRole('dialog')).className.split(/\s+/)).not.toContain('p-1')
|
|
262
|
+
})
|
|
263
|
+
})
|
package/src/Popover.tsx
CHANGED
|
@@ -91,8 +91,23 @@ export interface PopoverProps {
|
|
|
91
91
|
* point at it instead, with `aria-labelledby`. */
|
|
92
92
|
ariaLabel?: string
|
|
93
93
|
children: ReactNode
|
|
94
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* On the panel's surface — its width. **Not its padding**: see
|
|
96
|
+
* `contentClassName`.
|
|
97
|
+
*/
|
|
95
98
|
className?: string
|
|
99
|
+
/**
|
|
100
|
+
* The padding around the children, as a class. Default `p-2`, 8px — what a
|
|
101
|
+
* panel of rows or a small form wants. **A toolbar wants `p-1`.**
|
|
102
|
+
*
|
|
103
|
+
* The padding is on the scrolling content, not on the panel, so a scrollbar
|
|
104
|
+
* is drawn over it and hugs the panel's edge (D63). A padding class on
|
|
105
|
+
* `className` therefore does not replace this one — it adds to it: that is
|
|
106
|
+
* how every toolbar in a `Popover` grew 8px a side at `0.12.6`, measured 13px
|
|
107
|
+
* from the panel's edge to the toolbar where it had been 5px (PLAN Finding
|
|
108
|
+
* 60). Set it here.
|
|
109
|
+
*/
|
|
110
|
+
contentClassName?: string
|
|
96
111
|
/**
|
|
97
112
|
* A cap on the scrolling box, as a class — `max-h-[360px]`. Without one the
|
|
98
113
|
* panel grows to the room the positioner has, which is the right default for
|
|
@@ -121,7 +136,7 @@ export interface PopoverProps {
|
|
|
121
136
|
const GAP = 4
|
|
122
137
|
const VIEWPORT_PAD = 8
|
|
123
138
|
|
|
124
|
-
export function Popover({ trigger, anchor, align = 'left', side = 'bottom', open, onOpenChange, finalFocus, actionsRef, ariaLabel, children, className, maxHeight }: PopoverProps) {
|
|
139
|
+
export function Popover({ trigger, anchor, align = 'left', side = 'bottom', open, onOpenChange, finalFocus, actionsRef, ariaLabel, children, className, contentClassName, maxHeight }: PopoverProps) {
|
|
125
140
|
/* A rect is not an element, so it becomes a virtual anchor — the one shape
|
|
126
141
|
Floating UI takes besides an element. */
|
|
127
142
|
const anchorTarget = useMemo(() => {
|
|
@@ -181,10 +196,11 @@ export function Popover({ trigger, anchor, align = 'left', side = 'bottom', open
|
|
|
181
196
|
{/* As in Menu: the cap on the scrolling box, and the padding on the
|
|
182
197
|
content rather than the panel, so the bar is drawn over the
|
|
183
198
|
padding instead of 9px inside it (D63). A caller's `maxHeight`
|
|
184
|
-
replaces the cap
|
|
199
|
+
replaces the cap, and a caller's `contentClassName` the padding
|
|
200
|
+
— see the props. */}
|
|
185
201
|
<ScrollArea
|
|
186
202
|
viewportClassName={maxHeight ?? 'max-h-[var(--available-height)]'}
|
|
187
|
-
contentClassName=
|
|
203
|
+
contentClassName={cn('flex flex-col p-2 [&>[role=separator]]:mx-0', contentClassName)}
|
|
188
204
|
>
|
|
189
205
|
{children}
|
|
190
206
|
</ScrollArea>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks'
|
|
2
|
+
import * as ProgressBarStories from './ProgressBar.stories'
|
|
3
|
+
|
|
4
|
+
<Meta of={ProgressBarStories} />
|
|
5
|
+
|
|
6
|
+
# ProgressBar
|
|
7
|
+
|
|
8
|
+
How much of something is done: a thin rounded track with a success-coloured
|
|
9
|
+
fill.
|
|
10
|
+
|
|
11
|
+
<Canvas of={ProgressBarStories.WithWords} />
|
|
12
|
+
|
|
13
|
+
## When
|
|
14
|
+
|
|
15
|
+
- **`default`** — 6px, the success colour: a bar someone reads, with the words
|
|
16
|
+
printed beside it.
|
|
17
|
+
- **`quiet`** — 4px, the muted success colour: a glance on the same line as a
|
|
18
|
+
count.
|
|
19
|
+
|
|
20
|
+
<Canvas of={ProgressBarStories.Quiet} />
|
|
21
|
+
|
|
22
|
+
## When not
|
|
23
|
+
|
|
24
|
+
- Something still loading, with no amount to show → **Skeleton**.
|
|
25
|
+
- A state with a verdict rather than an amount → **Chip**.
|
|
26
|
+
|
|
27
|
+
## How
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { ProgressBar } from '@estiva-app/ui'
|
|
31
|
+
|
|
32
|
+
<ProgressBar value={done} max={total} label="Items done" />
|
|
33
|
+
<span className="text-caption text-text-muted">{done} of {total} done</span>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- `label` is required: it is the bar's accessible name. A screen reader hears
|
|
37
|
+
the name and the share as a percentage; print the counts beside the bar for
|
|
38
|
+
everyone else.
|
|
39
|
+
- A value above `max` fills the bar and no further. `max={0}` is an empty bar.
|
|
40
|
+
- It is built on Base UI's `Progress`, which owns the `progressbar` role and
|
|
41
|
+
its numbers. Do not nest it inside a button: the bar's name would become part
|
|
42
|
+
of the button's.
|
|
43
|
+
- `className` is for placement: a width, a margin, `flex-1` in a row.
|
|
44
|
+
|
|
45
|
+
## Props
|
|
46
|
+
|
|
47
|
+
<Controls of={ProgressBarStories.Default} />
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { ProgressBar } from './ProgressBar'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Feedback/ProgressBar',
|
|
6
|
+
component: ProgressBar,
|
|
7
|
+
args: { value: 10, max: 14, label: 'Items done', variant: 'default' },
|
|
8
|
+
argTypes: { variant: { control: 'inline-radio', options: ['default', 'quiet'] } },
|
|
9
|
+
decorators: [(Story) => <div className="w-[346px]">{Story()}</div>],
|
|
10
|
+
} satisfies Meta<typeof ProgressBar>
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof meta>
|
|
14
|
+
|
|
15
|
+
/** 6px, the success colour: a bar someone reads. */
|
|
16
|
+
export const Default: Story = {}
|
|
17
|
+
|
|
18
|
+
/** The bar, then the words that say it. */
|
|
19
|
+
export const WithWords: Story = {
|
|
20
|
+
render: (args) => (
|
|
21
|
+
<div className="flex flex-col gap-2">
|
|
22
|
+
<ProgressBar {...args} />
|
|
23
|
+
<span className="text-caption tabular-nums text-text-muted">
|
|
24
|
+
{args.value} of {args.max} done
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
),
|
|
28
|
+
// axe color-contrast is off here until PLAN.md stage 0.10 is ruled: the words are
|
|
29
|
+
// muted caption text, 3.94:1 in signal (computed 2026-09-14; AA 4.5:1) — the same
|
|
30
|
+
// number NavItem's count is excepted for.
|
|
31
|
+
parameters: { a11y: { config: { rules: [{ id: 'color-contrast', enabled: false }] } } },
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 4px, the muted success colour: a glance, on the same line as a count — a caption label, 12px before the bar. */
|
|
35
|
+
export const Quiet: Story = {
|
|
36
|
+
args: { variant: 'quiet', value: 3, max: 4 },
|
|
37
|
+
render: (args) => (
|
|
38
|
+
<div className="flex items-center gap-3">
|
|
39
|
+
<span className="shrink-0 text-caption text-text-secondary">
|
|
40
|
+
Items ({args.value}/{args.max})
|
|
41
|
+
</span>
|
|
42
|
+
<ProgressBar {...args} className="flex-1" />
|
|
43
|
+
</div>
|
|
44
|
+
),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Empty: Story = { args: { value: 0, max: 14 } }
|
|
48
|
+
export const Complete: Story = { args: { value: 14, max: 14 } }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* What the ProgressBar page claims, pinned: it is a named progressbar carrying
|
|
4
|
+
* its numbers; the fill is the done share of the track, capped at full; an
|
|
5
|
+
* empty set is an empty bar, not a division by zero; each look is its own
|
|
6
|
+
* class list; and the caller's className lands on the outside, for placement.
|
|
7
|
+
*/
|
|
8
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
9
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
10
|
+
import { ProgressBar } from './ProgressBar'
|
|
11
|
+
|
|
12
|
+
afterEach(cleanup)
|
|
13
|
+
|
|
14
|
+
const classesOf = (el: Element) => el.getAttribute('class')?.split(' ').filter(Boolean) ?? []
|
|
15
|
+
const fillOf = (bar: HTMLElement) => bar.firstElementChild!.firstElementChild as HTMLElement
|
|
16
|
+
|
|
17
|
+
describe('ProgressBar', () => {
|
|
18
|
+
it('is a progressbar named by its label, carrying its numbers', () => {
|
|
19
|
+
render(<ProgressBar value={10} max={14} label="Items done" />)
|
|
20
|
+
const bar = screen.getByRole('progressbar', { name: 'Items done' })
|
|
21
|
+
expect(bar.getAttribute('aria-valuenow')).toBe('10')
|
|
22
|
+
expect(bar.getAttribute('aria-valuemin')).toBe('0')
|
|
23
|
+
expect(bar.getAttribute('aria-valuemax')).toBe('14')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('fills the done share of the track', () => {
|
|
27
|
+
render(<ProgressBar value={7} max={14} label="Items done" />)
|
|
28
|
+
expect(fillOf(screen.getByRole('progressbar')).style.width).toBe('50%')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('never fills past full', () => {
|
|
32
|
+
render(<ProgressBar value={20} max={14} label="Items done" />)
|
|
33
|
+
expect(fillOf(screen.getByRole('progressbar')).style.width).toBe('100%')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('an empty set is an empty bar, and still says its max is 0', () => {
|
|
37
|
+
render(<ProgressBar value={0} max={0} label="Items done" />)
|
|
38
|
+
const bar = screen.getByRole('progressbar')
|
|
39
|
+
expect(fillOf(bar).style.width).toBe('0%')
|
|
40
|
+
expect(bar.getAttribute('aria-valuemax')).toBe('0')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('default is 6px with the success colour; quiet is 4px with the muted one', () => {
|
|
44
|
+
render(
|
|
45
|
+
<>
|
|
46
|
+
<ProgressBar value={1} max={2} label="Default" />
|
|
47
|
+
<ProgressBar value={1} max={2} label="Quiet" variant="quiet" />
|
|
48
|
+
</>,
|
|
49
|
+
)
|
|
50
|
+
const standard = screen.getByRole('progressbar', { name: 'Default' })
|
|
51
|
+
expect(classesOf(standard.firstElementChild!)).toContain('h-1.5')
|
|
52
|
+
expect(classesOf(fillOf(standard))).toContain('bg-success-default')
|
|
53
|
+
const quiet = screen.getByRole('progressbar', { name: 'Quiet' })
|
|
54
|
+
expect(classesOf(quiet.firstElementChild!)).toContain('h-1')
|
|
55
|
+
expect(classesOf(fillOf(quiet))).toContain('bg-success-muted')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("the caller's className lands on the outside", () => {
|
|
59
|
+
render(<ProgressBar value={1} max={2} label="Items done" className="flex-1" />)
|
|
60
|
+
expect(classesOf(screen.getByRole('progressbar'))).toContain('flex-1')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Progress } from '@base-ui/react/progress'
|
|
2
|
+
import { cn } from './cn'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How much of something is done: a thin rounded track with a success-coloured
|
|
6
|
+
* fill. On Base UI's `Progress`, which owns the `progressbar` role and its
|
|
7
|
+
* numbers; this component writes the look.
|
|
8
|
+
*
|
|
9
|
+
* Both apps hand-built one, each with its own `role="progressbar"` (UIG-27).
|
|
10
|
+
* Their two looks are both kept, because each does a different job:
|
|
11
|
+
*
|
|
12
|
+
* - `default` is Ship's (`ui/ProgressBar.tsx`): 6px, the success colour — a
|
|
13
|
+
* bar someone reads, with the words printed beside it.
|
|
14
|
+
* - `quiet` is Peek's (`ui/ProjectTickets.tsx`): 4px, the muted success colour
|
|
15
|
+
* — a glance on the same line as a count, "not a chart".
|
|
16
|
+
*
|
|
17
|
+
* The props are Ship's, unchanged. The bar alone says nothing a screen reader
|
|
18
|
+
* can use beyond its numbers, so `label` is required; Base UI reads the value
|
|
19
|
+
* out as a percentage.
|
|
20
|
+
*/
|
|
21
|
+
export type ProgressBarVariant = 'default' | 'quiet'
|
|
22
|
+
|
|
23
|
+
const TRACK_CLASSES: Record<ProgressBarVariant, string> = {
|
|
24
|
+
default: 'h-1.5 w-full overflow-hidden rounded-full bg-bg-inset',
|
|
25
|
+
// 4px, Katerina's ruling of 14 September; Peek's own bar is 3px until it takes this one.
|
|
26
|
+
quiet: 'h-1 w-full overflow-hidden rounded-full bg-bg-inset',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const INDICATOR_CLASSES: Record<ProgressBarVariant, string> = {
|
|
30
|
+
default: 'h-full rounded-full bg-success-default transition-[width]',
|
|
31
|
+
quiet: 'h-full rounded-full bg-success-muted transition-[width] duration-300',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ProgressBarProps {
|
|
35
|
+
/** How many are done. */
|
|
36
|
+
value: number
|
|
37
|
+
/** How many there are. At `0` the bar is empty. */
|
|
38
|
+
max: number
|
|
39
|
+
/** The accessible name — "Items done". */
|
|
40
|
+
label: string
|
|
41
|
+
/** Default `default`. */
|
|
42
|
+
variant?: ProgressBarVariant
|
|
43
|
+
/** Placement only: a width, a margin, `flex-1` in a row. */
|
|
44
|
+
className?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ProgressBar({ value, max, label, variant = 'default', className }: ProgressBarProps) {
|
|
48
|
+
return (
|
|
49
|
+
// `max={0}` needs no guard: Base UI reads the 0/0 share as an empty bar.
|
|
50
|
+
<Progress.Root value={value} max={max} aria-label={label} className={className}>
|
|
51
|
+
<Progress.Track className={TRACK_CLASSES[variant]}>
|
|
52
|
+
<Progress.Indicator className={INDICATOR_CLASSES[variant]} />
|
|
53
|
+
</Progress.Track>
|
|
54
|
+
</Progress.Root>
|
|
55
|
+
)
|
|
56
|
+
}
|
package/src/ReactionPicker.mdx
CHANGED
|
@@ -32,7 +32,7 @@ The reactions on offer, to choose one from — icon buttons holding emoji, on a
|
|
|
32
32
|
```tsx
|
|
33
33
|
import { ReactionPicker } from '@estiva-app/ui'
|
|
34
34
|
|
|
35
|
-
<Popover side="top" align="right" trigger={reactButton} ariaLabel="Reactions" className="w-auto p-1">
|
|
35
|
+
<Popover side="top" align="right" trigger={reactButton} ariaLabel="Reactions" className="w-auto" contentClassName="p-1">
|
|
36
36
|
<ReactionPicker
|
|
37
37
|
surface={false}
|
|
38
38
|
options={[
|
|
@@ -25,7 +25,7 @@ const OPTIONS: ReactionOption[] = [
|
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
const meta = {
|
|
28
|
-
title: '
|
|
28
|
+
title: 'Components/ReactionPicker',
|
|
29
29
|
component: ReactionPicker,
|
|
30
30
|
args: { options: OPTIONS, onSelect: () => {} },
|
|
31
31
|
argTypes: { options: { control: false }, onSelect: { control: false } },
|
|
@@ -62,7 +62,8 @@ export const FromATrigger: Story = {
|
|
|
62
62
|
side="top"
|
|
63
63
|
align="right"
|
|
64
64
|
ariaLabel="Reactions"
|
|
65
|
-
className="w-auto min-w-0
|
|
65
|
+
className="w-auto min-w-0"
|
|
66
|
+
contentClassName="p-1"
|
|
66
67
|
trigger={
|
|
67
68
|
<ToolbarButton aria-label="React" tooltip="React">
|
|
68
69
|
<IconMoodPlus size={16} stroke={1.5} />
|
package/src/Toolbar.stories.tsx
CHANGED
|
@@ -129,7 +129,8 @@ export const OnAnExistingSurface: Story = {
|
|
|
129
129
|
/* Above the control that opened it: a strip acts on what is under it.
|
|
130
130
|
`side` is the preference; Base UI flips it when there is no room. */
|
|
131
131
|
side="top"
|
|
132
|
-
className="w-auto min-w-0
|
|
132
|
+
className="w-auto min-w-0"
|
|
133
|
+
contentClassName="p-1"
|
|
133
134
|
>
|
|
134
135
|
<Toolbar aria-label="Formatting" surface={false}>
|
|
135
136
|
<ToolbarButton aria-label="Item one" tooltip="Item one">{icon}</ToolbarButton>
|
package/src/index.ts
CHANGED
|
@@ -10,13 +10,24 @@
|
|
|
10
10
|
* TypeScript that produced it.
|
|
11
11
|
*/
|
|
12
12
|
export { AppShell, type AppShellProps } from './AppShell'
|
|
13
|
+
export { AttachmentCard, type AttachmentCardProps, type AttachmentCardState } from './AttachmentCard'
|
|
13
14
|
export { Avatar, hueFor, initialsFor, type AvatarProps } from './Avatar'
|
|
14
15
|
export { AvatarGroup, type AvatarGroupMember, type AvatarGroupProps } from './AvatarGroup'
|
|
15
16
|
export { Banner, type BannerProps, type BannerTone } from './Banner'
|
|
16
17
|
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant } from './Button'
|
|
18
|
+
export { Card, type CardAttention, type CardFill, type CardHover, type CardProps } from './Card'
|
|
17
19
|
export { Checkbox, type CheckboxProps } from './Checkbox'
|
|
18
20
|
export { Chip, type ChipProps, type ChipType } from './Chip'
|
|
19
21
|
export { ChipInput, InputChip, type ChipInputOption, type ChipInputProps, type InputChipProps } from './ChipInput'
|
|
22
|
+
export {
|
|
23
|
+
INLINE_CHIP_CLASSES,
|
|
24
|
+
INLINE_CHIP_TONE_CLASSES,
|
|
25
|
+
InlineChip,
|
|
26
|
+
inlineChipClassName,
|
|
27
|
+
type InlineChipProps,
|
|
28
|
+
type InlineChipTone,
|
|
29
|
+
} from './InlineChip'
|
|
30
|
+
export { Link, type LinkProps, type LinkVariant } from './Link'
|
|
20
31
|
export { IconButton, type IconButtonProps, type IconButtonVariant } from './IconButton'
|
|
21
32
|
export { Kbd, type KbdProps } from './Kbd'
|
|
22
33
|
export { IdentityMenu, IdentityPanel, type Identity, type IdentityMenuProps, type IdentityPanelProps } from './IdentityMenu'
|
|
@@ -30,6 +41,7 @@ export { DialogShell, type DialogShellProps } from './DialogShell'
|
|
|
30
41
|
export { Divider, type DividerProps } from './Divider'
|
|
31
42
|
export { EditableText, type EditableTextProps } from './EditableText'
|
|
32
43
|
export { EmptyState, type EmptyStateProps } from './EmptyState'
|
|
44
|
+
export { ProgressBar, type ProgressBarProps, type ProgressBarVariant } from './ProgressBar'
|
|
33
45
|
export { SkeletonBar, SkeletonList, SkeletonRow } from './Skeleton'
|
|
34
46
|
export { Breadcrumb, type BreadcrumbProps, type Crumb } from './Breadcrumb'
|
|
35
47
|
export { EnterHint, Menu, MenuItem, MenuPanel, MenuRow, MenuSection, MenuSub, type MenuItemProps, type MenuPanelProps, type MenuProps, type MenuSubProps } from './Menu'
|
package/stories/Choosing.mdx
CHANGED
|
@@ -69,11 +69,15 @@ fork freely, owe nothing back, mention it on the package's ticket.
|
|
|
69
69
|
| You need | Reach for |
|
|
70
70
|
|---|---|
|
|
71
71
|
| Label–value rows (a details rail) | **Property** — `row` and `stacked` |
|
|
72
|
+
| One thing — a conversation, a project, an object — as a box you can pick | **Card** — the fill by what it sits on; `href` for a link, `hover="fill"` in a feed |
|
|
72
73
|
| A section heading, chevron, hover actions | **SectionHeader** |
|
|
73
74
|
| A group of rows that opens and closes, and stays how you left it | **CollapsibleSection** — `storageKey` remembers |
|
|
74
75
|
| The uppercase micro-label | **SectionLabel** |
|
|
75
76
|
| A hairline | **Divider** |
|
|
76
77
|
| Where am I, and the way back up | **Breadcrumb** |
|
|
78
|
+
| A link — in text, on a title, beside a note, or a whole box | **Link** — `text`, `quiet`, `underlined`, `plain`; `external` for a new tab |
|
|
79
|
+
| A document or an image attached to something, posted or waiting to be sent | **AttachmentCard** — the file decides the shape; `pending` in a composer |
|
|
80
|
+
| A person or a thing named inside a sentence | **InlineChip** — one line high, level with the words; `href` makes it a link |
|
|
77
81
|
| The key that does this too | **Kbd** — or the `shortcut` prop on **MenuItem**, **Tooltip** and **SearchInput** |
|
|
78
82
|
|
|
79
83
|
## Feedback
|
|
@@ -81,6 +85,7 @@ fork freely, owe nothing back, mention it on the package's ticket.
|
|
|
81
85
|
| You need | Reach for |
|
|
82
86
|
|---|---|
|
|
83
87
|
| Data is on its way | **Skeleton** — shaped like what will arrive |
|
|
88
|
+
| How much of something is done | **ProgressBar** — `default` to be read, with the words beside it; `quiet` for a glance beside a count |
|
|
84
89
|
| Nothing to show | **EmptyState** — says what would fill it; `page` when the whole page is empty (icon, centred), `section` when one part of it is (the line alone, left) |
|
|
85
90
|
| It happened, briefly | **Toast**, via `useToast` inside a `ToastProvider` |
|
|
86
91
|
| It happened, and must not be missed | **Toast** with `durationMs: 0` and an action — it floats and stays until dismissed (D21) |
|