mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 01:15:47 +01:00
🚸 Improve accessibility (#980)
* 🚸 Improve accessibility * 🌐 Add missing translations
This commit is contained in:
11
public/locales/en/settings/customization/accessibility.json
Normal file
11
public/locales/en/settings/customization/accessibility.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"disablePulse": {
|
||||
"label": "Disable ping pulse",
|
||||
"description": "By default, ping indicators in Homarr will pulse. This may be irritating. This slider will deactivate the animation"
|
||||
},
|
||||
"replaceIconsWithDots": {
|
||||
"label": "Replace ping dots with icons",
|
||||
"description": "For colorblind users, ping dots may be unrecognizable. This will replace indicators with icons"
|
||||
},
|
||||
"alert": "Are you missing something? We'll gladly extend the accessibility of Homarr"
|
||||
}
|
||||
@@ -16,6 +16,10 @@
|
||||
"appereance": {
|
||||
"name": "Appearance",
|
||||
"description": "Customize the background, colors and apps appearance"
|
||||
},
|
||||
"accessibility": {
|
||||
"name": "Accessibility",
|
||||
"description": "Configure Homarr for disabled and handicapped users"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Indicator, Tooltip } from '@mantine/core';
|
||||
import { Box, Indicator, Tooltip } from '@mantine/core';
|
||||
import { IconCheck, IconCheckbox, IconDownload, IconLoader, IconX } from '@tabler/icons-react';
|
||||
import Consola from 'consola';
|
||||
import { TargetAndTransition, Transition, motion } from 'framer-motion';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
@@ -16,6 +18,7 @@ export const AppPing = ({ app }: AppPingProps) => {
|
||||
const active =
|
||||
(config?.settings.customization.layout.enabledPing && app.network.enabledStatusChecker) ??
|
||||
false;
|
||||
|
||||
const { data, isLoading, isFetching, isSuccess } = api.app.ping.useQuery(app.id, {
|
||||
retry: false,
|
||||
enabled: active,
|
||||
@@ -32,8 +35,37 @@ export const AppPing = ({ app }: AppPingProps) => {
|
||||
|
||||
if (!active) return null;
|
||||
|
||||
const isOnline = data?.state === 'online';
|
||||
|
||||
const disablePulse = config?.settings.customization.accessibility?.disablePingPulse ?? false;
|
||||
const replaceDotWithIcon =
|
||||
config?.settings.customization.accessibility?.replacePingDotsWithIcons ?? false;
|
||||
|
||||
const scaleAnimation = isOnline ? [1, 0.7, 1] : 1;
|
||||
const animate: TargetAndTransition | undefined = disablePulse
|
||||
? undefined
|
||||
: {
|
||||
scale: scaleAnimation,
|
||||
};
|
||||
const transition: Transition | undefined = disablePulse
|
||||
? undefined
|
||||
: {
|
||||
repeat: Infinity,
|
||||
duration: 2.5,
|
||||
ease: 'easeInOut',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'absolute', bottom: 15, right: 15, zIndex: 2 }}>
|
||||
<motion.div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: replaceDotWithIcon ? 5 : 20,
|
||||
right: replaceDotWithIcon ? 8 : 20,
|
||||
zIndex: 2,
|
||||
}}
|
||||
animate={animate}
|
||||
transition={transition}
|
||||
>
|
||||
<Tooltip
|
||||
withinPortal
|
||||
radius="lg"
|
||||
@@ -45,17 +77,40 @@ export const AppPing = ({ app }: AppPingProps) => {
|
||||
: `${data?.statusText} ${data?.status}`
|
||||
}
|
||||
>
|
||||
<Indicator
|
||||
size={15}
|
||||
processing={isSuccess}
|
||||
color={isFetching ? 'yellow' : data?.state === 'online' ? 'green' : 'red'}
|
||||
children={null}
|
||||
/>
|
||||
{config?.settings.customization.accessibility?.replacePingDotsWithIcons ? (
|
||||
<Box>
|
||||
<AccessibleIndicatorPing isLoading={isLoading} isOnline={isOnline} />
|
||||
</Box>
|
||||
) : (
|
||||
<Indicator
|
||||
size={15}
|
||||
color={isLoading ? 'yellow' : isOnline ? 'green' : 'red'}
|
||||
children={null}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const AccessibleIndicatorPing = ({
|
||||
isLoading,
|
||||
isOnline,
|
||||
}: {
|
||||
isOnline: boolean;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
if (isOnline) {
|
||||
return <IconCheck color="green" />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <IconLoader />;
|
||||
}
|
||||
|
||||
return <IconX color="red" />;
|
||||
};
|
||||
|
||||
export const getIsOk = (app: AppType, status: number) => {
|
||||
if (app.network.okStatus === undefined || app.network.statusCodes.length >= 1) {
|
||||
Consola.log('Using new status codes');
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Alert, Stack, Switch } from '@mantine/core';
|
||||
import { IconInfoCircle } from '@tabler/icons-react';
|
||||
import { BaseSyntheticEvent } from 'react';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const AccessibilitySettings = () => {
|
||||
const { t } = useTranslation('settings/customization/accessibility');
|
||||
const { updateConfig } = useConfigStore();
|
||||
const { config, name: configName } = useConfigContext();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Switch
|
||||
label={t('disablePulse.label')}
|
||||
description={t('disablePulse.description')}
|
||||
defaultChecked={config?.settings.customization.accessibility?.disablePingPulse ?? false}
|
||||
onChange={(value: BaseSyntheticEvent) => {
|
||||
if (!configName) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateConfig(
|
||||
configName,
|
||||
(previousConfig) => ({
|
||||
...previousConfig,
|
||||
settings: {
|
||||
...previousConfig.settings,
|
||||
customization: {
|
||||
...previousConfig.settings.customization,
|
||||
accessibility: {
|
||||
...previousConfig.settings.customization.accessibility,
|
||||
disablePingPulse: value.target.checked,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
false,
|
||||
true
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={t('replaceIconsWithDots.label')}
|
||||
description={t('replaceIconsWithDots.description')}
|
||||
defaultChecked={config?.settings.customization.accessibility?.disablePingPulse ?? false}
|
||||
onChange={(value: BaseSyntheticEvent) => {
|
||||
if (!configName) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateConfig(
|
||||
configName,
|
||||
(previousConfig) => ({
|
||||
...previousConfig,
|
||||
settings: {
|
||||
...previousConfig.settings,
|
||||
customization: {
|
||||
...previousConfig.settings.customization,
|
||||
accessibility: {
|
||||
...previousConfig.settings.customization.accessibility,
|
||||
replacePingDotsWithIcons: value.target.checked,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
false,
|
||||
true
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Alert icon={<IconInfoCircle size="1rem" />} color="blue">
|
||||
{t('alert')}
|
||||
</Alert>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Accordion, Checkbox, Grid, Group, Stack, Text } from '@mantine/core';
|
||||
import { IconBrush, IconChartCandle, IconCode, IconDragDrop, IconLayout } from '@tabler/icons-react';
|
||||
import { IconAccessible, IconBrush, IconChartCandle, IconCode, IconDragDrop, IconLayout } from '@tabler/icons-react';
|
||||
import { i18n, useTranslation } from 'next-i18next';
|
||||
import { ReactNode } from 'react';
|
||||
import { GridstackConfiguration } from './Layout/GridstackConfiguration';
|
||||
@@ -13,6 +13,7 @@ import { ColorSelector } from './Theme/ColorSelector';
|
||||
import { CustomCssChanger } from './Theme/CustomCssChanger';
|
||||
import { DashboardTilesOpacitySelector } from './Theme/OpacitySelector';
|
||||
import { ShadeSelector } from './Theme/ShadeSelector';
|
||||
import { AccessibilitySettings } from './Accessibility/AccessibilitySettings';
|
||||
|
||||
export const CustomizationSettingsAccordeon = () => {
|
||||
const items = getItems().map((item) => (
|
||||
@@ -70,6 +71,13 @@ const getItems = () => {
|
||||
description: t('accordeon.gridstack.description'),
|
||||
content: <GridstackConfiguration />,
|
||||
},
|
||||
{
|
||||
id: 'accessibility',
|
||||
image: <IconAccessible />,
|
||||
label: t('accordeon.accessibility.name'),
|
||||
description: t('accordeon.accessibility.description'),
|
||||
content: <AccessibilitySettings />,
|
||||
},
|
||||
{
|
||||
id: 'page_metadata',
|
||||
image: <IconChartCandle />,
|
||||
|
||||
@@ -159,7 +159,7 @@ export const LayoutSelector = () => {
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('layout.enableping')}
|
||||
checked={ping}
|
||||
checked={enabledPing}
|
||||
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -44,6 +44,10 @@ export function migrateConfig(config: Config): BackendConfigType {
|
||||
enabledRightSidebar: false,
|
||||
enabledSearchbar: config.modules.search?.enabled ?? true,
|
||||
},
|
||||
accessibility: {
|
||||
disablePingPulse: false,
|
||||
replacePingDotsWithIcons: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wrappers: [
|
||||
|
||||
@@ -13,6 +13,7 @@ export const dashboardNamespaces = [
|
||||
'settings/general/internationalization',
|
||||
'settings/general/search-engine',
|
||||
'settings/general/widget-positions',
|
||||
'settings/customization/accessibility',
|
||||
'settings/customization/general',
|
||||
'settings/customization/color-selector',
|
||||
'settings/customization/page-appearance',
|
||||
|
||||
@@ -45,6 +45,12 @@ export interface CustomizationSettingsType {
|
||||
colors: ColorsCustomizationSettingsType;
|
||||
appOpacity?: number;
|
||||
gridstack?: GridstackSettingsType;
|
||||
accessibility: AccessibilitySettings;
|
||||
}
|
||||
|
||||
export interface AccessibilitySettings {
|
||||
disablePingPulse: boolean;
|
||||
replacePingDotsWithIcons: boolean;
|
||||
}
|
||||
|
||||
export interface GridstackSettingsType {
|
||||
|
||||
Reference in New Issue
Block a user