mirror of
https://github.com/suitenumerique/meet.git
synced 2026-11-14 22:18:33 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06c4e277db | |||
| b6b3ce28db | |||
| f27f2fb7de | |||
| 061025b928 | |||
| 100044eeaa | |||
| e158b4faaf | |||
| 3a1f0a342d | |||
| 62571cd9fc | |||
| 8fccf46429 | |||
| 9481350383 | |||
| e9a58d270b | |||
| e3351075aa | |||
| 68198e9961 | |||
| e3140e0300 |
@@ -29,6 +29,7 @@ and this project adheres to
|
||||
- 🍱(frontend) add Portuguese glyphs to Marianne subsetted fonts
|
||||
- 🚸(frontend) bring new speakers to the top of the carousel
|
||||
- 🚸(frontend) bring raised hands into view in the carousel
|
||||
- ⚡️(frontend) serve illustrations as WebP with a PNG fallback
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ server {
|
||||
}
|
||||
|
||||
# Serve static files with caching
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|webp|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ server {
|
||||
}
|
||||
|
||||
# Serve static files with caching
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|webp|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ server {
|
||||
}
|
||||
|
||||
# Serve static files with caching
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|webp|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ const config: Config = {
|
||||
jsxFramework: 'react',
|
||||
outdir: 'src/styled-system',
|
||||
globalFontface: {},
|
||||
globalVars: {
|
||||
'--global-font-body': 'var(--fonts-sans)',
|
||||
},
|
||||
theme: {
|
||||
...pandaPreset.theme,
|
||||
// media queries are defined in em so that zooming with text-only mode triggers breakpoints
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -7,7 +7,11 @@ import {
|
||||
} from 'react-aria-components'
|
||||
import { RiArrowDownSFill } from '@remixicon/react'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import type { ConnectionTestStepResult } from '../types'
|
||||
import {
|
||||
DIAGNOSTIC_OUTPUT_LANG,
|
||||
getSummaryLang,
|
||||
type ConnectionTestStepResult,
|
||||
} from '../types'
|
||||
import { StepStatusIndicator } from './StepStatusIndicator'
|
||||
|
||||
/** Each step is its own bounded card, collapsed or not. */
|
||||
@@ -65,7 +69,7 @@ const labelClass = css({
|
||||
const valueClass = css({
|
||||
fontFamily: 'mono',
|
||||
textStyle: 'xs',
|
||||
color: 'greyscale.500',
|
||||
color: 'greyscale.600',
|
||||
overflowWrap: 'anywhere',
|
||||
})
|
||||
|
||||
@@ -110,7 +114,11 @@ const StepRowContent = ({ step }: { step: ConnectionTestStepResult }) => {
|
||||
<>
|
||||
<span className={identityClass}>
|
||||
<span className={labelClass}>{t(`steps.${step.id}`)}</span>
|
||||
{step.summary && <span className={valueClass}>{step.summary}</span>}
|
||||
{step.summary && (
|
||||
<span className={valueClass} lang={getSummaryLang(step)}>
|
||||
{step.summary}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<StepStatusIndicator
|
||||
status={step.status}
|
||||
@@ -170,7 +178,7 @@ export const ConnectionTestStepRow = ({
|
||||
{/* Collapsed panels stay in the DOM for aria-controls, but the log
|
||||
lines themselves are only mounted when actually visible. */}
|
||||
{isExpanded && (
|
||||
<ul className={logListClass}>
|
||||
<ul className={logListClass} lang={DIAGNOSTIC_OUTPUT_LANG}>
|
||||
{step.logs?.map((log, index) => (
|
||||
<li key={`${log.level}-${index}`} className={logItemClass}>
|
||||
{log.message}
|
||||
|
||||
@@ -153,7 +153,7 @@ const counterValueClass = css({
|
||||
})
|
||||
|
||||
/** A zero count is context, not a result: it recedes instead of shouting. */
|
||||
const emptyCounterClass = css({ color: 'greyscale.400' })
|
||||
const emptyCounterClass = css({ color: 'greyscale.600' })
|
||||
const emptySquareClass = css({
|
||||
backgroundColor: 'transparent!',
|
||||
border: '1px solid {colors.greyscale.250}',
|
||||
|
||||
@@ -25,10 +25,10 @@ export const statusSquareClass: Record<ConnectionTestStepStatus, string> = {
|
||||
* failure and warning.
|
||||
*/
|
||||
export const statusTextClass: Record<ConnectionTestStepStatus, string> = {
|
||||
pending: css({ color: 'greyscale.500' }),
|
||||
pending: css({ color: 'greyscale.600' }),
|
||||
running: css({ color: 'greyscale.700' }),
|
||||
success: css({ color: 'greyscale.1000' }),
|
||||
warning: css({ color: 'warning', fontWeight: 'medium' }),
|
||||
failed: css({ color: 'danger.600', fontWeight: 'medium' }),
|
||||
skipped: css({ color: 'greyscale.500' }),
|
||||
skipped: css({ color: 'greyscale.600' }),
|
||||
}
|
||||
|
||||
@@ -136,14 +136,16 @@ const ConnectionTest = () => {
|
||||
<h2 className={sectionTitleClass}>
|
||||
{t(`groups.${group.id}`)}
|
||||
</h2>
|
||||
<div className={rowsClass}>
|
||||
<ul className={rowsClass}>
|
||||
{group.steps.map((id) => {
|
||||
const step = stepsById.get(id)
|
||||
return step ? (
|
||||
<ConnectionTestStepRow key={id} step={step} />
|
||||
<li key={id}>
|
||||
<ConnectionTestStepRow step={step} />
|
||||
</li>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
|
||||
|
||||
@@ -32,6 +32,23 @@ export type ConnectionTestStepResult = {
|
||||
data?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const DIAGNOSTIC_OUTPUT_LANG = 'en'
|
||||
|
||||
const LANGUAGE_NEUTRAL_STEPS: ReadonlySet<ConnectionTestStepId> = new Set([
|
||||
'browser',
|
||||
'microphone',
|
||||
'camera',
|
||||
'devices',
|
||||
])
|
||||
|
||||
export const getSummaryLang = ({
|
||||
id,
|
||||
status,
|
||||
}: Pick<ConnectionTestStepResult, 'id' | 'status'>) =>
|
||||
LANGUAGE_NEUTRAL_STEPS.has(id) && status === 'success'
|
||||
? undefined
|
||||
: DIAGNOSTIC_OUTPUT_LANG
|
||||
|
||||
export type ConnectionTestGroupId = 'local' | 'network'
|
||||
|
||||
/** Display order: everything local first, then everything that leaves the machine. */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Button } from '@/primitives'
|
||||
import { Button, Picture } from '@/primitives'
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -30,29 +30,32 @@ const Body = styled('p', {
|
||||
},
|
||||
})
|
||||
|
||||
const Image = styled('img', {
|
||||
base: {
|
||||
maxHeight: '362px',
|
||||
height: '100%',
|
||||
width: 'fit-content',
|
||||
},
|
||||
const image = css({
|
||||
maxHeight: '362px',
|
||||
height: '100%',
|
||||
width: 'fit-content',
|
||||
})
|
||||
|
||||
const Dot = styled('div', {
|
||||
base: {
|
||||
borderRadius: '50%',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px',
|
||||
boxSizing: 'border-box',
|
||||
display: 'inline-block',
|
||||
height: '.375rem',
|
||||
height: '.5rem',
|
||||
margin: '0 .25rem',
|
||||
width: '.375rem',
|
||||
width: '.5rem',
|
||||
},
|
||||
variants: {
|
||||
selected: {
|
||||
true: {
|
||||
backgroundColor: 'primary.800',
|
||||
borderColor: 'primary.800',
|
||||
},
|
||||
false: {
|
||||
backgroundColor: 'primary.300',
|
||||
backgroundColor: 'primary.100',
|
||||
borderColor: 'primary.500',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -160,7 +163,6 @@ const carouselNavButton = css({
|
||||
},
|
||||
})
|
||||
|
||||
// todo - optimize how images are imported
|
||||
const SLIDES: Slide[] = [
|
||||
{
|
||||
key: 'slide1',
|
||||
@@ -246,7 +248,12 @@ export const IntroSlider = () => {
|
||||
visible={index === slideIndex}
|
||||
key={index}
|
||||
>
|
||||
<Image src={slide.src} alt="" role="presentation" />
|
||||
<Picture
|
||||
src={slide.src}
|
||||
alt=""
|
||||
role="presentation"
|
||||
className={image}
|
||||
/>
|
||||
<TextAnimation visible={index == slideIndex}>
|
||||
<Heading>{t(`${slide.key}.title`)}</Heading>
|
||||
<Body>{t(`${slide.key}.body`)}</Body>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { A, Div, H, Text } from '@/primitives'
|
||||
import { A, Div, H, Picture, Text } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoginPrompt } from './LoginPrompt'
|
||||
@@ -45,7 +45,7 @@ export const NoAccessView = ({
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
>
|
||||
<img
|
||||
<Picture
|
||||
src={imagePath}
|
||||
alt=""
|
||||
className={css({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Div, H, Text } from '@/primitives'
|
||||
import { Button, Div, Picture, Text } from '@/primitives'
|
||||
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
|
||||
@@ -169,7 +169,7 @@ export const ScreenRecordingSidePanel = () => {
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
>
|
||||
<img
|
||||
<Picture
|
||||
src="/assets/intro-slider/4.png"
|
||||
alt=""
|
||||
className={css({
|
||||
@@ -189,9 +189,9 @@ export const ScreenRecordingSidePanel = () => {
|
||||
})}
|
||||
/>
|
||||
<VStack gap={0} marginBottom={15}>
|
||||
<H lvl={1} margin={'sm'} fullWidth>
|
||||
<Text as="h2" variant="h1" margin="sm" fullWidth>
|
||||
{t('heading')}
|
||||
</H>
|
||||
</Text>
|
||||
<LimitDescription
|
||||
keyPrefix={'screenRecording'}
|
||||
supportArticleLink={data?.support?.help_article_recording}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { A, Button, Div, H, Text } from '@/primitives'
|
||||
import { A, Button, Div, Picture, Text } from '@/primitives'
|
||||
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
|
||||
@@ -183,7 +183,7 @@ export const TranscriptSidePanel = () => {
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
>
|
||||
<img
|
||||
<Picture
|
||||
src="/assets/intro-slider/3.png"
|
||||
alt=""
|
||||
className={css({
|
||||
@@ -203,9 +203,9 @@ export const TranscriptSidePanel = () => {
|
||||
})}
|
||||
/>
|
||||
<VStack gap={0} marginBottom={15}>
|
||||
<H lvl={1} margin={'sm'}>
|
||||
<Text as="h2" variant="h1" margin="sm" fullWidth>
|
||||
{t('heading')}
|
||||
</H>
|
||||
</Text>
|
||||
<LimitDescription
|
||||
keyPrefix={'transcript'}
|
||||
supportArticleLink={data?.support?.help_article_transcript}
|
||||
@@ -240,6 +240,9 @@ export const TranscriptSidePanel = () => {
|
||||
<Button
|
||||
variant="text"
|
||||
size="xs"
|
||||
aria-label={t('details.languageAriaLabel', {
|
||||
language: selectedLanguageLabel,
|
||||
})}
|
||||
onPress={() =>
|
||||
openSettingsDialog(SettingsDialogExtendedKey.TRANSCRIPTION)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { mediaUrl } from '@/api/mediaUrl'
|
||||
import { useUser } from '@/features/auth/api/useUser'
|
||||
import { UserAware } from '@/features/auth/components/UserAware'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { H, LinkButton, Text } from '@/primitives'
|
||||
import { H, LinkButton, Picture, Text } from '@/primitives'
|
||||
import { formatDate } from '@/utils/formatDate'
|
||||
import { ErrorScreen } from '@/components/ErrorScreen'
|
||||
import { LoadingScreen } from '@/components/LoadingScreen'
|
||||
@@ -118,7 +118,7 @@ const RecordingDownload = () => {
|
||||
<Screen layout="centered" footer={false}>
|
||||
<Center>
|
||||
<VStack>
|
||||
<img
|
||||
<Picture
|
||||
src="/assets/intro-slider/4.png"
|
||||
alt={''}
|
||||
className={css({
|
||||
|
||||
@@ -102,46 +102,93 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
|
||||
</Heading>
|
||||
)}
|
||||
<TabList border={false}>
|
||||
<Tab icon highlight id={SettingsDialogExtendedKey.ACCOUNT}>
|
||||
<Tab
|
||||
icon
|
||||
highlight
|
||||
id={SettingsDialogExtendedKey.ACCOUNT}
|
||||
aria-label={t(`tabs.${SettingsDialogExtendedKey.ACCOUNT}`)}
|
||||
>
|
||||
<RiAccountCircleLine />
|
||||
{isWideScreen && t(`tabs.${SettingsDialogExtendedKey.ACCOUNT}`)}
|
||||
</Tab>
|
||||
<Tab icon highlight id={SettingsDialogExtendedKey.AUDIO}>
|
||||
<Tab
|
||||
icon
|
||||
highlight
|
||||
id={SettingsDialogExtendedKey.AUDIO}
|
||||
aria-label={t(`tabs.${SettingsDialogExtendedKey.AUDIO}`)}
|
||||
>
|
||||
<RiSpeakerLine />
|
||||
{isWideScreen && t(`tabs.${SettingsDialogExtendedKey.AUDIO}`)}
|
||||
</Tab>
|
||||
<Tab icon highlight id={SettingsDialogExtendedKey.VIDEO}>
|
||||
<Tab
|
||||
icon
|
||||
highlight
|
||||
id={SettingsDialogExtendedKey.VIDEO}
|
||||
aria-label={t(`tabs.${SettingsDialogExtendedKey.VIDEO}`)}
|
||||
>
|
||||
<RiVideoOnLine />
|
||||
{isWideScreen && t(`tabs.${SettingsDialogExtendedKey.VIDEO}`)}
|
||||
</Tab>
|
||||
<Tab icon highlight id={SettingsDialogExtendedKey.GENERAL}>
|
||||
<Tab
|
||||
icon
|
||||
highlight
|
||||
id={SettingsDialogExtendedKey.GENERAL}
|
||||
aria-label={t(`tabs.${SettingsDialogExtendedKey.GENERAL}`)}
|
||||
>
|
||||
<RiSettings3Line />
|
||||
{isWideScreen && t(`tabs.${SettingsDialogExtendedKey.GENERAL}`)}
|
||||
</Tab>
|
||||
<Tab icon highlight id={SettingsDialogExtendedKey.NOTIFICATIONS}>
|
||||
<Tab
|
||||
icon
|
||||
highlight
|
||||
id={SettingsDialogExtendedKey.NOTIFICATIONS}
|
||||
aria-label={t(`tabs.${SettingsDialogExtendedKey.NOTIFICATIONS}`)}
|
||||
>
|
||||
<RiNotification3Line />
|
||||
{isWideScreen &&
|
||||
t(`tabs.${SettingsDialogExtendedKey.NOTIFICATIONS}`)}
|
||||
</Tab>
|
||||
<Tab icon highlight id={SettingsDialogExtendedKey.SHORTCUTS}>
|
||||
<Tab
|
||||
icon
|
||||
highlight
|
||||
id={SettingsDialogExtendedKey.SHORTCUTS}
|
||||
aria-label={t(`tabs.${SettingsDialogExtendedKey.SHORTCUTS}`)}
|
||||
>
|
||||
<RiKeyboardBoxLine />
|
||||
{isWideScreen && t(`tabs.${SettingsDialogExtendedKey.SHORTCUTS}`)}
|
||||
</Tab>
|
||||
{isAdminOrOwner && (
|
||||
<Tab icon highlight id={SettingsDialogExtendedKey.TRANSCRIPTION}>
|
||||
<Tab
|
||||
icon
|
||||
highlight
|
||||
id={SettingsDialogExtendedKey.TRANSCRIPTION}
|
||||
aria-label={t(
|
||||
`tabs.${SettingsDialogExtendedKey.TRANSCRIPTION}`
|
||||
)}
|
||||
>
|
||||
<Icon name="speech_to_text" />
|
||||
{isWideScreen &&
|
||||
t(`tabs.${SettingsDialogExtendedKey.TRANSCRIPTION}`)}
|
||||
</Tab>
|
||||
)}
|
||||
{showRecordingTab && (
|
||||
<Tab icon highlight id={SettingsDialogExtendedKey.RECORDING}>
|
||||
<Tab
|
||||
icon
|
||||
highlight
|
||||
id={SettingsDialogExtendedKey.RECORDING}
|
||||
aria-label={t(`tabs.${SettingsDialogExtendedKey.RECORDING}`)}
|
||||
>
|
||||
<RiRecordCircleLine />
|
||||
{isWideScreen &&
|
||||
t(`tabs.${SettingsDialogExtendedKey.RECORDING}`)}
|
||||
</Tab>
|
||||
)}
|
||||
<Tab icon highlight id={SettingsDialogExtendedKey.ACCESSIBILITY}>
|
||||
<Tab
|
||||
icon
|
||||
highlight
|
||||
id={SettingsDialogExtendedKey.ACCESSIBILITY}
|
||||
aria-label={t(`tabs.${SettingsDialogExtendedKey.ACCESSIBILITY}`)}
|
||||
>
|
||||
<RiEyeLine />
|
||||
{isWideScreen &&
|
||||
t(`tabs.${SettingsDialogExtendedKey.ACCESSIBILITY}`)}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const useLanguageLabels = () => {
|
||||
key: lang,
|
||||
value: lang,
|
||||
label: languageLabels[lang],
|
||||
lang,
|
||||
}))
|
||||
return {
|
||||
languagesList,
|
||||
|
||||
@@ -492,6 +492,7 @@
|
||||
"destination": "Ein neues Dokument wird erstellt auf",
|
||||
"destinationUnknown": "Ein neues Dokument wird erstellt",
|
||||
"language": "Meeting-Sprache:",
|
||||
"languageAriaLabel": "{{language}}, Meeting-Sprache ändern",
|
||||
"recording": "Auch eine Videoaufzeichnung starten"
|
||||
},
|
||||
"button": {
|
||||
|
||||
@@ -492,6 +492,7 @@
|
||||
"destination": "A new document will be created on",
|
||||
"destinationUnknown": "A new document will be created",
|
||||
"language": "Meeting language:",
|
||||
"languageAriaLabel": "{{language}}, change the meeting language",
|
||||
"recording": "Also start a video recording"
|
||||
},
|
||||
"button": {
|
||||
|
||||
@@ -491,6 +491,7 @@
|
||||
"destination": "Se creará un nuevo documento en",
|
||||
"destinationUnknown": "Se creará un nuevo documento",
|
||||
"language": "Idioma de la reunión:",
|
||||
"languageAriaLabel": "{{language}}, cambiar el idioma de la reunión",
|
||||
"recording": "Iniciar también una grabación de vídeo"
|
||||
},
|
||||
"button": {
|
||||
|
||||
@@ -492,6 +492,7 @@
|
||||
"destination": "Un nouveau document sera créé sur",
|
||||
"destinationUnknown": "Un nouveau document sera créé",
|
||||
"language": "Langue de la réunion :",
|
||||
"languageAriaLabel": "{{language}}, modifier la langue de la réunion",
|
||||
"recording": "Démarrer aussi un enregistrement vidéo"
|
||||
},
|
||||
"button": {
|
||||
|
||||
@@ -492,6 +492,7 @@
|
||||
"destination": "Er wordt een nieuw document aangemaakt op",
|
||||
"destinationUnknown": "Een nieuw document wordt aangemaakt",
|
||||
"language": "Vergadertalen:",
|
||||
"languageAriaLabel": "{{language}}, vergadertaal wijzigen",
|
||||
"recording": "Start ook een video-opname"
|
||||
},
|
||||
"button": {
|
||||
|
||||
@@ -492,6 +492,7 @@
|
||||
"destination": "Um novo documento será criado em",
|
||||
"destinationUnknown": "Um novo documento será criado",
|
||||
"language": "Idioma da reunião:",
|
||||
"languageAriaLabel": "{{language}}, alterar o idioma da reunião",
|
||||
"recording": "Também iniciar uma gravação"
|
||||
},
|
||||
"button": {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { type ComponentPropsWithoutRef } from 'react'
|
||||
import { css } from '@/styled-system/css'
|
||||
|
||||
const WEBP_CONVERTIBLE_EXTENSION = /\.(png|jpe?g)$/i
|
||||
|
||||
export type PictureProps = Omit<ComponentPropsWithoutRef<'img'>, 'src'> & {
|
||||
src: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Serves the WebP version of an image, and falls back on the original file
|
||||
* for browsers that do not support WebP.
|
||||
*
|
||||
* The `<picture>` wrapper uses `display: contents`, so the `<img>` is laid out
|
||||
* exactly as if it was rendered alone: style it through the usual props.
|
||||
*/
|
||||
export const Picture = ({ src, alt, ...props }: PictureProps) => {
|
||||
const hasWebpVersion = WEBP_CONVERTIBLE_EXTENSION.test(src)
|
||||
return (
|
||||
<picture className={css({ display: 'contents' })}>
|
||||
{hasWebpVersion && (
|
||||
<source
|
||||
srcSet={src.replace(WEBP_CONVERTIBLE_EXTENSION, '.webp')}
|
||||
type="image/webp"
|
||||
/>
|
||||
)}
|
||||
<img src={src} alt={alt} {...props} />
|
||||
</picture>
|
||||
)
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export type SelectProps<T> = Omit<
|
||||
> & {
|
||||
iconComponent?: RemixiconComponentType
|
||||
label: ReactNode
|
||||
items: Array<{ value: T; label: ReactNode }>
|
||||
items: Array<{ value: T; label: ReactNode; lang?: string }>
|
||||
errors?: ReactNode
|
||||
placement?: Placement
|
||||
variant?: 'light' | 'dark'
|
||||
@@ -150,7 +150,11 @@ export const Select = <T extends string | number>({
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<>
|
||||
{item.label}
|
||||
{item.lang ? (
|
||||
<span lang={item.lang}>{item.label}</span>
|
||||
) : (
|
||||
item.label
|
||||
)}
|
||||
{isSelected && (
|
||||
<VisuallyHidden>, {t('selected')}</VisuallyHidden>
|
||||
)}
|
||||
|
||||
@@ -23,6 +23,7 @@ export { Link } from './Link'
|
||||
export { Menu } from './Menu'
|
||||
export { MenuList } from './MenuList'
|
||||
export { P } from './P'
|
||||
export { Picture } from './Picture'
|
||||
export { Popover } from './Popover'
|
||||
export { ScreenReaderAnnouncer } from './ScreenReaderAnnouncer'
|
||||
export { Text } from './Text'
|
||||
|
||||
@@ -6,6 +6,13 @@ body,
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* RGAA 10.5: every text color must be paired with a declared background
|
||||
color, so custom browser or system colors cannot make text unreadable. */
|
||||
body {
|
||||
background-color: var(--colors-default-bg);
|
||||
color: var(--colors-default-text);
|
||||
}
|
||||
|
||||
:root {
|
||||
--avatar-cap-height: 0.7;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ data:
|
||||
}
|
||||
|
||||
# Serve static files with caching
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|webp|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user