@agent-native/core-corpus 0.218.0 → 0.219.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/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/hubspot/SKILL.md +5 -3
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/CHANGELOG.md +5 -0
- package/corpus/templates/content/app/components/editor/CommentStack.tsx +301 -0
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +651 -149
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +19 -14
- package/corpus/templates/content/app/components/editor/block-field-comment-threads.ts +15 -0
- package/corpus/templates/content/app/components/editor/block-field-comments.tsx +5 -9
- package/corpus/templates/content/app/components/editor/comment-stacks.ts +143 -0
- package/corpus/templates/content/app/i18n-data.ts +8 -0
- package/corpus/templates/content/changelog/2026-10-22-blocks-with-several-comment-threads-fold-into-one-stack-in-t.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-opening-a-page-no-longer-saves-an-edit-again-after-another-o.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +8 -1
- package/corpus/templates/design/.generated/bridge/interact-links.generated.ts +8 -8
- package/corpus/templates/design/CHANGELOG.md +5 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +18 -1
- package/corpus/templates/design/app/components/design/bridge/interact-links.bridge.ts +8 -8
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +15 -13
- package/corpus/templates/design/app/i18n/en-US.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +15 -6
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +29 -5
- package/corpus/templates/design/app/pages/design-editor/prototype-navigation-transition.ts +423 -0
- package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +35 -9
- package/corpus/templates/design/changelog/2026-10-20-the-design-full-app-toggle-on-the-home-page-no-longer-hides-.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-prototype-links-can-play-code-authored-screen-transitions.md +6 -0
- package/corpus/templates/design/shared/motion-authored-css-import.ts +938 -7
- package/corpus/templates/design/shared/motion-html-source.ts +1689 -135
- package/corpus/templates/design/shared/motion-prototype-transition.ts +329 -0
- package/corpus/templates/design/shared/motion-react-source.ts +2154 -126
- package/corpus/templates/design/shared/motion-text-runtime-v1.fixture.txt +221 -0
- package/package.json +1 -1
|
@@ -129,6 +129,22 @@ import {
|
|
|
129
129
|
startCommentAiSubmission,
|
|
130
130
|
type CommentAiController,
|
|
131
131
|
} from "./comment-ai";
|
|
132
|
+
import {
|
|
133
|
+
commentBlockKey,
|
|
134
|
+
commentStackFront,
|
|
135
|
+
commentStackKey,
|
|
136
|
+
groupCommentStacks,
|
|
137
|
+
measureCommentBlocks,
|
|
138
|
+
type CommentBlockPlacement,
|
|
139
|
+
type CommentStack,
|
|
140
|
+
} from "./comment-stacks";
|
|
141
|
+
import {
|
|
142
|
+
CommentStackCard,
|
|
143
|
+
CommentStackGuide,
|
|
144
|
+
CommentStackHeader,
|
|
145
|
+
CommentStackSheets,
|
|
146
|
+
CommentStackShowAll,
|
|
147
|
+
} from "./CommentStack";
|
|
132
148
|
import {
|
|
133
149
|
ReviewAddReactionButton,
|
|
134
150
|
ReviewCommentMenu,
|
|
@@ -342,6 +358,26 @@ export function estimateThreadCardHeight(thread: ThreadLayoutIdentity) {
|
|
|
342
358
|
*/
|
|
343
359
|
export const PENDING_COMMENT_LAYOUT_ID = "pending-comment";
|
|
344
360
|
const PENDING_COMMENT_CARD_ESTIMATE = 112;
|
|
361
|
+
/** An open stack's Collapse row, laid out like a card above its threads. */
|
|
362
|
+
const STACK_HEADER_LAYOUT_ID = "comment-stack-header";
|
|
363
|
+
const STACK_HEADER_HEIGHT = 28;
|
|
364
|
+
/** How long the pointer rests on a highlight before its thread comes to the front. */
|
|
365
|
+
const STACK_HOVER_INTENT_MS = 180;
|
|
366
|
+
const NO_BLOCKS: ReadonlyMap<string, CommentBlockPlacement> = new Map();
|
|
367
|
+
const NO_TOUCHES: ReadonlyMap<string, number> = new Map();
|
|
368
|
+
|
|
369
|
+
function sameBlocks(
|
|
370
|
+
left: ReadonlyMap<string, CommentBlockPlacement>,
|
|
371
|
+
right: ReadonlyMap<string, CommentBlockPlacement>,
|
|
372
|
+
) {
|
|
373
|
+
if (left.size !== right.size) return false;
|
|
374
|
+
for (const [threadId, placement] of right) {
|
|
375
|
+
const prior = left.get(threadId);
|
|
376
|
+
if (prior?.key !== placement.key || prior.order !== placement.order)
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
345
381
|
|
|
346
382
|
type CommentLayoutItem<T extends ThreadLayoutIdentity> = {
|
|
347
383
|
thread: T;
|
|
@@ -774,6 +810,8 @@ interface CommentsSidebarOptions {
|
|
|
774
810
|
onActivateSuggestion?: (id: string) => void;
|
|
775
811
|
onSelectedThreadChange?: (id: string | null) => void;
|
|
776
812
|
onHoveredThreadChange?: (id: string | null) => void;
|
|
813
|
+
/** The thread whose highlight the pointer is on. */
|
|
814
|
+
hoveredThreadId?: string | null;
|
|
777
815
|
currentUserEmail?: string;
|
|
778
816
|
/** The signed-in person's display name, for their own suggestions. */
|
|
779
817
|
currentUserName?: string;
|
|
@@ -860,6 +898,7 @@ export function CommentsSidebar({
|
|
|
860
898
|
onActivateSuggestion,
|
|
861
899
|
onSelectedThreadChange,
|
|
862
900
|
onHoveredThreadChange,
|
|
901
|
+
hoveredThreadId,
|
|
863
902
|
currentUserEmail,
|
|
864
903
|
currentUserName,
|
|
865
904
|
currentUserOrgId,
|
|
@@ -1133,6 +1172,226 @@ export function CommentsSidebar({
|
|
|
1133
1172
|
)
|
|
1134
1173
|
setReplyingThreadId(selectedThreadId);
|
|
1135
1174
|
}, [canComment, presentation, selectedThreadId, selectedThreadIsOpen]);
|
|
1175
|
+
|
|
1176
|
+
// In the margin, a block with two or more open threads folds into one
|
|
1177
|
+
// card. Opening it lays its threads out as cards; one stack opens at once.
|
|
1178
|
+
const stacksEnabled =
|
|
1179
|
+
presentation === "inline" &&
|
|
1180
|
+
surface === "rail" &&
|
|
1181
|
+
alignToAnchors &&
|
|
1182
|
+
!visibleThreadId;
|
|
1183
|
+
const [threadBlocks, setThreadBlocks] = useState(NO_BLOCKS);
|
|
1184
|
+
// The block a new comment's text is in, once measured; `key` is null for
|
|
1185
|
+
// text in no block, such as a Property.
|
|
1186
|
+
const [pendingBlock, setPendingBlock] = useState<{
|
|
1187
|
+
id: symbol;
|
|
1188
|
+
key: string | null;
|
|
1189
|
+
} | null>(null);
|
|
1190
|
+
// The open stack's threads, as last laid out. Whichever stack still holds
|
|
1191
|
+
// one of them is open, so it outlasts its block key shifting and the thread
|
|
1192
|
+
// it was opened on being resolved.
|
|
1193
|
+
const [openStack, setOpenStack] = useState<ReadonlySet<string> | null>(null);
|
|
1194
|
+
// When each thread was last touched, so a folded stack shows that one.
|
|
1195
|
+
// Thread ids are unique across tabs, so a touch never crosses tabs.
|
|
1196
|
+
const [stackTouches, setStackTouches] = useState(NO_TOUCHES);
|
|
1197
|
+
const touchCountRef = useRef(0);
|
|
1198
|
+
const touchThread = useCallback((threadId: string) => {
|
|
1199
|
+
touchCountRef.current += 1;
|
|
1200
|
+
const touch = touchCountRef.current;
|
|
1201
|
+
setStackTouches((prev) => new Map(prev).set(threadId, touch));
|
|
1202
|
+
}, []);
|
|
1203
|
+
// A thread just posted is selected without moving the open stack.
|
|
1204
|
+
const postedThreadIdRef = useRef<string | null>(null);
|
|
1205
|
+
const focusStackKeyRef = useRef<string | null>(null);
|
|
1206
|
+
const commentStacks = useMemo(
|
|
1207
|
+
() => (stacksEnabled ? groupCommentStacks(openThreads, threadBlocks) : []),
|
|
1208
|
+
[openThreads, stacksEnabled, threadBlocks],
|
|
1209
|
+
);
|
|
1210
|
+
const stackOfThread = useMemo(() => {
|
|
1211
|
+
const stacks = new Map<string, CommentStack<CommentThread>>();
|
|
1212
|
+
for (const stack of commentStacks) {
|
|
1213
|
+
for (const member of stack.threads) stacks.set(member.threadId, stack);
|
|
1214
|
+
}
|
|
1215
|
+
return stacks;
|
|
1216
|
+
}, [commentStacks]);
|
|
1217
|
+
const stackOfThreadRef = useRef(stackOfThread);
|
|
1218
|
+
stackOfThreadRef.current = stackOfThread;
|
|
1219
|
+
const pendingStackKey =
|
|
1220
|
+
displayedPendingComment && pendingBlock?.id === displayedPendingComment.id
|
|
1221
|
+
? pendingBlock.key
|
|
1222
|
+
: undefined;
|
|
1223
|
+
const openedStack = openStack
|
|
1224
|
+
? (commentStacks.find((stack) =>
|
|
1225
|
+
stack.threads.some((member) => openStack.has(member.threadId)),
|
|
1226
|
+
) ?? null)
|
|
1227
|
+
: null;
|
|
1228
|
+
// A new comment outside the open stack folds it, as does leaving it with
|
|
1229
|
+
// nothing selected.
|
|
1230
|
+
const pendingFoldsOpenStack =
|
|
1231
|
+
pendingStackKey !== undefined &&
|
|
1232
|
+
pendingStackKey !== (openedStack?.key ?? null);
|
|
1233
|
+
const expandedStack =
|
|
1234
|
+
pendingFoldsOpenStack || (!selectedThreadId && !pendingComment)
|
|
1235
|
+
? null
|
|
1236
|
+
: openedStack;
|
|
1237
|
+
// A new comment on a folded stack opens on top of it.
|
|
1238
|
+
const composingStack =
|
|
1239
|
+
pendingStackKey == null || pendingStackKey === expandedStack?.key
|
|
1240
|
+
? null
|
|
1241
|
+
: (commentStacks.find((stack) => stack.key === pendingStackKey) ?? null);
|
|
1242
|
+
|
|
1243
|
+
const openStackOn = useCallback((threadId: string) => {
|
|
1244
|
+
const stack = stackOfThreadRef.current.get(threadId);
|
|
1245
|
+
setOpenStack(
|
|
1246
|
+
new Set(
|
|
1247
|
+
stack ? stack.threads.map((member) => member.threadId) : [threadId],
|
|
1248
|
+
),
|
|
1249
|
+
);
|
|
1250
|
+
}, []);
|
|
1251
|
+
const previousSelectionRef = useRef<string | null>(null);
|
|
1252
|
+
useEffect(() => {
|
|
1253
|
+
const previous = previousSelectionRef.current;
|
|
1254
|
+
previousSelectionRef.current = selectedThreadId ?? null;
|
|
1255
|
+
if (!stacksEnabled || !selectedThreadId || selectedThreadId === previous)
|
|
1256
|
+
return;
|
|
1257
|
+
touchThread(selectedThreadId);
|
|
1258
|
+
const posted = postedThreadIdRef.current === selectedThreadId;
|
|
1259
|
+
postedThreadIdRef.current = null;
|
|
1260
|
+
if (!posted) openStackOn(selectedThreadId);
|
|
1261
|
+
}, [openStackOn, selectedThreadId, stacksEnabled, touchThread]);
|
|
1262
|
+
useEffect(() => {
|
|
1263
|
+
if (!openStack) return;
|
|
1264
|
+
if ((!selectedThreadId && !pendingComment) || pendingFoldsOpenStack) {
|
|
1265
|
+
setOpenStack(null);
|
|
1266
|
+
} else if (openedStack) {
|
|
1267
|
+
const members = openedStack.threads;
|
|
1268
|
+
if (
|
|
1269
|
+
members.length !== openStack.size ||
|
|
1270
|
+
members.some((member) => !openStack.has(member.threadId))
|
|
1271
|
+
)
|
|
1272
|
+
setOpenStack(new Set(members.map((member) => member.threadId)));
|
|
1273
|
+
} else if (
|
|
1274
|
+
// A thread selected before the margin measured its block may still
|
|
1275
|
+
// turn out to stack. Otherwise none of them does, and keeping them
|
|
1276
|
+
// would open a stack their block forms later by itself.
|
|
1277
|
+
!openThreads.some(
|
|
1278
|
+
(thread) =>
|
|
1279
|
+
!thread.resolved &&
|
|
1280
|
+
openStack.has(thread.threadId) &&
|
|
1281
|
+
!threadBlocks.has(thread.threadId),
|
|
1282
|
+
)
|
|
1283
|
+
) {
|
|
1284
|
+
setOpenStack(null);
|
|
1285
|
+
}
|
|
1286
|
+
}, [
|
|
1287
|
+
openStack,
|
|
1288
|
+
openThreads,
|
|
1289
|
+
openedStack,
|
|
1290
|
+
pendingComment,
|
|
1291
|
+
pendingFoldsOpenStack,
|
|
1292
|
+
selectedThreadId,
|
|
1293
|
+
threadBlocks,
|
|
1294
|
+
]);
|
|
1295
|
+
// A highlight click opens its stack and touches its thread, even one already
|
|
1296
|
+
// selected, whose selection doesn't change.
|
|
1297
|
+
useEffect(() => {
|
|
1298
|
+
const container = scrollContainerRef?.current;
|
|
1299
|
+
if (!stacksEnabled || !container) return;
|
|
1300
|
+
const openFromHighlight = (event: MouseEvent) => {
|
|
1301
|
+
const threadId = (event.target as Element | null)
|
|
1302
|
+
?.closest?.("[data-comment-thread]")
|
|
1303
|
+
?.getAttribute("data-comment-thread");
|
|
1304
|
+
if (!threadId || !stackOfThreadRef.current.has(threadId)) return;
|
|
1305
|
+
touchThread(threadId);
|
|
1306
|
+
openStackOn(threadId);
|
|
1307
|
+
};
|
|
1308
|
+
container.addEventListener("click", openFromHighlight);
|
|
1309
|
+
return () => container.removeEventListener("click", openFromHighlight);
|
|
1310
|
+
}, [openStackOn, scrollContainerRef, stacksEnabled, touchThread]);
|
|
1311
|
+
// Resting on a highlight brings its thread to the front of a folded stack;
|
|
1312
|
+
// passing over one on the way to the margin doesn't.
|
|
1313
|
+
useEffect(() => {
|
|
1314
|
+
if (!hoveredThreadId) return;
|
|
1315
|
+
const stack = stackOfThread.get(hoveredThreadId);
|
|
1316
|
+
if (!stack || stack === expandedStack) return;
|
|
1317
|
+
// Touching the stack's last-touched thread again would only rerun this.
|
|
1318
|
+
const touch = stackTouches.get(hoveredThreadId);
|
|
1319
|
+
if (
|
|
1320
|
+
touch !== undefined &&
|
|
1321
|
+
stack.threads.every(
|
|
1322
|
+
(member) => (stackTouches.get(member.threadId) ?? 0) <= touch,
|
|
1323
|
+
)
|
|
1324
|
+
)
|
|
1325
|
+
return;
|
|
1326
|
+
const timer = setTimeout(
|
|
1327
|
+
() => touchThread(hoveredThreadId),
|
|
1328
|
+
STACK_HOVER_INTENT_MS,
|
|
1329
|
+
);
|
|
1330
|
+
return () => clearTimeout(timer);
|
|
1331
|
+
}, [
|
|
1332
|
+
expandedStack,
|
|
1333
|
+
hoveredThreadId,
|
|
1334
|
+
stackOfThread,
|
|
1335
|
+
stackTouches,
|
|
1336
|
+
touchThread,
|
|
1337
|
+
]);
|
|
1338
|
+
const expandedStackKey = expandedStack?.key ?? null;
|
|
1339
|
+
const selectedInExpandedStack =
|
|
1340
|
+
!!selectedThreadId &&
|
|
1341
|
+
!!expandedStack?.threads.some(
|
|
1342
|
+
(member) => member.threadId === selectedThreadId,
|
|
1343
|
+
);
|
|
1344
|
+
useLayoutEffect(() => {
|
|
1345
|
+
if (!selectedInExpandedStack || !selectedThreadId) return;
|
|
1346
|
+
const card = sidebarRef.current?.querySelector<HTMLElement>(
|
|
1347
|
+
`[data-thread-card="${cssEscape(selectedThreadId)}"]`,
|
|
1348
|
+
);
|
|
1349
|
+
if (!card) return;
|
|
1350
|
+
// Opening from the folded card removes the button that had focus.
|
|
1351
|
+
const active = document.activeElement;
|
|
1352
|
+
if (!active || active === document.body)
|
|
1353
|
+
card.focus({ preventScroll: true });
|
|
1354
|
+
const timer = setTimeout(
|
|
1355
|
+
() => card.scrollIntoView?.({ block: "nearest", behavior: "smooth" }),
|
|
1356
|
+
300,
|
|
1357
|
+
);
|
|
1358
|
+
return () => clearTimeout(timer);
|
|
1359
|
+
}, [expandedStackKey, selectedInExpandedStack, selectedThreadId]);
|
|
1360
|
+
const pendingInExpandedStack =
|
|
1361
|
+
!!expandedStack && pendingStackKey === expandedStack.key;
|
|
1362
|
+
useLayoutEffect(() => {
|
|
1363
|
+
if (!pendingInExpandedStack) return;
|
|
1364
|
+
// Show all, under a new comment, removes the button that had focus.
|
|
1365
|
+
const active = document.activeElement;
|
|
1366
|
+
if (active && active !== document.body) return;
|
|
1367
|
+
sidebarRef.current
|
|
1368
|
+
?.querySelector<HTMLElement>(
|
|
1369
|
+
'[data-comment-pending] [contenteditable="true"]',
|
|
1370
|
+
)
|
|
1371
|
+
?.focus({ preventScroll: true });
|
|
1372
|
+
}, [pendingInExpandedStack]);
|
|
1373
|
+
useLayoutEffect(() => {
|
|
1374
|
+
const key = focusStackKeyRef.current;
|
|
1375
|
+
if (key === null) return;
|
|
1376
|
+
const stack = [
|
|
1377
|
+
...(sidebarRef.current?.querySelectorAll<HTMLElement>(
|
|
1378
|
+
"[data-comment-stack]",
|
|
1379
|
+
) ?? []),
|
|
1380
|
+
].find((element) => element.dataset.commentStack === key);
|
|
1381
|
+
const target = stack?.querySelector<HTMLElement>(
|
|
1382
|
+
"[data-comment-stack-open], [data-comment-stack-show-all]",
|
|
1383
|
+
);
|
|
1384
|
+
if (!target) return;
|
|
1385
|
+
focusStackKeyRef.current = null;
|
|
1386
|
+
target.focus({ preventScroll: true });
|
|
1387
|
+
});
|
|
1388
|
+
const collapseStack = (stack: CommentStack<CommentThread>) => {
|
|
1389
|
+
focusStackKeyRef.current = stack.key;
|
|
1390
|
+
setOpenStack(null);
|
|
1391
|
+
setReplyingThreadId(null);
|
|
1392
|
+
onSelectedThreadChange?.(null);
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1136
1395
|
const historyAuthors = useMemo(() => {
|
|
1137
1396
|
const authors = new Map<string, string>();
|
|
1138
1397
|
// A name from any record outranks the email's local part from another.
|
|
@@ -1339,6 +1598,7 @@ export function CommentsSidebar({
|
|
|
1339
1598
|
pendingDraft.beginSubmission(clientOperationId);
|
|
1340
1599
|
onPendingChange(id, () => ({ submitting: true }));
|
|
1341
1600
|
setPendingHandoff({ id, operationId: clientOperationId });
|
|
1601
|
+
touchThread(`optimistic-${clientOperationId}`);
|
|
1342
1602
|
try {
|
|
1343
1603
|
const result = await createComment.mutateAsync({
|
|
1344
1604
|
clientOperationId,
|
|
@@ -1362,7 +1622,11 @@ export function CommentsSidebar({
|
|
|
1362
1622
|
});
|
|
1363
1623
|
pendingDraft.finishSubmission(clientOperationId);
|
|
1364
1624
|
setPendingHandoff(null);
|
|
1625
|
+
// The new thread is its stack's front. Selecting it leaves its stack
|
|
1626
|
+
// as it was, open or folded.
|
|
1627
|
+
touchThread(result.threadId);
|
|
1365
1628
|
if (pendingCommentRef.current?.id === id) {
|
|
1629
|
+
postedThreadIdRef.current = result.threadId;
|
|
1366
1630
|
onPendingDone(id, result.threadId);
|
|
1367
1631
|
}
|
|
1368
1632
|
return { id: result.id, threadId: result.threadId };
|
|
@@ -1522,18 +1786,35 @@ export function CommentsSidebar({
|
|
|
1522
1786
|
proposalMemberCounts,
|
|
1523
1787
|
threadPositions,
|
|
1524
1788
|
]);
|
|
1789
|
+
// A folded stack is laid out as its first thread; a new comment opened on
|
|
1790
|
+
// top of it takes its place.
|
|
1791
|
+
const stackHiddenIds = useMemo(() => {
|
|
1792
|
+
const hidden = new Set<string>();
|
|
1793
|
+
for (const stack of commentStacks) {
|
|
1794
|
+
if (stack === expandedStack) continue;
|
|
1795
|
+
const folded =
|
|
1796
|
+
stack === composingStack ? stack.threads : stack.threads.slice(1);
|
|
1797
|
+
for (const member of folded) hidden.add(member.threadId);
|
|
1798
|
+
}
|
|
1799
|
+
return hidden;
|
|
1800
|
+
}, [commentStacks, composingStack, expandedStack]);
|
|
1525
1801
|
const layoutThreads = useMemo(
|
|
1526
1802
|
() =>
|
|
1527
|
-
inlineThreads.filter(
|
|
1528
|
-
|
|
1529
|
-
!("
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1803
|
+
inlineThreads.filter((thread) =>
|
|
1804
|
+
"suggestion" in thread
|
|
1805
|
+
? !("proposalId" in thread.suggestion) ||
|
|
1806
|
+
!thread.suggestion.proposalId ||
|
|
1807
|
+
(proposalMemberCounts.get(thread.suggestion.proposalId) ?? 0) < 2 ||
|
|
1808
|
+
inlineProposalLeaders.get(thread.suggestion.proposalId) ===
|
|
1809
|
+
thread.threadId
|
|
1810
|
+
: !stackHiddenIds.has(thread.threadId),
|
|
1535
1811
|
),
|
|
1536
|
-
[
|
|
1812
|
+
[
|
|
1813
|
+
inlineThreads,
|
|
1814
|
+
inlineProposalLeaders,
|
|
1815
|
+
proposalMemberCounts,
|
|
1816
|
+
stackHiddenIds,
|
|
1817
|
+
],
|
|
1537
1818
|
);
|
|
1538
1819
|
const openThreadKey = inlineThreads
|
|
1539
1820
|
.map(
|
|
@@ -1555,9 +1836,36 @@ export function CommentsSidebar({
|
|
|
1555
1836
|
);
|
|
1556
1837
|
|
|
1557
1838
|
const hasPendingComment = !!displayedPendingComment;
|
|
1839
|
+
const pendingCommentId = displayedPendingComment?.id;
|
|
1840
|
+
const pendingFieldId = displayedPendingComment?.fieldId ?? null;
|
|
1841
|
+
const pendingOnProperty = !!displayedPendingComment?.property;
|
|
1558
1842
|
const recomputeOffsets = useCallback(() => {
|
|
1559
1843
|
const container = scrollContainerRef?.current ?? null;
|
|
1560
1844
|
setThreadPositionsMeasured(!!container);
|
|
1845
|
+
const blocks =
|
|
1846
|
+
stacksEnabled && container
|
|
1847
|
+
? measureCommentBlocks(container, openThreads)
|
|
1848
|
+
: NO_BLOCKS;
|
|
1849
|
+
setThreadBlocks((prev) => (sameBlocks(prev, blocks) ? prev : blocks));
|
|
1850
|
+
if (stacksEnabled && container && pendingCommentId) {
|
|
1851
|
+
const highlight = pendingOnProperty
|
|
1852
|
+
? null
|
|
1853
|
+
: container.querySelector(".comment-highlight--pending");
|
|
1854
|
+
const block =
|
|
1855
|
+
highlight && !inHiddenBlockFieldPanel(highlight)
|
|
1856
|
+
? commentBlockKey(highlight)
|
|
1857
|
+
: null;
|
|
1858
|
+
// Text not drawn yet isn't known to be outside the open stack.
|
|
1859
|
+
if (pendingOnProperty || highlight) {
|
|
1860
|
+
const key =
|
|
1861
|
+
block === null ? null : commentStackKey(pendingFieldId, block);
|
|
1862
|
+
setPendingBlock((prev) =>
|
|
1863
|
+
prev?.id === pendingCommentId && prev.key === key
|
|
1864
|
+
? prev
|
|
1865
|
+
: { id: pendingCommentId, key },
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1561
1869
|
const layoutContainer = alignToAnchors ? sidebarRef.current : null;
|
|
1562
1870
|
const nextPendingPosition =
|
|
1563
1871
|
hasPendingComment && alignToAnchors
|
|
@@ -1612,7 +1920,17 @@ export function CommentsSidebar({
|
|
|
1612
1920
|
}
|
|
1613
1921
|
return positions;
|
|
1614
1922
|
});
|
|
1615
|
-
}, [
|
|
1923
|
+
}, [
|
|
1924
|
+
alignToAnchors,
|
|
1925
|
+
hasPendingComment,
|
|
1926
|
+
inlineThreads,
|
|
1927
|
+
openThreads,
|
|
1928
|
+
pendingCommentId,
|
|
1929
|
+
pendingFieldId,
|
|
1930
|
+
pendingOnProperty,
|
|
1931
|
+
scrollContainerRef,
|
|
1932
|
+
stacksEnabled,
|
|
1933
|
+
]);
|
|
1616
1934
|
|
|
1617
1935
|
// Measured before paint, so a card under a new id never paints unplaced.
|
|
1618
1936
|
useLayoutEffect(() => {
|
|
@@ -1636,14 +1954,18 @@ export function CommentsSidebar({
|
|
|
1636
1954
|
subtree: true,
|
|
1637
1955
|
characterData: true,
|
|
1638
1956
|
attributes: true,
|
|
1639
|
-
attributeFilter: ["hidden"],
|
|
1957
|
+
attributeFilter: ["hidden", "data-comment-thread"],
|
|
1640
1958
|
});
|
|
1641
1959
|
}
|
|
1642
1960
|
} else {
|
|
1961
|
+
// A posted comment's highlight can reuse the draft's span, gaining only
|
|
1962
|
+
// its thread id, and a stack needs that id to place the thread.
|
|
1643
1963
|
observer.observe(primaryBlockFieldEditor(container) ?? container, {
|
|
1644
1964
|
childList: true,
|
|
1645
1965
|
subtree: true,
|
|
1646
1966
|
characterData: true,
|
|
1967
|
+
attributes: true,
|
|
1968
|
+
attributeFilter: ["data-comment-thread"],
|
|
1647
1969
|
});
|
|
1648
1970
|
}
|
|
1649
1971
|
const resizeObserver =
|
|
@@ -1662,7 +1984,6 @@ export function CommentsSidebar({
|
|
|
1662
1984
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1663
1985
|
}, [openThreadKey, hasPendingComment, recomputeOffsets]);
|
|
1664
1986
|
|
|
1665
|
-
const pendingCommentId = displayedPendingComment?.id;
|
|
1666
1987
|
useLayoutEffect(() => {
|
|
1667
1988
|
const element = pendingCardRef.current;
|
|
1668
1989
|
if (!element) {
|
|
@@ -1713,6 +2034,10 @@ export function CommentsSidebar({
|
|
|
1713
2034
|
const activeSavedSuggestion = inlineSuggestions.find(
|
|
1714
2035
|
(suggestion) => suggestion.id === activeSuggestionId,
|
|
1715
2036
|
);
|
|
2037
|
+
// A thread in a folded stack is laid out as the stack's card.
|
|
2038
|
+
const selectedStack = selectedThreadId
|
|
2039
|
+
? stackOfThread.get(selectedThreadId)
|
|
2040
|
+
: undefined;
|
|
1716
2041
|
const selectedLayoutThreadId =
|
|
1717
2042
|
(activeSavedSuggestion?.proposalId
|
|
1718
2043
|
? inlineProposalLeaders.get(activeSavedSuggestion.proposalId)
|
|
@@ -1720,7 +2045,9 @@ export function CommentsSidebar({
|
|
|
1720
2045
|
inlineDraftSuggestions.find(
|
|
1721
2046
|
(suggestion) => suggestion.id === activeSuggestionId,
|
|
1722
2047
|
)?.threadId ??
|
|
1723
|
-
|
|
2048
|
+
(selectedStack && selectedStack !== expandedStack
|
|
2049
|
+
? selectedStack.threads[0].threadId
|
|
2050
|
+
: selectedThreadId);
|
|
1724
2051
|
const restingItems = layoutCommentThreads(
|
|
1725
2052
|
layoutThreads,
|
|
1726
2053
|
threadPositions,
|
|
@@ -1737,32 +2064,105 @@ export function CommentsSidebar({
|
|
|
1737
2064
|
: null;
|
|
1738
2065
|
const pendingAnchorTop =
|
|
1739
2066
|
pendingPosition?.layoutTop ?? displayedPendingComment?.offsetTop ?? 0;
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
2067
|
+
const pendingAnchor: CommentThreadPosition = {
|
|
2068
|
+
documentTop: pendingPosition?.documentTop ?? pendingAnchorTop,
|
|
2069
|
+
layoutTop: pendingAnchorTop,
|
|
2070
|
+
};
|
|
2071
|
+
// An open stack's Collapse row is laid out like a card above its first one.
|
|
2072
|
+
const stackHeaderLayout: ThreadLayoutIdentity | null = expandedStack
|
|
2073
|
+
? { threadId: STACK_HEADER_LAYOUT_ID, comments: [] }
|
|
2074
|
+
: null;
|
|
2075
|
+
const firstStackAnchor = expandedStack
|
|
2076
|
+
? [
|
|
2077
|
+
...expandedStack.threads.map((member) =>
|
|
2078
|
+
threadPositions.get(member.threadId),
|
|
1750
2079
|
),
|
|
1751
|
-
|
|
2080
|
+
pendingInExpandedStack ? pendingAnchor : undefined,
|
|
2081
|
+
].reduce<CommentThreadPosition | null>(
|
|
2082
|
+
(first, position) =>
|
|
2083
|
+
position && (!first || position.documentTop < first.documentTop)
|
|
2084
|
+
? position
|
|
2085
|
+
: first,
|
|
2086
|
+
null,
|
|
1752
2087
|
)
|
|
1753
|
-
:
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
2088
|
+
: null;
|
|
2089
|
+
const layoutPositions = new Map(threadPositions);
|
|
2090
|
+
const layoutHeights = new Map(threadCardHeights);
|
|
2091
|
+
if (pendingLayout) {
|
|
2092
|
+
layoutPositions.set(PENDING_COMMENT_LAYOUT_ID, pendingAnchor);
|
|
2093
|
+
layoutHeights.set(
|
|
2094
|
+
PENDING_COMMENT_LAYOUT_ID,
|
|
2095
|
+
pendingCardHeight ?? PENDING_COMMENT_CARD_ESTIMATE,
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
if (stackHeaderLayout) {
|
|
2099
|
+
if (firstStackAnchor) {
|
|
2100
|
+
layoutPositions.set(STACK_HEADER_LAYOUT_ID, {
|
|
2101
|
+
documentTop: firstStackAnchor.documentTop - 0.5,
|
|
2102
|
+
layoutTop:
|
|
2103
|
+
firstStackAnchor.layoutTop === null
|
|
2104
|
+
? null
|
|
2105
|
+
: firstStackAnchor.layoutTop - STACK_HEADER_HEIGHT - 12,
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
layoutHeights.set(STACK_HEADER_LAYOUT_ID, STACK_HEADER_HEIGHT);
|
|
2109
|
+
}
|
|
2110
|
+
const anchoredItems = layoutCommentThreads<
|
|
2111
|
+
LayoutThread | ThreadLayoutIdentity
|
|
2112
|
+
>(
|
|
2113
|
+
[
|
|
2114
|
+
...layoutThreads,
|
|
2115
|
+
...(pendingLayout ? [pendingLayout] : []),
|
|
2116
|
+
...(stackHeaderLayout ? [stackHeaderLayout] : []),
|
|
2117
|
+
],
|
|
2118
|
+
layoutPositions,
|
|
2119
|
+
layoutHeights,
|
|
2120
|
+
pendingLayout ? PENDING_COMMENT_LAYOUT_ID : selectedLayoutThreadId,
|
|
2121
|
+
);
|
|
2122
|
+
// Keeping the selected card level with its highlight pushes the cards above
|
|
2123
|
+
// it upward. Near the top of a page that would put an open stack's Collapse
|
|
2124
|
+
// row and first cards above the margin, where nothing can scroll to them, so
|
|
2125
|
+
// the stack starts at the top instead.
|
|
2126
|
+
const stackOverflow = Math.max(
|
|
2127
|
+
0,
|
|
2128
|
+
-(
|
|
2129
|
+
anchoredItems.find((item) => item.thread === stackHeaderLayout)?.top ?? 0
|
|
2130
|
+
),
|
|
2131
|
+
);
|
|
2132
|
+
const selectedItems = stackOverflow
|
|
2133
|
+
? anchoredItems.map((item) => ({ ...item, top: item.top + stackOverflow }))
|
|
2134
|
+
: anchoredItems;
|
|
1759
2135
|
const pendingTop =
|
|
1760
2136
|
selectedItems.find((item) => item.thread === pendingLayout)?.top ??
|
|
1761
2137
|
pendingAnchorTop;
|
|
2138
|
+
const stackHeaderTop = selectedItems.find(
|
|
2139
|
+
(item) => item.thread === stackHeaderLayout,
|
|
2140
|
+
)?.top;
|
|
1762
2141
|
const items = selectedItems.filter(
|
|
1763
2142
|
(item): item is CommentLayoutItem<LayoutThread> =>
|
|
1764
|
-
item.thread !== pendingLayout,
|
|
2143
|
+
item.thread !== pendingLayout && item.thread !== stackHeaderLayout,
|
|
1765
2144
|
);
|
|
2145
|
+
// The guide beside an open stack spans its cards, and a new comment in it.
|
|
2146
|
+
const stackSpans = expandedStack
|
|
2147
|
+
? selectedItems
|
|
2148
|
+
.filter((item) =>
|
|
2149
|
+
item.thread === pendingLayout
|
|
2150
|
+
? pendingInExpandedStack
|
|
2151
|
+
: expandedStack.threads.some(
|
|
2152
|
+
(member) => member.threadId === item.thread.threadId,
|
|
2153
|
+
),
|
|
2154
|
+
)
|
|
2155
|
+
.map((item) => ({
|
|
2156
|
+
top: item.top,
|
|
2157
|
+
bottom:
|
|
2158
|
+
item.top +
|
|
2159
|
+
(layoutHeights.get(item.thread.threadId) ??
|
|
2160
|
+
estimateThreadCardHeight(item.thread)),
|
|
2161
|
+
}))
|
|
2162
|
+
: [];
|
|
2163
|
+
const stackGuideTop = Math.min(...stackSpans.map((span) => span.top)) + 16;
|
|
2164
|
+
const stackGuideHeight =
|
|
2165
|
+
Math.max(...stackSpans.map((span) => span.bottom)) - 16 - stackGuideTop;
|
|
1766
2166
|
|
|
1767
2167
|
const changeResolution = async (thread: CommentThread, resolved: boolean) => {
|
|
1768
2168
|
if (
|
|
@@ -1808,10 +2208,39 @@ export function CommentsSidebar({
|
|
|
1808
2208
|
);
|
|
1809
2209
|
};
|
|
1810
2210
|
|
|
2211
|
+
const renderFoldedStack = (
|
|
2212
|
+
stack: CommentStack<CommentThread>,
|
|
2213
|
+
layoutId: string,
|
|
2214
|
+
) => {
|
|
2215
|
+
const front = commentStackFront(stack.threads, stackTouches);
|
|
2216
|
+
return (
|
|
2217
|
+
<CommentStackCard
|
|
2218
|
+
stackKey={stack.key}
|
|
2219
|
+
heightId={layoutId}
|
|
2220
|
+
threads={stack.threads}
|
|
2221
|
+
front={front}
|
|
2222
|
+
isActive={stack.threads.some(
|
|
2223
|
+
(member) => member.threadId === activeThreadId,
|
|
2224
|
+
)}
|
|
2225
|
+
onOpen={() => {
|
|
2226
|
+
openStackOn(front.threadId);
|
|
2227
|
+
onActivateThread?.(front.threadId);
|
|
2228
|
+
if (canComment) setReplyingThreadId(front.threadId);
|
|
2229
|
+
}}
|
|
2230
|
+
onHoverChange={(hovered) =>
|
|
2231
|
+
onHoveredThreadChange?.(hovered ? front.threadId : null)
|
|
2232
|
+
}
|
|
2233
|
+
onHeightChange={handleThreadCardHeightChange}
|
|
2234
|
+
t={t}
|
|
2235
|
+
/>
|
|
2236
|
+
);
|
|
2237
|
+
};
|
|
2238
|
+
|
|
1811
2239
|
const renderCommentThread = (
|
|
1812
2240
|
thread: CommentThread,
|
|
1813
2241
|
marginTop = 0,
|
|
1814
2242
|
isActive = false,
|
|
2243
|
+
inStack = false,
|
|
1815
2244
|
) => {
|
|
1816
2245
|
const aiRequest = commentAi
|
|
1817
2246
|
? latestCommentAiRequest(commentAi.requests, thread.threadId)
|
|
@@ -2019,6 +2448,7 @@ export function CommentsSidebar({
|
|
|
2019
2448
|
threadActions={renderThreadActions?.(thread)}
|
|
2020
2449
|
currentUserEmail={currentUserEmail}
|
|
2021
2450
|
quote={thread.quotedText}
|
|
2451
|
+
showQuote={inStack}
|
|
2022
2452
|
propertyTarget={
|
|
2023
2453
|
thread.property
|
|
2024
2454
|
? propertyThreadTarget(thread.property, documentProperties)
|
|
@@ -2482,6 +2912,18 @@ export function CommentsSidebar({
|
|
|
2482
2912
|
ref={sidebarRef}
|
|
2483
2913
|
className="relative flow-root w-full min-w-0 shrink-0 pb-16"
|
|
2484
2914
|
data-comments-sidebar
|
|
2915
|
+
onClick={(event) => {
|
|
2916
|
+
// The margin's empty space is outside every card, so it folds.
|
|
2917
|
+
const target = event.target as HTMLElement;
|
|
2918
|
+
if (
|
|
2919
|
+
expandedStack &&
|
|
2920
|
+
(target === event.currentTarget ||
|
|
2921
|
+
target.hasAttribute("data-comment-layout-thread") ||
|
|
2922
|
+
target.dataset.commentStackState === "open" ||
|
|
2923
|
+
target.hasAttribute("data-comment-stack-guide"))
|
|
2924
|
+
)
|
|
2925
|
+
collapseStack(expandedStack);
|
|
2926
|
+
}}
|
|
2485
2927
|
>
|
|
2486
2928
|
{isLoading ? (
|
|
2487
2929
|
<div className="space-y-3 px-2 pt-3" aria-hidden="true">
|
|
@@ -2493,108 +2935,143 @@ export function CommentsSidebar({
|
|
|
2493
2935
|
))}
|
|
2494
2936
|
</div>
|
|
2495
2937
|
) : null}
|
|
2938
|
+
{expandedStack && stackHeaderTop !== undefined ? (
|
|
2939
|
+
<CommentStackHeader
|
|
2940
|
+
stackKey={expandedStack.key}
|
|
2941
|
+
top={stackHeaderTop}
|
|
2942
|
+
onCollapse={() => collapseStack(expandedStack)}
|
|
2943
|
+
t={t}
|
|
2944
|
+
/>
|
|
2945
|
+
) : null}
|
|
2946
|
+
{expandedStack && stackSpans.length > 0 ? (
|
|
2947
|
+
<CommentStackGuide top={stackGuideTop} height={stackGuideHeight} />
|
|
2948
|
+
) : null}
|
|
2496
2949
|
{/* Pending new comment — positioned at the selection Y offset */}
|
|
2497
2950
|
{displayedPendingComment && (
|
|
2498
2951
|
<div
|
|
2499
2952
|
ref={pendingCardRef}
|
|
2500
2953
|
className={cn(
|
|
2501
|
-
"overflow-hidden rounded-xl bg-popover ring-1 ring-border/60 shadow-comment-raised",
|
|
2502
2954
|
alignToAnchors
|
|
2503
|
-
?
|
|
2955
|
+
? composingStack
|
|
2956
|
+
? "absolute inset-x-0 z-10 pb-2"
|
|
2957
|
+
: "absolute left-2 right-4 z-10"
|
|
2504
2958
|
: surface === "popover"
|
|
2505
2959
|
? "relative"
|
|
2506
2960
|
: "relative mx-2 mt-3",
|
|
2507
2961
|
)}
|
|
2508
2962
|
style={alignToAnchors ? { top: pendingTop } : undefined}
|
|
2509
2963
|
data-comment-pending
|
|
2964
|
+
data-comment-stack={composingStack?.key}
|
|
2965
|
+
data-comment-stack-state={composingStack ? "composing" : undefined}
|
|
2510
2966
|
>
|
|
2511
|
-
{
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2967
|
+
{composingStack ? <CommentStackSheets /> : null}
|
|
2968
|
+
<div
|
|
2969
|
+
className={cn(
|
|
2970
|
+
"relative overflow-hidden rounded-xl bg-popover ring-1 ring-border/60 shadow-comment-raised",
|
|
2971
|
+
composingStack && "mx-2 mr-4",
|
|
2972
|
+
)}
|
|
2973
|
+
>
|
|
2974
|
+
{surface === "popover" ? (
|
|
2975
|
+
<div className="flex h-12 items-center gap-1 border-b border-border/70 pe-2 ps-4">
|
|
2976
|
+
<span className="min-w-0 flex-1 truncate text-sm font-semibold text-foreground">
|
|
2977
|
+
{t("comments.commentTitle")}
|
|
2978
|
+
</span>
|
|
2979
|
+
{onClose ? (
|
|
2980
|
+
<Tooltip>
|
|
2981
|
+
<TooltipTrigger asChild>
|
|
2982
|
+
<CommentIconButton
|
|
2983
|
+
aria-label={t("comments.close")}
|
|
2984
|
+
onClick={handlePendingCancel}
|
|
2985
|
+
>
|
|
2986
|
+
<IconX size={18} />
|
|
2987
|
+
</CommentIconButton>
|
|
2988
|
+
</TooltipTrigger>
|
|
2989
|
+
<TooltipContent>{t("comments.close")}</TooltipContent>
|
|
2990
|
+
</Tooltip>
|
|
2991
|
+
) : null}
|
|
2992
|
+
</div>
|
|
2993
|
+
) : null}
|
|
2994
|
+
<div className="flex items-start gap-2.5 p-3.5">
|
|
2995
|
+
<div className="flex h-9.5 shrink-0 items-center">
|
|
2996
|
+
<CommentAvatar
|
|
2997
|
+
email={currentUserEmail}
|
|
2998
|
+
name={currentUserEmail}
|
|
2543
2999
|
/>
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
3000
|
+
</div>
|
|
3001
|
+
<div className="min-w-0 flex-1">
|
|
3002
|
+
{displayedPendingComment.property ? (
|
|
3003
|
+
<PropertyThreadTargetLabel
|
|
3004
|
+
className="mb-2"
|
|
3005
|
+
target={propertyThreadTarget(
|
|
3006
|
+
displayedPendingComment.property,
|
|
3007
|
+
documentProperties,
|
|
3008
|
+
)}
|
|
3009
|
+
/>
|
|
3010
|
+
) : null}
|
|
3011
|
+
{!pendingTargetValid && (
|
|
3012
|
+
<p
|
|
3013
|
+
role="alert"
|
|
3014
|
+
className="mb-2 text-xs text-muted-foreground"
|
|
3015
|
+
>
|
|
3016
|
+
{t("comments.selectTextToComment")}
|
|
3017
|
+
</p>
|
|
3018
|
+
)}
|
|
3019
|
+
<CommentComposer
|
|
3020
|
+
ref={pendingInputRef}
|
|
3021
|
+
value={pendingText}
|
|
3022
|
+
onChange={(text) =>
|
|
3023
|
+
onPendingChange?.(displayedPendingComment.id, () => ({
|
|
3024
|
+
text,
|
|
3025
|
+
}))
|
|
3026
|
+
}
|
|
3027
|
+
mentions={pendingMentions}
|
|
3028
|
+
onMentionsChange={(mentions) =>
|
|
3029
|
+
onPendingChange?.(displayedPendingComment.id, () => ({
|
|
3030
|
+
mentions,
|
|
3031
|
+
}))
|
|
3032
|
+
}
|
|
3033
|
+
onSubmit={() => void handlePendingSubmit()}
|
|
3034
|
+
onAiSubmit={
|
|
3035
|
+
commentAi && !displayedPendingComment.fieldId
|
|
3036
|
+
? (selection) => void handlePendingAiSubmit(selection)
|
|
3037
|
+
: undefined
|
|
3038
|
+
}
|
|
3039
|
+
aiDraft={pendingDraft.draft.aiDraft}
|
|
3040
|
+
onAiDraftChange={pendingDraft.setAiDraft}
|
|
3041
|
+
aiModelStorageKey={
|
|
3042
|
+
commentAi && !displayedPendingComment.fieldId
|
|
3043
|
+
? aiModelStorageKey
|
|
3044
|
+
: undefined
|
|
3045
|
+
}
|
|
3046
|
+
onCancel={handlePendingCancel}
|
|
3047
|
+
onEscape={() => {
|
|
3048
|
+
if (!pendingText.trim()) handlePendingCancel();
|
|
3049
|
+
}}
|
|
3050
|
+
onFocus={() => {
|
|
3051
|
+
const selection = pendingInputRef.current?.getSelection();
|
|
3052
|
+
if (selection && pendingFocus)
|
|
3053
|
+
pendingFocus.current = selection;
|
|
3054
|
+
}}
|
|
3055
|
+
onSelectionChange={(selection) => {
|
|
3056
|
+
if (pendingFocus) pendingFocus.current = selection;
|
|
3057
|
+
}}
|
|
3058
|
+
onBlur={() => {
|
|
3059
|
+
if (pendingFocus) pendingFocus.current = null;
|
|
3060
|
+
}}
|
|
3061
|
+
placeholder={t("comments.add")}
|
|
3062
|
+
submitLabel={t("comments.submit")}
|
|
3063
|
+
submitDisabled={!pendingTargetValid || ambiguousCreate()}
|
|
3064
|
+
disabled={pendingSubmitting}
|
|
3065
|
+
/>
|
|
3066
|
+
</div>
|
|
2597
3067
|
</div>
|
|
3068
|
+
{composingStack ? (
|
|
3069
|
+
<CommentStackShowAll
|
|
3070
|
+
threads={composingStack.threads}
|
|
3071
|
+
onOpen={() => openStackOn(composingStack.threads[0].threadId)}
|
|
3072
|
+
t={t}
|
|
3073
|
+
/>
|
|
3074
|
+
) : null}
|
|
2598
3075
|
</div>
|
|
2599
3076
|
</div>
|
|
2600
3077
|
)}
|
|
@@ -2605,26 +3082,33 @@ export function CommentsSidebar({
|
|
|
2605
3082
|
const restingItem = restingItemsById.get(thread.threadId);
|
|
2606
3083
|
const marginTop = restingItem?.marginTop ?? item.marginTop;
|
|
2607
3084
|
const translateY = top - (restingItem?.top ?? top);
|
|
2608
|
-
const
|
|
3085
|
+
const stack =
|
|
2609
3086
|
"suggestion" in thread
|
|
2610
|
-
?
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
3087
|
+
? undefined
|
|
3088
|
+
: stackOfThread.get(thread.threadId);
|
|
3089
|
+
const card =
|
|
3090
|
+
stack && stack !== expandedStack
|
|
3091
|
+
? renderFoldedStack(stack, thread.threadId)
|
|
3092
|
+
: "suggestion" in thread
|
|
3093
|
+
? "durability" in thread.suggestion
|
|
3094
|
+
? renderDraftSuggestionCard(thread.suggestion)
|
|
3095
|
+
: thread.suggestion.proposalId &&
|
|
3096
|
+
(proposalMemberCounts.get(thread.suggestion.proposalId) ??
|
|
3097
|
+
0) >= 2
|
|
3098
|
+
? renderProposalGroup(
|
|
3099
|
+
thread.suggestion.proposalId,
|
|
3100
|
+
inlineProposalMembers.get(
|
|
3101
|
+
thread.suggestion.proposalId,
|
|
3102
|
+
) ?? [thread.suggestion],
|
|
3103
|
+
thread.threadId,
|
|
3104
|
+
)
|
|
3105
|
+
: renderSuggestionCard(thread.suggestion)
|
|
3106
|
+
: renderCommentThread(
|
|
3107
|
+
thread,
|
|
3108
|
+
0,
|
|
3109
|
+
activeThreadId === thread.threadId,
|
|
3110
|
+
!!stack,
|
|
3111
|
+
);
|
|
2628
3112
|
// A Property thread has no highlight to lose, so it isn't one that
|
|
2629
3113
|
// went missing.
|
|
2630
3114
|
const lostHighlight = (candidate: (typeof items)[number]) =>
|
|
@@ -3565,6 +4049,7 @@ function ThreadView({
|
|
|
3565
4049
|
renderEntry,
|
|
3566
4050
|
surface = "rail",
|
|
3567
4051
|
quote,
|
|
4052
|
+
showQuote = false,
|
|
3568
4053
|
propertyTarget,
|
|
3569
4054
|
anchorUnavailable = false,
|
|
3570
4055
|
onClose,
|
|
@@ -3611,6 +4096,8 @@ function ThreadView({
|
|
|
3611
4096
|
surface?: CommentSurface;
|
|
3612
4097
|
/** The anchored text, shown as a quote line above the thread in the panel. */
|
|
3613
4098
|
quote?: string | null;
|
|
4099
|
+
/** Show the quote in the margin too, for a card away from its highlight. */
|
|
4100
|
+
showQuote?: boolean;
|
|
3614
4101
|
/** The Property a thread is on, shown above it on every surface. */
|
|
3615
4102
|
propertyTarget?: PropertyThreadTarget | null;
|
|
3616
4103
|
anchorUnavailable?: boolean;
|
|
@@ -3875,18 +4362,33 @@ function ThreadView({
|
|
|
3875
4362
|
target={propertyTarget}
|
|
3876
4363
|
/>
|
|
3877
4364
|
) : null}
|
|
3878
|
-
{isPanel && quote ? (
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
event
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
4365
|
+
{(isPanel || showQuote) && quote ? (
|
|
4366
|
+
canExpand ? (
|
|
4367
|
+
<button
|
|
4368
|
+
type="button"
|
|
4369
|
+
onClick={(event) => {
|
|
4370
|
+
event.stopPropagation();
|
|
4371
|
+
onExpand();
|
|
4372
|
+
}}
|
|
4373
|
+
className={cn(
|
|
4374
|
+
"-mb-1 border-s-2 border-border ps-2.5 text-start text-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
4375
|
+
isPanel ? "line-clamp-2" : "line-clamp-1",
|
|
4376
|
+
)}
|
|
4377
|
+
data-comment-quote
|
|
4378
|
+
>
|
|
4379
|
+
{quote}
|
|
4380
|
+
</button>
|
|
4381
|
+
) : (
|
|
4382
|
+
<p
|
|
4383
|
+
className={cn(
|
|
4384
|
+
"-mb-1 border-s-2 border-border ps-2.5 text-sm text-muted-foreground",
|
|
4385
|
+
isPanel ? "line-clamp-2" : "line-clamp-1",
|
|
4386
|
+
)}
|
|
4387
|
+
data-comment-quote
|
|
4388
|
+
>
|
|
4389
|
+
{quote}
|
|
4390
|
+
</p>
|
|
4391
|
+
)
|
|
3890
4392
|
) : null}
|
|
3891
4393
|
{anchorUnavailable ? (
|
|
3892
4394
|
<span
|