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 && ( +