mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-08 18:57:54 +02:00
🚚(frontend) move doc toolbox to Floating Bar
We are modifying the doc header, we are moving elements to a floating bar. In this case, we are moving the doc toolbox to the floating bar. We adapted the toolbox with the updated actions. We update the doc header to use all empty space. We moved the emoji part from the toolbox directly to the header.
This commit is contained in:
@@ -18,6 +18,7 @@ and this project adheres to
|
||||
|
||||
- ♻️(backend) allow global search in sub documents
|
||||
- ✨(backend) add a breadcrumb in the search response
|
||||
- ♻️(frontend) move doc action buttons to fix toolbar #2360
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PDFParse } from 'pdf-parse';
|
||||
|
||||
import {
|
||||
TestLanguage,
|
||||
clickInEditorMenu,
|
||||
createDoc,
|
||||
verifyDocName,
|
||||
waitForLanguageSwitch,
|
||||
@@ -24,11 +25,7 @@ test.describe('Doc Export', () => {
|
||||
browserName,
|
||||
}) => {
|
||||
await createDoc(page, 'doc-editor', browserName, 1);
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
await clickInEditorMenu(page, 'Download');
|
||||
|
||||
await expect(page.getByTestId('modal-export-title')).toBeVisible();
|
||||
await expect(
|
||||
@@ -54,11 +51,7 @@ test.describe('Doc Export', () => {
|
||||
test('it exports the doc to docx', async ({ page, browserName }) => {
|
||||
const randomDoc = await overrideDocContent({ page, browserName });
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
await clickInEditorMenu(page, 'Download');
|
||||
|
||||
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||
await page.getByRole('option', { name: 'Docx' }).click();
|
||||
@@ -84,11 +77,7 @@ test.describe('Doc Export', () => {
|
||||
test('it exports the doc to odt', async ({ page, browserName }) => {
|
||||
const randomDoc = await overrideDocContent({ page, browserName });
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
await clickInEditorMenu(page, 'Download');
|
||||
|
||||
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||
await page.getByRole('option', { name: 'Odt' }).click();
|
||||
@@ -139,11 +128,7 @@ test.describe('Doc Export', () => {
|
||||
// Give some time for the image to be fully processed
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
await clickInEditorMenu(page, 'Download');
|
||||
|
||||
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||
await page.getByRole('option', { name: 'HTML' }).click();
|
||||
@@ -231,11 +216,7 @@ test.describe('Doc Export', () => {
|
||||
.fill('https://docs.numerique.gouv.fr/assets/logo-gouv.png');
|
||||
await page.getByText('Embed image').click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
await clickInEditorMenu(page, 'Download');
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
@@ -322,11 +303,7 @@ test.describe('Doc Export', () => {
|
||||
|
||||
await overrideDocContent({ page, browserName });
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
await clickInEditorMenu(page, 'Download');
|
||||
|
||||
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||
await page.getByRole('option', { name: 'Print' }).click();
|
||||
@@ -375,11 +352,7 @@ test.describe('Doc Export', () => {
|
||||
|
||||
const randomDoc = await overrideDocContent({ page, browserName });
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
await clickInEditorMenu(page, 'Download');
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
clickInEditorMenu,
|
||||
clickInEditorShareButton,
|
||||
createDoc,
|
||||
getGridRow,
|
||||
@@ -83,15 +84,14 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'close' }).first().click();
|
||||
|
||||
await expect(card.getByText('Public document')).toBeVisible();
|
||||
|
||||
await expect(card.getByText('Public ·')).toBeVisible();
|
||||
await expect(card.getByText('Owner ·')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
await page
|
||||
.getByRole('button', { name: 'Open the document options' })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Export the document' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Open the document options' }),
|
||||
page.getByRole('menuitem', { name: 'Download' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -186,17 +186,15 @@ test.describe('Doc Header', () => {
|
||||
await createDoc(page, 'doc-update-emoji', browserName, 1);
|
||||
|
||||
const emojiPicker = page.locator('.--docs--doc-title').getByRole('button');
|
||||
const optionMenu = page.getByLabel('Open the document options');
|
||||
const addEmojiMenuItem = page.getByRole('menuitem', { name: 'Add emoji' });
|
||||
const removeEmojiMenuItem = page.getByRole('menuitem', {
|
||||
name: 'Remove emoji',
|
||||
const addEmoji = page.getByRole('button', { name: 'Add icon' });
|
||||
const removeEmoji = page.getByRole('button', {
|
||||
name: 'Remove icon',
|
||||
});
|
||||
|
||||
// Top parent should not have emoji picker
|
||||
await expect(emojiPicker).toBeHidden();
|
||||
await optionMenu.click();
|
||||
await expect(addEmojiMenuItem).toBeHidden();
|
||||
await expect(removeEmojiMenuItem).toBeHidden();
|
||||
await expect(addEmoji).toBeHidden();
|
||||
await expect(removeEmoji).toBeHidden();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
const { name: docChild } = await createRootSubPage(
|
||||
@@ -211,9 +209,8 @@ test.describe('Doc Header', () => {
|
||||
await expect(emojiPicker).toBeHidden();
|
||||
|
||||
// Add emoji
|
||||
await optionMenu.click();
|
||||
await expect(removeEmojiMenuItem).toBeHidden();
|
||||
await addEmojiMenuItem.click();
|
||||
await expect(removeEmoji).toBeHidden();
|
||||
await addEmoji.click();
|
||||
// The 1 April the emoji is a fish
|
||||
await expect(emojiPicker).toHaveText(/📄|🐟/);
|
||||
|
||||
@@ -235,17 +232,15 @@ test.describe('Doc Header', () => {
|
||||
await expect(row.getByText('😀')).toBeVisible();
|
||||
|
||||
// Remove emoji
|
||||
await optionMenu.click();
|
||||
await expect(addEmojiMenuItem).toBeHidden();
|
||||
await removeEmojiMenuItem.click();
|
||||
await expect(addEmoji).toBeHidden();
|
||||
await removeEmoji.click();
|
||||
await expect(emojiPicker).toBeHidden();
|
||||
});
|
||||
|
||||
test('it deletes the doc', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page.getByRole('menuitem', { name: 'Delete document' }).click();
|
||||
await clickInEditorMenu(page, 'Delete');
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
||||
@@ -301,15 +296,12 @@ test.describe('Doc Header', () => {
|
||||
page.getByRole('textbox', { name: 'Document title' }),
|
||||
).toContainText('Mocked document');
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Export the document' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Delete document' }),
|
||||
).toBeDisabled();
|
||||
page.getByRole('menuitem', { name: 'Download' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeHidden();
|
||||
|
||||
// Click somewhere else to close the options
|
||||
await page.locator('body').click({ position: { x: 0, y: 0 } });
|
||||
@@ -381,14 +373,12 @@ test.describe('Doc Header', () => {
|
||||
page.getByRole('textbox', { name: 'Document title' }),
|
||||
).toContainText('Mocked document');
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Export the document' }),
|
||||
).toBeVisible();
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Delete document' }),
|
||||
).toBeDisabled();
|
||||
page.getByRole('menuitem', { name: 'Download' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeHidden();
|
||||
|
||||
// Click somewhere else to close the options
|
||||
await page.locator('body').click({ position: { x: 0, y: 0 } });
|
||||
@@ -453,14 +443,12 @@ test.describe('Doc Header', () => {
|
||||
page.getByRole('heading', { name: 'Mocked document' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Export the document' }),
|
||||
).toBeVisible();
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Delete document' }),
|
||||
).toBeDisabled();
|
||||
page.getByRole('menuitem', { name: 'Download' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeHidden();
|
||||
|
||||
// Click somewhere else to close the options
|
||||
await page.locator('body').click({ position: { x: 0, y: 0 } });
|
||||
@@ -707,13 +695,18 @@ test.describe('Documents Header mobile', () => {
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Copy link' })).toBeHidden();
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Copy link' }),
|
||||
).toBeVisible();
|
||||
await page.getByRole('menuitem', { name: 'Share' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Copy link' })).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
const shareModal = page.getByRole('dialog', {
|
||||
name: 'Share the document',
|
||||
});
|
||||
await expect(
|
||||
shareModal.getByRole('button', { name: 'Copy link' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('it checks the close button on Share modal', async ({ page }) => {
|
||||
|
||||
@@ -116,7 +116,7 @@ test.describe('Doc Version', () => {
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Version history' }),
|
||||
).toBeDisabled();
|
||||
).toBeHidden();
|
||||
});
|
||||
|
||||
test('it restores the doc version', async ({ page, browserName }) => {
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
* Tooltip
|
||||
*/
|
||||
.c__tooltip {
|
||||
padding: var(--c--globals--font--sizes--sm) var(--c--globals--spacings--xxs);
|
||||
padding: var(--c--globals--font--sizes--t) var(--c--globals--spacings--xxs);
|
||||
}
|
||||
|
||||
.c__tooltip .react-aria-OverlayArrow svg {
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
const mockUpdateDocEmoji = vi.fn();
|
||||
|
||||
vi.mock('@/docs/doc-management', async () => {
|
||||
const actual = await vi.importActual('@/docs/doc-management');
|
||||
return {
|
||||
...actual,
|
||||
useDocTitleUpdate: () => ({ updateDocEmoji: mockUpdateDocEmoji }),
|
||||
};
|
||||
});
|
||||
|
||||
import { DocHeader } from '../components/DocHeader';
|
||||
|
||||
const doc = {
|
||||
id: 'doc-1',
|
||||
title: 'My document',
|
||||
is_favorite: false,
|
||||
nb_accesses_direct: 1,
|
||||
abilities: {
|
||||
versions_list: true,
|
||||
destroy: true,
|
||||
partial_update: true,
|
||||
duplicate: true,
|
||||
accesses_view: true,
|
||||
},
|
||||
} as any;
|
||||
|
||||
describe('DocHeader - Add emoji (April Fools easter egg)', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
mockUpdateDocEmoji.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
[
|
||||
{ emoji: '🐟', date: '2026-04-01' },
|
||||
{ emoji: '📄', date: '2026-03-30' },
|
||||
{ emoji: '📄', date: '2026-04-02' },
|
||||
].forEach(({ emoji, date }) => {
|
||||
test(`uses ${emoji} emoji on ${date}`, () => {
|
||||
vi.setSystemTime(new Date(date));
|
||||
|
||||
render(<DocHeader doc={doc} />, { wrapper: AppWrapper });
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add icon' }));
|
||||
|
||||
expect(mockUpdateDocEmoji).toHaveBeenCalledWith(
|
||||
'doc-1',
|
||||
'My document',
|
||||
emoji,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { DropdownMenuOption } from '@/components';
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
const mockUpdateDocEmoji = vi.fn();
|
||||
let capturedOptions: DropdownMenuOption[] = [];
|
||||
|
||||
vi.mock('@/components', async () => {
|
||||
const actual = await vi.importActual<any>('@/components');
|
||||
return {
|
||||
...actual,
|
||||
DropdownMenu: ({ options }: { options: DropdownMenuOption[] }) => {
|
||||
capturedOptions = options;
|
||||
return null;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('next/router', async () => ({
|
||||
...(await vi.importActual('next/router')),
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('@/docs/doc-management', async () => {
|
||||
const actual = await vi.importActual<any>('@/docs/doc-management');
|
||||
return {
|
||||
...actual,
|
||||
useDocTitleUpdate: () => ({ updateDocEmoji: mockUpdateDocEmoji }),
|
||||
useDocUtils: () => ({ isChild: true, isTopRoot: false }),
|
||||
useCopyDocLink: () => vi.fn(),
|
||||
useCreateFavoriteDoc: () => ({ mutate: vi.fn() }),
|
||||
useDeleteFavoriteDoc: () => ({ mutate: vi.fn() }),
|
||||
useDuplicateDoc: () => ({ mutate: vi.fn() }),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/stores', () => ({
|
||||
useFocusStore: (selector?: (state: any) => any) => {
|
||||
const state = { addLastFocus: vi.fn(), restoreFocus: vi.fn() };
|
||||
return selector ? selector(state) : state;
|
||||
},
|
||||
useResponsiveStore: () => ({
|
||||
isSmallMobile: false,
|
||||
isMobile: false,
|
||||
isDesktop: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useCopyCurrentEditorToClipboard', () => ({
|
||||
useCopyCurrentEditorToClipboard: () => vi.fn(),
|
||||
}));
|
||||
|
||||
import { DocToolBox } from '../components/DocToolBox';
|
||||
|
||||
const doc = {
|
||||
id: 'doc-1',
|
||||
title: 'My document',
|
||||
is_favorite: false,
|
||||
nb_accesses_direct: 1,
|
||||
abilities: {
|
||||
versions_list: true,
|
||||
destroy: true,
|
||||
partial_update: true,
|
||||
duplicate: true,
|
||||
accesses_view: true,
|
||||
},
|
||||
} as any;
|
||||
|
||||
describe('DocToolBox - Add emoji (April Fools easter egg)', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
mockUpdateDocEmoji.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
[
|
||||
{ emoji: '🐟', date: '2026-04-01' },
|
||||
{ emoji: '📄', date: '2026-03-30' },
|
||||
{ emoji: '📄', date: '2026-04-02' },
|
||||
].forEach(({ emoji, date }) => {
|
||||
test(`uses ${emoji} emoji on ${date}`, () => {
|
||||
vi.setSystemTime(new Date(date));
|
||||
|
||||
render(<DocToolBox doc={doc} />, { wrapper: AppWrapper });
|
||||
|
||||
const addEmojiOption = capturedOptions.find(
|
||||
(o) => o.label === 'Add emoji',
|
||||
);
|
||||
void addEmojiOption?.callback?.();
|
||||
|
||||
expect(mockUpdateDocEmoji).toHaveBeenCalledWith(
|
||||
'doc-1',
|
||||
'My document',
|
||||
emoji,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+26
-10
@@ -9,9 +9,33 @@ vi.mock('next/router', async () => ({
|
||||
...(await vi.importActual('next/router')),
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
pathname: '/docs/doc-1',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@gouvfr-lasuite/ui-kit', async () => {
|
||||
const actual = await vi.importActual('@gouvfr-lasuite/ui-kit');
|
||||
return {
|
||||
...actual,
|
||||
DropdownMenu: ({ options, children }: any) => (
|
||||
<>
|
||||
{children}
|
||||
<ul>
|
||||
{options
|
||||
.filter((o: any) => !o.isHidden)
|
||||
.map((o: any) => (
|
||||
<li key={o.label}>{o.label}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../hooks/useCopyCurrentEditorToClipboard', () => ({
|
||||
useCopyCurrentEditorToClipboard: () => vi.fn(),
|
||||
}));
|
||||
|
||||
const doc = {
|
||||
nb_accesses: 1,
|
||||
abilities: {
|
||||
@@ -39,9 +63,7 @@ describe('DocToolBox - Licence', () => {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByLabelText('Export the document'),
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText('Download')).toBeInTheDocument();
|
||||
}, 15000);
|
||||
|
||||
test('The export button is not rendered when MIT version is activated', async () => {
|
||||
@@ -53,12 +75,6 @@ describe('DocToolBox - Licence', () => {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByLabelText('Open the document options'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.queryByLabelText('Export the document'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Download')).not.toBeInTheDocument();
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
@@ -1,62 +1,90 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RemoveEmojiSVG from '@/assets/icons/ui-kit/face-remove.svg';
|
||||
import AddEmojiSVG from '@/assets/icons/ui-kit/face.svg';
|
||||
import { Box, HorizontalSeparator } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, useIsCollaborativeEditable } from '@/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
import {
|
||||
Doc,
|
||||
getEmojiAndTitle,
|
||||
useDocTitleUpdate,
|
||||
useDocUtils,
|
||||
useIsCollaborativeEditable,
|
||||
} from '@/docs/doc-management';
|
||||
|
||||
import { AlertNetwork } from './AlertNetwork';
|
||||
import { AlertRestore } from './AlertRestore';
|
||||
import { DocHeaderInfo } from './DocHeaderInfo';
|
||||
import { DocTitle } from './DocTitle';
|
||||
import { DocToolBox } from './DocToolBox';
|
||||
|
||||
interface DocHeaderProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { t } = useTranslation();
|
||||
const { isEditable } = useIsCollaborativeEditable(doc);
|
||||
const isDeletedDoc = !!doc.deleted_at;
|
||||
// Emoji Management
|
||||
const { emoji } = getEmojiAndTitle(doc.title ?? '');
|
||||
const { updateDocEmoji } = useDocTitleUpdate();
|
||||
const { isTopRoot } = useDocUtils(doc);
|
||||
const displayEmojiButton = doc.abilities.partial_update && !isTopRoot;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
$width="100%"
|
||||
$padding={{ top: isDesktop ? '0' : 'md' }}
|
||||
$gap={spacingsTokens['base']}
|
||||
aria-label={t('It is the card information about the document.')}
|
||||
className="--docs--doc-header"
|
||||
$minHeight="125px"
|
||||
>
|
||||
{isDeletedDoc && <AlertRestore doc={doc} />}
|
||||
{!isEditable && <AlertNetwork />}
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$width="100%"
|
||||
$padding={{ bottom: 'xs' }}
|
||||
$gap="base"
|
||||
$padding={{
|
||||
bottom: isDeletedDoc || !isEditable ? 'base' : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$css="flex:1;"
|
||||
$gap="0.5rem 1rem"
|
||||
$align="center"
|
||||
$maxWidth="100%"
|
||||
>
|
||||
<Box $gap={spacingsTokens['3xs']} $overflow="auto">
|
||||
<DocTitle doc={doc} />
|
||||
<Box $direction="row">
|
||||
<DocHeaderInfo doc={doc} />
|
||||
</Box>
|
||||
</Box>
|
||||
{!isDeletedDoc && <DocToolBox doc={doc} />}
|
||||
</Box>
|
||||
{isDeletedDoc && <AlertRestore doc={doc} />}
|
||||
{!isEditable && <AlertNetwork />}
|
||||
</Box>
|
||||
<HorizontalSeparator $margin="none" />
|
||||
<Box $gap="sm">
|
||||
<Box $minHeight="32px">
|
||||
{displayEmojiButton && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const today = new Date();
|
||||
const isAprilFools =
|
||||
today.getMonth() === 3 && today.getDate() === 1;
|
||||
emoji
|
||||
? updateDocEmoji(doc.id, doc.title ?? '', '')
|
||||
: updateDocEmoji(
|
||||
doc.id,
|
||||
doc.title ?? '',
|
||||
isAprilFools ? '🐟' : '📄',
|
||||
);
|
||||
}}
|
||||
aria-label={emoji ? t('Remove icon') : t('Add icon')}
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
icon={
|
||||
emoji ? (
|
||||
<RemoveEmojiSVG width={24} height={24} aria-hidden="true" />
|
||||
) : (
|
||||
<AddEmojiSVG width={24} height={24} aria-hidden="true" />
|
||||
)
|
||||
}
|
||||
style={{ width: 'fit-content' }}
|
||||
>
|
||||
{emoji ? t('Remove icon') : t('Add icon')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<DocTitle doc={doc} />
|
||||
<DocHeaderInfo doc={doc} />
|
||||
</Box>
|
||||
<HorizontalSeparator $margin={{ top: '24px' }} />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { t } from 'i18next';
|
||||
|
||||
import PublicSVG from '@/assets/icons/ui-kit/public.svg';
|
||||
import ProtedtedSVG from '@/assets/icons/ui-kit/vpn_lock.svg';
|
||||
import { Text } from '@/components';
|
||||
import { Box, Text } from '@/components';
|
||||
import { useConfig } from '@/core';
|
||||
import {
|
||||
Doc,
|
||||
@@ -40,7 +40,7 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box $direction="row">
|
||||
<Text
|
||||
$variation="tertiary"
|
||||
$size="s"
|
||||
@@ -55,7 +55,7 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => {
|
||||
<Text $variation="tertiary" $size="s">
|
||||
{dateToDisplay}
|
||||
</Text>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -42,11 +42,7 @@ export const DocTitleText = () => {
|
||||
|
||||
return (
|
||||
<Box className={CLASS_DOC_TITLE} $direction="row" $align="center">
|
||||
<Text
|
||||
as="h2"
|
||||
$margin={{ all: 'none', left: 'none' }}
|
||||
$size={isMobile ? 'h4' : 'h2'}
|
||||
>
|
||||
<Text as="h2" $margin="none" $size={isMobile ? 'h4' : 'h2'}>
|
||||
{currentDoc?.title || untitledDocument}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -110,7 +106,7 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
|
||||
};
|
||||
|
||||
const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
const { isLargeScreen } = useResponsiveStore();
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
const { t } = useTranslation();
|
||||
const { isTopRoot } = useDocUtils(doc);
|
||||
const { untitledDocument } = useTrans();
|
||||
@@ -200,7 +196,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
$minHeight="40px"
|
||||
>
|
||||
{!isTopRoot && <DocTitleEmojiPicker doc={doc} />}
|
||||
|
||||
<Tooltip content={t('Rename')} aria-hidden={true} placement="top">
|
||||
<Box
|
||||
as="span"
|
||||
@@ -217,7 +212,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
}
|
||||
onPasteCapture={handlePaste}
|
||||
onDropCapture={handleDrop}
|
||||
$padding={{ right: 'big' }}
|
||||
$css={css`
|
||||
&[contenteditable='true']:empty:not(:focus):before {
|
||||
content: '${untitledDocument}';
|
||||
@@ -227,11 +221,12 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
pointer-events: none;
|
||||
font-style: italic;
|
||||
}
|
||||
font-size: ${isLargeScreen
|
||||
? 'var(--c--globals--font--sizes--h2)'
|
||||
: 'var(--c--globals--font--sizes--sm)'};
|
||||
font-size: ${isSmallMobile
|
||||
? 'var(--c--globals--font--sizes--h4)'
|
||||
: 'var(--c--globals--font--sizes--h2)'};
|
||||
font-weight: 700;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
{titleDisplay}
|
||||
|
||||
@@ -1,42 +1,36 @@
|
||||
import { Button, useModal } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuOption,
|
||||
useTreeContext,
|
||||
} from '@gouvfr-lasuite/ui-kit';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import AddLinkSVG from '@/assets/icons/ui-kit/add_link.svg';
|
||||
import ContentCopySVG from '@/assets/icons/ui-kit/content_copy.svg';
|
||||
import DeleteSVG from '@/assets/icons/ui-kit/delete.svg';
|
||||
import DownloadSVG from '@/assets/icons/ui-kit/download.svg';
|
||||
import RemoveEmojiSVG from '@/assets/icons/ui-kit/face-remove.svg';
|
||||
import AddEmojiSVG from '@/assets/icons/ui-kit/face.svg';
|
||||
import SharedSVG from '@/assets/icons/ui-kit/group.svg';
|
||||
import HistorySVG from '@/assets/icons/ui-kit/history.svg';
|
||||
import KeepSVG from '@/assets/icons/ui-kit/keep.svg';
|
||||
import KeepOffSVG from '@/assets/icons/ui-kit/keep_off.svg';
|
||||
import MarkdownCopySVG from '@/assets/icons/ui-kit/markdown_copy.svg';
|
||||
import {
|
||||
Box,
|
||||
DropdownMenu,
|
||||
DropdownMenuOption,
|
||||
Icon,
|
||||
IconOptions,
|
||||
} from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import MoreSVG from '@/assets/icons/ui-kit/more_horiz.svg';
|
||||
import {
|
||||
Doc,
|
||||
KEY_DOC,
|
||||
KEY_LIST_DOC,
|
||||
KEY_LIST_FAVORITE_DOC,
|
||||
getEmojiAndTitle,
|
||||
useCopyDocLink,
|
||||
useCreateFavoriteDoc,
|
||||
useDeleteFavoriteDoc,
|
||||
useDocTitleUpdate,
|
||||
useDocUtils,
|
||||
useDuplicateDoc,
|
||||
} from '@/docs/doc-management';
|
||||
import { useAuth } from '@/features/auth';
|
||||
import { useFocusStore, useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard';
|
||||
@@ -57,6 +51,14 @@ const ModalSelectVersion = dynamic(
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const DocShareModal = dynamic(
|
||||
() =>
|
||||
import('@/docs/doc-share/components/DocShareModal').then((mod) => ({
|
||||
default: mod.DocShareModal,
|
||||
})),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const ModalExport =
|
||||
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false'
|
||||
? dynamic(
|
||||
@@ -76,16 +78,18 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const { t } = useTranslation();
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
const router = useRouter();
|
||||
const { isChild, isTopRoot } = useDocUtils(doc);
|
||||
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const { isTopRoot } = useDocUtils(doc);
|
||||
const { authenticated } = useAuth();
|
||||
const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard();
|
||||
|
||||
const [openDropdown, setOpenDropdown] = useState(false);
|
||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
||||
const [isModalExportOpen, setIsModalExportOpen] = useState(false);
|
||||
const shareModal = useModal();
|
||||
const selectHistoryModal = useModal();
|
||||
|
||||
const { addLastFocus, restoreFocus } = useFocusStore();
|
||||
const { isSmallMobile, isMobile } = useResponsiveStore();
|
||||
const { restoreFocus } = useFocusStore();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const copyDocLink = useCopyDocLink(doc.id);
|
||||
const { mutate: duplicateDoc } = useDuplicateDoc({
|
||||
onSuccess: (data) => {
|
||||
@@ -99,19 +103,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC, KEY_LIST_FAVORITE_DOC],
|
||||
});
|
||||
|
||||
// Emoji Management
|
||||
const { emoji } = getEmojiAndTitle(doc.title ?? '');
|
||||
const { updateDocEmoji } = useDocTitleUpdate();
|
||||
|
||||
const options: DropdownMenuOption[] = [
|
||||
{
|
||||
label: t('Export'),
|
||||
icon: <DownloadSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
setIsModalExportOpen(true);
|
||||
},
|
||||
show: !!ModalExport && isSmallMobile,
|
||||
},
|
||||
{
|
||||
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||
icon: doc.is_favorite ? (
|
||||
@@ -126,55 +118,53 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
makeFavoriteDoc.mutate({ id: doc.id });
|
||||
}
|
||||
},
|
||||
isHidden: !doc.abilities.favorite,
|
||||
testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
|
||||
},
|
||||
{
|
||||
label: t('Version history'),
|
||||
icon: <HistorySVG width={24} height={24} aria-hidden="true" />,
|
||||
disabled: !doc.abilities.versions_list,
|
||||
callback: () => {
|
||||
selectHistoryModal.open();
|
||||
},
|
||||
show: !isMobile,
|
||||
showSeparator: isTopRoot ? true : false,
|
||||
},
|
||||
{
|
||||
label: t('Remove emoji'),
|
||||
icon: <RemoveEmojiSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
updateDocEmoji(doc.id, doc.title ?? '', '');
|
||||
},
|
||||
showSeparator: true,
|
||||
show: !!emoji && doc.abilities.partial_update && !isTopRoot,
|
||||
},
|
||||
{
|
||||
label: t('Add emoji'),
|
||||
icon: <AddEmojiSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
const today = new Date();
|
||||
const isAprilFools = today.getMonth() === 3 && today.getDate() === 1;
|
||||
updateDocEmoji(doc.id, doc.title ?? '', isAprilFools ? '🐟' : '📄');
|
||||
},
|
||||
showSeparator: true,
|
||||
show: !emoji && doc.abilities.partial_update && !isTopRoot,
|
||||
},
|
||||
{
|
||||
label: t('Copy link'),
|
||||
icon: <AddLinkSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: copyDocLink,
|
||||
},
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: <SharedSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
shareModal.open();
|
||||
},
|
||||
isHidden: !isTopRoot || !authenticated,
|
||||
},
|
||||
{
|
||||
label: t('Download'),
|
||||
icon: <DownloadSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
setIsModalExportOpen(true);
|
||||
},
|
||||
isHidden: !ModalExport,
|
||||
},
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'Markdown' }),
|
||||
icon: <MarkdownCopySVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
void copyCurrentEditorToClipboard('markdown');
|
||||
},
|
||||
showSeparator: isMobile || !doc.abilities.versions_list,
|
||||
},
|
||||
{
|
||||
label: t('Version history'),
|
||||
icon: <HistorySVG width={24} height={24} aria-hidden="true" />,
|
||||
isDisabled: !doc.abilities.versions_list,
|
||||
callback: () => {
|
||||
selectHistoryModal.open();
|
||||
},
|
||||
isHidden: isMobile || !doc.abilities.versions_list,
|
||||
showSeparator: true,
|
||||
},
|
||||
{
|
||||
label: t('Duplicate'),
|
||||
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
|
||||
disabled: !doc.abilities.duplicate,
|
||||
isDisabled: !doc.abilities.duplicate,
|
||||
callback: () => {
|
||||
duplicateDoc({
|
||||
docId: doc.id,
|
||||
@@ -182,65 +172,40 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
canSave: doc.abilities.partial_update,
|
||||
});
|
||||
},
|
||||
isHidden: !doc.abilities.duplicate,
|
||||
showSeparator: true,
|
||||
},
|
||||
{
|
||||
label: isChild ? t('Delete sub-document') : t('Delete document'),
|
||||
label: t('Delete'),
|
||||
icon: <DeleteSVG width={24} height={24} aria-hidden="true" />,
|
||||
disabled: !doc.abilities.destroy,
|
||||
callback: () => {
|
||||
setIsModalRemoveOpen(true);
|
||||
},
|
||||
isHidden: !doc.abilities.destroy,
|
||||
},
|
||||
];
|
||||
|
||||
const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard();
|
||||
|
||||
return (
|
||||
<Box
|
||||
$margin={{ left: 'auto' }}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="0.5rem 1.5rem"
|
||||
$wrap={isSmallMobile ? 'wrap' : 'nowrap'}
|
||||
className="--docs--doc-toolbox"
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$margin={{ left: 'auto' }}
|
||||
$gap={spacingsTokens['2xs']}
|
||||
<>
|
||||
<DropdownMenu
|
||||
options={options}
|
||||
isOpen={openDropdown}
|
||||
shouldCloseOnInteractOutside={() => true}
|
||||
onOpenChange={setOpenDropdown}
|
||||
>
|
||||
{!isSmallMobile && ModalExport && (
|
||||
<Button
|
||||
data-testid="doc-open-modal-download-button"
|
||||
variant="tertiary"
|
||||
icon={
|
||||
<Icon iconName="download" $color="inherit" aria-hidden={true} />
|
||||
}
|
||||
onClick={(e) => {
|
||||
addLastFocus(e.currentTarget);
|
||||
setIsModalExportOpen(true);
|
||||
}}
|
||||
size={isSmallMobile ? 'small' : 'medium'}
|
||||
aria-label={t('Export the document')}
|
||||
/>
|
||||
)}
|
||||
<DropdownMenu
|
||||
options={options}
|
||||
label={t('Open the document options')}
|
||||
buttonCss={css`
|
||||
padding: ${spacingsTokens['xs']};
|
||||
${isSmallMobile
|
||||
? css`
|
||||
border: 1px solid ${colorsTokens['gray-300']};
|
||||
`
|
||||
: ''}
|
||||
`}
|
||||
>
|
||||
<IconOptions aria-hidden="true" isHorizontal $color="inherit" />
|
||||
</DropdownMenu>
|
||||
</Box>
|
||||
<Button
|
||||
aria-label={t('Open the document options')}
|
||||
size="small"
|
||||
icon={<MoreSVG width={24} height={24} aria-hidden="true" />}
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setOpenDropdown((o) => !o);
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
||||
{isModalExportOpen && ModalExport && (
|
||||
<ModalExport
|
||||
@@ -285,6 +250,16 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
doc={doc}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{shareModal.isOpen && (
|
||||
<DocShareModal
|
||||
onClose={() => {
|
||||
shareModal.close();
|
||||
restoreFocus();
|
||||
}}
|
||||
doc={doc}
|
||||
isRootDoc={treeContext?.root?.id === doc.id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { Box, Card } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham/useCunninghamTheme';
|
||||
import { useDocStore } from '@/docs/doc-management/stores/useDocStore';
|
||||
import { DocShareButton } from '@/features/docs/doc-share/components/DocShareButton';
|
||||
@@ -9,6 +9,8 @@ import { LeftPanelCollapseButton } from '@/features/left-panel/components/LeftPa
|
||||
import { RightPanelCollapseButton } from '@/features/right-panel/components/RightPanelCollapseButton';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocToolBox } from './DocToolBox';
|
||||
|
||||
/**
|
||||
* Sticky bar trick (desktop):
|
||||
* - MainContent has padding `base`; we extend the bar width and apply
|
||||
@@ -23,7 +25,6 @@ export const FloatingBar = () => {
|
||||
const isDeletedDoc = !!currentDoc?.deleted_at;
|
||||
|
||||
const FLOATING_STYLES = useMemo(() => {
|
||||
const base = spacingsTokens['base'];
|
||||
const sm = spacingsTokens['sm'];
|
||||
return css`
|
||||
position: sticky;
|
||||
@@ -33,7 +34,6 @@ export const FloatingBar = () => {
|
||||
width: 100%;
|
||||
min-height: 64px;
|
||||
padding: ${sm};
|
||||
margin-top: calc(-${base});
|
||||
z-index: 21; // Under editor select box but above other elements (e.g., doc title, suggestion menu)
|
||||
align-items: flex-start;
|
||||
isolation: isolate;
|
||||
@@ -76,7 +76,20 @@ export const FloatingBar = () => {
|
||||
{isLargeScreen ? <LeftPanelCollapseButton /> : <Box />}
|
||||
<Box $direction="row" $align="center" $gap="2xs">
|
||||
{!isDeletedDoc && currentDoc && <DocShareButton doc={currentDoc} />}
|
||||
<RightPanelCollapseButton />
|
||||
<Card
|
||||
className="--docs--right-panel-collapse-button"
|
||||
$direction="row"
|
||||
$css={css`
|
||||
padding: var(--c--globals--spacings--xxxs);
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--xxxs);
|
||||
border-radius: var(--c--globals--spacings--xs);
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
`}
|
||||
>
|
||||
<RightPanelCollapseButton />
|
||||
{!isDeletedDoc && currentDoc && <DocToolBox doc={currentDoc} />}
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
+2
-14
@@ -1,7 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Card } from '@/components';
|
||||
import { CommentSideBarButton } from '@/features/docs/doc-editor/components/comments/CommentSideBar';
|
||||
import { useEditorStore } from '@/features/docs/doc-editor/stores/useEditorStore';
|
||||
import { useHeadingStore } from '@/features/docs/doc-editor/stores/useHeadingStore';
|
||||
@@ -30,19 +28,9 @@ export const RightPanelCollapseButton = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="--docs--right-panel-collapse-button"
|
||||
$direction="row"
|
||||
$css={css`
|
||||
padding: var(--c--globals--spacings--xxxs);
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--xxxs);
|
||||
border-radius: var(--c--globals--spacings--xs);
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
`}
|
||||
>
|
||||
<>
|
||||
{hasHeadings && <TableContentSideBarButton />}
|
||||
{hasThreads && <CommentSideBarButton />}
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { css, keyframes } from 'styled-components';
|
||||
|
||||
import { Box, BoxType } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { SkeletonCircle, SkeletonLine } from './SkeletionUI';
|
||||
import { SkeletonLine } from './SkeletionUI';
|
||||
|
||||
export const DocEditorSkeleton = () => {
|
||||
return (
|
||||
@@ -16,6 +15,7 @@ export const DocEditorSkeleton = () => {
|
||||
$height="100%"
|
||||
className="--docs--doc-editor-skeleton"
|
||||
>
|
||||
<SkeletonEditorFloatingBar />
|
||||
<SkeletonEditorHeader />
|
||||
<SkeletonEditorCore />
|
||||
</Box>
|
||||
@@ -23,58 +23,63 @@ export const DocEditorSkeleton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const SkeletonEditorFloatingBar = () => {
|
||||
return (
|
||||
<Box
|
||||
className="--docs--doc-editor-floating-bar-skeleton"
|
||||
$width="100%"
|
||||
$padding={{ top: 'sm', horizontal: 'sm' }}
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
>
|
||||
<SkeletonLine $width="42px" $height="42px" />
|
||||
<Box $direction="row" $gap="2xs" $align="center">
|
||||
<SkeletonLine $width="75px" $height="42px" />
|
||||
<SkeletonLine $width="42px" $height="42px" />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const SkeletonEditorHeader = () => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
$padding={{ horizontal: isDesktop ? '54px' : 'base' }}
|
||||
$padding={{ horizontal: isDesktop ? '54px' : 'base', top: '55px' }}
|
||||
className="--docs--doc-editor-header-skeleton"
|
||||
$width="100%"
|
||||
$gap="base"
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$width="100%"
|
||||
$padding={{ top: isDesktop ? '65px' : 'md' }}
|
||||
$gap={spacingsTokens['base']}
|
||||
$padding={{ bottom: 'xs' }}
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$css="flex:1;"
|
||||
$gap="0.5rem 1rem"
|
||||
$align="center"
|
||||
$width="100%"
|
||||
$padding={{ bottom: 'xs' }}
|
||||
$maxWidth="100%"
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$css="flex:1;"
|
||||
$gap="0.5rem 1rem"
|
||||
$align="center"
|
||||
$maxWidth="100%"
|
||||
>
|
||||
{/* Title and metadata skeleton */}
|
||||
<Box $gap="0.25rem" $css="flex:1;">
|
||||
{/* Title - "Untitled Document" style */}
|
||||
<SkeletonLine $width="35%" $height="40px" />
|
||||
{/* Title and metadata skeleton */}
|
||||
<Box $gap="base" $css="flex:1;">
|
||||
{/* Title - "Untitled Document" style */}
|
||||
<SkeletonLine $width="255px" $height="40px" />
|
||||
|
||||
{/* Metadata (role and last update) */}
|
||||
<Box $direction="row" $gap="0.5rem" $align="center">
|
||||
<SkeletonLine $maxWidth="260px" $height="12px" />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Toolbox skeleton (buttons) */}
|
||||
<Box $direction="row" $gap={spacingsTokens['t']} $align="center">
|
||||
{/* Download icon */}
|
||||
<SkeletonCircle $width="40px" $height="40px" />
|
||||
{/* Menu icon */}
|
||||
<SkeletonCircle $width="40px" $height="40px" />
|
||||
{/* Metadata (role and last update) */}
|
||||
<Box $direction="row" $gap="0.5rem" $align="center">
|
||||
<SkeletonLine $maxWidth="210px" $height="12px" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Separator */}
|
||||
<SkeletonLine $height="1px" />
|
||||
</Box>
|
||||
|
||||
{/* Separator */}
|
||||
<SkeletonLine $height="1px" />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
const SvgMock = () => null;
|
||||
export default SvgMock;
|
||||
@@ -1,4 +1,6 @@
|
||||
/// <reference types="vitest" />
|
||||
import path from 'path';
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
@@ -16,6 +18,12 @@ export default defineConfig({
|
||||
'process.env.NODE_ENV': 'test',
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^.*\.svg$/,
|
||||
replacement: path.resolve(__dirname, 'src/tests/__mocks__/svgMock.tsx'),
|
||||
},
|
||||
],
|
||||
tsconfigPaths: true,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user