@astryxdesign/lab 0.7.2-canary.f16dc9f → 0.7.2-canary.f2b60a0
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/blocks/CommentComposerShowcase.doc.mjs +15 -0
- package/blocks/CommentComposerShowcase.tsx +45 -0
- package/blocks/CommentPinInMargin.doc.mjs +14 -0
- package/blocks/CommentPinInMargin.tsx +77 -0
- package/blocks/CommentPinShowcase.doc.mjs +15 -0
- package/blocks/CommentPinShowcase.tsx +188 -0
- package/blocks/CommentThreadInPopover.doc.mjs +21 -0
- package/blocks/CommentThreadInPopover.tsx +89 -0
- package/blocks/CommentThreadShowcase.doc.mjs +15 -0
- package/blocks/CommentThreadShowcase.tsx +67 -0
- package/blocks/CommentsInCard.doc.mjs +14 -0
- package/blocks/CommentsInCard.tsx +78 -0
- package/blocks/CommentsInline.doc.mjs +14 -0
- package/blocks/CommentsInline.tsx +297 -0
- package/blocks/CommentsShowcase.doc.mjs +15 -0
- package/blocks/CommentsShowcase.tsx +341 -0
- package/blocks/EmojiPickerInPopover.doc.mjs +14 -0
- package/blocks/EmojiPickerInPopover.tsx +48 -0
- package/blocks/EmojiPickerShowcase.doc.mjs +15 -0
- package/blocks/EmojiPickerShowcase.tsx +33 -0
- package/dist/Chat/ChatTypingIndicator.d.ts.map +1 -1
- package/dist/Chat/ChatTypingIndicator.js +1 -1
- package/dist/CommentPin/CommentPin.d.ts +86 -0
- package/dist/CommentPin/CommentPin.d.ts.map +1 -0
- package/dist/CommentPin/CommentPin.js +260 -0
- package/dist/CommentPin/index.d.ts +9 -0
- package/dist/CommentPin/index.d.ts.map +1 -0
- package/dist/CommentPin/index.js +11 -0
- package/dist/Comments/CommentActions.d.ts +55 -0
- package/dist/Comments/CommentActions.d.ts.map +1 -0
- package/dist/Comments/CommentActions.js +472 -0
- package/dist/Comments/CommentComposer.d.ts +108 -0
- package/dist/Comments/CommentComposer.d.ts.map +1 -0
- package/dist/Comments/CommentComposer.js +507 -0
- package/dist/Comments/CommentMarkdown.d.ts +15 -0
- package/dist/Comments/CommentMarkdown.d.ts.map +1 -0
- package/dist/Comments/CommentMarkdown.js +136 -0
- package/dist/Comments/CommentParts.d.ts +238 -0
- package/dist/Comments/CommentParts.d.ts.map +1 -0
- package/dist/Comments/CommentParts.js +823 -0
- package/dist/Comments/CommentRow.d.ts +20 -0
- package/dist/Comments/CommentRow.d.ts.map +1 -0
- package/dist/Comments/CommentRow.js +693 -0
- package/dist/Comments/CommentThread.d.ts +66 -0
- package/dist/Comments/CommentThread.d.ts.map +1 -0
- package/dist/Comments/CommentThread.js +169 -0
- package/dist/Comments/Comments.d.ts +105 -0
- package/dist/Comments/Comments.d.ts.map +1 -0
- package/dist/Comments/Comments.js +426 -0
- package/dist/Comments/CommentsContext.d.ts +117 -0
- package/dist/Comments/CommentsContext.d.ts.map +1 -0
- package/dist/Comments/CommentsContext.js +515 -0
- package/dist/Comments/i18n.d.ts +18 -0
- package/dist/Comments/i18n.d.ts.map +1 -0
- package/dist/Comments/i18n.js +34 -0
- package/dist/Comments/icons.d.ts +24 -0
- package/dist/Comments/icons.d.ts.map +1 -0
- package/dist/Comments/icons.js +93 -0
- package/dist/Comments/index.d.ts +16 -0
- package/dist/Comments/index.d.ts.map +1 -0
- package/dist/Comments/index.js +15 -0
- package/dist/Comments/types.d.ts +341 -0
- package/dist/Comments/types.d.ts.map +1 -0
- package/dist/Comments/types.js +100 -0
- package/dist/Comments/useCommentThreadDismissal.d.ts +88 -0
- package/dist/Comments/useCommentThreadDismissal.d.ts.map +1 -0
- package/dist/Comments/useCommentThreadDismissal.js +344 -0
- package/dist/Comments/utils.d.ts +44 -0
- package/dist/Comments/utils.d.ts.map +1 -0
- package/dist/Comments/utils.js +84 -0
- package/dist/EmojiPicker/EmojiPicker.d.ts +96 -0
- package/dist/EmojiPicker/EmojiPicker.d.ts.map +1 -0
- package/dist/EmojiPicker/EmojiPicker.js +451 -0
- package/dist/EmojiPicker/emojiData.d.ts +13 -0
- package/dist/EmojiPicker/emojiData.d.ts.map +1 -0
- package/dist/EmojiPicker/emojiData.js +36 -0
- package/dist/EmojiPicker/index.d.ts +9 -0
- package/dist/EmojiPicker/index.d.ts.map +1 -0
- package/dist/EmojiPicker/index.js +11 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/lab.css +105 -46
- package/package.json +8 -6
- package/src/Chat/ChatTypingIndicator.tsx +3 -0
- package/src/CommentPin/CommentPin.doc.mjs +328 -0
- package/src/CommentPin/CommentPin.spec.md +307 -0
- package/src/CommentPin/CommentPin.test.tsx +350 -0
- package/src/CommentPin/CommentPin.tsx +484 -0
- package/src/CommentPin/index.ts +13 -0
- package/src/Comments/CommentActions.tsx +542 -0
- package/src/Comments/CommentComposer.doc.mjs +177 -0
- package/src/Comments/CommentComposer.test.tsx +255 -0
- package/src/Comments/CommentComposer.tsx +680 -0
- package/src/Comments/CommentMarkdown.tsx +184 -0
- package/src/Comments/CommentParts.tsx +937 -0
- package/src/Comments/CommentRow.tsx +918 -0
- package/src/Comments/CommentThread.doc.mjs +580 -0
- package/src/Comments/CommentThread.test.tsx +634 -0
- package/src/Comments/CommentThread.tsx +226 -0
- package/src/Comments/Comments.doc.mjs +648 -0
- package/src/Comments/Comments.test.tsx +1335 -0
- package/src/Comments/Comments.tsx +643 -0
- package/src/Comments/CommentsContext.tsx +755 -0
- package/src/Comments/i18n.ts +40 -0
- package/src/Comments/icons.tsx +129 -0
- package/src/Comments/index.ts +39 -0
- package/src/Comments/types.ts +392 -0
- package/src/Comments/useCommentThreadDismissal.ts +426 -0
- package/src/Comments/utils.ts +114 -0
- package/src/EmojiPicker/EmojiPicker.doc.mjs +186 -0
- package/src/EmojiPicker/EmojiPicker.test.tsx +276 -0
- package/src/EmojiPicker/EmojiPicker.tsx +574 -0
- package/src/EmojiPicker/emojiData.test.ts +32 -0
- package/src/EmojiPicker/emojiData.ts +3138 -0
- package/src/EmojiPicker/index.ts +13 -0
- package/src/index.ts +46 -8
- package/blocks/DrawerFullWidthMobile.doc.mjs +0 -21
- package/blocks/DrawerFullWidthMobile.tsx +0 -42
- package/blocks/DrawerHeaderShowcase.doc.mjs +0 -15
- package/blocks/DrawerHeaderShowcase.tsx +0 -73
- package/blocks/DrawerHeaderSubtitleEndContent.doc.mjs +0 -22
- package/blocks/DrawerHeaderSubtitleEndContent.tsx +0 -49
- package/blocks/DrawerHeaderWithCloseButton.doc.mjs +0 -21
- package/blocks/DrawerHeaderWithCloseButton.tsx +0 -42
- package/blocks/DrawerMobileReveal.doc.mjs +0 -21
- package/blocks/DrawerMobileReveal.tsx +0 -43
- package/blocks/DrawerScrim.doc.mjs +0 -22
- package/blocks/DrawerScrim.tsx +0 -68
- package/blocks/DrawerShowcase.doc.mjs +0 -22
- package/blocks/DrawerShowcase.tsx +0 -41
- package/blocks/DrawerStart.doc.mjs +0 -21
- package/blocks/DrawerStart.tsx +0 -41
- package/blocks/DrawerWidths.doc.mjs +0 -22
- package/blocks/DrawerWidths.tsx +0 -54
- package/dist/Drawer/Drawer.d.ts +0 -181
- package/dist/Drawer/Drawer.d.ts.map +0 -1
- package/dist/Drawer/Drawer.js +0 -388
- package/dist/Drawer/DrawerHeader.d.ts +0 -82
- package/dist/Drawer/DrawerHeader.d.ts.map +0 -1
- package/dist/Drawer/DrawerHeader.js +0 -131
- package/dist/Drawer/index.d.ts +0 -11
- package/dist/Drawer/index.d.ts.map +0 -1
- package/dist/Drawer/index.js +0 -11
- package/dist/Drawer/useDrawerDialogPresence.d.ts +0 -42
- package/dist/Drawer/useDrawerDialogPresence.d.ts.map +0 -1
- package/dist/Drawer/useDrawerDialogPresence.js +0 -235
- package/src/Drawer/Drawer.doc.mjs +0 -470
- package/src/Drawer/Drawer.spec.md +0 -343
- package/src/Drawer/Drawer.test.tsx +0 -1039
- package/src/Drawer/Drawer.tsx +0 -618
- package/src/Drawer/DrawerHeader.doc.mjs +0 -291
- package/src/Drawer/DrawerHeader.test.tsx +0 -89
- package/src/Drawer/DrawerHeader.tsx +0 -237
- package/src/Drawer/index.ts +0 -13
- package/src/Drawer/useDrawerDialogPresence.ts +0 -283
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @input A Markdown-like document, its anchored threads, and controlled
|
|
7
|
+
* document/thread selection
|
|
8
|
+
* @output A realistic document review: highlighted passages stay synchronized
|
|
9
|
+
* with a Comments side panel, and a selected passage starts a quoted thread
|
|
10
|
+
* @position Copyable Lab Comments inline-comments example
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {useRef, useState} from 'react';
|
|
14
|
+
import * as stylex from '@stylexjs/stylex';
|
|
15
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
16
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
17
|
+
import {HStack, VStack} from '@astryxdesign/core/Layout';
|
|
18
|
+
import {Text} from '@astryxdesign/core/Text';
|
|
19
|
+
import {
|
|
20
|
+
borderVars,
|
|
21
|
+
colorVars,
|
|
22
|
+
focusVars,
|
|
23
|
+
radiusVars,
|
|
24
|
+
spacingVars,
|
|
25
|
+
} from '@astryxdesign/core/theme/tokens.stylex';
|
|
26
|
+
import {
|
|
27
|
+
Comments,
|
|
28
|
+
type CommentAnchor,
|
|
29
|
+
type CommentData,
|
|
30
|
+
type CommentThreadData,
|
|
31
|
+
type CommentUser,
|
|
32
|
+
} from '@astryxdesign/lab';
|
|
33
|
+
|
|
34
|
+
const VIEWER: CommentUser = {id: 'avery', name: 'Avery Kim'};
|
|
35
|
+
const MAYA: CommentUser = {id: 'maya', name: 'Maya Chen'};
|
|
36
|
+
const JORDAN: CommentUser = {id: 'jordan', name: 'Jordan Lee'};
|
|
37
|
+
|
|
38
|
+
const ROLLOUT_PASSAGE =
|
|
39
|
+
'The final cohort completes rollout in three days after the dry run.';
|
|
40
|
+
const GUARDRAIL_PASSAGE =
|
|
41
|
+
'Pause if the 95th percentile exceeds 420 ms for two consecutive checks.';
|
|
42
|
+
const REVIEW_PASSAGE =
|
|
43
|
+
'Publish the review note after the final cohort is complete.';
|
|
44
|
+
|
|
45
|
+
const INITIAL_COMMENTS: readonly CommentThreadData[] = [
|
|
46
|
+
{
|
|
47
|
+
id: 'rollout-window',
|
|
48
|
+
author: MAYA,
|
|
49
|
+
body: 'Can we make the dry-run dependency explicit? It explains the extra day.',
|
|
50
|
+
createdAt: '2026-10-14T17:18:00Z',
|
|
51
|
+
anchor: {quote: ROLLOUT_PASSAGE},
|
|
52
|
+
replies: [
|
|
53
|
+
{
|
|
54
|
+
id: 'rollout-reply',
|
|
55
|
+
author: VIEWER,
|
|
56
|
+
body: 'Updated. The window now starts after the dry run finishes.',
|
|
57
|
+
createdAt: '2026-10-14T17:31:00Z',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'latency-threshold',
|
|
63
|
+
author: JORDAN,
|
|
64
|
+
body: 'This matches the alert. I would keep the two-check window to avoid pausing on a single spike.',
|
|
65
|
+
createdAt: '2026-10-14T18:04:00Z',
|
|
66
|
+
anchor: {quote: GUARDRAIL_PASSAGE},
|
|
67
|
+
isUnread: true,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const styles = stylex.create({
|
|
72
|
+
passage: {
|
|
73
|
+
appearance: 'none',
|
|
74
|
+
backgroundColor: colorVars['--color-background-yellow'],
|
|
75
|
+
borderStyle: 'solid',
|
|
76
|
+
borderWidth: 0,
|
|
77
|
+
borderBlockEndWidth: borderVars['--border-width'],
|
|
78
|
+
borderBlockEndColor: 'transparent',
|
|
79
|
+
borderRadius: radiusVars['--radius-inner'],
|
|
80
|
+
color: 'inherit',
|
|
81
|
+
cursor: 'pointer',
|
|
82
|
+
font: 'inherit',
|
|
83
|
+
lineHeight: 'inherit',
|
|
84
|
+
marginInline: spacingVars['--spacing-0-5'],
|
|
85
|
+
paddingBlock: 0,
|
|
86
|
+
paddingInline: spacingVars['--spacing-0-5'],
|
|
87
|
+
textAlign: 'start',
|
|
88
|
+
outlineStyle: {
|
|
89
|
+
default: 'none',
|
|
90
|
+
':focus-visible': focusVars['--focus-outline-style'],
|
|
91
|
+
},
|
|
92
|
+
outlineWidth: focusVars['--focus-outline-width'],
|
|
93
|
+
outlineColor: focusVars['--focus-outline-color'],
|
|
94
|
+
outlineOffset: focusVars['--focus-outline-offset'],
|
|
95
|
+
},
|
|
96
|
+
selectedPassage: {
|
|
97
|
+
borderBlockEndColor: colorVars['--color-border-yellow'],
|
|
98
|
+
},
|
|
99
|
+
selectablePassage: {
|
|
100
|
+
backgroundColor: {
|
|
101
|
+
default: 'transparent',
|
|
102
|
+
':hover': colorVars['--color-background-yellow'],
|
|
103
|
+
},
|
|
104
|
+
borderBlockEndStyle: 'dashed',
|
|
105
|
+
borderBlockEndColor: colorVars['--color-border-emphasized'],
|
|
106
|
+
},
|
|
107
|
+
document: {
|
|
108
|
+
overflowY: 'auto',
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function updateComment(
|
|
113
|
+
comments: readonly CommentThreadData[],
|
|
114
|
+
id: string,
|
|
115
|
+
update: (comment: CommentData) => CommentData,
|
|
116
|
+
): CommentThreadData[] {
|
|
117
|
+
return comments.map(comment =>
|
|
118
|
+
comment.id === id
|
|
119
|
+
? {...comment, ...update(comment)}
|
|
120
|
+
: {
|
|
121
|
+
...comment,
|
|
122
|
+
replies: comment.replies?.map(reply =>
|
|
123
|
+
reply.id === id ? update(reply) : reply,
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default function CommentsInline() {
|
|
130
|
+
const [comments, setComments] = useState<CommentThreadData[]>(() => [
|
|
131
|
+
...INITIAL_COMMENTS,
|
|
132
|
+
]);
|
|
133
|
+
const [selectedThreadId, setSelectedThreadId] = useState<string | null>(
|
|
134
|
+
'rollout-window',
|
|
135
|
+
);
|
|
136
|
+
const [selectedPassage, setSelectedPassage] = useState<string | null>(null);
|
|
137
|
+
const [composerAnchor, setComposerAnchor] = useState<CommentAnchor>();
|
|
138
|
+
const nextId = useRef(1);
|
|
139
|
+
|
|
140
|
+
const post = (body: string, parentCommentId?: string) => {
|
|
141
|
+
const id = `review-comment-${nextId.current++}`;
|
|
142
|
+
const comment: CommentData = {
|
|
143
|
+
id,
|
|
144
|
+
author: VIEWER,
|
|
145
|
+
body,
|
|
146
|
+
createdAt: new Date().toISOString(),
|
|
147
|
+
};
|
|
148
|
+
if (parentCommentId != null) {
|
|
149
|
+
setComments(current =>
|
|
150
|
+
current.map(thread =>
|
|
151
|
+
thread.id === parentCommentId
|
|
152
|
+
? {...thread, replies: [...(thread.replies ?? []), comment]}
|
|
153
|
+
: thread,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
setComments(current => [...current, {...comment, anchor: composerAnchor}]);
|
|
159
|
+
setComposerAnchor(undefined);
|
|
160
|
+
setSelectedPassage(null);
|
|
161
|
+
setSelectedThreadId(id);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<HStack gap={3} align="start">
|
|
166
|
+
<Card width={520} height={620} padding={4}>
|
|
167
|
+
<VStack gap={4} width="100%" xstyle={styles.document}>
|
|
168
|
+
<VStack gap={1}>
|
|
169
|
+
<Text as="h1" type="display-3" weight="bold">
|
|
170
|
+
Launch plan
|
|
171
|
+
</Text>
|
|
172
|
+
<Text type="supporting" color="secondary">
|
|
173
|
+
Draft · updated today
|
|
174
|
+
</Text>
|
|
175
|
+
</VStack>
|
|
176
|
+
|
|
177
|
+
<VStack gap={2}>
|
|
178
|
+
<Text as="h2" type="body" weight="bold">
|
|
179
|
+
Rollout
|
|
180
|
+
</Text>
|
|
181
|
+
<Text type="body">
|
|
182
|
+
Begin with the internal cohort, then expand by region.
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
{...stylex.props(
|
|
186
|
+
styles.passage,
|
|
187
|
+
selectedThreadId === 'rollout-window' &&
|
|
188
|
+
styles.selectedPassage,
|
|
189
|
+
)}
|
|
190
|
+
aria-label={`Open comments on “${ROLLOUT_PASSAGE}”`}
|
|
191
|
+
onClick={() => {
|
|
192
|
+
setSelectedPassage(null);
|
|
193
|
+
setComposerAnchor(undefined);
|
|
194
|
+
setSelectedThreadId('rollout-window');
|
|
195
|
+
}}>
|
|
196
|
+
{ROLLOUT_PASSAGE}
|
|
197
|
+
</button>
|
|
198
|
+
Owners confirm each step before the next begins.
|
|
199
|
+
</Text>
|
|
200
|
+
</VStack>
|
|
201
|
+
|
|
202
|
+
<VStack gap={2}>
|
|
203
|
+
<Text as="h2" type="body" weight="bold">
|
|
204
|
+
Guardrails
|
|
205
|
+
</Text>
|
|
206
|
+
<Text type="body">
|
|
207
|
+
Watch latency and error rate throughout the rollout.
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
{...stylex.props(
|
|
211
|
+
styles.passage,
|
|
212
|
+
selectedThreadId === 'latency-threshold' &&
|
|
213
|
+
styles.selectedPassage,
|
|
214
|
+
)}
|
|
215
|
+
aria-label={`Open comments on “${GUARDRAIL_PASSAGE}”`}
|
|
216
|
+
onClick={() => {
|
|
217
|
+
setSelectedPassage(null);
|
|
218
|
+
setComposerAnchor(undefined);
|
|
219
|
+
setSelectedThreadId('latency-threshold');
|
|
220
|
+
}}>
|
|
221
|
+
{GUARDRAIL_PASSAGE}
|
|
222
|
+
</button>
|
|
223
|
+
Resume only after the metric returns to baseline.
|
|
224
|
+
</Text>
|
|
225
|
+
</VStack>
|
|
226
|
+
|
|
227
|
+
<VStack gap={2} align="start">
|
|
228
|
+
<Text as="h2" type="body" weight="bold">
|
|
229
|
+
Review
|
|
230
|
+
</Text>
|
|
231
|
+
<Text type="body">
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
{...stylex.props(
|
|
235
|
+
styles.passage,
|
|
236
|
+
styles.selectablePassage,
|
|
237
|
+
selectedPassage === REVIEW_PASSAGE && styles.selectedPassage,
|
|
238
|
+
)}
|
|
239
|
+
aria-label={`Select “${REVIEW_PASSAGE}” for a comment`}
|
|
240
|
+
onClick={() => {
|
|
241
|
+
setSelectedThreadId(null);
|
|
242
|
+
setComposerAnchor(undefined);
|
|
243
|
+
setSelectedPassage(REVIEW_PASSAGE);
|
|
244
|
+
}}>
|
|
245
|
+
{REVIEW_PASSAGE}
|
|
246
|
+
</button>
|
|
247
|
+
</Text>
|
|
248
|
+
{selectedPassage === REVIEW_PASSAGE && (
|
|
249
|
+
<Button
|
|
250
|
+
label="Comment on selection"
|
|
251
|
+
variant="secondary"
|
|
252
|
+
size="sm"
|
|
253
|
+
onClick={() => {
|
|
254
|
+
setComposerAnchor({quote: REVIEW_PASSAGE});
|
|
255
|
+
setSelectedPassage(null);
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
)}
|
|
259
|
+
</VStack>
|
|
260
|
+
</VStack>
|
|
261
|
+
</Card>
|
|
262
|
+
|
|
263
|
+
<Card width={360} height={620} padding={0}>
|
|
264
|
+
<Comments
|
|
265
|
+
label="Comments on Launch plan"
|
|
266
|
+
comments={comments}
|
|
267
|
+
currentUser={VIEWER}
|
|
268
|
+
container="card"
|
|
269
|
+
density="compact"
|
|
270
|
+
selectedThreadId={selectedThreadId}
|
|
271
|
+
onSelectedThreadChange={setSelectedThreadId}
|
|
272
|
+
composerAnchor={composerAnchor}
|
|
273
|
+
onComposerAnchorDismiss={() => setComposerAnchor(undefined)}
|
|
274
|
+
onSubmit={post}
|
|
275
|
+
onResolveThread={commentId =>
|
|
276
|
+
setComments(current =>
|
|
277
|
+
updateComment(current, commentId, comment => ({
|
|
278
|
+
...comment,
|
|
279
|
+
isResolved: true,
|
|
280
|
+
resolvedBy: VIEWER,
|
|
281
|
+
})),
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
onReopenThread={commentId =>
|
|
285
|
+
setComments(current =>
|
|
286
|
+
updateComment(current, commentId, comment => ({
|
|
287
|
+
...comment,
|
|
288
|
+
isResolved: false,
|
|
289
|
+
resolvedBy: undefined,
|
|
290
|
+
})),
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
/>
|
|
294
|
+
</Card>
|
|
295
|
+
</HStack>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export default {
|
|
5
|
+
type: 'block',
|
|
6
|
+
name: 'Comments panel',
|
|
7
|
+
displayName: 'Comments panel',
|
|
8
|
+
description:
|
|
9
|
+
'A working Comments panel: threads with replies, reactions, edit history, controlled typing with hidden seen metadata, inline comments, a non-interrupting unread-reply reveal, an agent reply still being written, a resolved thread, a comment that failed to send, activity events, and earlier comments to load. Post, reply, react, edit, delete, and resolve; the host updates the controlled comments.',
|
|
10
|
+
exampleFor: 'Comments',
|
|
11
|
+
isReady: true,
|
|
12
|
+
isShowcase: true,
|
|
13
|
+
aspectRatio: 3 / 4,
|
|
14
|
+
componentsUsed: ['Comments', 'VStack'],
|
|
15
|
+
};
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @input Controlled comments, events, and every Comments callback
|
|
7
|
+
* @output A working Comments panel: threads with replies, reactions,
|
|
8
|
+
* edit history, inline comments, controlled typing and hidden seen metadata,
|
|
9
|
+
* a non-interrupting unread-reply reveal, an agent's reply in progress, a
|
|
10
|
+
* failed comment, activity events, and earlier comments to load
|
|
11
|
+
* @position Lab Comments's docsite showcase and copyable CLI block
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {useRef, useState} from 'react';
|
|
15
|
+
import {VStack} from '@astryxdesign/core/Layout';
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_COMMENT_REACTIONS,
|
|
18
|
+
Comments,
|
|
19
|
+
type CommentData,
|
|
20
|
+
type CommentReactionOption,
|
|
21
|
+
type CommentThreadData,
|
|
22
|
+
type CommentUser,
|
|
23
|
+
type CommentVersion,
|
|
24
|
+
type CommentActivityEvent,
|
|
25
|
+
} from '@astryxdesign/lab';
|
|
26
|
+
|
|
27
|
+
const VIEWER: CommentUser = {id: 'avery', name: 'Avery Kim'};
|
|
28
|
+
const MAYA: CommentUser = {id: 'maya', name: 'Maya Chen'};
|
|
29
|
+
const NOAH: CommentUser = {id: 'noah', name: 'Noah Williams'};
|
|
30
|
+
const ASSISTANT: CommentUser = {
|
|
31
|
+
id: 'release-assistant',
|
|
32
|
+
name: 'Release Assistant',
|
|
33
|
+
kind: 'agent',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const EVENTS: readonly CommentActivityEvent[] = [
|
|
37
|
+
{
|
|
38
|
+
id: 'checklist',
|
|
39
|
+
label: 'Rollout checklist attached',
|
|
40
|
+
description: 'Added the launch checklist to the discussion.',
|
|
41
|
+
createdAt: '2026-09-30T18:46:00Z',
|
|
42
|
+
actor: MAYA,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'status',
|
|
46
|
+
label: 'Status changed to Ready for review',
|
|
47
|
+
createdAt: '2026-09-30T19:02:00Z',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const INITIAL_COMMENTS: readonly CommentThreadData[] = [
|
|
52
|
+
{
|
|
53
|
+
id: 'plan',
|
|
54
|
+
author: MAYA,
|
|
55
|
+
body: 'The revised rollout plan is ready. **Avery**, could you check the final cohort?',
|
|
56
|
+
createdAt: '2026-09-30T18:30:00Z',
|
|
57
|
+
isEdited: true,
|
|
58
|
+
isUnread: true,
|
|
59
|
+
typingUsers: [NOAH],
|
|
60
|
+
seenBy: [NOAH],
|
|
61
|
+
previousVersions: [
|
|
62
|
+
{
|
|
63
|
+
id: 'plan-v1',
|
|
64
|
+
body: 'The revised rollout plan is ready for review.',
|
|
65
|
+
editedAt: '2026-09-30T18:10:00Z',
|
|
66
|
+
author: MAYA,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
reactions: [
|
|
70
|
+
{
|
|
71
|
+
key: 'like',
|
|
72
|
+
label: '👍',
|
|
73
|
+
description: 'Like',
|
|
74
|
+
count: 2,
|
|
75
|
+
reactedByViewer: true,
|
|
76
|
+
actors: [VIEWER, NOAH],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
replies: [
|
|
80
|
+
{
|
|
81
|
+
id: 'plan-reply-1',
|
|
82
|
+
author: NOAH,
|
|
83
|
+
body: 'I checked the cohort and the guardrail query. Both look good.',
|
|
84
|
+
createdAt: '2026-09-30T18:42:00Z',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'plan-reply-2',
|
|
88
|
+
author: ASSISTANT,
|
|
89
|
+
body: '',
|
|
90
|
+
createdAt: '2026-09-30T18:45:00Z',
|
|
91
|
+
progress: {label: 'Checking the rollout dependencies…'},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'inline',
|
|
97
|
+
author: NOAH,
|
|
98
|
+
body: 'Should this say three days? The dry run adds one.',
|
|
99
|
+
createdAt: '2026-09-30T18:52:00Z',
|
|
100
|
+
anchor: {quote: 'The final cohort completes rollout in two days.'},
|
|
101
|
+
replies: [
|
|
102
|
+
{
|
|
103
|
+
id: 'inline-reply-1',
|
|
104
|
+
author: MAYA,
|
|
105
|
+
body: 'Yes — I updated the rollout window to three days.',
|
|
106
|
+
createdAt: '2026-09-30T19:04:00Z',
|
|
107
|
+
isUnread: true,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'orphaned',
|
|
113
|
+
author: MAYA,
|
|
114
|
+
body: 'Typo: "Teh".',
|
|
115
|
+
createdAt: '2026-09-30T18:55:00Z',
|
|
116
|
+
anchor: {quote: 'Teh guardrail query runs hourly.', status: 'orphaned'},
|
|
117
|
+
isResolved: true,
|
|
118
|
+
resolvedBy: VIEWER,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'failed',
|
|
122
|
+
author: VIEWER,
|
|
123
|
+
body: 'Posting this update failed, but the text is kept.',
|
|
124
|
+
createdAt: '2026-09-30T19:10:00Z',
|
|
125
|
+
status: 'error',
|
|
126
|
+
errorMessage: "Couldn't post this comment.",
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
function updateComment(
|
|
131
|
+
comments: readonly CommentThreadData[],
|
|
132
|
+
id: string,
|
|
133
|
+
update: (comment: CommentData) => CommentData,
|
|
134
|
+
): CommentThreadData[] {
|
|
135
|
+
return comments.map(comment =>
|
|
136
|
+
comment.id === id
|
|
137
|
+
? {...comment, ...update(comment)}
|
|
138
|
+
: {
|
|
139
|
+
...comment,
|
|
140
|
+
replies: comment.replies?.map(reply =>
|
|
141
|
+
reply.id === id ? update(reply) : reply,
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function findComment(
|
|
148
|
+
comments: readonly CommentThreadData[],
|
|
149
|
+
id: string,
|
|
150
|
+
): CommentData | undefined {
|
|
151
|
+
for (const comment of comments) {
|
|
152
|
+
if (comment.id === id) {
|
|
153
|
+
return comment;
|
|
154
|
+
}
|
|
155
|
+
const reply = comment.replies?.find(candidate => candidate.id === id);
|
|
156
|
+
if (reply != null) {
|
|
157
|
+
return reply;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default function CommentsShowcase() {
|
|
164
|
+
const [comments, setComments] = useState<CommentThreadData[]>(() => [
|
|
165
|
+
...INITIAL_COMMENTS,
|
|
166
|
+
]);
|
|
167
|
+
const [selectedThreadId, setSelectedThreadId] = useState<string | null>(
|
|
168
|
+
'plan',
|
|
169
|
+
);
|
|
170
|
+
const [hasOlderComments, setHasOlderComments] = useState(true);
|
|
171
|
+
const nextId = useRef(1);
|
|
172
|
+
|
|
173
|
+
const post = (body: string, parentCommentId?: string) => {
|
|
174
|
+
const comment = {
|
|
175
|
+
id: `demo-${nextId.current++}`,
|
|
176
|
+
author: VIEWER,
|
|
177
|
+
body,
|
|
178
|
+
createdAt: new Date().toISOString(),
|
|
179
|
+
};
|
|
180
|
+
setComments(current =>
|
|
181
|
+
parentCommentId == null
|
|
182
|
+
? [...current, comment]
|
|
183
|
+
: current.map(root =>
|
|
184
|
+
root.id === parentCommentId
|
|
185
|
+
? {...root, replies: [...(root.replies ?? []), comment]}
|
|
186
|
+
: root,
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const saveEdit = (commentId: string, body: string) =>
|
|
192
|
+
setComments(current =>
|
|
193
|
+
updateComment(current, commentId, comment => ({
|
|
194
|
+
...comment,
|
|
195
|
+
body,
|
|
196
|
+
isEdited: true,
|
|
197
|
+
previousVersions: [
|
|
198
|
+
{
|
|
199
|
+
id: `${comment.id}-${Date.now()}`,
|
|
200
|
+
body: comment.body,
|
|
201
|
+
editedAt: new Date().toISOString(),
|
|
202
|
+
author: comment.author,
|
|
203
|
+
},
|
|
204
|
+
...(comment.previousVersions ?? []),
|
|
205
|
+
],
|
|
206
|
+
})),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const remove = (commentId: string) =>
|
|
210
|
+
setComments(current =>
|
|
211
|
+
current
|
|
212
|
+
.filter(comment => comment.id !== commentId)
|
|
213
|
+
.map(comment => ({
|
|
214
|
+
...comment,
|
|
215
|
+
replies: comment.replies?.filter(reply => reply.id !== commentId),
|
|
216
|
+
})),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const react = (
|
|
220
|
+
commentId: string,
|
|
221
|
+
key: string,
|
|
222
|
+
picked?: CommentReactionOption,
|
|
223
|
+
) => {
|
|
224
|
+
const option =
|
|
225
|
+
picked ??
|
|
226
|
+
DEFAULT_COMMENT_REACTIONS.find(candidate => candidate.key === key);
|
|
227
|
+
if (option == null) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
setComments(current =>
|
|
231
|
+
updateComment(current, commentId, comment => {
|
|
232
|
+
const existing = comment.reactions?.find(
|
|
233
|
+
reaction => reaction.key === key,
|
|
234
|
+
);
|
|
235
|
+
const reactions =
|
|
236
|
+
existing == null
|
|
237
|
+
? [
|
|
238
|
+
...(comment.reactions ?? []),
|
|
239
|
+
{...option, count: 1, reactedByViewer: true, actors: [VIEWER]},
|
|
240
|
+
]
|
|
241
|
+
: (comment.reactions ?? [])
|
|
242
|
+
.map(reaction =>
|
|
243
|
+
reaction.key === key
|
|
244
|
+
? {
|
|
245
|
+
...reaction,
|
|
246
|
+
count:
|
|
247
|
+
reaction.count + (reaction.reactedByViewer ? -1 : 1),
|
|
248
|
+
reactedByViewer: !reaction.reactedByViewer,
|
|
249
|
+
}
|
|
250
|
+
: reaction,
|
|
251
|
+
)
|
|
252
|
+
.filter(reaction => reaction.count > 0);
|
|
253
|
+
return {...comment, reactions};
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const setResolved = (commentId: string, isResolved: boolean) =>
|
|
259
|
+
setComments(current =>
|
|
260
|
+
current.map(comment =>
|
|
261
|
+
comment.id === commentId
|
|
262
|
+
? {
|
|
263
|
+
...comment,
|
|
264
|
+
isResolved,
|
|
265
|
+
resolvedBy: isResolved ? VIEWER : undefined,
|
|
266
|
+
}
|
|
267
|
+
: comment,
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const loadVersions = async (
|
|
272
|
+
commentId: string,
|
|
273
|
+
): Promise<readonly CommentVersion[]> => {
|
|
274
|
+
const comment = findComment(comments, commentId);
|
|
275
|
+
if (comment == null) {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
return [
|
|
279
|
+
{
|
|
280
|
+
id: `${comment.id}-current`,
|
|
281
|
+
body: comment.body,
|
|
282
|
+
editedAt: comment.updatedAt ?? comment.createdAt,
|
|
283
|
+
author: comment.author,
|
|
284
|
+
isCurrent: true,
|
|
285
|
+
},
|
|
286
|
+
...(comment.previousVersions ?? []),
|
|
287
|
+
];
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<VStack width="100%" height="100%">
|
|
292
|
+
<Comments
|
|
293
|
+
comments={comments}
|
|
294
|
+
events={EVENTS}
|
|
295
|
+
currentUser={VIEWER}
|
|
296
|
+
selectedThreadId={selectedThreadId}
|
|
297
|
+
onSelectedThreadChange={setSelectedThreadId}
|
|
298
|
+
density="compact"
|
|
299
|
+
hasOlderComments={hasOlderComments}
|
|
300
|
+
onLoadOlder={() => {
|
|
301
|
+
setComments(current => [
|
|
302
|
+
{
|
|
303
|
+
id: 'older',
|
|
304
|
+
author: NOAH,
|
|
305
|
+
body: 'Earlier context loads above the threads.',
|
|
306
|
+
createdAt: '2026-09-30T17:45:00Z',
|
|
307
|
+
},
|
|
308
|
+
...current,
|
|
309
|
+
]);
|
|
310
|
+
setHasOlderComments(false);
|
|
311
|
+
}}
|
|
312
|
+
agentCommands={[
|
|
313
|
+
{
|
|
314
|
+
command: 'ask',
|
|
315
|
+
name: 'Release Assistant',
|
|
316
|
+
description: 'Ask about the rollout',
|
|
317
|
+
},
|
|
318
|
+
]}
|
|
319
|
+
onUploadImages={async files =>
|
|
320
|
+
files.map(file => ({url: URL.createObjectURL(file), alt: file.name}))
|
|
321
|
+
}
|
|
322
|
+
onSubmit={post}
|
|
323
|
+
onSaveEdit={saveEdit}
|
|
324
|
+
onDelete={remove}
|
|
325
|
+
onReact={react}
|
|
326
|
+
onResolveThread={id => setResolved(id, true)}
|
|
327
|
+
onReopenThread={id => setResolved(id, false)}
|
|
328
|
+
onRetryComment={commentId =>
|
|
329
|
+
setComments(current =>
|
|
330
|
+
updateComment(current, commentId, comment => ({
|
|
331
|
+
...comment,
|
|
332
|
+
status: 'sent',
|
|
333
|
+
errorMessage: undefined,
|
|
334
|
+
})),
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
loadVersions={loadVersions}
|
|
338
|
+
/>
|
|
339
|
+
</VStack>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export default {
|
|
5
|
+
type: 'block',
|
|
6
|
+
name: 'In a popover',
|
|
7
|
+
displayName: 'In a popover',
|
|
8
|
+
description:
|
|
9
|
+
'Open the picker from an emoji button in a Popover with no padding. Leave the Popover’s autofocus on: it lands in the search field, so typing searches right away.',
|
|
10
|
+
exampleFor: 'EmojiPicker',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 4 / 3,
|
|
13
|
+
componentsUsed: ['EmojiPicker', 'Popover', 'IconButton', 'Text', 'HStack'],
|
|
14
|
+
};
|