♻️(frontend) introduce an Icon primitive
Encapsulate icon and symbol rendering in a dedicated component that applies aria-hidden and disables translation attributes. This prevents browsers from translating icon names and breaking the UI, and ensures screen readers do not announce decorative icons. This is a first draft and can be extended with additional variants later.
This commit is contained in:
committed by
aleb_the_flash
parent
d7f1b7b94c
commit
c47e830b40
@@ -1,3 +1,5 @@
|
|||||||
|
import '@fontsource/material-icons-outlined'
|
||||||
|
import '@fontsource-variable/material-symbols-outlined'
|
||||||
import '@livekit/components-styles'
|
import '@livekit/components-styles'
|
||||||
import '@/styles/index.css'
|
import '@/styles/index.css'
|
||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css, cx } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { HStack } from '@/styled-system/jsx'
|
import { HStack } from '@/styled-system/jsx'
|
||||||
import { Spinner } from '@/primitives/Spinner'
|
import { Spinner } from '@/primitives/Spinner'
|
||||||
import { Button, Text } from '@/primitives'
|
import { Button, Icon, Text } from '@/primitives'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RecordingStatuses } from '../hooks/useRecordingStatuses'
|
import { RecordingStatuses } from '../hooks/useRecordingStatuses'
|
||||||
import { ReactNode, useEffect, useRef, useState } from 'react'
|
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||||
@@ -141,31 +141,23 @@ export const ControlsButton = ({
|
|||||||
})}
|
})}
|
||||||
onPress={() => openSidePanel()}
|
onPress={() => openSidePanel()}
|
||||||
>
|
>
|
||||||
<span
|
<Icon
|
||||||
className={cx(
|
className={css({
|
||||||
'material-icons',
|
color: 'primary.500',
|
||||||
css({
|
marginRight: '1rem',
|
||||||
color: 'primary.500',
|
})}
|
||||||
marginRight: '1rem',
|
name="info"
|
||||||
})
|
/>
|
||||||
)}
|
|
||||||
>
|
|
||||||
info
|
|
||||||
</span>
|
|
||||||
<Text variant={'smNote'}>
|
<Text variant={'smNote'}>
|
||||||
{parseLineBreaks(t('button.anotherModeStarted'))}
|
{parseLineBreaks(t('button.anotherModeStarted'))}
|
||||||
</Text>
|
</Text>
|
||||||
<span
|
<Icon
|
||||||
className={cx(
|
className={css({
|
||||||
'material-icons',
|
color: 'primary.500',
|
||||||
css({
|
marginLeft: 'auto',
|
||||||
color: 'primary.500',
|
})}
|
||||||
marginLeft: 'auto',
|
name="chevron_right"
|
||||||
})
|
/>
|
||||||
)}
|
|
||||||
>
|
|
||||||
chevron_right
|
|
||||||
</span>
|
|
||||||
</RACButton>
|
</RACButton>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { H, Text } from '@/primitives'
|
import { H, Text, Icon } from '@/primitives'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { LoginButton } from '@/components/LoginButton'
|
import { LoginButton } from '@/components/LoginButton'
|
||||||
import { HStack } from '@/styled-system/jsx'
|
import { HStack } from '@/styled-system/jsx'
|
||||||
@@ -24,9 +24,7 @@ export const LoginPrompt = ({ heading, body }: LoginPromptProps) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<HStack justify="start" alignItems="center" marginBottom="0.5rem">
|
<HStack justify="start" alignItems="center" marginBottom="0.5rem">
|
||||||
<span className="material-symbols" aria-hidden={true}>
|
<Icon type="symbols" name="login" />
|
||||||
login
|
|
||||||
</span>
|
|
||||||
<H lvl={3} margin={false} padding={false}>
|
<H lvl={3} margin={false} padding={false}>
|
||||||
{heading}
|
{heading}
|
||||||
</H>
|
</H>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Spinner } from '@/primitives/Spinner'
|
import { Spinner } from '@/primitives/Spinner'
|
||||||
|
import { Icon } from '@/primitives'
|
||||||
|
|
||||||
interface RecordingStatusIconProps {
|
interface RecordingStatusIconProps {
|
||||||
isStarted: boolean
|
isStarted: boolean
|
||||||
@@ -14,8 +15,8 @@ export const RecordingStatusIcon = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isTranscriptActive) {
|
if (isTranscriptActive) {
|
||||||
return <span className="material-symbols">speech_to_text</span>
|
return <Icon type="symbols" name="speech_to_text" />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span className="material-symbols">screen_record</span>
|
return <Icon type="symbols" name="screen_record" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, H, Text } from '@/primitives'
|
import { Button, Icon, H, Text } from '@/primitives'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { HStack } from '@/styled-system/jsx'
|
import { HStack } from '@/styled-system/jsx'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
@@ -59,7 +59,7 @@ export const RequestRecording = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<HStack justify="start" alignItems="center" marginBottom="0.5rem">
|
<HStack justify="start" alignItems="center" marginBottom="0.5rem">
|
||||||
<span className="material-symbols">person_raised_hand</span>
|
<Icon type="symbols" name="person_raised_hand" />
|
||||||
<H lvl={3} margin={false} padding={false}>
|
<H lvl={3} margin={false} padding={false}>
|
||||||
{heading}
|
{heading}
|
||||||
</H>
|
</H>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
import { Icon } from '@/primitives'
|
||||||
|
|
||||||
type RowPosition = 'first' | 'middle' | 'last' | 'single'
|
type RowPosition = 'first' | 'middle' | 'last' | 'single'
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ export const RowWrapper = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* fixme - doesn't handle properly material-symbols */}
|
{/* fixme - doesn't handle properly material-symbols */}
|
||||||
<span className="material-icons">{iconName}</span>
|
<Icon name={iconName} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { A, Div, Text } from '@/primitives'
|
import { A, Div, Icon, Text } from '@/primitives'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { Button as RACButton } from 'react-aria-components'
|
import { Button as RACButton } from 'react-aria-components'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -87,7 +87,7 @@ const ToolButton = ({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span className="material-symbols">chevron_forward</span>
|
<Icon type="symbols" name="chevron_forward" />
|
||||||
</div>
|
</div>
|
||||||
</RACButton>
|
</RACButton>
|
||||||
)
|
)
|
||||||
@@ -163,7 +163,7 @@ export const Tools = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
{isTranscriptEnabled && (
|
{isTranscriptEnabled && (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
icon={<span className="material-symbols">speech_to_text</span>}
|
icon={<Icon type="symbols" name="speech_to_text" />}
|
||||||
title={t('tools.transcript.title')}
|
title={t('tools.transcript.title')}
|
||||||
description={t('tools.transcript.body')}
|
description={t('tools.transcript.body')}
|
||||||
onPress={() => openTranscript()}
|
onPress={() => openTranscript()}
|
||||||
@@ -171,7 +171,7 @@ export const Tools = () => {
|
|||||||
)}
|
)}
|
||||||
{isScreenRecordingEnabled && (
|
{isScreenRecordingEnabled && (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
icon={<span className="material-symbols">mode_standby</span>}
|
icon={<Icon type="symbols" name="mode_standby" />}
|
||||||
title={t('tools.screenRecording.title')}
|
title={t('tools.screenRecording.title')}
|
||||||
description={t('tools.screenRecording.body')}
|
description={t('tools.screenRecording.body')}
|
||||||
onPress={() => openScreenRecording()}
|
onPress={() => openScreenRecording()}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Dialog, type DialogProps } from '@/primitives'
|
|||||||
import { Tab, Tabs, TabList } from '@/primitives/Tabs.tsx'
|
import { Tab, Tabs, TabList } from '@/primitives/Tabs.tsx'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { text } from '@/primitives/Text.tsx'
|
import { text } from '@/primitives/Text.tsx'
|
||||||
|
import { Icon } from '@/primitives/Icon'
|
||||||
import { Heading } from 'react-aria-components'
|
import { Heading } from 'react-aria-components'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
@@ -106,7 +107,7 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
|
|||||||
</Tab>
|
</Tab>
|
||||||
{isAdminOrOwner && (
|
{isAdminOrOwner && (
|
||||||
<Tab icon highlight id={SettingsDialogExtendedKey.TRANSCRIPTION}>
|
<Tab icon highlight id={SettingsDialogExtendedKey.TRANSCRIPTION}>
|
||||||
<span className="material-symbols">speech_to_text</span>
|
<Icon type="symbols" name="speech_to_text" />
|
||||||
{isWideScreen &&
|
{isWideScreen &&
|
||||||
t(`tabs.${SettingsDialogExtendedKey.TRANSCRIPTION}`)}
|
t(`tabs.${SettingsDialogExtendedKey.TRANSCRIPTION}`)}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
71
src/frontend/src/primitives/Icon.tsx
Normal file
71
src/frontend/src/primitives/Icon.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { cva, RecipeVariantProps } from '@/styled-system/css'
|
||||||
|
import { ComponentPropsWithoutRef } from 'react'
|
||||||
|
|
||||||
|
const iconRecipe = cva({
|
||||||
|
base: {
|
||||||
|
fontWeight: 'normal',
|
||||||
|
fontStyle: 'normal',
|
||||||
|
display: 'inline-block',
|
||||||
|
lineHeight: 1,
|
||||||
|
textTransform: 'none',
|
||||||
|
letterSpacing: 'normal',
|
||||||
|
wordWrap: 'normal',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
direction: 'ltr',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
type: {
|
||||||
|
icons: {
|
||||||
|
fontFamily: 'Material Icons Outlined',
|
||||||
|
webkitFontSmoothing: 'antialiased',
|
||||||
|
mozOsxFontSmoothing: 'grayscale',
|
||||||
|
textRendering: 'optimizeLegibility',
|
||||||
|
fontFeatureSettings: '"liga"',
|
||||||
|
},
|
||||||
|
symbols: {
|
||||||
|
fontFamily: 'Material Symbols Outlined Variable',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
sm: {
|
||||||
|
fontSize: '18px',
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
fontSize: '32px',
|
||||||
|
},
|
||||||
|
xl: {
|
||||||
|
fontSize: '40px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
type: 'icons',
|
||||||
|
size: 'md',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type IconRecipeProps = RecipeVariantProps<typeof iconRecipe>
|
||||||
|
|
||||||
|
export type IconProps = IconRecipeProps &
|
||||||
|
ComponentPropsWithoutRef<'span'> & {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Icon = ({ name, ...props }: IconProps) => {
|
||||||
|
const [variantProps, componentProps] = iconRecipe.splitVariantProps(props)
|
||||||
|
const { className, ...remainingComponentProps } = componentProps
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
translate="no"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={[iconRecipe(variantProps), className].join(' ')}
|
||||||
|
{...remainingComponentProps}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -30,3 +30,4 @@ export { Ul } from './Ul'
|
|||||||
export { VerticallyOffCenter } from './VerticallyOffCenter'
|
export { VerticallyOffCenter } from './VerticallyOffCenter'
|
||||||
export { TextArea } from './TextArea'
|
export { TextArea } from './TextArea'
|
||||||
export { Switch } from './Switch'
|
export { Switch } from './Switch'
|
||||||
|
export { Icon } from './Icon'
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
@import '@fontsource/material-icons-outlined';
|
|
||||||
|
|
||||||
.material-icons,
|
|
||||||
.material-symbols {
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 24px;
|
|
||||||
display: inline-block;
|
|
||||||
line-height: 1;
|
|
||||||
text-transform: none;
|
|
||||||
letter-spacing: normal;
|
|
||||||
word-wrap: normal;
|
|
||||||
white-space: nowrap;
|
|
||||||
direction: ltr;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
font-feature-settings: 'liga';
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-icons {
|
|
||||||
font-family: 'Material Icons Outlined';
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-symbols {
|
|
||||||
font-family: 'Material Symbols Outlined Variable';
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
@import './livekit.css';
|
@import './livekit.css';
|
||||||
@import './icons.css';
|
|
||||||
@layer reset, base, tokens, recipes, utilities;
|
@layer reset, base, tokens, recipes, utilities;
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
|
|||||||
Reference in New Issue
Block a user