(frontend) enhance document editor and header components

- Improved styling for headings in BlockNoteEditor for better visual
hierarchy.
- Adjusted padding in DocEditor and DocHeader based on device type for
responsive design.
- Updated DocTitle and ModalExport components to enhance typography and
spacing.
- Refactored DocToolBox to improve share button functionality and access
display.
- Enhanced versioning modal with better layout and accessibility
features.
- Cleaned up unused imports and optimized component structures for
maintainability.
This commit is contained in:
Nathan Panchout
2024-12-23 10:31:57 +01:00
committed by Anthony LC
parent 6ad1e27acf
commit fc27043e9e
11 changed files with 239 additions and 139 deletions

View File

@@ -24,6 +24,49 @@ const cssEditor = (readonly: boolean) => `
&, & > .bn-container, & .ProseMirror {
height:100%;
.bn-side-menu[data-block-type=heading][data-level="1"] {
height: 50px;
}
.bn-side-menu[data-block-type=heading][data-level="2"] {
height: 43px;
}
.bn-side-menu[data-block-type=heading][data-level="3"] {
height: 35px;
}
h1 {
font-size: 1.875rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
a {
color: var(--c--theme--colors--greyscale-500);
cursor: pointer;
}
.bn-block-group
.bn-block-group
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
}
.bn-editor {
color: var(--c--theme--colors--greyscale-700);
}
.bn-block-outer:not(:first-child) {
&:has(h1) {
padding-top: 32px;
}
&:has(h2) {
padding-top: 24px;
}
&:has(h3) {
padding-top: 16px;
}
};
& .bn-inline-content code {

View File

@@ -50,7 +50,7 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
</Box>
)}
<Box $maxWidth="868px" $width="100%" $height="100%">
<Box $padding={{ horizontal: '54px' }}>
<Box $padding={{ horizontal: isDesktop ? '54px' : 'base' }}>
{isVersion ? (
<DocVersionHeader title={doc.title} />
) : (

View File

@@ -35,7 +35,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<>
<Box
$width="100%"
$padding={{ top: 'base' }}
$padding={{ top: isDesktop ? '4xl' : 'md' }}
$gap={spacings['base']}
aria-label={t('It is the card information about the document.')}
>
@@ -72,10 +72,10 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<Box $direction="row">
{isDesktop && (
<>
<Text $variation="400" $size="s" $weight="bold">
<Text $variation="600" $size="s" $weight="bold">
{transRole(currentDocRole(doc.abilities))}&nbsp;·&nbsp;
</Text>
<Text $variation="400" $size="s">
<Text $variation="600" $size="s">
{t('Last update: {{update}}', {
update: DateTime.fromISO(doc.updated_at).toRelative(),
})}
@@ -92,7 +92,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<DocToolBox doc={doc} />
</Box>
</Box>
<HorizontalSeparator />
<HorizontalSeparator $withPadding={true} />
</Box>
</>
);

View File

@@ -43,6 +43,7 @@ export const DocTitleText = ({ title }: DocTitleTextProps) => {
as="h2"
$margin={{ all: 'none', left: 'none' }}
$size={isMobile ? 'h4' : 'h2'}
$variation="1000"
>
{title}
</Text>
@@ -113,7 +114,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
onBlurCapture={(event) =>
handleTitleSubmit(event.target.textContent || '')
}
$color={colorsTokens()['greyscale-text']}
$color={colorsTokens()['greyscale-1000']}
$margin={{ left: '-2px', right: '10px' }}
$css={css`
&[contenteditable='true']:empty:not(:focus):before {

View File

@@ -4,7 +4,8 @@ import {
useModal,
useToastProvider,
} from '@openfun/cunningham-react';
import { useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -17,12 +18,12 @@ import {
} from '@/components';
import { useAuthStore } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import {
useEditorStore,
usePanelEditorStore,
} from '@/features/docs/doc-editor/';
import { useEditorStore } from '@/features/docs/doc-editor/';
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
import { ModalSelectVersion } from '@/features/docs/doc-versioning';
import {
KEY_LIST_DOC_VERSIONS,
ModalSelectVersion,
} from '@/features/docs/doc-versioning';
import { useResponsiveStore } from '@/stores';
import { DocShareModal } from '../../doc-share/component/DocShareModal';
@@ -35,6 +36,8 @@ interface DocToolBoxProps {
export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { t } = useTranslation();
const hasAccesses = doc.nb_accesses > 1;
const queryClient = useQueryClient();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const spacings = spacingsTokens();
@@ -44,7 +47,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
const selectHistoryModal = useModal();
const modalShare = useModal();
const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore();
const { isSmallMobile, isDesktop } = useResponsiveStore();
const { authenticated } = useAuthStore();
@@ -80,14 +82,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
show: isDesktop,
},
{
label: t('Table of contents'),
icon: 'summarize',
callback: () => {
setIsPanelOpen(true);
setIsPanelTableContentOpen(true);
},
},
{
label: t('Copy as {{format}}', { format: 'Markdown' }),
icon: 'content_copy',
@@ -135,6 +130,16 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
}
};
useEffect(() => {
if (selectHistoryModal.isOpen) {
return;
}
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC_VERSIONS],
});
}, [selectHistoryModal.isOpen, queryClient]);
return (
<Box
$margin={{ left: 'auto' }}
@@ -143,21 +148,55 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
$gap="0.5rem 1.5rem"
$wrap={isSmallMobile ? 'wrap' : 'nowrap'}
>
<Box $direction="row" $margin={{ left: 'auto' }} $gap={spacings['2xs']}>
<Box
$direction="row"
$align="center"
$margin={{ left: 'auto' }}
$gap={spacings['2xs']}
>
{authenticated && !isSmallMobile && (
<Button
color="primary-text"
onClick={() => {
modalShare.open();
}}
size={isSmallMobile ? 'small' : 'medium'}
>
{t('Share')}
</Button>
<>
{!hasAccesses && (
<Button
color="tertiary-text"
onClick={() => {
modalShare.open();
}}
size={isSmallMobile ? 'small' : 'medium'}
>
{t('Share')}
</Button>
)}
{hasAccesses && (
<Box
$css={css`
.c__button--medium {
height: 32px;
padding: 10px var(--c--theme--spacings--xs);
gap: 7px;
}
`}
>
<Button
color="tertiary"
aria-label="Share button"
icon={
<Icon iconName="group" $theme="primary" $variation="800" />
}
onClick={() => {
modalShare.open();
}}
size={isSmallMobile ? 'small' : 'medium'}
>
{doc.nb_accesses}
</Button>
</Box>
)}
</>
)}
{!isSmallMobile && (
<Button
color="primary-text"
color="tertiary-text"
icon={
<Icon iconName="download" $theme="primary" $variation="800" />
}
@@ -171,15 +210,18 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
<IconOptions
isHorizontal
$theme="primary"
$radius={spacings['3xs']}
$css={
isSmallMobile
$padding={{ all: 'xs' }}
$css={css`
&:hover {
background-color: ${colors['greyscale-100']};
}
${isSmallMobile
? css`
padding: 10px;
border: 1px solid ${colors['greyscale-300']};
`
: ''
}
: ''}
`}
aria-label={t('Open the document options')}
/>
</DropdownMenu>

View File

@@ -130,7 +130,6 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
data-testid="modal-export"
isOpen
closeOnClickOutside
hideCloseButton
onClose={() => onClose()}
rightActions={
<>
@@ -155,7 +154,7 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
}
size={ModalSize.MEDIUM}
title={
<Text $size="h6" $align="flex-start">
<Text $size="h6" $variation="1000" $align="flex-start">
{t('Download')}
</Text>
}
@@ -163,9 +162,9 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
<Box
$margin={{ bottom: 'xl' }}
aria-label={t('Content modal to export the document')}
$gap="1.5rem"
$gap="1rem"
>
<Text $variation="600">
<Text $variation="600" $size="sm">
{t(
'Upload your docs to a Microsoft Word, Open Office or PDF document.',
)}

View File

@@ -38,7 +38,7 @@ export function useUpdateDoc({
mutationFn: updateDoc,
onSuccess: (data) => {
listInvalideQueries?.forEach((queryKey) => {
void queryClient.resetQueries({
void queryClient.invalidateQueries({
queryKey: [queryKey],
});
});

View File

@@ -1,5 +1,4 @@
import {
Alert,
Button,
Modal,
ModalSize,
@@ -48,41 +47,36 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
isOpen
closeOnClickOutside
hideCloseButton
leftActions={
<Button
aria-label={t('Close the modal')}
color="secondary"
fullWidth
onClick={() => onClose()}
>
{t('Cancel')}
</Button>
}
onClose={() => onClose()}
rightActions={
<Button
aria-label={t('Confirm deletion')}
color="danger"
fullWidth
onClick={() =>
removeDoc({
docId: doc.id,
})
}
>
{t('Confirm deletion')}
</Button>
<>
<Button
aria-label={t('Close the modal')}
color="secondary"
fullWidth
onClick={() => onClose()}
>
{t('Cancel')}
</Button>
<Button
aria-label={t('Confirm deletion')}
color="danger"
fullWidth
onClick={() =>
removeDoc({
docId: doc.id,
})
}
>
{t('Delete')}
</Button>
</>
}
size={ModalSize.MEDIUM}
size={ModalSize.SMALL}
title={
<Box $align="center" $gap="1rem">
<Text $isMaterialIcon $size="48px" $theme="primary" $variation="600">
delete_forever
</Text>
<Text as="h2" $size="h3" $margin="none">
{t('Deleting the document "{{title}}"', { title: doc.title })}
</Text>
</Box>
<Text $size="h6" as="h6" $margin={{ all: '0' }} $align="flex-start">
{t('Delete a doc')}
</Text>
}
>
<Box
@@ -90,13 +84,11 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
aria-label={t('Content modal to delete document')}
>
{!isError && (
<Alert canClose={false} type={VariantType.WARNING}>
<Text>
{t('Are you sure you want to delete the document "{{title}}"?', {
title: doc.title,
})}
</Text>
</Alert>
<Text $size="sm" $variation="600">
{t('Are you sure you want to delete the document "{{title}}"?', {
title: doc.title,
})}
</Text>
)}
{isError && <TextErrors causes={error.cause} />}

View File

@@ -2,7 +2,8 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
import { Box, BoxButton, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useEditorStore, useHeadingStore } from '@/features/docs/doc-editor';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
@@ -11,6 +12,8 @@ import { Heading } from './Heading';
export const TableContent = () => {
const { headings } = useHeadingStore();
const { editor } = useEditorStore();
const { spacingsTokens } = useCunninghamTheme();
const spacing = spacingsTokens();
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
@@ -58,33 +61,33 @@ export const TableContent = () => {
};
}, [headings, setHeadingIdHighlight]);
const onOpen = () => {
setIsHover(true);
setTimeout(() => {
const element = document.getElementById(`heading-${headingIdHighlight}`);
element?.scrollIntoView({
behavior: 'instant',
inline: 'center',
block: 'center',
});
}, 0); // 300ms is the transition time of the box
};
const onClose = () => {
setIsHover(false);
};
if (!editor) {
return null;
}
return (
<Box
onMouseEnter={() => {
setIsHover(true);
setTimeout(() => {
const element = document.getElementById(
`heading-${headingIdHighlight}`,
);
element?.scrollIntoView({
behavior: 'smooth',
inline: 'center',
block: 'center',
});
}, 250); // 300ms is the transition time of the box
}}
onMouseLeave={() => {
setIsHover(false);
}}
id="summaryContainer"
$effect="show"
$width="40px"
$height="40px"
$width={!isHover ? '40px' : '200px'}
$height={!isHover ? '40px' : 'auto'}
$maxHeight="calc(50vh - 60px)"
$zIndex={1000}
$align="center"
$padding="xs"
@@ -94,51 +97,69 @@ export const TableContent = () => {
overflow: hidden;
border-radius: var(--c--theme--spacings--3xs);
background: var(--c--theme--colors--greyscale-000);
&:hover {
overflow-y: auto;
${isHover &&
css`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: var(--c--theme--spacings--2xs);
width: 200px;
height: auto;
max-height: calc(100vh - 60px - 15vh);
}
`}
`}
>
{!isHover && (
<Box $justify="center" $align="center">
<BoxButton onClick={onOpen} $justify="center" $align="center">
<Icon iconName="list" $theme="primary" $variation="800" />
</Box>
</BoxButton>
)}
{isHover && (
<Box $width="100%">
<Box
$width="100%"
$overflow="hidden"
$css={css`
user-select: none;
`}
>
<Box
$margin={{ bottom: '20px' }}
$margin={{ bottom: '10px' }}
$direction="row"
$justify="space-between"
$align="center"
>
<Text $weight="bold" $variation="800" $theme="primary">
<Text $weight="500" $size="sm" $variation="800" $theme="primary">
{t('Summary')}
</Text>
<Icon iconName="list" $theme="primary" $variation="800" />
<BoxButton
onClick={onClose}
$justify="center"
$align="center"
$css={css`
transform: rotate(180deg);
`}
>
<Icon iconName="menu_open" $theme="primary" $variation="800" />
</BoxButton>
</Box>
<Box
$gap={spacing['3xs']}
$css={css`
overflow-y: auto;
`}
>
{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>
{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>

View File

@@ -52,7 +52,8 @@ export const ModalSelectVersion = ({
aria-label="version history modal"
className="noPadding"
$direction="row"
$height="calc(100vh - 50px);"
$height="100%"
$maxHeight="calc(100vh - 2em - 12px)"
$overflow="hidden"
>
<Box
@@ -64,7 +65,11 @@ export const ModalSelectVersion = ({
flex: 1;
`}
>
<Box $width="100%" $padding="base" $align="center">
<Box
$width="100%"
$padding={{ horizontal: 'base', vertical: 'xl' }}
$align="center"
>
{selectedVersionId && (
<DocEditor doc={doc} versionId={selectedVersionId} />
)}
@@ -81,7 +86,7 @@ export const ModalSelectVersion = ({
$direction="column"
$justify="space-between"
$width="250px"
$height="calc(100vh - 2em - 30px);"
$height="calc(100vh - 2em - 12px)"
$css={css`
overflow-y: hidden;
border-left: 1px solid var(--c--theme--colors--greyscale-200);
@@ -105,7 +110,7 @@ export const ModalSelectVersion = ({
`}
$padding="sm"
>
<Text $size="h6" $weight="bold">
<Text $size="h6" $variation="1000" $weight="bold">
{t('History')}
</Text>
<Button
@@ -123,7 +128,7 @@ export const ModalSelectVersion = ({
/>
</Box>
<Box
$padding="base"
$padding="xs"
$css={css`
border-top: 1px solid var(--c--theme--colors--greyscale-200);
`}

View File

@@ -1,6 +1,5 @@
import { Loader } from '@openfun/cunningham-react';
import { DateTime } from 'luxon';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { APIError } from '@/api';
@@ -105,11 +104,9 @@ export const VersionList = ({
docId: doc.id,
});
const versions = useMemo(() => {
return data?.pages.reduce((acc, page) => {
return acc.concat(page.versions);
}, [] as Versions[]);
}, [data?.pages]);
const versions = data?.pages.reduce((acc, page) => {
return acc.concat(page.versions);
}, [] as Versions[]);
return (
<Box $css="overflow-y: auto; overflow-x: hidden;">