Compare commits

...

3 Commits

Author SHA1 Message Date
Emmanuel Pelletier 27b0f44b2a 💄(dinum) Apply UI-Kit styles through custom CSS
Use hand-picked tokens from @gouvfr-lasuite/ui-tokens and apply them on
our components.
Depending on the context, we apply light or dark variants.
2026-10-19 13:19:55 +02:00
Emmanuel Pelletier 0b33806d52 💄(css) add [data-page] attr for easier theming through custom CSS
When customizing meet through the custom CSS file
(FRONTEND_CUSTOM_CSS_URL feature), it can be handy to easily know on
what page we are with CSS.
This exposes the current route on the `<html>` tag with a new
`[data-page]` attribute to allow easier targeting through custom CSS.
2026-10-19 13:15:53 +02:00
Emmanuel Pelletier 0fa8e45b3c 💄(css) add classes to components for easier theming through custom CSS
Since Panda generates atomic CSS, it's hard to modify styling through
custom CSS (FRONTEND_CUSTOM_CSS_URL feature).
This adds a few classes on some low-lvl components to allow easy
customization through the custom CSS file.
2026-10-19 13:13:18 +02:00
9 changed files with 310 additions and 47 deletions
+193 -32
View File
@@ -1,35 +1,3 @@
:root {
--fonts-sans: 'Marianne', ui-sans-serif, system-ui, sans-serif;
--avatar-cap-height: 0.7;
}
.Header-beforeLogo {
display: block;
}
.Header-beforeLogo::before {
content: '';
display: block;
background-image: url(/assets/marianne.svg);
background-position: 0 -0.046875rem;
background-size: 2.0625rem 0.84375rem;
height: 0.75rem;
margin-bottom: 0.1rem;
width: 2.0625rem;
}
.Header-beforeLogo::after {
content: '';
display: block;
background-image: url(/assets/gouvernement.svg), url(/assets/devise.svg);
background-repeat: no-repeat, no-repeat;
background-size: 108.8px 10px, 40px 29px;
background-position: 0 3px, 0 18.9px;
width: 108.8px;
height: 48px;
margin-top: 0.25rem;
}
@font-face {
font-family: 'Marianne';
src: url('/assets/fonts/Marianne-Regular-subset.woff2') format('woff2');
@@ -69,3 +37,196 @@
font-style: normal;
font-display: swap;
}
.Header-beforeLogo {
display: block;
}
.Header-beforeLogo::before {
content: '';
display: block;
background-image: url(/assets/marianne.svg);
background-position: 0 -0.046875rem;
background-size: 2.0625rem 0.84375rem;
height: 0.75rem;
margin-bottom: 0.1rem;
width: 2.0625rem;
}
.Header-beforeLogo::after {
content: '';
display: block;
background-image: url(/assets/gouvernement.svg), url(/assets/devise.svg);
background-repeat: no-repeat, no-repeat;
background-size: 108.8px 10px, 40px 29px;
background-position: 0 3px, 0 18.9px;
width: 108.8px;
height: 48px;
margin-top: 0.25rem;
}
:root {
--fonts-sans: Marianne, "Mona Sans VF", -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans Backtick Fix", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
--global-font-body: var(--fonts-sans);
--avatar-cap-height: 0.7;
/* use hand-picked variables from UI-kit and apply them to meet's variables */
--c--globals--colors--brand-050: #EDF0FF;
--c--globals--colors--brand-100: #DAE2FF;
--c--globals--colors--brand-250: #A2B6FF;
--c--globals--colors--brand-550: #3E5DE7;
--c--globals--colors--brand-600: #304DDF;
--c--globals--colors--brand-650: #2845C1;
--c--globals--colors--brand-700: #223E9E;
--c--globals--colors--gray-050: #F0F0F3;
--c--globals--colors--gray-100: #DFE2EA;
--c--globals--colors--gray-150: #CFD5DE;
--c--globals--colors--gray-250: #B2B9C7;
--c--globals--colors--gray-300: #A4ABBC;
--c--globals--colors--gray-550: #626A80;
--c--globals--colors--gray-600: #555E74;
--c--globals--colors--gray-650: #4A5267;
--c--globals--colors--gray-700: #3F4759;
--c--globals--colors--gray-750: #363B4C;
--c--globals--colors--gray-800: #2B303D;
--c--globals--colors--gray-850: #222631;
--c--globals--colors--gray-900: #181B24;
--c--globals--colors--white-050: #F6F8F90D;
--c--globals--colors--white-950: #F6F8F9F2;
--c--globals--colors--black-050: #181B240D;
--c--globals--colors--black-850: #181B24D9;
--c--globals--colors--error-550: #CF202D;
--c--globals--colors--error-650: #9D2227;
--colors-primary-800: var(--c--contextuals--background--semantic--brand--primary);
--colors-primary: var(--c--contextuals--background--semantic--brand--primary);
--colors-primary-action: var(--c--contextuals--background--semantic--brand--primary-hover);
--colors-default-subtle-text: var(--c--contextuals--content--semantic--neutral--secondary);
--colors-default-text: var(--c--contextuals--content--semantic--neutral--primary);
--colors-primary-subtle: var(--c--contextuals--background--semantic--brand--secondary);
--colors-primary-subtle-text: var(--c--contextuals--content--semantic--brand--secondary);
--colors-greyscale-250: var(--c--globals--colors--gray-100);
/* for the room's page background color */
--colors-primary-dark-50: var(--c--globals--colors--gray-900);
/* light theme specific UI-kit semantic values */
--c--contextuals--background--semantic--brand--primary: var(--c--globals--colors--brand-550);
--c--contextuals--background--semantic--brand--primary-hover: var(--c--globals--colors--brand-650);
--c--contextuals--content--semantic--brand--on-brand: var(--c--globals--colors--brand-050);
--c--contextuals--content--semantic--neutral--secondary: var(--c--globals--colors--gray-600);
--c--contextuals--content--semantic--neutral--primary: var(--c--globals--colors--gray-850);
--c--contextuals--background--semantic--brand--secondary: var(--c--globals--colors--brand-100);
--c--contextuals--content--semantic--brand--secondary: var(--c--globals--colors--brand-600);
--c--contextuals--content--semantic--brand--tertiary: var(--c--globals--colors--brand-550);
--c--contextuals--background--semantic--contextual--primary: var(--c--globals--colors--black-050);
--c--contextuals--content--semantic--contextual--primary: var(--c--globals--colors--white-950);
--c--contextuals--background--semantic--error--primary: var(--c--globals--colors--error-550);
--c--contextuals--background--semantic--error--primary-hover: var(--c--globals--colors--error-650);
--c--contextuals--background--semantic--neutral--tertiary: var(--c--globals--colors--gray-050);
--c--contextuals--border--semantic--neutral--tertiary: var(--c--globals--colors--gray-150);
--c--contextuals--content--semantic--neutral--tertiary: var(--c--globals--colors--gray-550);
/* try to match the UI-kit's control border color, but keep a 3:1 contrast ratio with white to pass WCAG rules on input borders */
--colors-control-border: #8996A9;
}
/* dark theme matching (room page) */
.Box--variant-dark,
.Tooltip,
#control-bar {
--c--contextuals--background--semantic--brand--primary: var(--c--globals--colors--brand-550);
--c--contextuals--background--semantic--brand--primary-hover: var(--c--globals--colors--brand-650);
--c--contextuals--content--semantic--brand--on-brand: var(--c--globals--colors--brand-050);
--c--contextuals--content--semantic--neutral--secondary: var(--c--globals--colors--gray-100);
--c--contextuals--content--semantic--neutral--primary: var(--c--globals--colors--gray-050);
--c--contextuals--background--semantic--brand--secondary: var(--c--globals--colors--brand-700);
--c--contextuals--content--semantic--brand--secondary: var(--c--globals--colors--brand-100);
--c--contextuals--content--semantic--brand--tertiary: var(--c--globals--colors--brand-250);
--c--contextuals--background--semantic--contextual--primary: var(--c--globals--colors--white-050);
--c--contextuals--content--semantic--contextual--primary: var(--c--globals--colors--black-850);
--c--contextuals--background--semantic--error--primary: var(--c--globals--colors--error-550);
--c--contextuals--background--semantic--error--primary-hover: var(--c--globals--colors--error-650);
--c--contextuals--background--semantic--neutral--secondary: var(--c--globals--colors--gray-700);
--c--contextuals--background--semantic--neutral--secondary-hover: var(--c--globals--colors--gray-750);
--c--contextuals--content--semantic--neutral--on-neutral: var(--c--globals--colors--gray-050);
--c--contextuals--background--surface--primary: var(--c--globals--colors--gray-800);
--c--contextuals--background--semantic--neutral--tertiary: var(--c--globals--colors--gray-750);
--c--contextuals--border--semantic--neutral--tertiary: var(--c--globals--colors--gray-700);
--c--contextuals--content--semantic--neutral--tertiary: var(--c--globals--colors--gray-250);
}
.Button--variant-primary {
background-color: var(--c--contextuals--background--semantic--brand--primary);
color: var(--c--contextuals--content--semantic--brand--on-brand);
}
.Button--variant-primary:is([data-hovered], [data-pressed]) {
background-color: var(--c--contextuals--background--semantic--brand--primary-hover);
}
.Button--variant-secondary {
border-color: #CFD5DE;
}
.Button--variant-secondaryText {
background: transparent;
color: var(--c--contextuals--content--semantic--brand--tertiary);
}
.Button--variant-secondaryText:is([data-hovered], [data-pressed]) {
background-color: var(--c--contextuals--background--semantic--contextual--primary);
}
.Button--variant-primaryDark {
background-color: var(--c--contextuals--background--semantic--neutral--secondary);
color: var(--c--contextuals--content--semantic--neutral--secondary);
}
.Button--variant-primaryDark:is([data-hovered], [data-pressed]) {
background-color: var(--c--contextuals--background--semantic--neutral--secondary-hover);
}
.Button--variant-danger {
background-color: var(--c--contextuals--background--semantic--error--primary);
color: var(--c--contextuals--content--semantic--brand--on-brand);
}
.Button--variant-danger:is([data-hovered], [data-pressed]) {
background-color: var(--c--contextuals--background--semantic--error--primary-hover);
}
.Button--variant-primaryTextDark {
background: transparent;
color: var(--c--contextuals--content--semantic--neutral--tertiary);
}
.Button--variant-primaryTextDark:is([data-hovered], [data-pressed]) {
background-color: var(--c--contextuals--background--semantic--contextual--primary);
}
.Box--variant-dark.Box--type-popover {
background-color: var(--c--contextuals--background--surface--primary);
}
.Tooltip {
background: var(--c--contextuals--background--semantic--neutral--tertiary);
border-color: var(--c--contextuals--border--semantic--neutral--tertiary);
color: var(--c--contextuals--content--semantic--neutral--tertiary);
}
/* switch component */
.indicator,
[data-selected="true"] .indicator::before {
background-color: var(--c--contextuals--content--semantic--contextual--primary);
}
.indicator .cross {
color: var(--c--contextuals--content--semantic--contextual--primary);
}
label:not([data-selected="true"]) .indicator::before,
[data-selected="true"] .indicator {
background: var(--colors-primary-800);
}
/* end switch component */
/* prevent impacting the toolbar buttons (primaryDark variant) */
.Button--size-default:is(.Button--variant-primary, .Button--variant-secondary) {
padding-block: 0.4375rem;
}
@@ -18,6 +18,7 @@ import { LogLevel, setLogLevel } from 'livekit-client'
import { useWatchDeviceAvailability } from '@/features/rooms/hooks/useWatchDeviceAvailability'
import { useWatchDeviceReleased } from '@/features/rooms/hooks/useWatchDeviceReleased'
import { useRoomPageTitle } from '@/features/rooms/livekit/hooks/useRoomPageTitle'
import { usePageDataAttribute } from '@/navigation/usePageDataAttribute'
const BaseRoom = ({ children }: { children: ReactNode }) => {
return (
@@ -38,10 +39,12 @@ const Room = () => {
const initialRoomData = history.state?.initialRoomData
const mode = isLoggedIn && history.state?.create ? 'create' : 'join'
const skipJoinScreen = isLoggedIn && mode === 'create'
const isJoinScreen = !hasSubmittedEntry && !skipJoinScreen
const { data } = useConfig()
useRoomPageTitle(roomId)
usePageDataAttribute(isJoinScreen ? 'join' : 'conference')
useEffect(() => {
const shouldSilenceLogs = data?.silence_livekit_debug_logs || false
@@ -75,7 +78,7 @@ const Room = () => {
return <ErrorScreen />
}
if (!hasSubmittedEntry && !skipJoinScreen) {
if (isJoinScreen) {
return (
<BaseRoom>
<Join enterRoom={() => setHasSubmittedEntry(true)} roomId={roomId} />
+3
View File
@@ -6,6 +6,7 @@ import { useSnapshot } from 'valtio'
import { Footer } from '@/layout/Footer'
import { ScreenReaderAnnouncer } from '@/primitives'
import { SkipLink, MAIN_CONTENT_ID } from './SkipLink'
import { usePageDataAttribute } from '@/navigation/usePageDataAttribute'
export type Layout = 'fullpage' | 'centered'
@@ -16,6 +17,8 @@ export type Layout = 'fullpage' | 'centered'
* In a specific page, use the `Screen` component and change its props to change global page layout.
*/
export const Layout = ({ children }: { children: ReactNode }) => {
usePageDataAttribute()
const layoutSnap = useSnapshot(layoutStore)
const showHeader = layoutSnap.showHeader
const showFooter = layoutSnap.showFooter
@@ -0,0 +1,57 @@
import { useEffect } from 'react'
import { matchRoute, useLocation, useRouter } from 'wouter'
import { proxy, useSnapshot } from 'valtio'
import { routes } from '@/routes'
const pageStore = proxy<{ override?: string }>({})
/**
* Apply a [data-page] attribute on the html element, that matches
* current route name, or a custom page name if provided.
*
* Used to allow easier customization of the app with custom CSS.
*
* This is meant to be used once without arguments in a wrapping component
* that renders the whole app. And, on a case by case basis with a `page` argument
* when needed.
*/
export const usePageDataAttribute = (page?: string) => {
const router = useRouter()
const [location] = useLocation()
const { override } = useSnapshot(pageStore)
useEffect(() => {
if (page === undefined) return
pageStore.override = page
return () => {
if (pageStore.override === page) {
pageStore.override = undefined
}
}
}, [page])
const routeName = Object.values(routes).find(
(route) => matchRoute(router.parser, route.path, location)[0]
)?.name
const pageName = page ?? override ?? routeName
useEffect(() => {
const root = document.documentElement
if (pageName) {
root.setAttribute('data-page', pageName)
} else {
root.removeAttribute('data-page')
}
}, [pageName])
useEffect(() => {
if (page !== undefined) return
return () => {
document.documentElement.removeAttribute('data-page')
}
}, [page])
}
+26 -5
View File
@@ -1,6 +1,12 @@
import { ComponentProps, forwardRef } from 'react'
import { cva } from '@/styled-system/css'
import { styled } from '@/styled-system/jsx'
const boxDefaultVariants = {
variant: 'light',
size: 'default',
} as const
const box = cva({
base: {
position: 'relative',
@@ -64,10 +70,25 @@ const box = cva({
},
},
},
defaultVariants: {
variant: 'light',
size: 'default',
},
defaultVariants: boxDefaultVariants,
})
export const Box = styled('div', box)
const StyledBox = styled('div', box)
export const Box = forwardRef<HTMLDivElement, ComponentProps<typeof StyledBox>>(
({ className, ...props }, ref) => (
<StyledBox
ref={ref}
className={[
// These are not necessary for the app, but can help people using custom CSS for theming
'Box',
`Box--variant-${props.variant || boxDefaultVariants.variant}`,
`Box--size-${props.size || boxDefaultVariants.size}`,
props.type && `Box--type-${props.type}`,
className,
].filter(Boolean).join(' ')}
{...props}
/>
)
)
Box.displayName = 'Box'
+9 -2
View File
@@ -3,7 +3,7 @@ import {
type ButtonProps as RACButtonsProps,
} from 'react-aria-components'
import { type RecipeVariantProps } from '@/styled-system/css'
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
import { buttonRecipe, buttonRecipeDefaultVariants, type ButtonRecipe } from './buttonRecipe'
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
import { ReactNode, forwardRef } from 'react'
import { Loader } from './Loader'
@@ -26,7 +26,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
<RACButton
ref={ref}
className={[buttonRecipe(variantProps), className].join(' ')}
className={[
buttonRecipe(variantProps),
className,
// These are not necessary for the app, but can help people using custom CSS for theming
'Button',
`Button--variant-${variantProps.variant || buttonRecipeDefaultVariants.variant}`,
`Button--size-${variantProps.size || buttonRecipeDefaultVariants.size}`,
].join(' ')}
{...(remainingComponentProps as RACButtonsProps)}
>
{!props.loading && props.icon}
+9 -2
View File
@@ -3,7 +3,7 @@ import {
ToggleButton as RACToggleButton,
ToggleButtonProps as RACToggleButtonProps,
} from 'react-aria-components'
import { type ButtonRecipeProps, buttonRecipe } from './buttonRecipe'
import { type ButtonRecipeProps, buttonRecipe, buttonRecipeDefaultVariants } from './buttonRecipe'
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
export type ToggleButtonProps = RACToggleButtonProps &
@@ -25,7 +25,14 @@ export const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(
<RACToggleButton
ref={ref}
{...componentProps}
className={[buttonRecipe(variantProps), props.className].join(' ')}
className={[
buttonRecipe(variantProps),
props.className,
// These are not necessary for the app, but can help people using custom CSS for theming
'Button Button--toggle',
`Button--variant-${variantProps.variant || buttonRecipeDefaultVariants.variant}`,
`Button--size-${variantProps.size || buttonRecipeDefaultVariants.size}`,
].join(' ')}
>
<>
{componentProps.children as ReactNode}
@@ -114,10 +114,11 @@ const TooltipArrow = () => {
const Tooltip = ({
children,
className,
...props
}: Omit<TooltipProps, 'children'> & { children: ReactNode }) => {
return (
<StyledTooltip {...props}>
<StyledTooltip {...props} className={[className, 'Tooltip'].filter(Boolean).join(' ')}>
<TooltipArrow />
{children}
</StyledTooltip>
+7 -4
View File
@@ -1,5 +1,11 @@
import { type RecipeVariantProps, cva } from '@/styled-system/css'
export const buttonRecipeDefaultVariants = {
size: 'default',
variant: 'primary',
} as const
export const buttonRecipe = cva({
base: {
display: 'flex',
@@ -393,10 +399,7 @@ export const buttonRecipe = cva({
},
},
],
defaultVariants: {
size: 'default',
variant: 'primary',
},
defaultVariants: buttonRecipeDefaultVariants,
})
export type ButtonRecipe = typeof buttonRecipe