From 2c4317947ba3c393c0119b32ef10371c3a3d2228 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 13 May 2026 17:13:35 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20comment=20side=20pa?= =?UTF-8?q?nel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comment side panel to the right panel. We will be able to manage the threads of the document and see the content of the comments in a side panel. The advantage of this approach is that we will be able to: - see the comments that have been removed because of deleted text - see the resolved comments - see the unresolved comments --- CHANGELOG.md | 1 + .../app-impress/doc-comments.spec.ts | 120 +++++++++++++++- .../e2e/__tests__/app-impress/utils-editor.ts | 24 ++-- .../icons/ui-kit/filter-notification.svg | 16 ++- .../src/assets/icons/ui-kit/filter_list.svg | 7 +- .../src/assets/icons/ui-kit/x-mark.svg | 7 +- .../src/components/modal/ButtonCloseModal.tsx | 12 +- .../doc-editor/components/BlockNoteEditor.tsx | 43 +++++- .../docs/doc-editor/components/DocEditor.tsx | 1 + .../components/comments/CommentSideBar.tsx | 135 +++++++++++++++++ .../comments/CommentToolbarButton.tsx | 11 -- .../doc-editor/components/comments/index.ts | 1 + .../doc-editor/components/comments/styles.tsx | 136 ++++++++++++++++-- .../comments/useCommentSidebarStore.ts | 21 +++ .../src/features/docs/doc-editor/styles.tsx | 26 ++-- .../right-panel/components/RightPanel.tsx | 7 +- .../components/RightPanelCollapseButton.tsx | 41 +++--- 17 files changed, 517 insertions(+), 92 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentSideBar.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/useCommentSidebarStore.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f5cb77820..b93aeed69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to ### Added - ✨(backend) support creating subdoc from file #1987 +- ✨(frontend) comment side panel #2279 - ✨(buildpack) add PaaS deployment support, tested with Scalingo #2293 - 🔧(backend) allow configuring settings OIDC_OP_USER_ENDPOINT_FORMAT diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts index 58bc330b5..7412307e1 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts @@ -6,7 +6,11 @@ import { getOtherBrowserName, verifyDocName, } from './utils-common'; -import { getEditor, writeInEditor } from './utils-editor'; +import { + getEditor, + tryFocusEditorContent, + writeInEditor, +} from './utils-editor'; import { addNewMember, connectOtherUserToDoc, @@ -430,3 +434,117 @@ test.describe('Doc Comments mobile', () => { await expect(thread.getByText('This is a comment').first()).toBeVisible(); }); }); + +test.describe('Doc Comments Side Panel', () => { + test('it checks comments side bar interaction', async ({ + page, + browserName, + }) => { + await createDoc(page, 'comment-doc-panel', browserName, 1); + + await expect( + page.getByRole('button', { name: 'Show the comments sidebar' }), + ).toBeHidden(); + + // Create comment thread + const editor = await writeInEditor({ page, text: 'Hello World' }); + await editor.getByText('Hello').selectText(); + await page.getByRole('button', { name: 'Add comment' }).click(); + + const thread = page.locator('.bn-thread'); + await thread.getByRole('paragraph').first().fill('This is a comment'); + await thread.locator('[data-test="save"]').click(); + + // Open comment side panel and check comment is visible in side panel + await page + .getByRole('button', { name: 'Show the comments sidebar' }) + .click(); + + const elCommentsSidePanel = page.getByLabel('Comments side panel'); + await expect( + elCommentsSidePanel.getByText('This is a comment'), + ).toBeVisible(); + + // Click on comment in side panel and check it scrolls to the comment in the doc + await tryFocusEditorContent({ page }); + for (let i = 0; i < 20; i++) { + await page.keyboard.press('Enter'); + } + await writeInEditor({ + page, + text: 'New paragraph', + }); + + await expect(editor.getByText('New paragraph')).toBeInViewport(); + await elCommentsSidePanel.getByText('This is a comment').click(); + + await expect(editor.getByText('Hello World')).toBeVisible(); + await expect(editor.getByText('New paragraph')).not.toBeInViewport(); + await expect(editor.getByText('Hello World')).toHaveClass( + 'bn-thread-mark-selected', + ); + + // Add a comment in the side panel + await elCommentsSidePanel + .locator( + '.bn-editor[contenteditable="true"] div[data-content-type="paragraph"]', + ) + .first() + .fill('This is another comment'); + await elCommentsSidePanel.locator('[data-test="save"]').click(); + await expect( + elCommentsSidePanel + .locator( + '.bn-editor[contenteditable="false"] div[data-content-type="paragraph"]', + ) + .getByText('This is another comment'), + ).toBeVisible(); + + // Close the side panel and check the comments in the doc + await page + .getByRole('button', { name: 'Close the comments sidebar' }) + .click(); + await expect(elCommentsSidePanel).toBeHidden(); + await editor.getByText('Hello World').click(); + await expect(thread.getByText('This is another comment')).toBeVisible(); + + // Resolve the comment and check it disappears from the side panel and the doc + await thread.getByText('This is a comment').first().hover(); + await thread.locator('[data-test="resolve"]').click(); + await expect(thread).toBeHidden(); + await page + .getByRole('button', { name: 'Show the comments sidebar' }) + .click(); + await expect( + elCommentsSidePanel.getByText('This is a comment'), + ).toBeHidden(); + + // Goto resolved part, the comment should be visible in the side panel + await elCommentsSidePanel + .getByRole('button', { name: 'Filter comments' }) + .click(); + await page.getByRole('menuitem', { name: 'Resolved' }).click(); + await elCommentsSidePanel.getByText('This is a comment').click(); + await expect(editor.getByText('Hello World')).toHaveClass( + 'bn-thread-mark-selected', + ); + + // Unresolve the comment and check it does not appears in the side panel resolved part + await thread.getByText('This is a comment').first().hover(); + await thread.locator('[data-test="re-open"]').click(); + await expect( + elCommentsSidePanel.getByText('This is a comment'), + ).toBeHidden(); + + // It should be back in the side panel and in the doc + await page.getByRole('button', { name: 'Filter comments' }).click(); + await page.getByRole('menuitem', { name: 'Open' }).click(); + await expect( + elCommentsSidePanel.getByText('This is a comment'), + ).toBeVisible(); + await elCommentsSidePanel.getByText('This is a comment').click(); + await expect(editor.getByText('Hello World')).toHaveClass( + 'bn-thread-mark-selected', + ); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-editor.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-editor.ts index d441ddd36..dd97844bb 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-editor.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-editor.ts @@ -3,11 +3,24 @@ import { Page } from '@playwright/test'; export const getEditor = async ({ page }: { page: Page }) => { - const editor = page.locator('.ProseMirror'); + const editor = page.locator('.--docs--editor-container .ProseMirror'); await editor.click(); return editor; }; +export const tryFocusEditorContent = async ({ page }: { page: Page }) => { + const editor = await getEditor({ page }); + if ( + (await editor.locator('.bn-trailing-block.ProseMirror-widget').count()) > 0 + ) { + await editor.locator('.bn-trailing-block.ProseMirror-widget').click(); + } else { + await editor.click(); + } + + return editor; +}; + export const openSuggestionMenu = async ({ page, suggestion, @@ -15,14 +28,7 @@ export const openSuggestionMenu = async ({ page: Page; suggestion?: string; }) => { - const editor = await getEditor({ page }); - if ( - (await editor.locator('.bn-trailing-block.ProseMirror-widget').count()) > 0 - ) { - await editor.locator('.bn-trailing-block.ProseMirror-widget').click(); - } else { - await editor.click(); - } + const editor = await tryFocusEditorContent({ page }); await page.keyboard.press('Enter'); await page.keyboard.type('/'); diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/filter-notification.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/filter-notification.svg index b0e76a40f..1e808b84e 100644 --- a/src/frontend/apps/impress/src/assets/icons/ui-kit/filter-notification.svg +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/filter-notification.svg @@ -1,6 +1,12 @@ - - - - - + + + + + diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/filter_list.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/filter_list.svg index 3943ad6c4..febadf7c0 100644 --- a/src/frontend/apps/impress/src/assets/icons/ui-kit/filter_list.svg +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/filter_list.svg @@ -1,3 +1,6 @@ - - + + diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/x-mark.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/x-mark.svg index 070d76987..def6c2ce5 100644 --- a/src/frontend/apps/impress/src/assets/icons/ui-kit/x-mark.svg +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/x-mark.svg @@ -1,3 +1,6 @@ - - + + diff --git a/src/frontend/apps/impress/src/components/modal/ButtonCloseModal.tsx b/src/frontend/apps/impress/src/components/modal/ButtonCloseModal.tsx index 75e350595..63cc8bc29 100644 --- a/src/frontend/apps/impress/src/components/modal/ButtonCloseModal.tsx +++ b/src/frontend/apps/impress/src/components/modal/ButtonCloseModal.tsx @@ -1,7 +1,6 @@ import { Button, type ButtonProps } from '@gouvfr-lasuite/cunningham-react'; -import React from 'react'; -import { Icon } from '@/components'; +import CloseIcon from '@/assets/icons/ui-kit/x-mark.svg'; export const ButtonCloseModal = (props: ButtonProps) => { return ( @@ -10,14 +9,7 @@ export const ButtonCloseModal = (props: ButtonProps) => { size="small" color="neutral" variant="tertiary" - icon={ - - } + icon={} {...props} /> ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 3f5dbef9c..6fb2bd437 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -11,9 +11,15 @@ import '@blocknote/core/fonts/inter.css'; import * as localesBN from '@blocknote/core/locales'; import { BlockNoteView } from '@blocknote/mantine'; import '@blocknote/mantine/style.css'; -import { useCreateBlockNote } from '@blocknote/react'; +import { + FloatingComposerController, + FloatingThreadController, + ThreadsSidebar, + useCreateBlockNote, +} from '@blocknote/react'; import { HocuspocusProvider } from '@hocuspocus/provider'; import { useEffect, useMemo, useRef } from 'react'; +import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import type { Awareness } from 'y-protocols/awareness'; import * as Y from 'yjs'; @@ -41,7 +47,11 @@ import { randomColor, sanitizeColor } from '../utils'; import BlockNoteAI from './AI'; import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu'; import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar'; -import { DocsCommentsStyle, useComments } from './comments/'; +import { + DocsCommentsStyle, + useCommentSidebarStore, + useComments, +} from './comments/'; import { CalloutBlock, PdfBlock, UploadLoaderBlock } from './custom-blocks'; const AIMenu = BlockNoteAI?.AIMenu; const AIMenuController = BlockNoteAI?.AIMenuController; @@ -82,11 +92,8 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { const { setEditor } = useEditorStore(); const { themeTokens } = useCunninghamTheme(); const refEditorContainer = useRef(null); - const canSeeComment = doc.abilities.comment; - // Determine if comments should be visible in the UI - const showComments = canSeeComment; - useSaveDoc(doc.id, provider.document); + const { i18n, t } = useTranslation(); const langLocalesBN = !i18n.resolvedLanguage || !(i18n.resolvedLanguage in localesBN) @@ -118,12 +125,22 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { const cursorName = collabName || t('Anonymous'); const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity'; + // Comments + const canSeeComment = doc.abilities.comment; + const showComments = canSeeComment; // Determine if comments should be visible in the UI const { resolveUsers, threadStore } = useComments( doc.id, canSeeComment, user, ); + // Comment sidebar + const { + threadsSidebarTarget, + filter: threadsSidebarFilter, + isSideBarOpen, + } = useCommentSidebarStore(); + const currentUserAvatarUrl = useMemo(() => { if (canSeeComment) { return avatarUrlFromName(collabName, themeTokens?.font?.families?.base); @@ -275,14 +292,26 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { formattingToolbar={false} slashMenu={false} theme="light" - comments={showComments} + comments={false} aria-label={t('Document editor')} + // To not clipped the floating part in the editor area + portalElements={{ default: null }} > {aiBlockNoteAllowed && AIMenuController && AIMenu && ( )} + {showComments && } + {showComments && !isSideBarOpen && } + {threadsSidebarTarget && + createPortal( + , + threadsSidebarTarget, + )} ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx index 32082b03a..864377e62 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx @@ -45,6 +45,7 @@ export const DocEditorContainer = ({ $width="100%" $flex="1" className={DOCS_EDITOR_CLASS} + $margin={{ horizontal: 'auto' }} > void; +} + +export const CommentSideBar = ({ onClose }: CommentSideBarProps) => { + const { t } = useTranslation(); + const { setThreadsSidebarTarget, filter, setFilter } = + useCommentSidebarStore(); + const portalRef = useRef(null); + const [open, setOpen] = useState(false); + + useEffect(() => { + if (portalRef.current) { + setThreadsSidebarTarget(portalRef.current); + } + return () => { + setThreadsSidebarTarget(null); + }; + }, [setThreadsSidebarTarget]); + + return ( + + + + + {t('Comments')} + + setFilter('open'), + isChecked: filter === 'open', + }, + { + label: t('Resolved'), + callback: () => setFilter('resolved'), + isChecked: filter === 'resolved', + }, + ]} + isOpen={open} + shouldCloseOnInteractOutside={() => true} + onOpenChange={setOpen} + > + + + ) : ( + + ) + } + color={filter === 'open' ? 'neutral' : 'brand'} + variant={filter === 'open' ? 'tertiary' : 'secondary'} + onClick={(e) => { + e.stopPropagation(); + e.preventDefault(); + setOpen((o) => !o); + }} + tabIndex={-1} + /> + + + + + + +
+ + ); +}; + +export const CommentSideBarButton = () => { + const { t } = useTranslation(); + const { isPanelOpen, togglePanel } = useRightPanelStore(); + const { setIsSideBarOpen } = useCommentSidebarStore(); + + useEffect(() => { + setIsSideBarOpen(isPanelOpen); + }, [isPanelOpen, setIsSideBarOpen]); + + const ariaLabel = isPanelOpen + ? t('Hide the comments sidebar') + : t('Show the comments sidebar'); + + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentToolbarButton.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentToolbarButton.tsx index 41d4e6dd7..368649ea8 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentToolbarButton.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentToolbarButton.tsx @@ -47,16 +47,6 @@ export const CommentToolbarButton = () => { return !!selectedBlocks.find((block) => block.content !== undefined); }, [selectedBlocks]); - const focusOnInputThread = () => { - // Use setTimeout to ensure the DOM has been updated with the new comment - setTimeout(() => { - const threadElement = document.querySelector( - '.bn-thread .bn-editor', - ); - threadElement?.focus(); - }, 400); - }; - if ( !comments || !show || @@ -74,7 +64,6 @@ export const CommentToolbarButton = () => { onClick={() => { comments.startPendingComment(); store.setState(false); - focusOnInputThread(); }} aria-haspopup="dialog" data-test="comment-toolbar-button" diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/index.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/index.ts index 99acd58df..03a884dbd 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/index.ts @@ -1,3 +1,4 @@ export * from './CommentToolbarButton'; export * from './styles'; +export * from './useCommentSidebarStore'; export * from './useComments'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/styles.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/styles.tsx index b1d8ec2ac..48f73db59 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/styles.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/styles.tsx @@ -5,7 +5,9 @@ export const DocsCommentsStyle = createGlobalStyle<{ currentUserAvatarUrl?: string; }>` .--docs--main-editor.bn-root, - .--docs--main-editor.bn-root .ProseMirror { + .--docs--main-editor.bn-root .ProseMirror, + .--docs--comments-sidebar.bn-root, + .--docs--comments-sidebar.bn-root .ProseMirror { // Comments marks in the editor .bn-editor { // Resets blocknote comments styles @@ -38,6 +40,22 @@ export const DocsCommentsStyle = createGlobalStyle<{ ); } } + + .bn-thread-mark[data-orphan='true']:has(> .bn-thread-mark-selected) { + background-color: color-mix( + in srgb, + var(--c--contextuals--background--palette--blue-1--tertiary) 40%, + transparent + ); + border-bottom: 2px solid + var(--c--contextuals--background--palette--blue-1--secondary); + + mix-blend-mode: multiply; + + transition: + background-color var(--c--globals--transitions--duration), + border-bottom-color var(--c--globals--transitions--duration); + } `} [data-show-selection] { @@ -56,7 +74,7 @@ export const DocsCommentsStyle = createGlobalStyle<{ min-width: calc(min(400px, 90vw)); max-width: calc(min(400px, 90vw)); max-height: calc(min(500px, 60vh)); - padding: 8px; + padding: var(--c--globals--spacings--xxs) var(--c--globals--spacings--xxxs); box-shadow: 0px 6px 18px 0px #00001229; margin-left: 20px; margin-right: 20px; @@ -81,10 +99,20 @@ export const DocsCommentsStyle = createGlobalStyle<{ display: none; } + & .bn-thread-comments { + gap: var(--c--globals--spacings--xxxs); + } + .bn-thread-comment { padding: 8px; flex-wrap: nowrap; gap: 0px; + flex-direction: column; + align-items: initial; + + & > div:first-child { + flex-direction: row; + } & .bn-editor { padding-left: var(--c--globals--spacings--lg); @@ -100,7 +128,7 @@ export const DocsCommentsStyle = createGlobalStyle<{ padding: var(--c--globals--spacings--0) var(--c--globals--spacings--st); background: none; - border: 1px solid var(--c--globals--colors--gray-300); + border: 1px solid var(--c--contextuals--border--semantic--neutral--tertiary); border-radius: var(--c--globals--spacings--st); height: var(--c--globals--spacings--md); } @@ -131,6 +159,8 @@ export const DocsCommentsStyle = createGlobalStyle<{ // Date span.mantine-focus-auto { + font-weight: 400; + margin-left: var(--c--globals--spacings--2xs) !important; } .bn-comment-actions { @@ -139,6 +169,9 @@ export const DocsCommentsStyle = createGlobalStyle<{ .mantine-Button-root { background-color: transparent; + height: var(--c--globals--spacings--md); + width: var(--c--globals--spacings--md); + padding: var(--c--globals--spacings--0); &:hover { background-color: var(--c--globals--colors--gray-100); @@ -167,19 +200,20 @@ export const DocsCommentsStyle = createGlobalStyle<{ & > button { height: var(--c--globals--spacings--md); padding-inline: var(--c--globals--spacings--st); + border: 1px solid + var(--c--contextuals--background--semantic--brand--primary); + background: var( + --c--contextuals--background--semantic--brand--primary + ); + color: var( + --c--contextuals--content--semantic--brand--on-brand + ); - &[data-test='save'] { - border: 1px solid - var(--c--contextuals--background--semantic--brand--primary); - background: var( - --c--contextuals--background--semantic--brand--primary - ); - color: var( - --c--contextuals--content--semantic--brand--on-brand - ); + &:hover { + background-color: var(--c--contextuals--background--semantic--brand--primary); } - &[data-test='cancel'] { + &:last-child { background: white; border: 1px solid var(--c--contextuals--border--surface--primary); @@ -214,6 +248,11 @@ export const DocsCommentsStyle = createGlobalStyle<{ background-repeat: no-repeat; background-size: cover; } + + & .bn-block-content:has(.ProseMirror-trailingBreak:only-child):after { + color: var(--c--contextuals--content--semantic--neutral--tertiary); + font-style: normal; + } } // Actions button send comment @@ -223,6 +262,7 @@ export const DocsCommentsStyle = createGlobalStyle<{ .bn-action-toolbar.bn-comment-actions { border: none; + background-color: transparent; button { font-size: 0; @@ -261,4 +301,74 @@ export const DocsCommentsStyle = createGlobalStyle<{ } } } + + /** + * Styles for the comments sidebar + */ + .--docs--comments-sidebar.bn-root{ + flex: 1; + min-height: 0; + overflow: auto; + + .bn-threads-sidebar { + gap: 0; + border-radius: 0; + min-height: 100%; + + .bn-thread-expand-prompt p { + font-size: var(--c--globals--font--sizes--xs); + } + + .bn-thread { + margin: 0; + max-width: 100%; + width: 100%; + min-width: 0; + overflow: visible; + padding: var(--c--globals--spacings--xxs) var(--c--globals--spacings--xxxs); + border: none; + border-radius: 0; + border-bottom: 1px solid var(--c--contextuals--border--surface--primary); + + &.selected { + border: none; + background: var(--c--contextuals--background--semantic--neutral--tertiary); + max-height: none; + } + + &:hover { + background: var(--c--contextuals--background--semantic--neutral--tertiary); + } + + & .bn-header-text { + display: none; + } + + &.bn-thread-orphaned { + & .bn-header-text { + display: block; + padding-inline: var(--c--globals--spacings--xs); + } + } + + .bn-thread-comment { + padding: var(--c--globals--spacings--xs); + + &:has(.bn-comment-actions) { + & > .mantine-Group-root:first-child { + background: linear-gradient( + to left, + var(--c--contextuals--background--semantic--neutral--tertiary) 90%, + rgba(255, 255, 255, 0) 100% + ); + } + + .bn-menu-dropdown { + box-shadow: 0px 0px 6px 0px #0000911a; + } + } + } + } + } + } `; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/useCommentSidebarStore.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/useCommentSidebarStore.ts new file mode 100644 index 000000000..3d4cb6e02 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/useCommentSidebarStore.ts @@ -0,0 +1,21 @@ +import { create } from 'zustand'; + +interface CommentSidebarStore { + filter: 'open' | 'resolved'; + isSideBarOpen: boolean; + setIsSideBarOpen: (isSideBarOpen: boolean) => void; + setThreadsSidebarTarget: (el: HTMLElement | null) => void; + setFilter: (filter: 'open' | 'resolved') => void; + threadsSidebarTarget: HTMLElement | null; +} + +export const useCommentSidebarStore = create((set) => ({ + filter: 'open', + isSideBarOpen: false, + setFilter: (filter) => set(() => ({ filter })), + setIsSideBarOpen: (isSideBarOpen) => set(() => ({ isSideBarOpen })), + setThreadsSidebarTarget: (threadsSidebarTarget) => { + set(() => ({ threadsSidebarTarget })); + }, + threadsSidebarTarget: null, +})); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx index 66f15e44e..fff7b6a24 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx @@ -4,6 +4,22 @@ export const DocsEditorStyle = createGlobalStyle` .bn-container { height: 100%; } + /** + * Token Blocknote + */ + .bn-root[data-color-scheme] { + --bn-colors-editor-text: var( + --c--contextuals--content--semantic--neutral--primary + ); + --bn-colors-side-menu: var( + --c--contextuals--content--semantic--neutral--tertiary + ); + } + .bn-root .mantine-Chip-label { + --chip-color: var(--c--contextuals--content--semantic--brand--tertiary); + --mantine-primary-color-filled-hover: var(--c--contextuals--content--semantic--brand--tertiary); + } + .bn-root { .bn-editor { height: 100%; @@ -14,16 +30,6 @@ export const DocsEditorStyle = createGlobalStyle` font-family: var(--c--components--button--font-family); } - /** - * Token Mantine - */ - --bn-colors-editor-text: var( - --c--contextuals--content--semantic--neutral--primary - ); - --bn-colors-side-menu: var( - --c--contextuals--content--semantic--neutral--tertiary - ); - /** * Ensure long placeholder text is truncated with ellipsis */ diff --git a/src/frontend/apps/impress/src/features/right-panel/components/RightPanel.tsx b/src/frontend/apps/impress/src/features/right-panel/components/RightPanel.tsx index 34f0b2000..982b3595d 100644 --- a/src/frontend/apps/impress/src/features/right-panel/components/RightPanel.tsx +++ b/src/frontend/apps/impress/src/features/right-panel/components/RightPanel.tsx @@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; import { Box } from '@/components'; +import { CommentSideBar } from '@/features/docs/doc-editor/components/comments/CommentSideBar'; import { useDocStore, useProviderStore } from '@/features/docs/doc-management'; import { HEADER_HEIGHT } from '@/features/header'; import { useResponsiveStore } from '@/stores'; @@ -11,7 +12,7 @@ import { useRightPanelStore } from './useRightPanelStore'; export const RightPanel = () => { const { t } = useTranslation(); const { currentDoc: doc } = useDocStore(); - const { _, isPanelOpen } = useRightPanelStore(); + const { setIsPanelOpen, isPanelOpen } = useRightPanelStore(); const { isMobile } = useResponsiveStore(); const { provider, isReady } = useProviderStore(); const isProviderReady = @@ -47,6 +48,8 @@ export const RightPanel = () => { width: 0; `} `} - > + > + setIsPanelOpen(false)} /> + ); }; diff --git a/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx b/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx index 025a5e55a..b6acba189 100644 --- a/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx +++ b/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx @@ -1,19 +1,29 @@ -import { Button } from '@gouvfr-lasuite/cunningham-react'; -import { useTranslation } from 'react-i18next'; +import { useEffect, useState } from 'react'; import { css } from 'styled-components'; -import CommentsIcon from '@/assets/icons/ui-kit/bubble-text.svg'; import { Card } from '@/components'; - -import { useRightPanelStore } from './useRightPanelStore'; +import { CommentSideBarButton } from '@/features/docs/doc-editor/components/comments/CommentSideBar'; +import { useEditorStore } from '@/features/docs/doc-editor/stores/useEditorStore'; export const RightPanelCollapseButton = () => { - const { t } = useTranslation(); - const { isPanelOpen, togglePanel } = useRightPanelStore(); + const { threadStore } = useEditorStore(); + const [hasThreads, setHasThreads] = useState( + !!threadStore?.getThreads().size, + ); - const ariaLabel = isPanelOpen - ? t('Hide the right side panel') - : t('Show the right side panel'); + useEffect(() => { + if (!threadStore) { + setHasThreads(false); + return; + } + return threadStore.subscribe((threads) => { + setHasThreads(threads.size > 0); + }); + }, [threadStore]); + + if (!hasThreads) { + return null; + } return ( { box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); `} > - + {hasThreads && } ); };