fix(frontend): make the panel usable with screen readers (#6907)

* fix(frontend): make the panel usable with screen readers

* fix(frontend): keep mobile tab names and the inbound expiry hint

Review of #6907 found two gaps in the screen-reader pass:

- On phone-width screens catTabLabel renders only the icon and names the
  tab by putting aria-label on it, but callers now pass that icon with
  aria-hidden for the desktop label, so every Settings, Xray and Host
  category tab lost its name under TalkBack. catTabLabel now clears
  aria-hidden on the icon it uses as the name.
- DateTimePicker forwarded id and aria-label but not aria-describedby,
  so the inbound expiry hint ("leave blank to never expire") was never
  attached. It now forwards it on both the Gregorian and Jalali input.
This commit is contained in:
Alireza Ahmadi
2026-10-23 18:29:47 +02:00
committed by GitHub
parent 775b2b0b26
commit e2184777a0
156 changed files with 2373 additions and 863 deletions
@@ -44,7 +44,12 @@ export default function ClientHwidListModal({
zIndex={zIndex}
onCancel={onClose}
footer={[
<Button key="refresh" icon={<ReloadOutlined />} loading={loading} onClick={onRefresh}>
<Button
key="refresh"
icon={<ReloadOutlined aria-hidden />}
loading={loading}
onClick={onRefresh}
>
{t('refresh')}
</Button>,
<Popconfirm
@@ -122,7 +127,7 @@ export default function ClientHwidListModal({
type="text"
size="small"
aria-label={t('pages.clients.deleteHwid')}
icon={<DeleteOutlined />}
icon={<DeleteOutlined aria-hidden />}
loading={deletingId === entry.id}
/>
</Popconfirm>
@@ -38,12 +38,17 @@ export default function ConfigBlock({
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
<div className="config-block-actions" onClick={(e: MouseEvent) => e.stopPropagation()}>
<Tooltip title={t('copy')}>
<Button size="small" icon={<CopyOutlined />} aria-label={t('copy')} onClick={copy} />
<Button
size="small"
icon={<CopyOutlined aria-hidden />}
aria-label={t('copy')}
onClick={copy}
/>
</Tooltip>
<Tooltip title={t('download')}>
<Button
size="small"
icon={<DownloadOutlined />}
icon={<DownloadOutlined aria-hidden />}
aria-label={t('download')}
onClick={() => FileManager.downloadTextFile(text, fileName)}
/>
@@ -56,7 +61,11 @@ export default function ConfigBlock({
content={<QrPanel value={text} remark={qrRemark || label} size={220} />}
>
<Tooltip title={t('pages.clients.qrCode')}>
<Button size="small" icon={<QrcodeOutlined />} aria-label={t('pages.clients.qrCode')} />
<Button
size="small"
icon={<QrcodeOutlined aria-hidden />}
aria-label={t('pages.clients.qrCode')}
/>
</Tooltip>
</Popover>
)}
@@ -262,9 +262,9 @@ export default function CommandPalette() {
title: c.email,
subtitle: `${trafficUsed} / ${trafficTotal}${c.comment ? ` · ${c.comment}` : ''}`,
icon: isOnline ? (
<CheckCircleFilled style={{ color: '#52c41a' }} />
<CheckCircleFilled aria-hidden style={{ color: '#52c41a' }} />
) : (
<CloseCircleFilled style={{ color: '#ff4d4f' }} />
<CloseCircleFilled aria-hidden style={{ color: '#ff4d4f' }} />
),
action: () => {
close();
@@ -274,7 +274,7 @@ export default function CommandPalette() {
c.subId && allSetting.subURI
? {
label: t('commandPalette.copySubscription'),
icon: <CopyOutlined />,
icon: <CopyOutlined aria-hidden />,
execute: (e) => {
e.stopPropagation();
copySubscription(c);
@@ -331,7 +331,7 @@ export default function CommandPalette() {
category: 'inbounds',
title: ib.remark || ib.tag || `Inbound #${ib.id}`,
subtitle: `Port ${ib.port || ''}`,
icon: <ImportOutlined style={{ color: '#1677ff' }} />,
icon: <ImportOutlined aria-hidden style={{ color: '#1677ff' }} />,
tag:
tags.length > 0 ? (
<div style={{ display: 'inline-flex', gap: 4, flexWrap: 'wrap' }}>{tags}</div>
@@ -348,7 +348,7 @@ export default function CommandPalette() {
path: '/',
title: t('menu.dashboard'),
keywords: ['overview', 'dashboard', 'cpu', 'ram', 'memory', 'traffic', 'speed'],
icon: <DashboardOutlined />,
icon: <DashboardOutlined aria-hidden />,
},
{
path: '/inbounds',
@@ -365,67 +365,67 @@ export default function CommandPalette() {
'hysteria',
'masque',
],
icon: <ImportOutlined />,
icon: <ImportOutlined aria-hidden />,
},
{
path: '/clients',
title: t('menu.clients'),
keywords: ['clients', 'users', 'sub', 'traffic', 'quota'],
icon: <TeamOutlined />,
icon: <TeamOutlined aria-hidden />,
},
{
path: '/groups',
title: t('menu.groups'),
keywords: ['groups', 'tags', 'batch'],
icon: <TagsOutlined />,
icon: <TagsOutlined aria-hidden />,
},
{
path: '/nodes',
title: t('menu.nodes'),
keywords: ['nodes', 'servers', 'cluster', 'remote nodes'],
icon: <ClusterOutlined />,
icon: <ClusterOutlined aria-hidden />,
},
{
path: '/hosts',
title: t('menu.hosts'),
keywords: ['hosts', 'sni', 'domains'],
icon: <GlobalOutlined />,
icon: <GlobalOutlined aria-hidden />,
},
{
path: '/outbound',
title: t('menu.outbounds'),
keywords: ['outbounds', 'freedom', 'blackhole', 'socks', 'http', 'warp', 'nord', 'pia'],
icon: <ExportOutlined />,
icon: <ExportOutlined aria-hidden />,
},
{
path: '/routing',
title: t('menu.routing'),
keywords: ['routing', 'rules', 'geoip', 'geosite', 'direct', 'block'],
icon: <SwapOutlined />,
icon: <SwapOutlined aria-hidden />,
},
{
path: '/settings',
title: t('menu.settings'),
keywords: ['settings', 'config', 'port', 'password', 'ssl', 'telegram'],
icon: <SettingOutlined />,
icon: <SettingOutlined aria-hidden />,
},
{
path: '/xray',
title: t('menu.xray'),
keywords: ['xray', 'templates', 'balancer', 'dns'],
icon: <ToolOutlined />,
icon: <ToolOutlined aria-hidden />,
},
{
path: '/api-docs',
title: t('menu.apiDocs'),
keywords: ['api', 'api docs', 'swagger', 'rest api', 'endpoints'],
icon: <ApiOutlined />,
icon: <ApiOutlined aria-hidden />,
},
{
path: '/sponsors',
title: t('menu.sponsors'),
keywords: ['sponsors', 'sponsor', 'partners'],
icon: <CrownOutlined />,
icon: <CrownOutlined aria-hidden />,
},
];
@@ -451,63 +451,63 @@ export default function CommandPalette() {
title: `${t('menu.settings')} · ${t('pages.settings.panelSettings')}`,
subtitle: t('pages.settings.panelSettings'),
keywords: ['general', 'webPort', 'webBasePath', 'listenIP', 'ssl', 'certificate'],
icon: <SettingOutlined />,
icon: <SettingOutlined aria-hidden />,
},
{
path: '/settings#security',
title: `${t('menu.settings')} · ${t('pages.settings.securitySettings')}`,
subtitle: t('pages.settings.securitySettings'),
keywords: ['security', 'password', 'username', '2fa', 'two factor', 'login limit'],
icon: <SafetyOutlined />,
icon: <SafetyOutlined aria-hidden />,
},
{
path: '/settings#telegram',
title: `${t('menu.settings')} · ${t('pages.settings.TGBotSettings')}`,
subtitle: t('pages.settings.TGBotSettings'),
keywords: ['telegram', 'tgbot', 'bot token', 'chat id', 'notifications', 'alerts'],
icon: <MessageOutlined />,
icon: <MessageOutlined aria-hidden />,
},
{
path: '/settings#email',
title: `${t('menu.settings')} · ${t('pages.settings.emailSettings')}`,
subtitle: t('pages.settings.emailSettings'),
keywords: ['email', 'smtp', 'mail', 'crash alerts'],
icon: <MailOutlined />,
icon: <MailOutlined aria-hidden />,
},
{
path: '/settings#discord',
title: `${t('menu.settings')} · ${t('pages.settings.discordSettings')}`,
subtitle: t('pages.settings.discordSettings'),
keywords: ['discord', 'bot', 'channel', 'notifications', 'alerts'],
icon: <DiscordOutlined />,
icon: <DiscordOutlined aria-hidden />,
},
{
path: '/settings#subscription',
title: `${t('menu.settings')} · ${t('pages.settings.subSettings')}`,
subtitle: t('pages.settings.subSettings'),
keywords: ['subscription', 'subPort', 'subURI', 'subDomain', 'reverse proxy'],
icon: <CloudServerOutlined />,
icon: <CloudServerOutlined aria-hidden />,
},
{
path: '/settings#subscription-formats',
title: `${t('menu.settings')} · ${t('menu.subFormats')}`,
subtitle: t('menu.subFormats'),
keywords: ['formats', 'clash', 'sing-box', 'v2ray', 'json', 'sub formats'],
icon: <CodeOutlined />,
icon: <CodeOutlined aria-hidden />,
},
{
path: '/settings#subscription-balancers',
title: `${t('menu.settings')} · ${t('pages.settings.subBalancers.menu')}`,
subtitle: t('pages.settings.subBalancers.menu'),
keywords: ['balancers', 'sub balancers', 'balancer nodes'],
icon: <ApartmentOutlined />,
icon: <ApartmentOutlined aria-hidden />,
},
{
path: '/xray#basic',
title: `${t('menu.xray')} · ${t('pages.xray.basicTemplate')}`,
subtitle: t('pages.xray.basicTemplate'),
keywords: ['basics', 'freedom strategy', 'happy eyeballs', 'torrent', 'connection'],
icon: <ToolOutlined />,
icon: <ToolOutlined aria-hidden />,
},
{
path: '/xray#basic',
@@ -522,56 +522,56 @@ export default function CommandPalette() {
'statsOutbound',
'metrics_out',
],
icon: <DashboardOutlined />,
icon: <DashboardOutlined aria-hidden />,
},
{
path: '/xray#basic',
title: `${t('menu.xray')} · ${t('pages.xray.connectionLimits')}`,
subtitle: t('pages.xray.connectionLimits'),
keywords: ['limits', 'idle timeout', 'bufferSize', 'connIdle', 'timeout'],
icon: <ClockCircleOutlined />,
icon: <ClockCircleOutlined aria-hidden />,
},
{
path: '/xray#basic',
title: `${t('menu.xray')} · ${t('pages.xray.logConfigs')}`,
subtitle: t('pages.xray.logConfigs'),
keywords: ['logs', 'access log', 'error log', 'dns log', 'mask address', 'loglevel'],
icon: <FileTextOutlined />,
icon: <FileTextOutlined aria-hidden />,
},
{
path: '/xray#balancer',
title: `${t('menu.xray')} · ${t('pages.xray.Balancers')}`,
subtitle: t('pages.xray.Balancers'),
keywords: ['balancers', 'leastPing', 'roundRobin', 'fallback', 'strategy'],
icon: <ClusterOutlined />,
icon: <ClusterOutlined aria-hidden />,
},
{
path: '/xray#dns',
title: `${t('menu.xray')} · DNS`,
subtitle: 'DNS',
keywords: ['dns', 'dns servers', 'hosts', 'doh', 'dot', 'cloudflare dns'],
icon: <DatabaseOutlined />,
icon: <DatabaseOutlined aria-hidden />,
},
{
path: '/xray#outbound',
title: `${t('menu.xray')} · ${t('pages.xray.Outbounds')}`,
subtitle: t('pages.xray.Outbounds'),
keywords: ['outbound', 'freedom', 'direct', 'proxy outbounds'],
icon: <ExportOutlined />,
icon: <ExportOutlined aria-hidden />,
},
{
path: '/xray#routing',
title: `${t('menu.xray')} · ${t('pages.xray.basicRouting')}`,
subtitle: t('pages.xray.basicRouting'),
keywords: ['routing', 'routing rules', 'geoip', 'geosite', 'block', 'direct'],
icon: <SwapOutlined />,
icon: <SwapOutlined aria-hidden />,
},
{
path: '/xray#advanced',
title: `${t('menu.xray')} · ${t('pages.xray.advancedTemplate')}`,
subtitle: t('pages.xray.advancedTemplate'),
keywords: ['advanced', 'json template', 'advanced config', 'custom json'],
icon: <CodeOutlined />,
icon: <CodeOutlined aria-hidden />,
},
];
@@ -599,7 +599,7 @@ export default function CommandPalette() {
title: t('commandPalette.restartXray'),
subtitle: t('pages.index.restartXray'),
keywords: ['restart', 'xray restart', 'reboot xray'],
icon: <ReloadOutlined style={{ color: '#faad14' }} />,
icon: <ReloadOutlined aria-hidden style={{ color: '#faad14' }} />,
action: restartXray,
},
{
@@ -608,7 +608,7 @@ export default function CommandPalette() {
title: t('menu.theme'),
subtitle: isUltra ? 'Ultra Dark' : isDark ? 'Dark' : 'Light',
keywords: ['theme', 'light', 'dark', 'ultra'],
icon: isDark ? <SunOutlined /> : <MoonOutlined />,
icon: isDark ? <SunOutlined aria-hidden /> : <MoonOutlined aria-hidden />,
action: cycleTheme,
},
{
@@ -617,7 +617,7 @@ export default function CommandPalette() {
title: t('pages.inbounds.addInbound'),
subtitle: t('menu.inbounds'),
keywords: ['add inbound', 'create inbound', 'new port', 'new inbound'],
icon: <PlusOutlined style={{ color: '#52c41a' }} />,
icon: <PlusOutlined aria-hidden style={{ color: '#52c41a' }} />,
action: () => {
close();
navigate('/inbounds');
@@ -629,7 +629,7 @@ export default function CommandPalette() {
title: t('pages.clients.addClient'),
subtitle: t('menu.clients'),
keywords: ['add client', 'create user', 'new client', 'new user'],
icon: <PlusOutlined style={{ color: '#52c41a' }} />,
icon: <PlusOutlined aria-hidden style={{ color: '#52c41a' }} />,
action: () => {
close();
navigate('/clients');
@@ -705,9 +705,9 @@ export default function CommandPalette() {
>
<div className="command-palette-header">
{isClientSearching ? (
<LoadingOutlined className="command-palette-search-icon spinning" />
<LoadingOutlined aria-hidden className="command-palette-search-icon spinning" />
) : (
<SearchOutlined className="command-palette-search-icon" />
<SearchOutlined aria-hidden className="command-palette-search-icon" />
)}
<input
ref={inputRef}
@@ -69,11 +69,11 @@ export default function TextModal({
footer={
<>
{fileName && (
<Button icon={<DownloadOutlined />} onClick={download}>
<Button icon={<DownloadOutlined aria-hidden />} onClick={download}>
{fileName}
</Button>
)}
<Button type="primary" icon={<CopyOutlined />} onClick={copy}>
<Button type="primary" icon={<CopyOutlined aria-hidden />} onClick={copy}>
{t('copy')}
</Button>
</>
@@ -19,6 +19,9 @@ interface DateTimePickerProps {
disabled?: boolean;
allowClear?: boolean;
maxDate?: Dayjs;
id?: string;
'aria-label'?: string;
'aria-describedby'?: string;
}
const LIGHT_THEME = {
@@ -57,6 +60,9 @@ export default function DateTimePicker({
disabled = false,
allowClear = true,
maxDate,
id,
'aria-label': ariaLabel,
'aria-describedby': ariaDescribedBy,
}: DateTimePickerProps) {
const { t } = useTranslation();
const { datepicker } = useDatepicker();
@@ -97,7 +103,11 @@ export default function DateTimePicker({
useEffect(() => {
if (datepicker !== 'jalalian') return;
const input = jalaliRef.current?.querySelector('input');
if (input) input.placeholder = placeholder;
if (!input) return;
input.placeholder = placeholder;
if (id) input.id = id;
if (ariaLabel) input.setAttribute('aria-label', ariaLabel);
if (ariaDescribedBy) input.setAttribute('aria-describedby', ariaDescribedBy);
});
if (datepicker === 'jalalian') {
@@ -137,7 +147,7 @@ export default function DateTimePicker({
setClearNonce((n) => n + 1);
}}
>
<CloseCircleFilled />
<CloseCircleFilled aria-hidden />
</button>
)}
</div>
@@ -156,6 +166,9 @@ export default function DateTimePicker({
disabled={disabled}
allowClear={allowClear}
maxDate={maxDate}
id={id}
aria-label={ariaLabel}
aria-describedby={ariaDescribedBy}
style={{ width: '100%' }}
/>
);
@@ -125,21 +125,23 @@ export default function HeaderMapEditor({ mode, value, onChange }: HeaderMapEdit
<Input
value={row.name}
placeholder="Name"
aria-label={t('pages.inbounds.form.headerName')}
onChange={(e) => setRow(idx, { name: e.target.value })}
/>
<Input
value={row.value}
placeholder="Value"
aria-label={t('pages.inbounds.form.headerValue')}
onChange={(e) => setRow(idx, { value: e.target.value })}
/>
<Button
aria-label={t('remove')}
icon={<MinusOutlined />}
icon={<MinusOutlined aria-hidden />}
onClick={() => removeRow(idx)}
/>
</Space.Compact>
))}
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={addRow}>
<Button size="small" type="primary" icon={<PlusOutlined aria-hidden />} onClick={addRow}>
Add
</Button>
</>
@@ -22,6 +22,9 @@ interface RemarkTemplateFieldProps {
multiline?: boolean;
rows?: number;
metadataOnly?: boolean;
id?: string;
'aria-label'?: string;
'aria-labelledby'?: string;
}
/**
@@ -37,6 +40,9 @@ export default function RemarkTemplateField({
multiline = false,
rows,
metadataOnly = false,
id,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
}: RemarkTemplateFieldProps) {
const { t } = useTranslation();
const inputRef = useRef<InputRef>(null);
@@ -76,7 +82,7 @@ export default function RemarkTemplateField({
<Button
type="text"
size="small"
icon={<CodeOutlined />}
icon={<CodeOutlined aria-hidden />}
aria-label={t('pages.hosts.remarkVars.title')}
style={{ marginInlineEnd: -7 }}
/>
@@ -90,6 +96,9 @@ export default function RemarkTemplateField({
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
<Input.TextArea
ref={textAreaRef}
id={id}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
value={value}
maxLength={maxLength}
placeholder={placeholder}
@@ -101,6 +110,9 @@ export default function RemarkTemplateField({
) : (
<Input
ref={inputRef}
id={id}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
value={value}
maxLength={maxLength}
placeholder={placeholder}
+14 -2
View File
@@ -49,8 +49,13 @@ export function FormField<T extends FieldValues = FieldValues>({
const { t } = useTranslation();
const dottedName = toDotted(name) as Path<T>;
const generatedId = useId();
const explicitId = (children as ReactElement<{ id?: string }>).props.id;
const fieldId = explicitId ?? generatedId;
const hintId = useId();
const childOwnProps = (children as ReactElement<{ id?: string; 'aria-describedby'?: string }>)
.props;
const fieldId = childOwnProps.id ?? generatedId;
const describedBy = tooltip
? [childOwnProps['aria-describedby'], hintId].filter(Boolean).join(' ')
: undefined;
return (
<Controller
@@ -74,6 +79,7 @@ export function FormField<T extends FieldValues = FieldValues>({
onBlur: field.onBlur,
ref: field.ref,
};
if (describedBy) childProps['aria-describedby'] = describedBy;
return (
<Form.Item
label={label}
@@ -88,6 +94,12 @@ export function FormField<T extends FieldValues = FieldValues>({
style={style}
>
{cloneElement(children as ReactElement<Record<string, unknown>>, childProps)}
{/* The tooltip only opens on hover, so screen readers get its text as a description. */}
{tooltip && (
<span id={hintId} hidden>
{tooltip}
</span>
)}
</Form.Item>
);
}}
@@ -332,6 +332,7 @@ export default function GeoBrowserModal({
value={categoryQuery}
onChange={(event) => setCategoryQuery(event.target.value)}
placeholder={t('pages.xray.geoBrowser.searchCategory')}
aria-label={t('pages.xray.geoBrowser.searchCategory')}
allowClear
/>
<Button
@@ -392,6 +393,7 @@ export default function GeoBrowserModal({
value={entryQuery}
onChange={(event) => setEntryQuery(event.target.value)}
placeholder={t('pages.xray.geoBrowser.searchEntries')}
aria-label={t('pages.xray.geoBrowser.searchEntries')}
allowClear
className="geo-entry-filter"
/>
@@ -30,6 +30,7 @@ export interface GeoTokenInputProps {
kind: GeoTokenKind;
placeholder?: string;
id?: string;
'aria-describedby'?: string;
ref?: Ref<InputRef>;
}
@@ -40,6 +41,7 @@ export default function GeoTokenInput({
kind,
placeholder,
id,
'aria-describedby': ariaDescribedBy,
ref,
}: GeoTokenInputProps) {
const { t } = useTranslation();
@@ -92,6 +94,7 @@ export default function GeoTokenInput({
<Input
ref={ref}
id={id}
aria-describedby={ariaDescribedBy}
value={value}
placeholder={placeholder}
onChange={(event) => onChange?.(event.target.value)}
@@ -99,7 +102,7 @@ export default function GeoTokenInput({
/>
<Tooltip title={t('pages.xray.geoBrowser.openTooltip')}>
<Button
icon={<DatabaseOutlined />}
icon={<DatabaseOutlined aria-hidden />}
aria-label={t('pages.xray.geoBrowser.openTooltip')}
onClick={() => setBrowsing(true)}
/>
@@ -63,13 +63,13 @@ export default function SponsorCard({ sponsor, variant = 'banner', onClose }: Sp
<span className="sponsor-aside">
<span className="sponsor-tag">{tag}</span>
<span className="sponsor-visit">
{t('pages.sponsors.visit')} <ExportOutlined />
{t('pages.sponsors.visit')} <ExportOutlined aria-hidden />
</span>
</span>
)}
{variant === 'card' && (
<span className="sponsor-visit">
{t('pages.sponsors.visit')} <ExportOutlined />
{t('pages.sponsors.visit')} <ExportOutlined aria-hidden />
</span>
)}
</a>
@@ -81,7 +81,7 @@ export default function SponsorCard({ sponsor, variant = 'banner', onClose }: Sp
title={t('close')}
onClick={onClose}
>
<CloseOutlined />
<CloseOutlined aria-hidden />
</button>
)}
</div>
@@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types';
const GROUPS: NotificationGroupConfig[] = [
{
icon: <CloudServerOutlined />,
icon: <CloudServerOutlined aria-hidden />,
title: 'eventGroupOutbound',
events: [
{ key: 'outbound.down', label: 'eventOutboundDown', settingKey: '' },
@@ -21,12 +21,12 @@ const GROUPS: NotificationGroupConfig[] = [
],
},
{
icon: <ThunderboltOutlined />,
icon: <ThunderboltOutlined aria-hidden />,
title: 'eventGroupXray',
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
},
{
icon: <DesktopOutlined />,
icon: <DesktopOutlined aria-hidden />,
title: 'eventGroupNode',
events: [
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
@@ -34,7 +34,7 @@ const GROUPS: NotificationGroupConfig[] = [
],
},
{
icon: <DashboardOutlined />,
icon: <DashboardOutlined aria-hidden />,
title: 'eventGroupSystem',
events: [
{
@@ -72,7 +72,7 @@ const GROUPS: NotificationGroupConfig[] = [
],
},
{
icon: <SafetyOutlined />,
icon: <SafetyOutlined aria-hidden />,
title: 'eventGroupSecurity',
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
},
@@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types';
const GROUPS: NotificationGroupConfig[] = [
{
icon: <CloudServerOutlined />,
icon: <CloudServerOutlined aria-hidden />,
title: 'eventGroupOutbound',
events: [
{
@@ -36,12 +36,12 @@ const GROUPS: NotificationGroupConfig[] = [
],
},
{
icon: <ThunderboltOutlined />,
icon: <ThunderboltOutlined aria-hidden />,
title: 'eventGroupXray',
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
},
{
icon: <DesktopOutlined />,
icon: <DesktopOutlined aria-hidden />,
title: 'eventGroupNode',
events: [
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
@@ -49,7 +49,7 @@ const GROUPS: NotificationGroupConfig[] = [
],
},
{
icon: <DashboardOutlined />,
icon: <DashboardOutlined aria-hidden />,
title: 'eventGroupSystem',
events: [
{
@@ -87,7 +87,7 @@ const GROUPS: NotificationGroupConfig[] = [
],
},
{
icon: <SafetyOutlined />,
icon: <SafetyOutlined aria-hidden />,
title: 'eventGroupSecurity',
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
},
@@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types';
const GROUPS: NotificationGroupConfig[] = [
{
icon: <CloudServerOutlined />,
icon: <CloudServerOutlined aria-hidden />,
title: 'eventGroupOutbound',
events: [
{
@@ -36,12 +36,12 @@ const GROUPS: NotificationGroupConfig[] = [
],
},
{
icon: <ThunderboltOutlined />,
icon: <ThunderboltOutlined aria-hidden />,
title: 'eventGroupXray',
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
},
{
icon: <DesktopOutlined />,
icon: <DesktopOutlined aria-hidden />,
title: 'eventGroupNode',
events: [
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
@@ -49,7 +49,7 @@ const GROUPS: NotificationGroupConfig[] = [
],
},
{
icon: <DashboardOutlined />,
icon: <DashboardOutlined aria-hidden />,
title: 'eventGroupSystem',
events: [
{
@@ -87,7 +87,7 @@ const GROUPS: NotificationGroupConfig[] = [
],
},
{
icon: <SafetyOutlined />,
icon: <SafetyOutlined aria-hidden />,
title: 'eventGroupSecurity',
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
},