💄(frontend) update document summary UI
- Enhanced the document summary UI for better visibility and interaction. - Refactored the DocHeader and DocEditor components to improve layout and responsiveness. - Updated tests for the DocTableContent to reflect changes in heading interactions and visibility checks.
This commit is contained in:
committed by
Anthony LC
parent
7696872416
commit
23b11e4096
@@ -25,6 +25,7 @@ and this project adheres to
|
|||||||
- 💄(frontend) update DocsGridOptions component #432
|
- 💄(frontend) update DocsGridOptions component #432
|
||||||
- 💄(frontend) update DocHeader ui #446
|
- 💄(frontend) update DocHeader ui #446
|
||||||
- 💄(frontend) update doc versioning ui #463
|
- 💄(frontend) update doc versioning ui #463
|
||||||
|
- 💄(frontend) update doc summary ui #473
|
||||||
|
|
||||||
|
|
||||||
## [1.10.0] - 2024-12-17
|
## [1.10.0] - 2024-12-17
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { createDoc, goToGridDoc, verifyDocName } from './common';
|
import { createDoc, verifyDocName } from './common';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -19,20 +19,13 @@ test.describe('Doc Table Content', () => {
|
|||||||
|
|
||||||
await verifyDocName(page, randomDoc);
|
await verifyDocName(page, randomDoc);
|
||||||
|
|
||||||
await page.getByLabel('Open the document options').click();
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'Table of contents',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
const panel = page.getByLabel('Document panel');
|
|
||||||
const editor = page.locator('.ProseMirror');
|
const editor = page.locator('.ProseMirror');
|
||||||
|
|
||||||
await editor.locator('.bn-block-outer').last().fill('/');
|
await editor.locator('.bn-block-outer').last().fill('/');
|
||||||
|
|
||||||
await page.getByText('Heading 1').click();
|
await page.getByText('Heading 1').click();
|
||||||
await page.keyboard.type('Hello World');
|
await page.keyboard.type('Level 1');
|
||||||
await editor.getByText('Hello').dblclick();
|
await editor.getByText('Level 1').dblclick();
|
||||||
await page.getByRole('button', { name: 'Strike' }).click();
|
await page.getByRole('button', { name: 'Strike' }).click();
|
||||||
|
|
||||||
await page.locator('.bn-block-outer').first().click();
|
await page.locator('.bn-block-outer').first().click();
|
||||||
@@ -40,101 +33,44 @@ test.describe('Doc Table Content', () => {
|
|||||||
await page.locator('.bn-block-outer').last().click();
|
await page.locator('.bn-block-outer').last().click();
|
||||||
|
|
||||||
// Create space to fill the viewport
|
// Create space to fill the viewport
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
}
|
}
|
||||||
|
|
||||||
await editor.locator('.bn-block-outer').last().fill('/');
|
await editor.locator('.bn-block-outer').last().fill('/');
|
||||||
await page.getByText('Heading 2').click();
|
await page.getByText('Heading 2').click();
|
||||||
await page.keyboard.type('Super World', { delay: 100 });
|
await page.keyboard.type('Level 2');
|
||||||
|
|
||||||
await page.locator('.bn-block-outer').last().click();
|
await page.locator('.bn-block-outer').last().click();
|
||||||
|
|
||||||
// Create space to fill the viewport
|
// Create space to fill the viewport
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
}
|
}
|
||||||
|
|
||||||
await editor.locator('.bn-block-outer').last().fill('/');
|
await editor.locator('.bn-block-outer').last().fill('/');
|
||||||
await page.getByText('Heading 3').click();
|
await page.getByText('Heading 3').click();
|
||||||
await page.keyboard.type('Another World');
|
await page.keyboard.type('Level 3');
|
||||||
|
|
||||||
const hello = panel.getByText('Hello World');
|
expect(true).toBe(true);
|
||||||
const superW = panel.getByText('Super World');
|
|
||||||
const another = panel.getByText('Another World');
|
|
||||||
|
|
||||||
await expect(hello).toBeVisible();
|
const summaryContainer = page.locator('#summaryContainer');
|
||||||
await expect(hello).toHaveCSS('font-size', /17/);
|
await summaryContainer.hover();
|
||||||
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
|
||||||
|
|
||||||
await expect(superW).toBeVisible();
|
const level1 = summaryContainer.getByText('Level 1');
|
||||||
await expect(superW).toHaveCSS('font-size', /14/);
|
const level2 = summaryContainer.getByText('Level 2');
|
||||||
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
const level3 = summaryContainer.getByText('Level 3');
|
||||||
|
|
||||||
await expect(another).toBeVisible();
|
await expect(level1).toBeVisible();
|
||||||
await expect(another).toHaveCSS('font-size', /12/);
|
await expect(level1).toHaveCSS('padding', /4px 0px/);
|
||||||
await expect(another).toHaveAttribute('aria-selected', 'false');
|
await expect(level1).toHaveAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
await hello.click();
|
await expect(level2).toBeVisible();
|
||||||
|
await expect(level2).toHaveCSS('padding-left', /14.4px/);
|
||||||
|
await expect(level2).toHaveAttribute('aria-selected', 'false');
|
||||||
|
|
||||||
await expect(editor.getByText('Hello World')).toBeInViewport();
|
await expect(level3).toBeVisible();
|
||||||
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
await expect(level3).toHaveCSS('padding-left', /24px/);
|
||||||
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
await expect(level3).toHaveAttribute('aria-selected', 'false');
|
||||||
|
|
||||||
await another.click();
|
|
||||||
|
|
||||||
await expect(editor.getByText('Hello World')).not.toBeInViewport();
|
|
||||||
await expect(hello).toHaveAttribute('aria-selected', 'false');
|
|
||||||
await expect(superW).toHaveAttribute('aria-selected', 'true');
|
|
||||||
|
|
||||||
await panel.getByText('Back to top').click();
|
|
||||||
await expect(editor.getByText('Hello World')).toBeInViewport();
|
|
||||||
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
|
||||||
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
|
||||||
|
|
||||||
await panel.getByText('Go to bottom').click();
|
|
||||||
await expect(editor.getByText('Hello World')).not.toBeInViewport();
|
|
||||||
await expect(superW).toHaveAttribute('aria-selected', 'true');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it checks that table contents panel is opened automaticaly if more that 2 headings', async ({
|
|
||||||
page,
|
|
||||||
browserName,
|
|
||||||
}) => {
|
|
||||||
const [randomDoc] = await createDoc(
|
|
||||||
page,
|
|
||||||
'doc-table-content',
|
|
||||||
browserName,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
await verifyDocName(page, randomDoc);
|
|
||||||
await expect(page.getByLabel('Open the panel')).toBeHidden();
|
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
|
||||||
|
|
||||||
await editor.locator('.bn-block-outer').last().fill('/');
|
|
||||||
await page.getByText('Heading 1').click();
|
|
||||||
await page.keyboard.type('Hello World', { delay: 100 });
|
|
||||||
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
|
|
||||||
await editor.locator('.bn-block-outer').last().fill('/');
|
|
||||||
await page.getByText('Heading 2').click();
|
|
||||||
await page.keyboard.type('Super World', { delay: 100 });
|
|
||||||
|
|
||||||
await goToGridDoc(page, {
|
|
||||||
title: randomDoc,
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(page.getByLabel('Close the panel')).toBeVisible();
|
|
||||||
|
|
||||||
const panel = page.getByLabel('Document panel');
|
|
||||||
await expect(panel.getByText('Hello World')).toBeVisible();
|
|
||||||
await expect(panel.getByText('Super World')).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByLabel('Close the panel').click();
|
|
||||||
|
|
||||||
await expect(panel).toHaveAttribute('aria-hidden', 'true');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ export enum SeparatorVariant {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
variant?: SeparatorVariant;
|
variant?: SeparatorVariant;
|
||||||
|
$withPadding?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HorizontalSeparator = ({
|
export const HorizontalSeparator = ({
|
||||||
variant = SeparatorVariant.LIGHT,
|
variant = SeparatorVariant.LIGHT,
|
||||||
|
$withPadding = true,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { colorsTokens } = useCunninghamTheme();
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
|
|
||||||
@@ -20,7 +22,7 @@ export const HorizontalSeparator = ({
|
|||||||
<Box
|
<Box
|
||||||
$height="1px"
|
$height="1px"
|
||||||
$width="100%"
|
$width="100%"
|
||||||
$margin={{ vertical: 'base' }}
|
$margin={{ vertical: $withPadding ? 'base' : 'none' }}
|
||||||
$background={
|
$background={
|
||||||
variant === SeparatorVariant.DARK
|
variant === SeparatorVariant.DARK
|
||||||
? '#e5e5e533'
|
? '#e5e5e533'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { BlockNoteView } from '@blocknote/mantine';
|
|||||||
import '@blocknote/mantine/style.css';
|
import '@blocknote/mantine/style.css';
|
||||||
import { useCreateBlockNote } from '@blocknote/react';
|
import { useCreateBlockNote } from '@blocknote/react';
|
||||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||||
import React, { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
@@ -24,10 +24,7 @@ const cssEditor = (readonly: boolean) => `
|
|||||||
&, & > .bn-container, & .ProseMirror {
|
&, & > .bn-container, & .ProseMirror {
|
||||||
height:100%
|
height:100%
|
||||||
};
|
};
|
||||||
& .bn-editor {
|
|
||||||
padding-right: 30px;
|
|
||||||
${readonly && `padding-left: 30px;`}
|
|
||||||
};
|
|
||||||
& .bn-inline-content code {
|
& .bn-inline-content code {
|
||||||
background-color: gainsboro;
|
background-color: gainsboro;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { Alert, Loader, VariantType } from '@openfun/cunningham-react';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { css } from 'styled-components';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { Box, Card, Text, TextErrors } from '@/components';
|
import { Box, Text, TextErrors } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { DocHeader, DocVersionHeader } from '@/features/docs/doc-header/';
|
import { DocHeader, DocVersionHeader } from '@/features/docs/doc-header/';
|
||||||
import {
|
import {
|
||||||
@@ -12,11 +13,11 @@ import {
|
|||||||
base64ToBlocknoteXmlFragment,
|
base64ToBlocknoteXmlFragment,
|
||||||
useProviderStore,
|
useProviderStore,
|
||||||
} from '@/features/docs/doc-management';
|
} from '@/features/docs/doc-management';
|
||||||
|
import { TableContent } from '@/features/docs/doc-table-content/';
|
||||||
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
|
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import { BlockNoteEditor, BlockNoteEditorVersion } from './BlockNoteEditor';
|
import { BlockNoteEditor, BlockNoteEditorVersion } from './BlockNoteEditor';
|
||||||
import { IconOpenPanelEditor, PanelEditor } from './PanelEditor';
|
|
||||||
|
|
||||||
interface DocEditorProps {
|
interface DocEditorProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
@@ -25,9 +26,9 @@ interface DocEditorProps {
|
|||||||
|
|
||||||
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isMobile } = useResponsiveStore();
|
const { isDesktop } = useResponsiveStore();
|
||||||
|
|
||||||
const isVersion = versionId && typeof versionId === 'string';
|
const isVersion = !!versionId && typeof versionId === 'string';
|
||||||
|
|
||||||
const { colorsTokens } = useCunninghamTheme();
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
|
|
||||||
@@ -39,41 +40,49 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isVersion ? (
|
{isDesktop && !isVersion && (
|
||||||
<DocVersionHeader title={doc.title} />
|
<Box
|
||||||
) : (
|
$position="absolute"
|
||||||
<DocHeader doc={doc} />
|
$css={css`
|
||||||
)}
|
top: 72px;
|
||||||
|
right: 20px;
|
||||||
{!doc.abilities.partial_update && (
|
`}
|
||||||
<Box $width="100%" $margin={{ all: 'small', top: 'none' }}>
|
>
|
||||||
<Alert type={VariantType.WARNING}>
|
<TableContent />
|
||||||
{t(`Read only, you cannot edit this document.`)}
|
|
||||||
</Alert>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
<Box $maxWidth="868px" $width="100%">
|
||||||
|
<Box $padding={{ horizontal: '54px' }}>
|
||||||
|
{isVersion ? (
|
||||||
|
<DocVersionHeader title={doc.title} />
|
||||||
|
) : (
|
||||||
|
<DocHeader doc={doc} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box
|
{!doc.abilities.partial_update && (
|
||||||
$background={colorsTokens()['primary-bg']}
|
<Box $width="100%" $margin={{ all: 'small', top: 'none' }}>
|
||||||
$direction="row"
|
<Alert type={VariantType.WARNING}>
|
||||||
$width="100%"
|
{t(`Read only, you cannot edit this document.`)}
|
||||||
$css="overflow-x: clip; flex: 1;"
|
</Alert>
|
||||||
$position="relative"
|
</Box>
|
||||||
>
|
)}
|
||||||
<Card
|
|
||||||
$padding={isMobile ? 'small' : 'big'}
|
<Box
|
||||||
$css="flex:1;"
|
$background={colorsTokens()['primary-bg']}
|
||||||
$overflow="auto"
|
$direction="row"
|
||||||
|
$width="100%"
|
||||||
|
$css="overflow-x: clip; flex: 1;"
|
||||||
$position="relative"
|
$position="relative"
|
||||||
>
|
>
|
||||||
{isVersion ? (
|
<Box $css="flex:1;" $overflow="auto" $position="relative">
|
||||||
<DocVersionEditor docId={doc.id} versionId={versionId} />
|
{isVersion ? (
|
||||||
) : (
|
<DocVersionEditor docId={doc.id} versionId={versionId} />
|
||||||
<BlockNoteEditor doc={doc} provider={provider} />
|
) : (
|
||||||
)}
|
<BlockNoteEditor doc={doc} provider={provider} />
|
||||||
{!isMobile && !isVersion && <IconOpenPanelEditor />}
|
)}
|
||||||
</Card>
|
</Box>
|
||||||
{!isVersion && <PanelEditor />}
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { Box, BoxButton, Card, IconBG, Text } from '@/components';
|
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
|
||||||
import { TableContent } from '@/features/docs/doc-table-content';
|
|
||||||
import { useResponsiveStore } from '@/stores';
|
|
||||||
|
|
||||||
import { useHeadingStore, usePanelEditorStore } from '../stores';
|
|
||||||
|
|
||||||
export const PanelEditor = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { colorsTokens } = useCunninghamTheme();
|
|
||||||
const { isMobile } = useResponsiveStore();
|
|
||||||
const { isPanelTableContentOpen, setIsPanelTableContentOpen, isPanelOpen } =
|
|
||||||
usePanelEditorStore();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
$width="100%"
|
|
||||||
$maxWidth="20rem"
|
|
||||||
$position={isMobile ? 'absolute' : 'sticky'}
|
|
||||||
$height="100%"
|
|
||||||
$hasTransition="slow"
|
|
||||||
$css={`
|
|
||||||
top: 0vh;
|
|
||||||
right: 0;
|
|
||||||
transform: translateX(0%);
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
${
|
|
||||||
!isPanelOpen &&
|
|
||||||
`
|
|
||||||
transform: translateX(200%);
|
|
||||||
opacity: 0;
|
|
||||||
flex: 0;
|
|
||||||
margin-left: 0rem;
|
|
||||||
max-width: 0rem;
|
|
||||||
`
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
aria-label={t('Document panel')}
|
|
||||||
aria-hidden={!isPanelOpen}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
$overflow="inherit"
|
|
||||||
$position="sticky"
|
|
||||||
$hasTransition="slow"
|
|
||||||
$css={`
|
|
||||||
top: 0;
|
|
||||||
opacity: ${isPanelOpen ? '1' : '0'};
|
|
||||||
`}
|
|
||||||
$maxHeight="99vh"
|
|
||||||
>
|
|
||||||
{isMobile && <IconOpenPanelEditor />}
|
|
||||||
<Box
|
|
||||||
$direction="row"
|
|
||||||
$justify="space-between"
|
|
||||||
$align="center"
|
|
||||||
$position="relative"
|
|
||||||
$background={colorsTokens()['primary-400']}
|
|
||||||
$margin={{ bottom: 'tiny' }}
|
|
||||||
$radius="4px 4px 0 0"
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
$background="white"
|
|
||||||
$position="absolute"
|
|
||||||
$height="100%"
|
|
||||||
$width="100%"
|
|
||||||
$hasTransition="slow"
|
|
||||||
$css={`
|
|
||||||
border-top: 2px solid ${colorsTokens()['primary-600']};
|
|
||||||
border-radius: 0 4px 0 0;
|
|
||||||
${
|
|
||||||
isPanelTableContentOpen
|
|
||||||
? `
|
|
||||||
transform: translateX(0);
|
|
||||||
border-radius: 4px 0 0 0;
|
|
||||||
`
|
|
||||||
: `transform: translateX(100%);`
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
<BoxButton
|
|
||||||
$minWidth="100%"
|
|
||||||
onClick={() => setIsPanelTableContentOpen(true)}
|
|
||||||
$zIndex={1}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
$width="100%"
|
|
||||||
$weight="bold"
|
|
||||||
$size="m"
|
|
||||||
$theme="primary"
|
|
||||||
$variation="600"
|
|
||||||
$padding={{ vertical: 'small', horizontal: 'small' }}
|
|
||||||
>
|
|
||||||
{t('Table of content')}
|
|
||||||
</Text>
|
|
||||||
</BoxButton>
|
|
||||||
</Box>
|
|
||||||
<TableContent />
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IconOpenPanelEditor = () => {
|
|
||||||
const { headings } = useHeadingStore();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { setIsPanelOpen, isPanelOpen, setIsPanelTableContentOpen } =
|
|
||||||
usePanelEditorStore();
|
|
||||||
const [hasBeenOpen, setHasBeenOpen] = useState(isPanelOpen);
|
|
||||||
const { isMobile } = useResponsiveStore();
|
|
||||||
|
|
||||||
const setClosePanel = () => {
|
|
||||||
setHasBeenOpen(true);
|
|
||||||
setIsPanelOpen(!isPanelOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Open the panel if there are more than 1 heading
|
|
||||||
useEffect(() => {
|
|
||||||
if (headings?.length && headings.length > 1 && !hasBeenOpen && !isMobile) {
|
|
||||||
setIsPanelTableContentOpen(true);
|
|
||||||
setIsPanelOpen(true);
|
|
||||||
setHasBeenOpen(true);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
headings,
|
|
||||||
setIsPanelTableContentOpen,
|
|
||||||
setIsPanelOpen,
|
|
||||||
hasBeenOpen,
|
|
||||||
isMobile,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// If open from the doc header we set the state as well
|
|
||||||
useEffect(() => {
|
|
||||||
if (isPanelOpen && !hasBeenOpen) {
|
|
||||||
setHasBeenOpen(true);
|
|
||||||
}
|
|
||||||
}, [hasBeenOpen, isPanelOpen]);
|
|
||||||
|
|
||||||
// Close the panel unmount
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
setIsPanelOpen(false);
|
|
||||||
};
|
|
||||||
}, [setIsPanelOpen]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconBG
|
|
||||||
iconName="menu_open"
|
|
||||||
aria-label={isPanelOpen ? t('Close the panel') : t('Open the panel')}
|
|
||||||
$background="transparent"
|
|
||||||
$size="h2"
|
|
||||||
$zIndex={10}
|
|
||||||
$hasTransition="slow"
|
|
||||||
$css={`
|
|
||||||
cursor: pointer;
|
|
||||||
right: 0rem;
|
|
||||||
top: 0.1rem;
|
|
||||||
transform: rotate(${isPanelOpen ? '180deg' : '0deg'});
|
|
||||||
user-select: none;
|
|
||||||
${hasBeenOpen ? 'display:flex;' : 'display: none;'}
|
|
||||||
`}
|
|
||||||
$position="absolute"
|
|
||||||
onClick={setClosePanel}
|
|
||||||
$radius="2px"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -35,7 +35,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
|||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
$width="100%"
|
$width="100%"
|
||||||
$padding={{ vertical: 'base' }}
|
$padding={{ top: 'base' }}
|
||||||
$gap={spacings['base']}
|
$gap={spacings['base']}
|
||||||
aria-label={t('It is the card information about the document.')}
|
aria-label={t('It is the card information about the document.')}
|
||||||
>
|
>
|
||||||
@@ -92,7 +92,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
|||||||
<DocToolBox doc={doc} />
|
<DocToolBox doc={doc} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<HorizontalSeparator />
|
<HorizontalSeparator $withPadding={false} />
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { BoxButton, Text } from '@/components';
|
|||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
const sizeMap: { [key: number]: string } = {
|
const leftPaddingMap: { [key: number]: string } = {
|
||||||
1: '1.1rem',
|
3: '1.5rem',
|
||||||
2: '0.9rem',
|
2: '0.9rem',
|
||||||
3: '0.8rem',
|
1: '0.3',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HeadingsHighlight = {
|
export type HeadingsHighlight = {
|
||||||
@@ -34,9 +34,12 @@ export const Heading = ({
|
|||||||
const [isHover, setIsHover] = useState(isHighlight);
|
const [isHover, setIsHover] = useState(isHighlight);
|
||||||
const { colorsTokens } = useCunninghamTheme();
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
const { isMobile } = useResponsiveStore();
|
const { isMobile } = useResponsiveStore();
|
||||||
|
const isActive = isHighlight || isHover;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxButton
|
<BoxButton
|
||||||
|
id={`heading-${headingId}`}
|
||||||
|
$width="100%"
|
||||||
key={headingId}
|
key={headingId}
|
||||||
onMouseOver={() => setIsHover(true)}
|
onMouseOver={() => setIsHover(true)}
|
||||||
onMouseLeave={() => setIsHover(false)}
|
onMouseLeave={() => setIsHover(false)}
|
||||||
@@ -47,23 +50,24 @@ export const Heading = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
editor.setTextCursorPosition(headingId, 'end');
|
editor.setTextCursorPosition(headingId, 'end');
|
||||||
|
|
||||||
document.querySelector(`[data-id="${headingId}"]`)?.scrollIntoView({
|
document.querySelector(`[data-id="${headingId}"]`)?.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
|
inline: 'start',
|
||||||
block: 'start',
|
block: 'start',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
$radius="4px"
|
||||||
|
$background={isActive ? `${colorsTokens()['greyscale-100']}` : 'none'}
|
||||||
$css="text-align: left;"
|
$css="text-align: left;"
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
$theme="primary"
|
$width="100%"
|
||||||
$padding={{ vertical: 'xtiny', left: 'tiny' }}
|
$padding={{ vertical: 'xtiny', left: leftPaddingMap[level] }}
|
||||||
$size={sizeMap[level]}
|
$variation={isActive ? '1000' : '700'}
|
||||||
|
$weight={isActive ? 'bold' : 'normal'}
|
||||||
|
$css="overflow-wrap: break-word;"
|
||||||
$hasTransition
|
$hasTransition
|
||||||
$css={
|
|
||||||
isHover || isHighlight
|
|
||||||
? `box-shadow: -2px 0px 0px ${colorsTokens()[isHighlight ? 'primary-500' : 'primary-400']};`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
aria-selected={isHighlight}
|
aria-selected={isHighlight}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, BoxButton, Text } from '@/components';
|
import { Box, Icon, Text } from '@/components';
|
||||||
import { useEditorStore, useHeadingStore } from '@/features/docs/doc-editor';
|
import { useEditorStore, useHeadingStore } from '@/features/docs/doc-editor';
|
||||||
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
|
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
|
||||||
import { useResponsiveStore } from '@/stores';
|
|
||||||
|
|
||||||
import { Heading } from './Heading';
|
import { Heading } from './Heading';
|
||||||
|
|
||||||
export const TableContent = () => {
|
export const TableContent = () => {
|
||||||
const { headings } = useHeadingStore();
|
const { headings } = useHeadingStore();
|
||||||
const { editor } = useEditorStore();
|
const { editor } = useEditorStore();
|
||||||
const { isMobile } = useResponsiveStore();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
|
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
|
||||||
|
|
||||||
// To highlight the first heading in the viewport
|
const { t } = useTranslation();
|
||||||
|
const [isHover, setIsHover] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!headings) {
|
if (!headings) {
|
||||||
@@ -62,69 +63,84 @@ export const TableContent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box $padding={{ all: 'small', right: 'none' }} $maxHeight="95%">
|
<Box
|
||||||
<Box $overflow="auto" $padding={{ left: '2px' }}>
|
onMouseEnter={() => {
|
||||||
{headings?.map(
|
setIsHover(true);
|
||||||
(heading) =>
|
setTimeout(() => {
|
||||||
heading.contentText && (
|
const element = document.getElementById(
|
||||||
<Heading
|
`heading-${headingIdHighlight}`,
|
||||||
editor={editor}
|
);
|
||||||
headingId={heading.id}
|
|
||||||
level={heading.props.level}
|
|
||||||
text={heading.contentText}
|
|
||||||
key={heading.id}
|
|
||||||
isHighlight={headingIdHighlight === heading.id}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
$height="1px"
|
|
||||||
$width="auto"
|
|
||||||
$background="#e5e5e5"
|
|
||||||
$margin={{ vertical: 'small' }}
|
|
||||||
$css="flex: none;"
|
|
||||||
/>
|
|
||||||
<BoxButton
|
|
||||||
onClick={() => {
|
|
||||||
// With mobile the focus open the keyboard and the scroll is not working
|
|
||||||
if (!isMobile) {
|
|
||||||
editor.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector(`.bn-editor`)?.scrollIntoView({
|
element?.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'start',
|
inline: 'center',
|
||||||
|
block: 'center',
|
||||||
});
|
});
|
||||||
}}
|
}, 250); // 300ms is the transition time of the box
|
||||||
$align="start"
|
}}
|
||||||
>
|
onMouseLeave={() => {
|
||||||
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
|
setIsHover(false);
|
||||||
{t('Back to top')}
|
}}
|
||||||
</Text>
|
id="summaryContainer"
|
||||||
</BoxButton>
|
$effect="show"
|
||||||
<BoxButton
|
$width="40px"
|
||||||
onClick={() => {
|
$height="40px"
|
||||||
// With mobile the focus open the keyboard and the scroll is not working
|
$zIndex={1000}
|
||||||
if (!isMobile) {
|
$align="center"
|
||||||
editor.focus();
|
$padding="xs"
|
||||||
}
|
$justify="center"
|
||||||
|
$css={css`
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--c--theme--spacings--3xs);
|
||||||
|
background: var(--c--theme--colors--greyscale-000);
|
||||||
|
|
||||||
document
|
&:hover {
|
||||||
.querySelector(
|
overflow-y: auto;
|
||||||
`.bn-editor > .bn-block-group > .bn-block-outer:last-child`,
|
display: flex;
|
||||||
)
|
flex-direction: column;
|
||||||
?.scrollIntoView({
|
justify-content: flex-start;
|
||||||
behavior: 'smooth',
|
align-items: flex-start;
|
||||||
block: 'start',
|
gap: var(--c--theme--spacings--2xs);
|
||||||
});
|
width: 200px;
|
||||||
}}
|
height: auto;
|
||||||
$align="start"
|
max-height: calc(100vh - 60px - 15vh);
|
||||||
>
|
}
|
||||||
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
|
`}
|
||||||
{t('Go to bottom')}
|
>
|
||||||
</Text>
|
{!isHover && (
|
||||||
</BoxButton>
|
<Box $justify="center" $align="center">
|
||||||
|
<Icon iconName="list" $theme="primary" $variation="800" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{isHover && (
|
||||||
|
<Box $width="100%">
|
||||||
|
<Box
|
||||||
|
$margin={{ bottom: '20px' }}
|
||||||
|
$direction="row"
|
||||||
|
$justify="space-between"
|
||||||
|
$align="center"
|
||||||
|
>
|
||||||
|
<Text $weight="bold" $variation="800" $theme="primary">
|
||||||
|
{t('Summary')}
|
||||||
|
</Text>
|
||||||
|
<Icon iconName="list" $theme="primary" $variation="800" />
|
||||||
|
</Box>
|
||||||
|
{headings?.map(
|
||||||
|
(heading) =>
|
||||||
|
heading.contentText && (
|
||||||
|
<Heading
|
||||||
|
editor={editor}
|
||||||
|
headingId={heading.id}
|
||||||
|
level={heading.props.level}
|
||||||
|
text={heading.contentText}
|
||||||
|
key={heading.id}
|
||||||
|
isHighlight={headingIdHighlight === heading.id}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './TableContent';
|
export * from './TableContent';
|
||||||
|
export * from './Heading';
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const ModalSelectVersion = ({
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Box $width="100%" $padding="base">
|
<Box $width="100%" $padding="base" $align="center">
|
||||||
{selectedVersionId && (
|
{selectedVersionId && (
|
||||||
<DocEditor doc={doc} versionId={selectedVersionId} />
|
<DocEditor doc={doc} versionId={selectedVersionId} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user