diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index ae253bce..94296f70 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -11,6 +11,7 @@ "@livekit/components-react": "2.3.3", "@livekit/components-styles": "1.0.12", "@pandacss/preset-panda": "0.41.0", + "@remixicon/react": "4.2.0", "@tanstack/react-query": "5.49.2", "hoofd": "1.7.1", "i18next": "23.12.1", @@ -3177,6 +3178,14 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/@remixicon/react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@remixicon/react/-/react-4.2.0.tgz", + "integrity": "sha512-eGhKpZ88OU0qkcY9pJu6khBmItDV82nU130E6C68yc+FbljueHlUYy/4CrJsmf860RIDMay2Rpzl27OSJ81miw==", + "peerDependencies": { + "react": ">=18.2.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index ac7e14e7..05116704 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -14,6 +14,7 @@ "@livekit/components-react": "2.3.3", "@livekit/components-styles": "1.0.12", "@pandacss/preset-panda": "0.41.0", + "@remixicon/react": "4.2.0", "@tanstack/react-query": "5.49.2", "hoofd": "1.7.1", "i18next": "23.12.1", diff --git a/src/frontend/panda.config.ts b/src/frontend/panda.config.ts index d99ddf52..8d4b06d1 100644 --- a/src/frontend/panda.config.ts +++ b/src/frontend/panda.config.ts @@ -179,7 +179,7 @@ const config: Config = { hover: { value: '{colors.gray.200}' }, active: { value: '{colors.gray.300}' }, text: { value: '{colors.default.text}' }, - border: { value: '{colors.gray.300}' }, + border: { value: '{colors.gray.500}' }, }, primary: { DEFAULT: { value: '{colors.blue.700}' }, @@ -232,9 +232,17 @@ const config: Config = { paragraph: { value: '{spacing.1}' }, heading: { value: '{spacing.1}' }, gutter: { value: '{spacing.1}' }, + textfield: { value: '{spacing.0.5}' }, }, }), textStyles: defineTextStyles({ + display: { + value: { + fontSize: '3rem', + lineHeight: '2rem', + fontWeight: 700, + }, + }, h1: { value: { fontSize: '1.5rem', @@ -253,7 +261,6 @@ const config: Config = { value: { fontSize: '1.125rem', lineHeight: '1.75rem', - fontWeight: 700, }, }, body: { @@ -262,7 +269,7 @@ const config: Config = { lineHeight: '1.5', }, }, - small: { + sm: { value: { fontSize: '0.875rem', lineHeight: '1.25rem', diff --git a/src/frontend/src/features/home/routes/Home.tsx b/src/frontend/src/features/home/routes/Home.tsx index 37e14702..43545ea5 100644 --- a/src/frontend/src/features/home/routes/Home.tsx +++ b/src/frontend/src/features/home/routes/Home.tsx @@ -1,5 +1,18 @@ import { useTranslation } from 'react-i18next' -import { A, Button, Italic, P, Div, H, Box } from '@/primitives' +import { navigate } from 'wouter/use-browser-location' +import { Form } from 'react-aria-components' +import { HStack } from '@/styled-system/jsx' +import { + Button, + P, + Div, + Text, + H, + VerticallyOffCenter, + Dialog, + TextField, +} from '@/primitives' +import { useCloseDialog } from '@/primitives/Dialog' import { authUrl, useUser } from '@/features/auth' import { navigateToNewRoom } from '@/features/rooms' import { Screen } from '@/layout/Screen' @@ -8,30 +21,85 @@ export const Home = () => { const { t } = useTranslation('home') const { isLoggedIn } = useUser() return ( - - - {t('heading')} -

{t('intro')}

-
- - {isLoggedIn ? ( - + + + - ) : ( -

- {t('login')} -

- )} -
+ + +
-

- {t('or')} -

- -

{t('copyMeetingUrl')}

-
-
+
) } + +const JoinMeetingDialogContent = () => { + const { t } = useTranslation('home') + const closeDialog = useCloseDialog() + const fieldOk = /^.*([a-z]{3}-[a-z]{4}-[a-z]{3})$/ + return ( +
+
{ + event.preventDefault() + const roomInput = document.getElementById( + 'join-meeting-input' + ) as HTMLInputElement + const value = roomInput.value + const matches = value.match(fieldOk) + if (matches) { + navigate(`/${matches[1]}`) + } + }} + > + { + if (!fieldOk.test(value)) { + return t('joinInputError') + } + return null + }} + /> + + + + + + {t('joinMeetingTipHeading')} +

{t('joinMeetingTipContent')}

+
+ ) +} diff --git a/src/frontend/src/layout/Box.tsx b/src/frontend/src/layout/Box.tsx index 48f16be5..90fc2b44 100644 --- a/src/frontend/src/layout/Box.tsx +++ b/src/frontend/src/layout/Box.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { useTranslation } from 'react-i18next' -import { Box as BoxDiv, H, Link } from '@/primitives' +import { Box as BoxDiv, H, Link, VerticallyOffCenter } from '@/primitives' export type BoxProps = { children?: ReactNode @@ -15,16 +15,18 @@ export const Box = ({ }: BoxProps) => { const { t } = useTranslation() return ( - - {!!title && {title}} - {children} - {!!withBackButton && ( -

- - {t('backToHome')} - -

- )} -
+ + + {!!title && {title}} + {children} + {!!withBackButton && ( +

+ + {t('backToHome')} + +

+ )} +
+
) } diff --git a/src/frontend/src/layout/Header.tsx b/src/frontend/src/layout/Header.tsx index 82f0bde0..1488f1e4 100644 --- a/src/frontend/src/layout/Header.tsx +++ b/src/frontend/src/layout/Header.tsx @@ -20,7 +20,6 @@ export const Header = () => { borderBottomStyle: 'solid', padding: 1, flexShrink: 0, - boxShadow: 'box', })} > @@ -36,7 +35,7 @@ export const Header = () => { {!!user && (

{user.email} - + {t('logout')}

diff --git a/src/frontend/src/layout/Screen.tsx b/src/frontend/src/layout/Screen.tsx index 777d1b58..d5c144dc 100644 --- a/src/frontend/src/layout/Screen.tsx +++ b/src/frontend/src/layout/Screen.tsx @@ -2,22 +2,30 @@ import type { ReactNode } from 'react' import { css } from '@/styled-system/css' import { Header } from './Header' -export const Screen = ({ children }: { children?: ReactNode }) => { +export const Screen = ({ + type, + children, +}: { + type?: 'splash' + children?: ReactNode +}) => { return (
-
+ {type !== 'splash' &&
}
{children} diff --git a/src/frontend/src/locales/de/global.json b/src/frontend/src/locales/de/global.json index abcb1fee..bbcbc161 100644 --- a/src/frontend/src/locales/de/global.json +++ b/src/frontend/src/locales/de/global.json @@ -1,6 +1,8 @@ { "app": "Meet", "backToHome": "", + "cancel": "", + "closeDialog": "", "error": { "heading": "" }, diff --git a/src/frontend/src/locales/de/home.json b/src/frontend/src/locales/de/home.json index deec99a7..9cc59955 100644 --- a/src/frontend/src/locales/de/home.json +++ b/src/frontend/src/locales/de/home.json @@ -1,8 +1,13 @@ { - "copyMeetingUrl": "", "createMeeting": "", "heading": "", "intro": "", - "login": "", - "or": "" + "joinInputError": "", + "joinInputExample": "", + "joinInputLabel": "", + "joinInputSubmit": "", + "joinMeeting": "", + "joinMeetingTipContent": "", + "joinMeetingTipHeading": "", + "loginToCreateMeeting": "" } diff --git a/src/frontend/src/locales/en/global.json b/src/frontend/src/locales/en/global.json index f81372f8..9b376015 100644 --- a/src/frontend/src/locales/en/global.json +++ b/src/frontend/src/locales/en/global.json @@ -1,6 +1,8 @@ { "app": "Meet", "backToHome": "Back to homescreen", + "cancel": "Cancel", + "closeDialog": "Close dialog", "error": { "heading": "An error occured while loading the page" }, @@ -15,6 +17,6 @@ "login": "Login", "logout": "Logout", "notFound": { - "heading": "" + "heading": "Page not found" } } diff --git a/src/frontend/src/locales/en/home.json b/src/frontend/src/locales/en/home.json index b6769315..a23d3b24 100644 --- a/src/frontend/src/locales/en/home.json +++ b/src/frontend/src/locales/en/home.json @@ -1,8 +1,13 @@ { - "copyMeetingUrl": "copy a meeting URL in your browser address bar to join an existing conference call", - "createMeeting": "Create a conference call", + "createMeeting": "Create a meeting", "heading": "Welcome in Meet", - "intro": "What do you want to do? You can either:", - "login": "Login to create a conference call", - "or": "Or" + "intro": "Work easily, from anywhere.", + "joinInputError": "Use a meeting link or its code. Examples: \"https://meet.numerique.gouv.fr/aze-rtyu-iop\" or \"aze-rtyu-iop\".", + "joinInputExample": "URL or 10-letter code", + "joinInputLabel": "Meeting link", + "joinInputSubmit": "Join meeting", + "joinMeeting": "Join a meeting", + "joinMeetingTipContent": "You can join a meeting by pasting its full link in the browser's address bar.", + "joinMeetingTipHeading": "Did you know?", + "loginToCreateMeeting": "Login to create a meeting" } diff --git a/src/frontend/src/locales/fr/global.json b/src/frontend/src/locales/fr/global.json index b88b97dc..65630b4a 100644 --- a/src/frontend/src/locales/fr/global.json +++ b/src/frontend/src/locales/fr/global.json @@ -1,6 +1,8 @@ { "app": "Meet", "backToHome": "Retour à l'accueil", + "cancel": "Annuler", + "closeDialog": "Fermer la fenêtre modale", "error": { "heading": "Une erreur est survenue lors du chargement de la page" }, diff --git a/src/frontend/src/locales/fr/home.json b/src/frontend/src/locales/fr/home.json index 6185cc02..aed06d1e 100644 --- a/src/frontend/src/locales/fr/home.json +++ b/src/frontend/src/locales/fr/home.json @@ -1,8 +1,13 @@ { - "copyMeetingUrl": "copier une URL de conférence dans votre barre d'adresse pour rejoindre une conférence existante", "createMeeting": "Créer une conférence", - "heading": "Bienvenue dans Meet", - "intro": "Que voulez vous faire ? Vous pouvez :", - "login": "Vous connecter pour créer une conférence", - "or": "Ou" + "heading": "Meet", + "intro": "Collaborez en toute simplicité, où que vous soyez.", + "joinInputError": "Saisissez un lien de conférence valide ou uniquement son code. Exemples : \"https://meet.numerique.gouv.fr/aze-rtyu-iop\" ou \"aze-rtyu-iop\".", + "joinInputExample": "Lien complet ou code de 10 lettres", + "joinInputLabel": "Lien de la conférence", + "joinInputSubmit": "Rejoindre la conférence", + "joinMeeting": "Rejoindre une conférence", + "joinMeetingTipContent": "Vous pouvez rejoindre une conférence en copiant directement son lien complet dans la barre d'adresse du navigateur.", + "joinMeetingTipHeading": "Astuce", + "loginToCreateMeeting": "Connectez-vous pour créer une conférence" } diff --git a/src/frontend/src/primitives/A.tsx b/src/frontend/src/primitives/A.tsx index 5b11cc57..f23b9d4e 100644 --- a/src/frontend/src/primitives/A.tsx +++ b/src/frontend/src/primitives/A.tsx @@ -16,8 +16,8 @@ const link = cva({ }, variants: { size: { - small: { - textStyle: 'small', + sm: { + textStyle: 'sm', }, }, }, diff --git a/src/frontend/src/primitives/AlertDialog.tsx b/src/frontend/src/primitives/AlertDialog.tsx new file mode 100644 index 00000000..6dfdc609 --- /dev/null +++ b/src/frontend/src/primitives/AlertDialog.tsx @@ -0,0 +1,5 @@ +import { Dialog, type DialogProps } from './Dialog' + +export const AlertDialog = (props: DialogProps) => { + return +} diff --git a/src/frontend/src/primitives/Badge.tsx b/src/frontend/src/primitives/Badge.tsx index 08ac9b06..cfca1470 100644 --- a/src/frontend/src/primitives/Badge.tsx +++ b/src/frontend/src/primitives/Badge.tsx @@ -10,7 +10,7 @@ const badge = cva({ }, variants: { size: { - small: { + sm: { textStyle: 'badge', }, normal: {}, diff --git a/src/frontend/src/primitives/Box.tsx b/src/frontend/src/primitives/Box.tsx index d774949f..528a6bee 100644 --- a/src/frontend/src/primitives/Box.tsx +++ b/src/frontend/src/primitives/Box.tsx @@ -3,6 +3,7 @@ import { styled } from '../styled-system/jsx' const box = cva({ base: { + position: 'relative', gap: 'gutter', borderRadius: 8, padding: 'boxPadding', @@ -14,13 +15,16 @@ const box = cva({ margin: 'auto', width: '38rem', maxWidth: '100%', - marginTop: '6rem', textAlign: 'center', }, popover: { padding: 'boxPadding.xs', minWidth: '10rem', }, + dialog: { + width: '30rem', + maxWidth: '100%', + }, }, variant: { default: { @@ -29,7 +33,6 @@ const box = cva({ borderColor: 'box.border', backgroundColor: 'box.bg', color: 'box.text', - boxShadow: 'box', }, subtle: { color: 'default.subtle-text', diff --git a/src/frontend/src/primitives/Button.tsx b/src/frontend/src/primitives/Button.tsx index 01b07ea4..9958f719 100644 --- a/src/frontend/src/primitives/Button.tsx +++ b/src/frontend/src/primitives/Button.tsx @@ -33,6 +33,9 @@ const button = cva({ paddingX: '0.5', paddingY: '0.25', }, + xs: { + borderRadius: 4, + }, }, variant: { default: { @@ -55,6 +58,12 @@ const button = cva({ }, }, }, + invisible: { + true: { + borderColor: 'none!', + backgroundColor: 'none!', + }, + }, }, defaultVariants: { size: 'default', diff --git a/src/frontend/src/primitives/Dialog.tsx b/src/frontend/src/primitives/Dialog.tsx new file mode 100644 index 00000000..2690bda0 --- /dev/null +++ b/src/frontend/src/primitives/Dialog.tsx @@ -0,0 +1,123 @@ +import { useContext, type ReactNode } from 'react' +import { + Dialog as RACDialog, + DialogTrigger, + Modal, + type DialogProps as RACDialogProps, + ModalOverlay, + Heading, + OverlayTriggerStateContext, +} from 'react-aria-components' +import { RiCloseLine } from '@remixicon/react' +import { styled } from '@/styled-system/jsx' +import { Box } from './Box' +import { Div } from './Div' +import { VerticallyOffCenter } from './VerticallyOffCenter' +import { text } from './Text' +import { Button } from './Button' +import { useTranslation } from 'react-i18next' + +const StyledModalOverlay = styled(ModalOverlay, { + base: { + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.4)', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + zIndex: 1000, + }, +}) + +// disabled pointerEvents on the stuff surrouding the overlay is there so that clicking on the overlay to close the modal still works +const StyledModal = styled(Modal, { + base: { + width: 'full', + height: 'full', + pointerEvents: 'none', + }, +}) + +const StyledRACDialog = styled(RACDialog, { + base: { + width: 'full', + height: 'full', + pointerEvents: 'none', + }, +}) + +export type DialogProps = { + title: string + children: [ + trigger: ReactNode, + dialogContent: + | (({ close }: { close: () => void }) => ReactNode) + | ReactNode, + ] +} & RACDialogProps + +/** + * a Dialog is a tuple of a trigger component (most usually a Button) that toggles some interactive content in a Dialog on top of the app + */ +export const Dialog = ({ title, children, ...dialogProps }: DialogProps) => { + const { t } = useTranslation() + const isAlert = dialogProps['role'] === 'alertdialog' + const [trigger, dialogContent] = children + return ( + + {trigger} + + + + {({ close }) => ( + +
+ + + {title} + + {typeof dialogContent === 'function' + ? dialogContent({ close }) + : dialogContent} + {!isAlert && ( +
+ +
+ )} +
+
+
+ )} +
+
+
+
+ ) +} + +export const useCloseDialog = () => { + const dialogState = useContext(OverlayTriggerStateContext)! + return dialogState.close +} diff --git a/src/frontend/src/primitives/P.tsx b/src/frontend/src/primitives/P.tsx index c613d357..98161779 100644 --- a/src/frontend/src/primitives/P.tsx +++ b/src/frontend/src/primitives/P.tsx @@ -1,5 +1,7 @@ import { Text, type As } from './Text' -export const P = (props: React.HTMLAttributes & As) => { +export const P = ( + props: React.HTMLAttributes & As & { last?: boolean } +) => { return } diff --git a/src/frontend/src/primitives/Text.tsx b/src/frontend/src/primitives/Text.tsx index 4fc1dcbe..ee136467 100644 --- a/src/frontend/src/primitives/Text.tsx +++ b/src/frontend/src/primitives/Text.tsx @@ -1,21 +1,30 @@ import type { HTMLAttributes } from 'react' import { RecipeVariantProps, cva, cx } from '@/styled-system/css' -const text = cva({ +export const text = cva({ base: {}, variants: { variant: { h1: { textStyle: 'h1', marginBottom: 'heading', + '&:not(:first-child)': { + paddingTop: 'heading', + }, }, h2: { textStyle: 'h2', marginBottom: 'heading', + '&:not(:first-child)': { + paddingTop: 'heading', + }, }, h3: { textStyle: 'h3', marginBottom: 'heading', + '&:not(:first-child)': { + paddingTop: 'heading', + }, }, body: { textStyle: 'body', @@ -24,8 +33,15 @@ const text = cva({ textStyle: 'body', marginBottom: 'paragraph', }, - small: { - textStyle: 'small', + display: { + textStyle: 'display', + marginBottom: 'heading', + }, + sm: { + textStyle: 'sm', + }, + note: { + color: 'default.subtle-text', }, inherits: {}, }, @@ -46,6 +62,14 @@ const text = cva({ false: { margin: '0!', }, + sm: { + marginBottom: 0.5, + }, + }, + last: { + true: { + marginBottom: '0!', + }, }, }, defaultVariants: { diff --git a/src/frontend/src/primitives/TextField.tsx b/src/frontend/src/primitives/TextField.tsx new file mode 100644 index 00000000..aedb845d --- /dev/null +++ b/src/frontend/src/primitives/TextField.tsx @@ -0,0 +1,62 @@ +import { styled } from '@/styled-system/jsx' +import { + Label, + Input, + TextField as RACTextField, + Text, + FieldError, +} from 'react-aria-components' + +const StyledRACTextField = styled(RACTextField, { + base: { + marginBottom: 'textfield', + }, +}) + +const StyledLabel = styled(Label, { + base: { + display: 'block', + }, +}) + +const StyledInput = styled(Input, { + base: { + width: 'full', + paddingY: 0.125, + paddingX: 0.25, + border: '1px solid', + borderColor: 'control.border', + color: 'control.text', + borderRadius: 4, + }, +}) + +const StyledDescription = styled(Text, { + base: { + display: 'block', + textStyle: 'sm', + color: 'default.subtle-text', + marginTop: 0.125, + }, +}) + +const StyledFieldError = styled(FieldError, { + base: { + display: 'block', + textStyle: 'sm', + color: 'danger', + marginTop: 0.125, + }, +}) + +export const TextField = ({ validate, label, description, ...inputProps }) => { + const labelFor = inputProps.id + return ( + + {label} + + {description} + + + ) +} diff --git a/src/frontend/src/primitives/VerticallyOffCenter.tsx b/src/frontend/src/primitives/VerticallyOffCenter.tsx new file mode 100644 index 00000000..ab308948 --- /dev/null +++ b/src/frontend/src/primitives/VerticallyOffCenter.tsx @@ -0,0 +1,23 @@ +import { type ReactNode } from 'react' +import { Div } from './Div' + +/** + * Renders children almost vertically centered (a bit closer to the top). + * + * This is useful because most of the time we want "vertically centered" things, + * we actually want them to be a bit closer to the top, + * as a perfectly centered box of content really looks off + */ +export const VerticallyOffCenter = ({ children }: { children: ReactNode }) => { + return ( +
+ {/* make sure we can't click on those empty layout-specific divs, + to prevent click issues for example on dialog modal overlays */} +
+
+ {children} +
+
+
+ ) +} diff --git a/src/frontend/src/primitives/index.ts b/src/frontend/src/primitives/index.ts index ff90427d..d8fdd70c 100644 --- a/src/frontend/src/primitives/index.ts +++ b/src/frontend/src/primitives/index.ts @@ -1,8 +1,10 @@ export { A } from './A' +export { AlertDialog } from './AlertDialog' export { Badge } from './Badge' export { Bold } from './Bold' export { Box } from './Box' export { Button } from './Button' +export { Dialog } from './Dialog' export { Div } from './Div' export { H } from './H' export { Hr } from './Hr' @@ -12,3 +14,5 @@ export { P } from './P' export { Popover } from './Popover' export { PopoverList } from './PopoverList' export { Text } from './Text' +export { TextField } from './TextField' +export { VerticallyOffCenter } from './VerticallyOffCenter'