Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/superdoc/src/assets/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@
--sd-action-primary-hover: var(--sd-color-blue-600);

/* ─── Component: Comment Dialog ─── */
--sd-comment-bg: var(--sd-color-gray-100);
--sd-comment-bg-hover: var(--sd-surface-hover);
--sd-comment-bg: #f3f6fd;
--sd-comment-bg-hover: #f3f6fd;
--sd-comment-bg-active: var(--sd-surface-card);
--sd-comment-bg-resolved: #f0f0f0;
--sd-comment-border-active: var(--sd-border-subtle);
--sd-comment-radius: var(--sd-radius-lg);
--sd-comment-padding: 16px;
--sd-comment-shadow: 0 4px 20px rgba(15, 23, 42, 0.08);
--sd-comment-shadow: 0px 4px 12px 0px rgba(50, 50, 50, 0.15);
--sd-comment-max-width: 300px;
--sd-comment-min-width: 200px;
--sd-comment-separator: var(--sd-border-subtle);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,13 @@ describe('CommentDialog.vue', () => {
commentsStoreStub.activeComment.value = 'tc-parent';
await nextTick();

// Expand the collapsed thread (>= 2 children triggers collapse)
const collapsedPill = wrapper.find('.collapsed-replies');
if (collapsedPill.exists()) {
await collapsedPill.trigger('click');
await nextTick();
}

const headers = wrapper.findAllComponents(CommentHeaderStub);
expect(headers).toHaveLength(3);

Expand Down Expand Up @@ -696,6 +703,13 @@ describe('CommentDialog.vue', () => {
commentsStoreStub.activeComment.value = 'tc-parent';
await nextTick();

// Expand the collapsed thread (>= 2 children triggers collapse)
const collapsedPill = wrapper.find('.collapsed-replies');
if (collapsedPill.exists()) {
await collapsedPill.trigger('click');
await nextTick();
}

const headers = wrapper.findAllComponents(CommentHeaderStub);
expect(headers).toHaveLength(3);
expect(headers[0].props('comment').commentId).toBe('tc-parent');
Expand Down
41 changes: 20 additions & 21 deletions packages/superdoc/src/components/CommentsLayer/CommentDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,9 @@ watch(isActiveComment, (active) => {
}
});

/* ── Step 3: Thread collapse (Google Docs pattern) ──
* >=3 replies → collapse: parent + first reply + "N more replies" + last reply
* <3 replies → show all
/* ── Step 3: Thread collapse ──
* >=2 replies → collapse: parent + "N more replies" + last reply
* <2 replies → show all
* Clicking "N more replies" or the card → expand all + activate
* Deactivating → re-collapse
*/
Expand All @@ -218,27 +218,26 @@ const childComments = computed(() => comments.value.slice(1));

const shouldCollapseThread = computed(() => {
if (threadExpanded.value) return false;
return childComments.value.length >= 3;
return childComments.value.length >= 2;
});

const visibleComments = computed(() => {
if (!shouldCollapseThread.value) return comments.value;
// Collapsed: parent + first reply + last reply
// Collapsed: parent + last reply
const parent = comments.value[0];
const first = childComments.value[0];
const last = childComments.value[childComments.value.length - 1];
return [parent, first, last].filter(Boolean);
return [parent, last].filter(Boolean);
});

const collapsedReplyCount = computed(() => {
if (!shouldCollapseThread.value) return 0;
return childComments.value.length - 2; // first + last are shown
return childComments.value.length - 1; // only last is shown
});

const collapsedReplyAuthors = computed(() => {
if (!shouldCollapseThread.value) return [];
// Hidden = middle replies (first + last are visible)
const hidden = childComments.value.slice(1, -1);
// Hidden = all replies except last
const hidden = childComments.value.slice(0, -1);
const seen = new Set();
return hidden
.map((c) =>
Expand Down Expand Up @@ -543,9 +542,9 @@ watch(editingCommentId, (commentId) => {
/>
</div>
<div class="reply-actions">
<button class="reply-btn-cancel" @click.stop.prevent="handleCancel">Cancel</button>
<button class="sd-button reply-btn-cancel" @click.stop.prevent="handleCancel">Cancel</button>
<button
class="reply-btn-primary"
class="sd-button primary reply-btn-primary"
@click.stop.prevent="handleAddComment"
:disabled="!hasTextContent"
:class="{ 'is-disabled': !hasTextContent }"
Expand Down Expand Up @@ -669,8 +668,8 @@ watch(editingCommentId, (commentId) => {
</div>
</div>

<!-- Thread collapse: after first reply (index 1), show "N more replies" -->
<template v-if="shouldCollapseThread && index === 1">
<!-- Thread collapse: after parent (index 0), show "N more replies" -->
<template v-if="shouldCollapseThread && index === 0">
<div class="comment-separator"></div>
<div class="collapsed-replies" @click.stop.prevent="expandThread">
<div class="collapsed-avatars">
Expand Down Expand Up @@ -702,9 +701,9 @@ watch(editingCommentId, (commentId) => {
/>
</div>
<div class="reply-actions">
<button class="reply-btn-cancel" @click.stop.prevent="handleCancel">Cancel</button>
<button class="sd-button reply-btn-cancel" @click.stop.prevent="handleCancel">Cancel</button>
<button
class="reply-btn-primary"
class="sd-button primary reply-btn-primary"
@click.stop.prevent="handleAddComment"
:disabled="!hasTextContent"
:class="{ 'is-disabled': !hasTextContent }"
Expand All @@ -724,7 +723,7 @@ watch(editingCommentId, (commentId) => {
flex-direction: column;
padding: var(--sd-comment-padding, 16px);
border-radius: var(--sd-comment-radius, 12px);
background-color: var(--sd-comment-bg, #f5f5f5);
background-color: var(--sd-comment-bg, #f3f6fd);
border: 1px solid transparent;
font-family: var(--sd-ui-font-family, Arial, Helvetica, sans-serif);
font-size: var(--sd-comment-body-size, 14px);
Expand All @@ -740,7 +739,7 @@ watch(editingCommentId, (commentId) => {
cursor: pointer;
}
.comments-dialog:not(.is-active):not(.is-resolved):hover {
background-color: var(--sd-comment-bg-hover, #f2f2f2);
background-color: var(--sd-comment-bg-hover, #f3f6fd);
}
.comments-dialog:not(.is-resolved):hover :deep(.overflow-menu) {
opacity: 1;
Expand All @@ -749,7 +748,7 @@ watch(editingCommentId, (commentId) => {
.comments-dialog.is-active {
background-color: var(--sd-comment-bg-active, #ffffff);
border-color: var(--sd-comment-border-active, #e0e0e0);
box-shadow: var(--sd-comment-shadow, 0 4px 20px rgba(15, 23, 42, 0.08));
box-shadow: var(--sd-comment-shadow, 0px 4px 12px 0px rgba(50, 50, 50, 0.15));
z-index: 10;
}
.comments-dialog.is-resolved {
Expand Down Expand Up @@ -967,7 +966,7 @@ watch(editingCommentId, (commentId) => {
transition: background 150ms;
}
.reply-btn-primary:hover {
background: var(--sd-color-blue-600, #0f44cc);
background: var(--sd-action-primary-hover, #0f44cc);
}
.reply-btn-primary.is-disabled {
background: var(--sd-color-gray-400, #dbdbdb);
Expand All @@ -986,7 +985,7 @@ watch(editingCommentId, (commentId) => {
justify-content: flex-end;
width: 100%;
}
.sd-button {
.comment-footer .sd-button {
font-size: 12px;
margin-left: 5px;
}
Expand Down
5 changes: 3 additions & 2 deletions packages/superdoc/src/components/general/Avatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ const getInitials = (name, email) => {
<style scoped>
.user-container {
border-radius: 50%;
border: var(--sd-comment-avatar-border, 2px solid #333);
font-size: var(--sd-comment-avatar-font-size, 11px);
font-weight: 600;
color: var(--sd-comment-avatar-color, #1355ff);
background-color: var(--sd-comment-avatar-bg, #ebf0ff);
color: var(--sd-comment-avatar-color, #fff);
background-color: var(--sd-comment-avatar-bg, #00000098);

width: var(--sd-comment-avatar-size, 28px);
height: var(--sd-comment-avatar-size, 28px);
Expand Down
19 changes: 14 additions & 5 deletions tests/behavior/tests/comments/comment-thread-collapse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ test.fixme(
'v-click-outside races with programmatic activation on WebKit',
);

test('thread with 3+ replies collapses and expands on click', async ({ superdoc }) => {
test('thread with 2+ replies collapses and expands on click', async ({ superdoc }) => {
await assertDocumentApiReady(superdoc.page);

// Type text and add a comment through the UI
Expand All @@ -23,7 +23,7 @@ test('thread with 3+ replies collapses and expands on click', async ({ superdoc
commentText: 'parent comment',
});

// Add 4 replies to trigger collapse (threshold is childComments.length >= 3)
// Add 4 replies to trigger collapse (threshold is childComments.length >= 2)
await replyToComment(superdoc.page, { parentCommentId: commentId, text: 'reply one' });
await replyToComment(superdoc.page, { parentCommentId: commentId, text: 'reply two' });
await replyToComment(superdoc.page, { parentCommentId: commentId, text: 'reply three' });
Expand All @@ -33,16 +33,25 @@ test('thread with 3+ replies collapses and expands on click', async ({ superdoc
// Re-assert highlight exists — replies trigger re-renders that may temporarily remove highlights
await superdoc.assertCommentHighlightExists({ text: 'collapse', timeoutMs: 10_000 });

// Deactivate first so the dialog renders in collapsed state, then re-activate.
// On Firefox, replyToComment shifts activeComment to a child ID which can leave
// the thread in an expanded state.
await superdoc.page.evaluate(() => {
const sd = (window as any).superdoc;
sd.commentsStore.$patch({ activeComment: null });
});
await superdoc.waitForStable();

// Activate the comment dialog
const dialog = await activateCommentDialog(superdoc, 'collapse');

// The collapsed-replies pill should be visible with "more replies" text
const collapsedPill = dialog.locator('.collapsed-replies');
await expect(collapsedPill).toBeVisible({ timeout: 5_000 });
await expect(collapsedPill).toBeVisible({ timeout: 10_000 });
await expect(collapsedPill).toContainText('more replies');

// In collapsed state: parent + first reply + last reply = 3 visible conversation items
await expect(dialog.locator('.conversation-item')).toHaveCount(3);
// In collapsed state: parent + last reply = 2 visible conversation items
await expect(dialog.locator('.conversation-item')).toHaveCount(2);

// Click the collapsed pill to expand all replies
await collapsedPill.click();
Expand Down
Loading