diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c118dbe..e02b0034 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -89,6 +89,7 @@ and this project adheres to
- โจ(frontend) add sentry #424
- โจ(frontend) add crisp chatbot #450
- ๐(frontend) update DocsGridOptions component #432
+- ๐(frontend) update DocHeader ui #446
## Changed
diff --git a/src/frontend/apps/impress/src/components/Icon.tsx b/src/frontend/apps/impress/src/components/Icon.tsx
index b5d44381..224f87b6 100644
--- a/src/frontend/apps/impress/src/components/Icon.tsx
+++ b/src/frontend/apps/impress/src/components/Icon.tsx
@@ -1,3 +1,5 @@
+import { css } from 'styled-components';
+
import { Text, TextType } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
@@ -40,23 +42,21 @@ export const IconBG = ({ iconName, ...textProps }: IconBGProps) => {
);
};
-interface IconOptionsProps {
- isOpen: boolean;
- 'aria-label': string;
-}
+type IconOptionsProps = TextType & {
+ isHorizontal?: boolean;
+};
-export const IconOptions = ({ isOpen, ...props }: IconOptionsProps) => {
+export const IconOptions = ({ isHorizontal, ...props }: IconOptionsProps) => {
return (
- more_vert
+ {isHorizontal ? 'more_horiz' : 'more_vert'}
);
};
diff --git a/src/frontend/apps/impress/src/components/separators/HorizontalSeparator.tsx b/src/frontend/apps/impress/src/components/separators/HorizontalSeparator.tsx
new file mode 100644
index 00000000..b660e259
--- /dev/null
+++ b/src/frontend/apps/impress/src/components/separators/HorizontalSeparator.tsx
@@ -0,0 +1,31 @@
+import { useCunninghamTheme } from '@/cunningham';
+
+import { Box } from '../Box';
+
+export enum SeparatorVariant {
+ LIGHT = 'light',
+ DARK = 'dark',
+}
+
+type Props = {
+ variant?: SeparatorVariant;
+};
+
+export const HorizontalSeparator = ({
+ variant = SeparatorVariant.LIGHT,
+}: Props) => {
+ const { colorsTokens } = useCunninghamTheme();
+
+ return (
+
+ );
+};
diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-style.css b/src/frontend/apps/impress/src/cunningham/cunningham-style.css
index eb40fed5..4765ad48 100644
--- a/src/frontend/apps/impress/src/cunningham/cunningham-style.css
+++ b/src/frontend/apps/impress/src/cunningham/cunningham-style.css
@@ -352,8 +352,8 @@ input:-webkit-autofill:focus {
}
.c__button--nano {
- padding: 0 var(--c--theme--spacings--2xs) !important;
- gap: var(--c--theme--spacings--2xs) !important;
+ padding: 0 var(--c--theme--spacings--2xs);
+ gap: var(--c--theme--spacings--2xs);
}
.c__button--medium {
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
index 39c7ba42..1d29bd73 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
@@ -1,14 +1,18 @@
-import { Fragment } from 'react';
+import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
-import { Box, Card, StyledLink, Text } from '@/components';
+import { Box, Icon, Text } from '@/components';
+import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator';
import { useCunninghamTheme } from '@/cunningham';
-import { Doc, currentDocRole, useTrans } from '@/features/docs/doc-management';
-import { useDate } from '@/hook';
+import {
+ Doc,
+ LinkReach,
+ currentDocRole,
+ useTrans,
+} from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
-import { DocTagPublic } from './DocTagPublic';
import { DocTitle } from './DocTitle';
import { DocToolBox } from './DocToolBox';
@@ -17,90 +21,78 @@ interface DocHeaderProps {
}
export const DocHeader = ({ doc }: DocHeaderProps) => {
- const { colorsTokens } = useCunninghamTheme();
+ const { colorsTokens, spacingsTokens } = useCunninghamTheme();
+ const { isDesktop } = useResponsiveStore();
+ const spacings = spacingsTokens();
+ const colors = colorsTokens();
+
const { t } = useTranslation();
- const { formatDate } = useDate();
+ const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
+
const { transRole } = useTrans();
- const { isMobile, isSmallMobile } = useResponsiveStore();
return (
<>
-
-
-
-
- home
-
-
+ {docIsPublic && (
+ aria-label={t('Public document')}
+ $color={colors['primary-600']}
+ $background={colors['primary-100']}
+ $radius={spacings['3xs']}
+ $direction="row"
+ $padding="xs"
+ $flex={1}
+ $align="center"
+ $gap={spacings['3xs']}
+ $css={css`
+ border: 1px solid var(--c--theme--colors--primary-300, #e3e3fd);
+ `}
+ >
+
+ {t('Public document')}
+
+ )}
+
-
+
+
+
+ {isDesktop && (
+ <>
+
+ {transRole(currentDocRole(doc.abilities))} ยท
+
+
+ {t('Last update: {{update}}', {
+ update: DateTime.fromISO(doc.updated_at).toRelative(),
+ })}
+
+ >
+ )}
+ {!isDesktop && (
+
+ {DateTime.fromISO(doc.updated_at).toRelative()}
+
+ )}
+
+
-
-
-
-
- {t('Created at')} {formatDate(doc.created_at)}
-
-
-
- {t('Your role:')}{' '}
- {transRole(currentDocRole(doc.abilities))}
-
-
-
+
+
>
);
};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx
index 6c6a9dcf..e10f8bd1 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx
@@ -5,12 +5,12 @@ import {
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
-import React, { useCallback, useEffect, useRef, useState } from 'react';
+import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { css } from 'styled-components';
import { Box, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
-import { useHeadingStore } from '@/features/docs/doc-editor';
import {
Doc,
KEY_DOC,
@@ -19,7 +19,6 @@ import {
useUpdateDoc,
} from '@/features/docs/doc-management';
import { useBroadcastStore, useResponsiveStore } from '@/stores';
-import { isFirefox } from '@/utils/userAgent';
interface DocTitleProps {
doc: Doc;
@@ -32,7 +31,7 @@ export const DocTitle = ({ doc }: DocTitleProps) => {
return (
{doc.title}
@@ -44,20 +43,18 @@ export const DocTitle = ({ doc }: DocTitleProps) => {
};
const DocTitleInput = ({ doc }: DocTitleProps) => {
+ const { isDesktop } = useResponsiveStore();
const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme();
const [titleDisplay, setTitleDisplay] = useState(doc.title);
const { toast } = useToastProvider();
const { untitledDocument } = useTrans();
const isUntitled = titleDisplay === untitledDocument;
- const { headings } = useHeadingStore();
- const headingText = headings?.[0]?.contentText;
- const debounceRef = useRef();
- const { isMobile } = useResponsiveStore();
+
const { broadcast } = useBroadcastStore();
const { mutate: updateDoc } = useUpdateDoc({
- listInvalideQueries: [KEY_LIST_DOC],
+ listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
onSuccess(data) {
if (data.title !== untitledDocument) {
toast(t('Document title updated successfully'), VariantType.SUCCESS);
@@ -81,10 +78,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
// If mutation we update
if (sanitizedTitle !== doc.title) {
- if (debounceRef.current) {
- clearTimeout(debounceRef.current);
- debounceRef.current = undefined;
- }
+ setTitleDisplay(sanitizedTitle);
updateDoc({ id: doc.id, title: sanitizedTitle });
}
},
@@ -98,74 +92,38 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
}
};
- const handleOnClick = () => {
- if (isUntitled) {
- setTitleDisplay('');
- }
- };
-
- useEffect(() => {
- setTitleDisplay(doc.title);
- }, [doc.title]);
-
- useEffect(() => {
- if ((!debounceRef.current && !isUntitled) || !headingText) {
- return;
- }
-
- setTitleDisplay(headingText);
-
- if (debounceRef.current) {
- clearTimeout(debounceRef.current);
- }
-
- debounceRef.current = setTimeout(() => {
- handleTitleSubmit(headingText);
- debounceRef.current = undefined;
- }, 3000);
- }, [isUntitled, handleTitleSubmit, headingText]);
-
return (
<>
- handleTitleSubmit(e.currentTarget.textContent || '')
- }
+ as="span"
+ role="textbox"
+ contentEditable
+ defaultValue={isUntitled ? undefined : titleDisplay}
onKeyDownCapture={handleKeyDown}
suppressContentEditableWarning={true}
- $color={
- isUntitled
- ? colorsTokens()['greyscale-200']
- : colorsTokens()['greyscale-text']
+ aria-label={t('doc title input')}
+ onBlurCapture={(event) =>
+ handleTitleSubmit(event.target.textContent || '')
}
- $css={`
- ${isUntitled && 'font-style: italic;'}
- cursor: text;
- font-size: ${isMobile ? '1.2rem' : '1.5rem'};
- transition: box-shadow 0.5s, border-color 0.5s;
- border: 1px dashed transparent;
-
- &:hover {
- border-color: rgba(0, 123, 255, 0.25);
- border-style: dashed;
+ $color={colorsTokens()['greyscale-text']}
+ $margin={{ left: '-2px', right: '10px' }}
+ $css={css`
+ &[contenteditable='true']:empty:not(:focus):before {
+ content: '${untitledDocument}';
+ color: grey;
+ pointer-events: none;
+ font-style: italic;
}
+ font-size: ${isDesktop
+ ? css`var(--c--theme--font--sizes--h2)`
+ : css`var(--c--theme--font--sizes--sm)`};
+ font-weight: 700;
- &:focus {
- outline: none;
- border-color: transparent;
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
- }
+ outline: none;
`}
>
- {titleDisplay}
+ {isUntitled ? '' : titleDisplay}
>
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
index 67d49d10..e5358ab9 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
@@ -4,11 +4,19 @@ import {
useToastProvider,
} from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
-import React, { useState } from 'react';
+import { useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { css } from 'styled-components';
-import { Box, DropButton, IconOptions } from '@/components';
+import {
+ Box,
+ DropdownMenu,
+ DropdownMenuOption,
+ Icon,
+ IconOptions,
+} from '@/components';
import { useAuthStore } from '@/core';
+import { useCunninghamTheme } from '@/cunningham';
import {
useEditorStore,
usePanelEditorStore,
@@ -32,10 +40,15 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
query: { versionId },
} = useRouter();
const { t } = useTranslation();
+ const { spacingsTokens, colorsTokens } = useCunninghamTheme();
+
+ const spacings = spacingsTokens();
+ const colors = colorsTokens();
+
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
- const [isDropOpen, setIsDropOpen] = useState(false);
+
const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore();
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
const { isSmallMobile } = useResponsiveStore();
@@ -43,6 +56,66 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { editor } = useEditorStore();
const { toast } = useToastProvider();
+ const options: DropdownMenuOption[] = [
+ ...(isSmallMobile
+ ? [
+ {
+ label: t('Share'),
+ icon: 'upload',
+ callback: () => {
+ setIsModalShareOpen(true);
+ },
+ },
+ {
+ label: t('Export'),
+ icon: 'download',
+ callback: () => {
+ setIsModalPDFOpen(true);
+ },
+ },
+ ]
+ : []),
+ {
+ label: t('Version history'),
+ icon: 'history',
+ disabled: !doc.abilities.versions_list,
+ callback: () => {
+ setIsPanelOpen(true);
+ setIsPanelTableContentOpen(false);
+ },
+ },
+ {
+ label: t('Table of contents'),
+ icon: 'summarize',
+ callback: () => {
+ setIsPanelOpen(true);
+ setIsPanelTableContentOpen(true);
+ },
+ },
+ {
+ label: t('Copy as {{format}}', { format: 'Markdown' }),
+ icon: 'content_copy',
+ callback: () => {
+ void copyCurrentEditorToClipboard('markdown');
+ },
+ },
+ {
+ label: t('Copy as {{format}}', { format: 'HTML' }),
+ icon: 'content_copy',
+ callback: () => {
+ void copyCurrentEditorToClipboard('html');
+ },
+ },
+ {
+ label: t('Delete document'),
+ icon: 'delete',
+ disabled: !doc.abilities.destroy,
+ callback: () => {
+ setIsModalRemoveOpen(true);
+ },
+ },
+ ];
+
const copyCurrentEditorToClipboard = async (
asFormat: 'html' | 'markdown',
) => {
@@ -87,9 +160,10 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
)}
-
- {authenticated && (
+
+ {authenticated && !isSmallMobile && (
)}
-
- }
- onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
- isOpen={isDropOpen}
- >
-
- {doc.abilities.versions_list && (
-
- )}
-
-
- {doc.abilities.destroy && (
-
- )}
-
-
-
-
+ {!isSmallMobile && (
+
+ }
+ onClick={() => {
+ setIsModalPDFOpen(true);
+ }}
+ size={isSmallMobile ? 'small' : 'medium'}
+ />
+ )}
+
+
+
{isModalShareOpen && (
setIsModalShareOpen(false)} doc={doc} />
diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/VersionItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/VersionItem.tsx
index 626d6ff1..21a4aa91 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/VersionItem.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/VersionItem.tsx
@@ -1,5 +1,5 @@
import { Button } from '@openfun/cunningham-react';
-import React, { PropsWithChildren, useState } from 'react';
+import { PropsWithChildren, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, DropButton, IconOptions, StyledLink, Text } from '@/components';
@@ -70,10 +70,7 @@ export const VersionItem = ({
{isActive && versionId && (
+
}
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
isOpen={isDropOpen}