@estiva-app/ui 0.58.0 → 0.59.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/CommandPalette.d.ts.map +1 -1
- package/dist/ComposerTriggers.d.ts +13 -1
- package/dist/ComposerTriggers.d.ts.map +1 -1
- package/dist/Kbd.d.ts.map +1 -1
- package/dist/MembersDialog.d.ts +57 -0
- package/dist/MembersDialog.d.ts.map +1 -0
- package/dist/MembersPill.d.ts +22 -0
- package/dist/MembersPill.d.ts.map +1 -0
- package/dist/Tabs.d.ts +2 -0
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/editor.js +4 -4
- package/dist/editor.js.map +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1126 -1005
- package/dist/index.js.map +4 -4
- package/dist/looks.d.ts +2 -0
- package/dist/looks.d.ts.map +1 -1
- package/package.json +1 -1
- package/registry.json +150 -8
- package/src/CommandPalette.tsx +2 -1
- package/src/ComposerTriggers.mdx +7 -0
- package/src/ComposerTriggers.stories.tsx +43 -4
- package/src/ComposerTriggers.test.tsx +33 -0
- package/src/ComposerTriggers.tsx +17 -5
- package/src/Kbd.tsx +4 -2
- package/src/MembersDialog.mdx +69 -0
- package/src/MembersDialog.stories.tsx +68 -0
- package/src/MembersDialog.test.tsx +69 -0
- package/src/MembersDialog.tsx +184 -0
- package/src/MembersPill.mdx +45 -0
- package/src/MembersPill.stories.tsx +38 -0
- package/src/MembersPill.tsx +35 -0
- package/src/Tabs.mdx +5 -0
- package/src/Tabs.stories.tsx +15 -0
- package/src/Tabs.test.tsx +8 -0
- package/src/Tabs.tsx +12 -0
- package/src/index.ts +2 -0
- package/src/looks.ts +3 -0
- package/src/registry/registry.test.ts +4 -4
- package/stories/Choosing.mdx +2 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { useId, useState, type ReactNode } from 'react'
|
|
2
|
+
import { IconArrowLeft, IconUserMinus, IconUserPlus } from '@tabler/icons-react'
|
|
3
|
+
import { Avatar } from './Avatar'
|
|
4
|
+
import { Button } from './Button'
|
|
5
|
+
import { Chip } from './Chip'
|
|
6
|
+
import { ChipInput, type ChipInputOption } from './ChipInput'
|
|
7
|
+
import { DialogShell } from './DialogShell'
|
|
8
|
+
import { Field } from './Field'
|
|
9
|
+
import { Form } from './Form'
|
|
10
|
+
import { IconButton } from './IconButton'
|
|
11
|
+
import { MenuItem } from './Menu'
|
|
12
|
+
|
|
13
|
+
/** One row of the roster: a face, a name, and an optional second line. */
|
|
14
|
+
export interface MembersDialogMember {
|
|
15
|
+
id: string
|
|
16
|
+
name: string
|
|
17
|
+
/** A picture URL, resolved by the caller, as `Avatar` takes its `src`. */
|
|
18
|
+
picture?: string
|
|
19
|
+
/** The line under the name: a role, a relation ("Assignee"), or a state. */
|
|
20
|
+
caption?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Somebody who can be added: a `ChipInput` option with a face. */
|
|
24
|
+
export interface MembersDialogCandidate extends ChipInputOption {
|
|
25
|
+
picture?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What the viewer's own row offers: in when they are out, out when they are in. */
|
|
29
|
+
export type MembersDialogSelfAction = 'join' | 'leave'
|
|
30
|
+
|
|
31
|
+
export interface MembersDialogProps {
|
|
32
|
+
/** The roster, in the order to draw it. The viewer's own row is the caller's to name ("You"). */
|
|
33
|
+
members: MembersDialogMember[]
|
|
34
|
+
/** The number in the title's chip. Defaults to how many rows there are. */
|
|
35
|
+
count?: number
|
|
36
|
+
/**
|
|
37
|
+
* The viewer's own row on top: "Join" when they are not in, "Leave" when
|
|
38
|
+
* they are. Absent while it is not known, or where they cannot change it —
|
|
39
|
+
* no row is better than a wrong one.
|
|
40
|
+
*/
|
|
41
|
+
self?: {
|
|
42
|
+
action: MembersDialogSelfAction
|
|
43
|
+
onToggle: () => void | Promise<unknown>
|
|
44
|
+
}
|
|
45
|
+
/** Who "Add members" may offer. Members are left out by id. */
|
|
46
|
+
candidates?: MembersDialogCandidate[]
|
|
47
|
+
/**
|
|
48
|
+
* Adds the people chosen. With it, the "Add members" row shows and opens the
|
|
49
|
+
* add layer. Return `false` (or a promise of it) to stay on the add layer —
|
|
50
|
+
* nothing was added; anything else goes back to the roster.
|
|
51
|
+
*/
|
|
52
|
+
onAdd?: (chosen: MembersDialogCandidate[]) => void | boolean | Promise<void | boolean>
|
|
53
|
+
/** One line under the roster — what membership does and does not cover. */
|
|
54
|
+
note?: ReactNode
|
|
55
|
+
/** `add` opens straight on the add layer. */
|
|
56
|
+
initialView?: 'list' | 'add'
|
|
57
|
+
onClose: () => void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const LEADING_CLASSES = 'size-8 rounded-md bg-accent-muted flex items-center justify-center shrink-0 text-accent-primary'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Who is in this, with joining, leaving and adding people — the list a
|
|
64
|
+
* `MembersPill` opens.
|
|
65
|
+
*
|
|
66
|
+
* Moved from Peek's `MembersDialog` (2026-10-06; Katerina's design of 22 July,
|
|
67
|
+
* with the row and inset rulings of 15 September). Two layers in one dialog:
|
|
68
|
+
* the roster, with the viewer's Join or Leave and "Add members" on top, and the
|
|
69
|
+
* add layer, whose back arrow returns to the roster. Adding returns to the
|
|
70
|
+
* roster so the new people are visible at once.
|
|
71
|
+
*/
|
|
72
|
+
export function MembersDialog({ members, count, self, candidates = [], onAdd, note, initialView = 'list', onClose }: MembersDialogProps) {
|
|
73
|
+
const [view, setView] = useState<'list' | 'add'>(onAdd ? initialView : 'list')
|
|
74
|
+
const [chosen, setChosen] = useState<MembersDialogCandidate[]>([])
|
|
75
|
+
const [toggling, setToggling] = useState(false)
|
|
76
|
+
const formId = useId()
|
|
77
|
+
|
|
78
|
+
const toggle = async () => {
|
|
79
|
+
if (!self || toggling) return
|
|
80
|
+
setToggling(true)
|
|
81
|
+
try {
|
|
82
|
+
await self.onToggle()
|
|
83
|
+
} finally {
|
|
84
|
+
setToggling(false)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const add = async () => {
|
|
89
|
+
if (!onAdd || chosen.length === 0) return
|
|
90
|
+
if ((await onAdd(chosen)) === false) return
|
|
91
|
+
setChosen([])
|
|
92
|
+
setView('list')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (view === 'add' && onAdd) {
|
|
96
|
+
return (
|
|
97
|
+
<DialogShell
|
|
98
|
+
title="Add members"
|
|
99
|
+
onClose={onClose}
|
|
100
|
+
headerContent={
|
|
101
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
102
|
+
<IconButton tooltip="Back to members" aria-label="Back to members" className="-ml-2" onClick={() => setView('list')}>
|
|
103
|
+
<IconArrowLeft size={16} stroke={1.5} />
|
|
104
|
+
</IconButton>
|
|
105
|
+
<span className="truncate text-h4 text-text-primary">Add members</span>
|
|
106
|
+
</div>
|
|
107
|
+
}
|
|
108
|
+
bodyClassName="flex flex-col gap-6"
|
|
109
|
+
footer={
|
|
110
|
+
<>
|
|
111
|
+
<Button variant="muted" onClick={onClose}>Cancel</Button>
|
|
112
|
+
<Button variant="primary" type="submit" form={formId} disabled={chosen.length === 0}>Invite</Button>
|
|
113
|
+
</>
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
<Form id={formId} onSubmit={add}>
|
|
117
|
+
<Field label="Invite people" required>
|
|
118
|
+
<ChipInput
|
|
119
|
+
value={chosen}
|
|
120
|
+
onChange={setChosen}
|
|
121
|
+
options={candidates}
|
|
122
|
+
placeholder="Search people..."
|
|
123
|
+
excludeIds={members.map((m) => m.id)}
|
|
124
|
+
chipLeading={(o) => <Avatar name={o.label} src={o.picture} size={16} />}
|
|
125
|
+
rowLeading={(o) => <Avatar name={o.label} src={o.picture} size={32} />}
|
|
126
|
+
autoFocus
|
|
127
|
+
/>
|
|
128
|
+
</Field>
|
|
129
|
+
</Form>
|
|
130
|
+
</DialogShell>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<DialogShell
|
|
136
|
+
title="Members"
|
|
137
|
+
onClose={onClose}
|
|
138
|
+
headerContent={
|
|
139
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
140
|
+
<span className="truncate text-h4 text-text-primary">Members</span>
|
|
141
|
+
<Chip label={String(count ?? members.length)} />
|
|
142
|
+
</div>
|
|
143
|
+
}
|
|
144
|
+
bodyMaxHeight="max-h-[400px]"
|
|
145
|
+
bodyClassName="p-0 py-2"
|
|
146
|
+
>
|
|
147
|
+
{/* MenuItem outside a Menu is a plain button. Its fill sits 8px in from the dialog's sides,
|
|
148
|
+
and its own 12px inset puts the square over the rows' faces, 20px in, at the same 48px. */}
|
|
149
|
+
{(self || onAdd) && (
|
|
150
|
+
<div className="flex flex-col px-2">
|
|
151
|
+
{self && (
|
|
152
|
+
<MenuItem
|
|
153
|
+
size="tall"
|
|
154
|
+
className="py-2"
|
|
155
|
+
disabled={toggling}
|
|
156
|
+
onClick={() => void toggle()}
|
|
157
|
+
leading={<span className={LEADING_CLASSES}>{self.action === 'join' ? <IconUserPlus size={16} stroke={1.5} /> : <IconUserMinus size={16} stroke={1.5} />}</span>}
|
|
158
|
+
label={self.action === 'join' ? 'Join' : 'Leave'}
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
{onAdd && (
|
|
162
|
+
<MenuItem
|
|
163
|
+
size="tall"
|
|
164
|
+
className="py-2"
|
|
165
|
+
onClick={() => setView('add')}
|
|
166
|
+
leading={<span className={LEADING_CLASSES}><IconUserPlus size={16} stroke={1.5} /></span>}
|
|
167
|
+
label="Add members"
|
|
168
|
+
/>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
{members.map((member) => (
|
|
173
|
+
<div key={member.id} className="flex h-12 items-center gap-3 px-5">
|
|
174
|
+
<Avatar size={32} name={member.name} src={member.picture} />
|
|
175
|
+
<div className="flex min-w-0 flex-1 flex-col justify-center gap-[2px]">
|
|
176
|
+
<div className="truncate text-body-2 text-text-primary">{member.name}</div>
|
|
177
|
+
{member.caption && <div className="truncate text-caption text-text-secondary">{member.caption}</div>}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
181
|
+
{note && <div className="px-5 pb-2 pt-3 text-caption text-text-secondary">{note}</div>}
|
|
182
|
+
</DialogShell>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks'
|
|
2
|
+
import * as MembersPillStories from './MembersPill.stories'
|
|
3
|
+
|
|
4
|
+
<Meta of={MembersPillStories} />
|
|
5
|
+
|
|
6
|
+
# MembersPill
|
|
7
|
+
|
|
8
|
+
Who is in this: up to three faces and the total, in a small outlined pill
|
|
9
|
+
that opens the full list.
|
|
10
|
+
|
|
11
|
+
<Canvas of={MembersPillStories.TwoMembers} />
|
|
12
|
+
|
|
13
|
+
## When
|
|
14
|
+
|
|
15
|
+
- A page or conversation header that says, at a glance, who is in it, and
|
|
16
|
+
opens the list — a **MembersDialog** — on a press.
|
|
17
|
+
|
|
18
|
+
## When not
|
|
19
|
+
|
|
20
|
+
- The faces alone, with nothing to open → **AvatarGroup**.
|
|
21
|
+
- The people need to be readable → **MembersDialog**, or a list of **Person** rows.
|
|
22
|
+
|
|
23
|
+
## How
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { MembersPill } from '@estiva-app/ui'
|
|
27
|
+
|
|
28
|
+
const people: { name: string; picture?: string }[] = []
|
|
29
|
+
const [open, setOpen] = useState(false)
|
|
30
|
+
|
|
31
|
+
<MembersPill members={people} onClick={() => setOpen(true)} />
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- `members` is what **AvatarGroup** takes; the first three are drawn.
|
|
35
|
+
- `count` is the number beside the faces when it is not `members.length` —
|
|
36
|
+
pass three faces and the real total.
|
|
37
|
+
- No `onClick` disables it; no members draws nothing.
|
|
38
|
+
|
|
39
|
+
## What it owns
|
|
40
|
+
|
|
41
|
+
Nothing. It only draws. Clicks and keys are yours.
|
|
42
|
+
|
|
43
|
+
## Props
|
|
44
|
+
|
|
45
|
+
<Controls of={MembersPillStories.TwoMembers} />
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { MembersPill } from './MembersPill'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Primitives/MembersPill',
|
|
6
|
+
component: MembersPill,
|
|
7
|
+
args: {
|
|
8
|
+
members: [{ name: 'Ana Duarte' }, { name: 'Ravi Mehta' }],
|
|
9
|
+
onClick: () => {},
|
|
10
|
+
},
|
|
11
|
+
} satisfies Meta<typeof MembersPill>
|
|
12
|
+
|
|
13
|
+
export default meta
|
|
14
|
+
type Story = StoryObj<typeof meta>
|
|
15
|
+
|
|
16
|
+
export const TwoMembers: Story = {}
|
|
17
|
+
|
|
18
|
+
export const One: Story = {
|
|
19
|
+
args: { members: [{ name: 'Ana Duarte' }] },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Twelve members, three faces: the count says the rest. */
|
|
23
|
+
export const Many: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
members: [{ name: 'Ana Duarte' }, { name: 'Ravi Mehta' }, { name: 'Marta Silva' }],
|
|
26
|
+
count: 12,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Nothing to open — the pill is disabled, not another element. */
|
|
31
|
+
export const Disabled: Story = {
|
|
32
|
+
args: { onClick: undefined },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** No members draws nothing at all. */
|
|
36
|
+
export const Empty: Story = {
|
|
37
|
+
args: { members: [] },
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AvatarGroup, type AvatarGroupMember } from './AvatarGroup'
|
|
2
|
+
import { Button } from './Button'
|
|
3
|
+
import { cn } from './cn'
|
|
4
|
+
import { MONO_CAPTION_CLASSES } from './looks'
|
|
5
|
+
|
|
6
|
+
export interface MembersPillProps {
|
|
7
|
+
/** Who is in it, in order. Up to three faces are drawn. */
|
|
8
|
+
members: AvatarGroupMember[]
|
|
9
|
+
/** The number beside the faces. Defaults to how many members there are. */
|
|
10
|
+
count?: number
|
|
11
|
+
/** Opens the list — usually a `MembersDialog`. Without it the pill is disabled. */
|
|
12
|
+
onClick?: () => void
|
|
13
|
+
className?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Who is in this: up to three faces and the total, in a small outlined pill
|
|
18
|
+
* that opens the full list.
|
|
19
|
+
*
|
|
20
|
+
* Moved from Peek's `MembersPill` (2026-10-06), unchanged: the package's
|
|
21
|
+
* Button, outlined, at its default 32px — the faces sit 3px inside the
|
|
22
|
+
* hairline on the top, the bottom and the left, 8px from the count (Katerina's
|
|
23
|
+
* pick of 15 September). `disabled` rather than another element when there is
|
|
24
|
+
* nothing to press, so one element keeps one set of pixels.
|
|
25
|
+
*/
|
|
26
|
+
export function MembersPill({ members, count, onClick, className }: MembersPillProps) {
|
|
27
|
+
const total = count ?? members.length
|
|
28
|
+
if (total === 0) return null
|
|
29
|
+
return (
|
|
30
|
+
<Button variant="outlined" aria-label={`${total} ${total === 1 ? 'member' : 'members'}`} disabled={!onClick} onClick={onClick} className={cn('gap-2 pl-[3px] pr-2', className)}>
|
|
31
|
+
<AvatarGroup members={members} />
|
|
32
|
+
<span className={cn(MONO_CAPTION_CLASSES, 'signal:tabular-nums')}>{total}</span>
|
|
33
|
+
</Button>
|
|
34
|
+
)
|
|
35
|
+
}
|
package/src/Tabs.mdx
CHANGED
|
@@ -17,8 +17,13 @@ is a neutral fill, not the accent — selection is a place, not a highlight.
|
|
|
17
17
|
baseline — the one way a number sits beside a label. A count of `0` is
|
|
18
18
|
still drawn ("Assigned to me · 0" is an answer); an absent count draws
|
|
19
19
|
nothing.
|
|
20
|
+
- A tab with `unread` draws the **UnreadDot** after its label (and count),
|
|
21
|
+
without the dot's 24px slot so the row keeps one height, and says "new" in
|
|
22
|
+
its accessible name.
|
|
20
23
|
- Two sizes: `default` (14px) and `small` (12px, the denser geometry).
|
|
21
24
|
|
|
25
|
+
<Canvas of={TabsStories.WithUnread} />
|
|
26
|
+
|
|
22
27
|
<Canvas of={TabsStories.WithAZero} />
|
|
23
28
|
|
|
24
29
|
## When not
|
package/src/Tabs.stories.tsx
CHANGED
|
@@ -74,6 +74,21 @@ export const WithAnIcon: Story = {
|
|
|
74
74
|
),
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** `unread`: something new behind a tab you are not on — the accent dot after the label. */
|
|
78
|
+
export const WithUnread: Story = {
|
|
79
|
+
args: { tabs: [], active: 'overview', onChange: () => {} },
|
|
80
|
+
render: () => (
|
|
81
|
+
<Demo
|
|
82
|
+
initial="overview"
|
|
83
|
+
tabs={[
|
|
84
|
+
{ id: 'overview', label: 'Overview' },
|
|
85
|
+
{ id: 'activity', label: 'Activity', unread: true },
|
|
86
|
+
{ id: 'all', label: 'All', count: 12, unread: true },
|
|
87
|
+
]}
|
|
88
|
+
/>
|
|
89
|
+
),
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
/** `small` is the 12px geometry. */
|
|
78
93
|
export const Small: Story = {
|
|
79
94
|
args: { tabs: [], active: 'conversations', onChange: () => {} },
|
package/src/Tabs.test.tsx
CHANGED
|
@@ -123,6 +123,14 @@ describe('Tabs', () => {
|
|
|
123
123
|
expect(list.getAttribute('aria-labelledby')).toBeNull()
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
+
it('an unread tab draws the dot and says "new" in its name; the others draw neither', () => {
|
|
127
|
+
const tabs: TabDef<Id>[] = [{ id: 'one', label: 'One' }, { id: 'two', label: 'Two', count: 3, unread: true }, { id: 'three', label: 'Three' }]
|
|
128
|
+
render(<Tabs tabs={tabs} active="one" onChange={() => {}} />)
|
|
129
|
+
expect(screen.getByRole('tab', { name: /^Two.*\bnew$/ }).querySelector('[data-unread]')).not.toBeNull()
|
|
130
|
+
expect(screen.getByRole('tab', { name: 'One' }).querySelector('[data-unread]')).toBeNull()
|
|
131
|
+
expect(screen.getByRole('tab', { name: 'Three' }).querySelector('[data-unread]')).toBeNull()
|
|
132
|
+
})
|
|
133
|
+
|
|
126
134
|
it('puts className on the outer box, around the row', () => {
|
|
127
135
|
const { container } = render(<Tabs tabs={THREE} active="one" onChange={() => {}} className="mt-4" />)
|
|
128
136
|
const outer = container.firstElementChild as HTMLElement
|
package/src/Tabs.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import { Tabs as BaseTabs } from '@base-ui/react/tabs'
|
|
3
3
|
import { cn } from './cn'
|
|
4
|
+
import { UnreadDot } from './UnreadDot'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* A row of tabs. Ship's Tabs (2026-09-01), which was Peek's TopicTabs with
|
|
@@ -26,6 +27,8 @@ export interface TabDef<T extends string> {
|
|
|
26
27
|
count?: number
|
|
27
28
|
/** 16px, stroke 1.5. */
|
|
28
29
|
icon?: ReactNode
|
|
30
|
+
/** Something new behind this tab: the `UnreadDot` after the label, and "new" in the tab's accessible name. */
|
|
31
|
+
unread?: boolean
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
export interface TabsProps<T extends string> {
|
|
@@ -91,6 +94,15 @@ export function Tabs<T extends string>({ tabs, active, onChange, size = 'default
|
|
|
91
94
|
<span className="ml-2.5 font-mono text-caption tabular-nums text-text-secondary">{tab.count}</span>
|
|
92
95
|
) : null}
|
|
93
96
|
</span>
|
|
97
|
+
{/* The dot without its 24px slot: a tab is a line of text, and the
|
|
98
|
+
slot would make an unread tab taller than its neighbours. The
|
|
99
|
+
disc says nothing to a screen reader, so the name says "new". */}
|
|
100
|
+
{tab.unread ? (
|
|
101
|
+
<>
|
|
102
|
+
<UnreadDot className="h-auto w-auto" />
|
|
103
|
+
<span className="sr-only">new</span>
|
|
104
|
+
</>
|
|
105
|
+
) : null}
|
|
94
106
|
</BaseTabs.Tab>
|
|
95
107
|
))}
|
|
96
108
|
</BaseTabs.List>
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ export { AppShell, type AppShellProps } from './AppShell'
|
|
|
13
13
|
export { AttachmentCard, type AttachmentCardProps, type AttachmentCardState } from './AttachmentCard'
|
|
14
14
|
export { Avatar, hueFor, initialsFor, type AvatarProps } from './Avatar'
|
|
15
15
|
export { AvatarGroup, type AvatarGroupMember, type AvatarGroupProps } from './AvatarGroup'
|
|
16
|
+
export { MembersPill, type MembersPillProps } from './MembersPill'
|
|
17
|
+
export { MembersDialog, type MembersDialogCandidate, type MembersDialogMember, type MembersDialogProps, type MembersDialogSelfAction } from './MembersDialog'
|
|
16
18
|
export { Banner, type BannerProps, type BannerTone } from './Banner'
|
|
17
19
|
export { BlockHandle, blockAtY, type BlockHandleProps, type BlockHandleEditing, type HandleBlock } from './BlockHandle'
|
|
18
20
|
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant } from './Button'
|
package/src/looks.ts
CHANGED
|
@@ -43,6 +43,9 @@ export const COUNT_TEXT_CLASSES = 'font-mono text-caption tabular-nums text-text
|
|
|
43
43
|
/** A chip's words: Chip's label and Reaction's count. */
|
|
44
44
|
export const CHIP_TEXT_CLASSES = 'text-chip signal:font-mono signal:text-small signal:font-semibold signal:tabular-nums'
|
|
45
45
|
|
|
46
|
+
/** Small secondary words that turn mono under signal: Kbd's key, CommandPalette's footer and MembersPill's count. */
|
|
47
|
+
export const MONO_CAPTION_CLASSES = 'text-caption text-text-secondary signal:font-mono signal:text-small'
|
|
48
|
+
|
|
46
49
|
/** Words typed straight into an input with no box of its own: SearchInput and ChipInput. */
|
|
47
50
|
export const BARE_INPUT_CLASSES = 'bg-transparent text-text-primary placeholder:text-text-muted outline-none'
|
|
48
51
|
|
|
@@ -48,19 +48,19 @@ describe('the registry builds', () => {
|
|
|
48
48
|
expect(registry.entries.map((one) => one.name).sort()).toEqual(values.map((one) => one.name).sort())
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
it('counts
|
|
51
|
+
it('counts 133 names over 72 files: 103 components, 29 helpers and 1 hook (NavTree, NavTreeSection, the composer triggers and UnreadDot joined on 2 October; BlockHandle and EditorBlockHandle (RIC-18) on 2 October, moveBlock and blockDropCursor, the drop in the gutter, on 5 October, MembersPill and MembersDialog, moved from Peek (SHI-31), on 6 October, and ShowMore on 7 October)', () => {
|
|
52
52
|
// The reconciliation GATES.md §24 explains. If this changes, that changes.
|
|
53
53
|
// The ten `EDITOR_EXTENSIONS` count as components by their names; they are
|
|
54
54
|
// editor extensions, documented on SelectionToolbar's, BlockNodes' and
|
|
55
55
|
// ComposerTriggers' pages.
|
|
56
56
|
const kinds = registry.entries.reduce<Record<string, number>>((all, one) => ({ ...all, [one.kind]: (all[one.kind] ?? 0) + 1 }), {})
|
|
57
|
-
expect(kinds).toEqual({ component:
|
|
58
|
-
expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(
|
|
57
|
+
expect(kinds).toEqual({ component: 103, helper: 29, hook: 1 })
|
|
58
|
+
expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(72)
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
it('gives every entry a purpose, from its own page or from the comment above it', () => {
|
|
62
62
|
expect(registry.entries.filter((one) => one.purpose.trim() === '')).toEqual([])
|
|
63
|
-
expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(
|
|
63
|
+
expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(68)
|
|
64
64
|
expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(65)
|
|
65
65
|
})
|
|
66
66
|
|
package/stories/Choosing.mdx
CHANGED
|
@@ -51,6 +51,8 @@ fork freely, owe nothing back, mention it on the package's ticket.
|
|
|
51
51
|
|---|---|
|
|
52
52
|
| One face | **Avatar** |
|
|
53
53
|
| Several faces, overlapping | **AvatarGroup** |
|
|
54
|
+
| Who is in it, at a glance, opening the list | **MembersPill** |
|
|
55
|
+
| Who is in it, with Join, Leave and adding people | **MembersDialog** |
|
|
54
56
|
| Face and name in a row | **Person** |
|
|
55
57
|
| The face that opens a menu | **PersonTrigger** |
|
|
56
58
|
| Who you are, where you are, sign out | **IdentityMenu** — the account menu, ready-made |
|