mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 09:25: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": {
|
"appereance": {
|
||||||
"name": "Appearance",
|
"name": "Appearance",
|
||||||
"description": "Customize the background, colors and apps 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 Consola from 'consola';
|
||||||
|
import { TargetAndTransition, Transition, motion } from 'framer-motion';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ export const AppPing = ({ app }: AppPingProps) => {
|
|||||||
const active =
|
const active =
|
||||||
(config?.settings.customization.layout.enabledPing && app.network.enabledStatusChecker) ??
|
(config?.settings.customization.layout.enabledPing && app.network.enabledStatusChecker) ??
|
||||||
false;
|
false;
|
||||||
|
|
||||||
const { data, isLoading, isFetching, isSuccess } = api.app.ping.useQuery(app.id, {
|
const { data, isLoading, isFetching, isSuccess } = api.app.ping.useQuery(app.id, {
|
||||||
retry: false,
|
retry: false,
|
||||||
enabled: active,
|
enabled: active,
|
||||||
@@ -32,8 +35,37 @@ export const AppPing = ({ app }: AppPingProps) => {
|
|||||||
|
|
||||||
if (!active) return null;
|
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 (
|
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
|
<Tooltip
|
||||||
withinPortal
|
withinPortal
|
||||||
radius="lg"
|
radius="lg"
|
||||||
@@ -45,17 +77,40 @@ export const AppPing = ({ app }: AppPingProps) => {
|
|||||||
: `${data?.statusText} ${data?.status}`
|
: `${data?.statusText} ${data?.status}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{config?.settings.customization.accessibility?.replacePingDotsWithIcons ? (
|
||||||
|
<Box>
|
||||||
|
<AccessibleIndicatorPing isLoading={isLoading} isOnline={isOnline} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
<Indicator
|
<Indicator
|
||||||
size={15}
|
size={15}
|
||||||
processing={isSuccess}
|
color={isLoading ? 'yellow' : isOnline ? 'green' : 'red'}
|
||||||
color={isFetching ? 'yellow' : data?.state === 'online' ? 'green' : 'red'}
|
|
||||||
children={null}
|
children={null}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Tooltip>
|
</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) => {
|
export const getIsOk = (app: AppType, status: number) => {
|
||||||
if (app.network.okStatus === undefined || app.network.statusCodes.length >= 1) {
|
if (app.network.okStatus === undefined || app.network.statusCodes.length >= 1) {
|
||||||
Consola.log('Using new status codes');
|
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 { 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 { i18n, useTranslation } from 'next-i18next';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { GridstackConfiguration } from './Layout/GridstackConfiguration';
|
import { GridstackConfiguration } from './Layout/GridstackConfiguration';
|
||||||
@@ -13,6 +13,7 @@ import { ColorSelector } from './Theme/ColorSelector';
|
|||||||
import { CustomCssChanger } from './Theme/CustomCssChanger';
|
import { CustomCssChanger } from './Theme/CustomCssChanger';
|
||||||
import { DashboardTilesOpacitySelector } from './Theme/OpacitySelector';
|
import { DashboardTilesOpacitySelector } from './Theme/OpacitySelector';
|
||||||
import { ShadeSelector } from './Theme/ShadeSelector';
|
import { ShadeSelector } from './Theme/ShadeSelector';
|
||||||
|
import { AccessibilitySettings } from './Accessibility/AccessibilitySettings';
|
||||||
|
|
||||||
export const CustomizationSettingsAccordeon = () => {
|
export const CustomizationSettingsAccordeon = () => {
|
||||||
const items = getItems().map((item) => (
|
const items = getItems().map((item) => (
|
||||||
@@ -70,6 +71,13 @@ const getItems = () => {
|
|||||||
description: t('accordeon.gridstack.description'),
|
description: t('accordeon.gridstack.description'),
|
||||||
content: <GridstackConfiguration />,
|
content: <GridstackConfiguration />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'accessibility',
|
||||||
|
image: <IconAccessible />,
|
||||||
|
label: t('accordeon.accessibility.name'),
|
||||||
|
description: t('accordeon.accessibility.description'),
|
||||||
|
content: <AccessibilitySettings />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'page_metadata',
|
id: 'page_metadata',
|
||||||
image: <IconChartCandle />,
|
image: <IconChartCandle />,
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export const LayoutSelector = () => {
|
|||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={t('layout.enableping')}
|
label={t('layout.enableping')}
|
||||||
checked={ping}
|
checked={enabledPing}
|
||||||
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export function migrateConfig(config: Config): BackendConfigType {
|
|||||||
enabledRightSidebar: false,
|
enabledRightSidebar: false,
|
||||||
enabledSearchbar: config.modules.search?.enabled ?? true,
|
enabledSearchbar: config.modules.search?.enabled ?? true,
|
||||||
},
|
},
|
||||||
|
accessibility: {
|
||||||
|
disablePingPulse: false,
|
||||||
|
replacePingDotsWithIcons: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wrappers: [
|
wrappers: [
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const dashboardNamespaces = [
|
|||||||
'settings/general/internationalization',
|
'settings/general/internationalization',
|
||||||
'settings/general/search-engine',
|
'settings/general/search-engine',
|
||||||
'settings/general/widget-positions',
|
'settings/general/widget-positions',
|
||||||
|
'settings/customization/accessibility',
|
||||||
'settings/customization/general',
|
'settings/customization/general',
|
||||||
'settings/customization/color-selector',
|
'settings/customization/color-selector',
|
||||||
'settings/customization/page-appearance',
|
'settings/customization/page-appearance',
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ export interface CustomizationSettingsType {
|
|||||||
colors: ColorsCustomizationSettingsType;
|
colors: ColorsCustomizationSettingsType;
|
||||||
appOpacity?: number;
|
appOpacity?: number;
|
||||||
gridstack?: GridstackSettingsType;
|
gridstack?: GridstackSettingsType;
|
||||||
|
accessibility: AccessibilitySettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessibilitySettings {
|
||||||
|
disablePingPulse: boolean;
|
||||||
|
replacePingDotsWithIcons: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridstackSettingsType {
|
export interface GridstackSettingsType {
|
||||||
|
|||||||
Reference in New Issue
Block a user