💄(home) have a cleaner homepage layout
- try a new layout for the homepage, making it easier to join a meeting - add a new Dialog component to easily build dialogs
This commit is contained in:
9
src/frontend/package-lock.json
generated
9
src/frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 (
|
||||
<Screen>
|
||||
<Box type="screen">
|
||||
<H lvl={1}>{t('heading')}</H>
|
||||
<P>{t('intro')}</P>
|
||||
<Div marginBottom="gutter">
|
||||
<Box variant="subtle" size="sm">
|
||||
{isLoggedIn ? (
|
||||
<Button variant="primary" onPress={() => navigateToNewRoom()}>
|
||||
{t('createMeeting')}
|
||||
<Screen type="splash">
|
||||
<VerticallyOffCenter>
|
||||
<Div margin="auto" width="fit-content">
|
||||
<Text as="h1" variant="display">
|
||||
{t('heading')}
|
||||
</Text>
|
||||
<Text as="p" variant="h3">
|
||||
{t('intro')}
|
||||
</Text>
|
||||
{!isLoggedIn && (
|
||||
<Text margin="sm" variant="note">
|
||||
{t('loginToCreateMeeting')}
|
||||
</Text>
|
||||
)}
|
||||
<HStack gap="gutter">
|
||||
<Button
|
||||
variant="primary"
|
||||
onPress={isLoggedIn ? () => navigateToNewRoom() : undefined}
|
||||
href={isLoggedIn ? undefined : authUrl()}
|
||||
>
|
||||
{isLoggedIn ? t('createMeeting') : t('login', { ns: 'global' })}
|
||||
</Button>
|
||||
|
||||
<Dialog title={t('joinMeeting')}>
|
||||
<Button variant="primary" outline>
|
||||
{t('joinMeeting')}
|
||||
</Button>
|
||||
) : (
|
||||
<p>
|
||||
<A href={authUrl()}>{t('login')}</A>
|
||||
</p>
|
||||
)}
|
||||
</Box>
|
||||
<JoinMeetingDialogContent />
|
||||
</Dialog>
|
||||
</HStack>
|
||||
</Div>
|
||||
<P>
|
||||
<Italic>{t('or')}</Italic>
|
||||
</P>
|
||||
<Box variant="subtle" size="sm">
|
||||
<p>{t('copyMeetingUrl')}</p>
|
||||
</Box>
|
||||
</Box>
|
||||
</VerticallyOffCenter>
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
|
||||
const JoinMeetingDialogContent = () => {
|
||||
const { t } = useTranslation('home')
|
||||
const closeDialog = useCloseDialog()
|
||||
const fieldOk = /^.*([a-z]{3}-[a-z]{4}-[a-z]{3})$/
|
||||
return (
|
||||
<Div>
|
||||
<Form
|
||||
onSubmit={(event) => {
|
||||
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]}`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
id="join-meeting-input"
|
||||
label={t('joinInputLabel')}
|
||||
description={t('joinInputExample', {
|
||||
example: 'https://meet.numerique.gouv.fr/azer-tyu-qsdf',
|
||||
})}
|
||||
validate={(value) => {
|
||||
if (!fieldOk.test(value)) {
|
||||
return t('joinInputError')
|
||||
}
|
||||
return null
|
||||
}}
|
||||
/>
|
||||
<HStack gap="gutter">
|
||||
<Button type="submit" variant="primary">
|
||||
{t('joinInputSubmit')}
|
||||
</Button>
|
||||
<Button variant="primary" outline onPress={closeDialog}>
|
||||
{t('cancel', { ns: 'global' })}
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
<H lvl={2}>{t('joinMeetingTipHeading')}</H>
|
||||
<P last>{t('joinMeetingTipContent')}</P>
|
||||
</Div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<BoxDiv type="screen">
|
||||
{!!title && <H lvl={1}>{title}</H>}
|
||||
{children}
|
||||
{!!withBackButton && (
|
||||
<p>
|
||||
<Link to="/" size="small">
|
||||
{t('backToHome')}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</BoxDiv>
|
||||
<VerticallyOffCenter>
|
||||
<BoxDiv type="screen">
|
||||
{!!title && <H lvl={1}>{title}</H>}
|
||||
{children}
|
||||
{!!withBackButton && (
|
||||
<p>
|
||||
<Link to="/" size="sm">
|
||||
{t('backToHome')}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</BoxDiv>
|
||||
</VerticallyOffCenter>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ export const Header = () => {
|
||||
borderBottomStyle: 'solid',
|
||||
padding: 1,
|
||||
flexShrink: 0,
|
||||
boxShadow: 'box',
|
||||
})}
|
||||
>
|
||||
<Stack direction="row" justify="space-between" align="center">
|
||||
@@ -36,7 +35,7 @@ export const Header = () => {
|
||||
{!!user && (
|
||||
<p className={flex({ gap: 1, align: 'center' })}>
|
||||
<Badge>{user.email}</Badge>
|
||||
<A href={logoutUrl()} size="small">
|
||||
<A href={logoutUrl()} size="sm">
|
||||
{t('logout')}
|
||||
</A>
|
||||
</p>
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={css({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'default.bg',
|
||||
backgroundColor: type === 'splash' ? 'white' : 'default.bg',
|
||||
color: 'default.text',
|
||||
})}
|
||||
>
|
||||
<Header />
|
||||
{type !== 'splash' && <Header />}
|
||||
<main
|
||||
className={css({
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"app": "Meet",
|
||||
"backToHome": "",
|
||||
"cancel": "",
|
||||
"closeDialog": "",
|
||||
"error": {
|
||||
"heading": ""
|
||||
},
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
{
|
||||
"copyMeetingUrl": "",
|
||||
"createMeeting": "",
|
||||
"heading": "",
|
||||
"intro": "",
|
||||
"login": "",
|
||||
"or": ""
|
||||
"joinInputError": "",
|
||||
"joinInputExample": "",
|
||||
"joinInputLabel": "",
|
||||
"joinInputSubmit": "",
|
||||
"joinMeeting": "",
|
||||
"joinMeetingTipContent": "",
|
||||
"joinMeetingTipHeading": "",
|
||||
"loginToCreateMeeting": ""
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ const link = cva({
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
small: {
|
||||
textStyle: 'small',
|
||||
sm: {
|
||||
textStyle: 'sm',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
5
src/frontend/src/primitives/AlertDialog.tsx
Normal file
5
src/frontend/src/primitives/AlertDialog.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Dialog, type DialogProps } from './Dialog'
|
||||
|
||||
export const AlertDialog = (props: DialogProps) => {
|
||||
return <Dialog role="alertdialog" {...props} />
|
||||
}
|
||||
@@ -10,7 +10,7 @@ const badge = cva({
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
small: {
|
||||
sm: {
|
||||
textStyle: 'badge',
|
||||
},
|
||||
normal: {},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
123
src/frontend/src/primitives/Dialog.tsx
Normal file
123
src/frontend/src/primitives/Dialog.tsx
Normal file
@@ -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 (
|
||||
<DialogTrigger>
|
||||
{trigger}
|
||||
<StyledModalOverlay
|
||||
isKeyboardDismissDisabled={isAlert}
|
||||
isDismissable={!isAlert}
|
||||
>
|
||||
<StyledModal>
|
||||
<StyledRACDialog {...dialogProps}>
|
||||
{({ close }) => (
|
||||
<VerticallyOffCenter>
|
||||
<Div
|
||||
width="fit-content"
|
||||
maxWidth="full"
|
||||
margin="auto"
|
||||
pointerEvents="auto"
|
||||
>
|
||||
<Box size="sm" type="dialog">
|
||||
<Heading
|
||||
slot="title"
|
||||
level={1}
|
||||
className={text({ variant: 'h1' })}
|
||||
>
|
||||
{title}
|
||||
</Heading>
|
||||
{typeof dialogContent === 'function'
|
||||
? dialogContent({ close })
|
||||
: dialogContent}
|
||||
{!isAlert && (
|
||||
<Div position="absolute" top="0" right="0">
|
||||
<Button
|
||||
invisible
|
||||
size="xs"
|
||||
onPress={() => close()}
|
||||
aria-label={t('closeDialog')}
|
||||
>
|
||||
<RiCloseLine />
|
||||
</Button>
|
||||
</Div>
|
||||
)}
|
||||
</Box>
|
||||
</Div>
|
||||
</VerticallyOffCenter>
|
||||
)}
|
||||
</StyledRACDialog>
|
||||
</StyledModal>
|
||||
</StyledModalOverlay>
|
||||
</DialogTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCloseDialog = () => {
|
||||
const dialogState = useContext(OverlayTriggerStateContext)!
|
||||
return dialogState.close
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Text, type As } from './Text'
|
||||
|
||||
export const P = (props: React.HTMLAttributes<HTMLElement> & As) => {
|
||||
export const P = (
|
||||
props: React.HTMLAttributes<HTMLElement> & As & { last?: boolean }
|
||||
) => {
|
||||
return <Text as="p" variant="paragraph" {...props} />
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
62
src/frontend/src/primitives/TextField.tsx
Normal file
62
src/frontend/src/primitives/TextField.tsx
Normal file
@@ -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 (
|
||||
<StyledRACTextField validate={validate}>
|
||||
<StyledLabel htmlFor={labelFor}>{label}</StyledLabel>
|
||||
<StyledInput {...inputProps} />
|
||||
<StyledDescription slot="description">{description}</StyledDescription>
|
||||
<StyledFieldError />
|
||||
</StyledRACTextField>
|
||||
)
|
||||
}
|
||||
23
src/frontend/src/primitives/VerticallyOffCenter.tsx
Normal file
23
src/frontend/src/primitives/VerticallyOffCenter.tsx
Normal file
@@ -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 (
|
||||
<Div display="flex" flexDirection="column" width="full" height="full">
|
||||
{/* make sure we can't click on those empty layout-specific divs,
|
||||
to prevent click issues for example on dialog modal overlays */}
|
||||
<Div flex="1 1 35%" pointerEvents="none" />
|
||||
<Div width="full" flex="1 1 100%">
|
||||
{children}
|
||||
</Div>
|
||||
<Div flex="1 1 65%" pointerEvents="none" />
|
||||
</Div>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user