This commit is contained in:
Nathan Panchout
2024-11-14 14:39:43 +01:00
parent 8268b26d7a
commit 4a4953b344
21 changed files with 482 additions and 496 deletions
+28 -8
View File
@@ -3,7 +3,7 @@ const config = {
default: {
theme: {
colors: {
'card-border': '#ededed',
'card-border': '#E5E5E5',
'primary-bg': '#FAFAFA',
'primary-100': '#EDF5FA',
'primary-150': '#E5EEFA',
@@ -14,15 +14,19 @@ const config = {
font: {
sizes: {
ml: '0.938rem',
xl: '1.50rem',
t: '0.6875rem',
s: '0.75rem',
h1: '2.2rem',
h2: '1.7rem',
h3: '1.37rem',
h4: '1.15rem',
h5: '1rem',
h6: '0.87rem',
xl: '20px',
lg: '18px',
md: '16px',
sm: '14px',
xs: '12px',
h1: '32px',
h2: '28px',
h3: '24px',
h4: '22px',
h5: '20px',
h6: '18px',
},
weights: {
thin: 100,
@@ -310,6 +314,22 @@ const config = {
accent: 'Marianne',
base: 'Marianne',
},
size: {
ml: '0.938rem',
t: '0.6875rem',
s: '0.75rem',
xl: '20px',
lg: '18px',
md: '16px',
sm: '14px',
xs: '12px',
h1: '32px',
h2: '28px',
h3: '24px',
h4: '22px',
h5: '20px',
h6: '18px',
},
},
logo: {
src: '/assets/logo-gouv.svg',
+1
View File
@@ -33,6 +33,7 @@
"react-dom": "*",
"react-i18next": "15.0.3",
"react-select": "5.8.1",
"react-intersection-observer": "9.13.1",
"styled-components": "6.1.13",
"y-protocols": "1.0.6",
"yjs": "*",
@@ -1,6 +1,6 @@
import { ComponentPropsWithRef, ReactHTML } from 'react';
import styled from 'styled-components';
import { CSSProperties } from 'styled-components/dist/types';
import { CSSProperties, RuleSet } from 'styled-components/dist/types';
import {
MarginPadding,
@@ -38,6 +38,7 @@ export interface BoxProps {
$width?: CSSProperties['width'];
$wrap?: CSSProperties['flexWrap'];
$zIndex?: CSSProperties['zIndex'];
$styledCss?: RuleSet<object>;
}
export type BoxType = ComponentPropsWithRef<typeof Box>;
@@ -45,6 +46,7 @@ export type BoxType = ComponentPropsWithRef<typeof Box>;
export const Box = styled('div')<BoxProps>`
display: flex;
flex-direction: column;
${({ $styledCss }) => $styledCss && $styledCss}
${({ $align }) => $align && `align-items: ${$align};`}
${({ $background }) => $background && `background: ${$background};`}
${({ $color }) => $color && `color: ${$color};`}
@@ -16,7 +16,7 @@ export const Card = ({
$background="white"
$radius="4px"
$css={`
box-shadow: 2px 2px 5px ${colorsTokens()['greyscale-300']};
border: 1px solid ${colorsTokens()['card-border']};
${$css}
`}
@@ -1,9 +1,4 @@
import React, {
PropsWithChildren,
ReactNode,
useEffect,
useState,
} from 'react';
import { PropsWithChildren, ReactNode, useEffect, useState } from 'react';
import { Button, DialogTrigger, Popover } from 'react-aria-components';
import styled from 'styled-components';
@@ -29,7 +24,7 @@ const StyledButton = styled(Button)`
text-wrap: nowrap;
`;
interface DropButtonProps {
export interface DropButtonProps {
button: ReactNode;
isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
@@ -23,6 +23,7 @@ export interface TextProps extends BoxProps {
$weight?: CSSProperties['fontWeight'];
$textAlign?: CSSProperties['textAlign'];
$size?: TextSizes | (string & {});
$theme?:
| 'primary'
| 'secondary'
@@ -31,6 +32,7 @@ export interface TextProps extends BoxProps {
| 'warning'
| 'danger'
| 'greyscale';
$variation?:
| 'text'
| '100'
@@ -41,7 +43,8 @@ export interface TextProps extends BoxProps {
| '600'
| '700'
| '800'
| '900';
| '900'
| '1000';
}
export type TextType = ComponentPropsWithRef<typeof Text>;
@@ -63,14 +66,16 @@ export const TextStyled = styled(Box)<TextProps>`
const Text = forwardRef<HTMLElement, ComponentPropsWithRef<typeof TextStyled>>(
({ className, $isMaterialIcon, ...props }, ref) => {
return (
<TextStyled
ref={ref}
as="span"
$theme="greyscale"
$variation="text"
className={`${className || ''}${$isMaterialIcon ? ' material-icons' : ''}`}
{...props}
/>
<>
<TextStyled
ref={ref}
as="span"
$theme={props.$theme ?? 'greyscale'}
$variation={props.$variation ?? 'text'}
className={`${className || ''}${$isMaterialIcon ? ' material-icons' : ''}`}
{...props}
/>
</>
);
},
);
@@ -0,0 +1,87 @@
import { PropsWithChildren, useMemo } from 'react';
import { DropButton, DropButtonProps } from '@/components';
import { Icon } from '@/components/Icon';
import styles from './dropdown-menu.module.scss';
export type DropdownMenuOption = {
icon?: string;
label: string;
callback?: () => void | Promise<unknown>;
danger?: boolean;
show?: boolean;
};
export type DropdownMenuProps = Omit<DropButtonProps, 'button'> & {
options: DropdownMenuOption[];
showArrow?: boolean;
arrowClassname?: string;
};
export const DropdownMenu = ({
options,
children,
showArrow = false,
arrowClassname,
...dropButtonProps
}: PropsWithChildren<DropdownMenuProps>) => {
const showDropdown = useMemo(() => {
let show = false;
options.forEach((option) => {
show = show || (option.show !== undefined ? option.show : true);
});
return show;
}, [options]);
const getButton = () => {
if (!showArrow) {
return children;
}
return (
<div className={styles.withArrowContainer}>
<div>{children}</div>
<Icon
className={arrowClassname ?? 'clr-primary-600'}
iconName={
dropButtonProps.isOpen ? 'arrow_drop_up' : 'arrow_drop_down'
}
/>
</div>
);
};
if (!showDropdown) {
return;
}
return (
<DropButton {...dropButtonProps} button={getButton()}>
<div className={styles.listOption}>
{options.map((option) => {
if (option.show !== undefined && !option.show) {
return;
}
return (
<button
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
dropButtonProps.onOpenChange?.(false);
void option.callback?.();
}}
key={option.label}
className={styles.item}
>
{option.icon && (
<Icon className={styles.itemIcon} iconName={option.icon} />
)}
{option.label}
</button>
);
})}
</div>
</DropButton>
);
};
@@ -0,0 +1,42 @@
.simpleContent {
display: flex;
align-items: center;
gap: var(--c--theme--spacings--st);
}
.listOption {
display: flex;
flex-direction: column;
.item:not(:last-child) {
border-bottom: 1px solid var(--c--theme--colors--greyscale-200);
}
}
.item {
display: flex;
align-items: center;
gap: var(--c--theme--spacings--200W);
border: none;
background-color: white;
font-size: var(--c--theme--font--sizes--sm);
color: var(--c--theme--colors--primary-600);
font-weight: 500;
padding: var(--c--theme--spacings--100W) var(--c--theme--spacings--200W);
width: 100%;
cursor: pointer;
user-select: none;
&:hover {
background-color: var(--c--theme--colors--greyscale-050);
}
.itemIcon {
font-size: 24px;
}
}
.withArrowContainer {
display: flex;
align-items: center;
gap: 5px;
}
@@ -0,0 +1,14 @@
import { useState } from 'react';
export const useDropdownMenu = () => {
const [isOpen, setIsOpen] = useState(false);
const onOpenChange = (isOpen: boolean) => {
setIsOpen(isOpen);
};
return {
isOpen,
onOpenChange,
};
};
File diff suppressed because it is too large Load Diff
@@ -13,7 +13,7 @@ export const tokens = {
'secondary-700': '#97A3AE',
'secondary-800': '#757E87',
'secondary-900': '#596067',
'info-text': '#FFFFFF',
'info-text': '#fff',
'info-100': '#EBF2FC',
'info-200': '#8CB5EA',
'info-300': '#5894E1',
@@ -32,7 +32,7 @@ export const tokens = {
'greyscale-700': '#555F6B',
'greyscale-800': '#303C4B',
'greyscale-900': '#0C1A2B',
'greyscale-000': '#FFFFFF',
'greyscale-000': '#fff',
'primary-100': '#EDF5FA',
'primary-200': '#8CB5EA',
'primary-300': '#5894E1',
@@ -69,29 +69,34 @@ export const tokens = {
'danger-700': '#9B0000',
'danger-800': '#780000',
'danger-900': '#5C0000',
'primary-text': '#FFFFFF',
'success-text': '#FFFFFF',
'warning-text': '#FFFFFF',
'danger-text': '#FFFFFF',
'card-border': '#ededed',
'primary-text': '#fff',
'success-text': '#fff',
'warning-text': '#fff',
'danger-text': '#fff',
'card-border': '#E5E5E5',
'primary-bg': '#FAFAFA',
'primary-150': '#E5EEFA',
'info-150': '#E5EEFA',
'greyscale-1000': '#161616',
},
font: {
sizes: {
h1: '2.2rem',
h2: '1.7rem',
h3: '1.37rem',
h4: '1.15rem',
h5: '1rem',
h6: '0.87rem',
h1: '32px',
h2: '28px',
h3: '24px',
h4: '22px',
h5: '20px',
h6: '18px',
l: '1rem',
m: '0.8125rem',
s: '0.75rem',
ml: '0.938rem',
xl: '1.50rem',
t: '0.6875rem',
xl: '20px',
lg: '18px',
md: '16px',
sm: '14px',
xs: '12px',
},
weights: {
thin: 100,
@@ -440,7 +445,25 @@ export const tokens = {
'danger-800': '#412121',
'danger-900': '#3a1c1c',
},
font: { families: { accent: 'Marianne', base: 'Marianne' } },
font: {
families: { accent: 'Marianne', base: 'Marianne' },
size: {
ml: '0.938rem',
t: '0.6875rem',
s: '0.75rem',
xl: '20px',
lg: '18px',
md: '16px',
sm: '14px',
xs: '12px',
h1: '32px',
h2: '28px',
h3: '24px',
h4: '22px',
h5: '20px',
h6: '18px',
},
},
logo: {
src: '/assets/logo-gouv.svg',
widthHeader: '110px',
@@ -5,6 +5,7 @@ import { tokens } from './cunningham-tokens';
type Tokens = typeof tokens.themes.default & Partial<typeof tokens.themes.dsfr>;
type ColorsTokens = Tokens['theme']['colors'];
type SpacingsTokens = Tokens['theme']['spacings'];
type ComponentTokens = Tokens['components'];
type Theme = 'default' | 'dsfr';
@@ -13,6 +14,7 @@ interface AuthStore {
setTheme: (theme: Theme) => void;
themeTokens: () => Partial<Tokens['theme']>;
colorsTokens: () => Partial<ColorsTokens>;
spacingsTokens: () => SpacingsTokens;
componentTokens: () => ComponentTokens;
}
@@ -25,6 +27,7 @@ const useCunninghamTheme = create<AuthStore>((set, get) => {
themeTokens: () => currentTheme().theme,
colorsTokens: () => currentTheme().theme.colors,
componentTokens: () => currentTheme().components,
spacingsTokens: () => currentTheme().theme.spacings,
setTheme: (theme: Theme) => {
set({ theme });
},
@@ -1,4 +1,8 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
import {
UseQueryOptions,
useInfiniteQuery,
useQuery,
} from '@tanstack/react-query';
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
@@ -52,3 +56,14 @@ export function useDocs(
...queryConfig,
});
}
export const useInfiniteDocs = (params: DocsParams) => {
return useInfiniteQuery({
initialPageParam: 1,
queryKey: [KEY_LIST_DOC, 'infinite', params],
queryFn: ({ pageParam }) => getDocs({ ...params, page: pageParam }),
getNextPageParam: (lastPage, allPages) => {
return lastPage.next ? allPages.length + 1 : undefined;
},
});
};
@@ -0,0 +1,9 @@
<svg width="32" height="36" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" fill="white"/>
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" stroke="#DCDCFC" stroke-width="0.472222"/>
<path d="M6.5 8.55556H15" stroke="#6A6AF4" stroke-width="1.88889" stroke-linecap="round"/>
<path d="M6.5 11.3889H23.5M6.5 14.2222H23.5M6.5 17.0556H23.5M6.5 19.8889H23.5M6.5 22.7222H20.6667" stroke="#CACAFB" stroke-width="1.88889" stroke-linecap="round"/>
<rect x="7" y="10" width="16" height="16" rx="8" fill="#6A6AF4"/>
<rect x="7" y="10" width="16" height="16" rx="8" stroke="white" stroke-width="1.5"/>
<path d="M16.8 18L18 19.2V20.1H15.45V22.95L15 23.4L14.55 22.95V20.1H12V19.2L13.2 18V14.7H12.6V13.8H17.4V14.7H16.8V18Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 853 B

@@ -0,0 +1,6 @@
<svg width="28" height="34" viewBox="0 0 28 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.01394" y="0.236111" width="25.9722" height="33.5278" rx="3.54167" fill="white"/>
<rect x="1.01394" y="0.236111" width="25.9722" height="33.5278" rx="3.54167" stroke="#DCDCFC" stroke-width="0.472222"/>
<path d="M5.5 7.55554H14" stroke="#6A6AF4" stroke-width="1.88889" stroke-linecap="round"/>
<path d="M5.5 10.3889H22.5M5.5 13.2222H22.5M5.5 16.0556H22.5M5.5 18.8889H22.5M5.5 21.7222H22.5M5.5 24.5556H22.5M5.5 27.3889H22.5M5.5 30.2222H22.5M5.5 33.0556H22.5" stroke="#CACAFB" stroke-width="1.88889" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 635 B

@@ -0,0 +1,65 @@
import { ReactNode } from 'react';
import styled, { css } from 'styled-components';
import { Box, Text } from '@/components';
import { Doc } from '@/features/docs';
import PinnedDocumentIcon from '@/features/docs/doc-management/assets/pinned-document.svg';
import SimpleFileIcon from '@/features/docs/doc-management/assets/simple-document.svg';
const ItemContainer = styled(Box)`
display: flex;
flex-direction: row;
align-items: center;
gap: var(--c--theme--spacings--100W);
border-radius: var(--c--theme--spacings--100V);
padding: var(--c--theme--spacings--150V);
cursor: pointer;
`;
const ItemTextCss = css`
overflow: hidden;
text-overflow: ellipsis;
white-space: initial;
display: -webkit-box;
line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
`;
type Props = {
doc: Doc;
isPinned?: boolean;
subText?: ReactNode | string;
};
export const SimpleDocItem = ({ doc, isPinned = false, subText }: Props) => {
return (
<ItemContainer>
<Box
$css={`
background-color: transparent;
filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.05));
display: flex;
align-items: center;
`}
>
{isPinned ? <PinnedDocumentIcon /> : <SimpleFileIcon />}
</Box>
<div>
<Text
$weight={500}
$variation="1000"
$size="sm"
$styledCss={ItemTextCss}
>
{doc.title}
</Text>
<Text $variation="500" $size="xs" $styledCss={ItemTextCss}>
{subText ??
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi vel ante libero. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed imperdiet neque quam, sed euismod metus mollis ut. '}
</Text>
</div>
</ItemContainer>
);
};
@@ -0,0 +1,14 @@
import { Doc } from '../../doc-management';
import { SimpleDocItem } from '../../doc-management/components/items/SimpleDocItem';
type Props = {
doc: Doc;
};
export const DocGridListItem = ({ doc }: Props) => {
return (
<div>
<SimpleDocItem doc={doc} />
</div>
);
};
@@ -0,0 +1,74 @@
import { useTranslation } from 'react-i18next';
import { Box, Card, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useInfiniteDocs } from '@/features/docs/doc-management/api/useDocs';
import { LEFT_PANEL_WIDTH } from '@/features/left-pannel/conf';
import { useResponsiveStore } from '@/stores';
import { DocGridListItem } from './DocGridListItem';
export const DocsGridList = () => {
const { themeTokens, spacingsTokens, colorsTokens } = useCunninghamTheme();
const spacings = spacingsTokens();
const colors = colorsTokens();
const { t } = useTranslation();
const { isResponsive } = useResponsiveStore();
const { data, isFetching, isLoading, fetchNextPage, hasNextPage } =
useInfiniteDocs({
page: 1,
});
const loading = isFetching || isLoading;
const loadMore = (inView: boolean) => {
if (!inView) {
return;
}
void fetchNextPage();
};
return (
<Card
$css={`
width: 960px;
max-width: calc(100dvw - ${isResponsive ? 34 : LEFT_PANEL_WIDTH}px);
padding: ${spacings['300W']};
`}
>
<Text
$margin={{ bottom: `${spacings['100W']}` }}
$css="margin-block: 0"
as="h4"
>
{t('All docs')}
</Text>
<section>
<Box
as="header"
$css={`
display: flex;
flex-direction: row;
margin-bottom: ${spacings['100W']};
font-size: ${themeTokens().font?.sizes.xs};
color: ${colors['greyscale-500']};
padding-bottom: ${spacings['150V']} ${spacings['100W']};
`}
>
<Box $flex={7}>{t('Name')}</Box>
{!isResponsive && <Box $flex={1}>{t('Update at')}</Box>}
<Box $flex={1} />
</Box>
</section>
<Box $gap={`${spacings['150V']}`}>
{data?.pages.map((currentPage) => {
return currentPage.results.map((doc) => (
<DocGridListItem doc={doc} key={doc.id} />
));
})}
</Box>
</Card>
);
};
@@ -0,0 +1 @@
export const LEFT_PANEL_WIDTH = 300;
@@ -1,16 +1,11 @@
import type { ReactElement } from 'react';
import { Box } from '@/components';
import { DocsGrid } from '@/features/docs/docs-grid/components/DocsGrid';
import { DocsGridList } from '@/features/docs/docs-grid/components/DocsGridList';
import { MainLayout, MainLayoutBackgroundColor } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
const Page: NextPageWithLayout = () => {
return (
<Box $width="100%">
<DocsGrid />
</Box>
);
return <DocsGridList />;
};
Page.getLayout = function getLayout(page: ReactElement) {
+5
View File
@@ -9892,6 +9892,11 @@ react-icons@^5.2.1:
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.3.0.tgz#ccad07a30aebd40a89f8cfa7d82e466019203f1c"
integrity sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==
[email protected]:
version "9.13.1"
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.13.1.tgz#6c61a75801162491c6348bad09967f2caf445584"
integrity sha512-tSzDaTy0qwNPLJHg8XZhlyHTgGW6drFKTtvjdL+p6um12rcnp8Z5XstE+QNBJ7c64n5o0Lj4ilUleA41bmDoMw==
[email protected]:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"