App tile UI change (#1231)

* 💄 Rework the App tile UI

* 🤡 Forgot one

* Make it so the app title gets hidden properly

Now if the value is missing it won't by "hover" or "hidden" so it won't hide

* Turn the `Tooltip` into `HoverCard`

* Make save and cancel button not wrap anymore

* 💄 Used InfoCard in options + translations

* ♻️ Remove fallback value for label translations

---------

Co-authored-by: Thomas Camlong <49837342+ajnart@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Tagaishi
2023-08-06 19:36:36 +02:00
committed by GitHub
parent 121d6eafab
commit 7d18a51d02
10 changed files with 216 additions and 99 deletions

View File

@@ -25,6 +25,10 @@
"label": "Open in new tab", "label": "Open in new tab",
"description": "Open the app in a new tab instead of the current one." "description": "Open the app in a new tab instead of the current one."
}, },
"tooltipDescription":{
"label": "Application Description",
"description": "The text you enter will appear when hovering over your app.\r\nUse this to give users more details about your app or leave empty to have nothing."
},
"customProtocolWarning": "Using a non-standard protocol. This may require pre-installed applications and can introduce security risks. Ensure that your address is secure and trusted." "customProtocolWarning": "Using a non-standard protocol. This may require pre-installed applications and can introduce security risks. Ensure that your address is secure and trusted."
}, },
"network": { "network": {
@@ -49,6 +53,25 @@
"title": "Loading external icons", "title": "Loading external icons",
"text": "This may take a few seconds" "text": "This may take a few seconds"
} }
},
"appNameStatus":{
"label":"App Name Status",
"description":"Choose where you want the title to show up, if at all.",
"dropdown": {
"normal":"Show title on tile only",
"hover":"Show title on tooltip hover only",
"hidden":"Don't show at all"
}
},
"positionAppName":{
"label":"App Name Position",
"description":"Position of the app's name relative to the icon.",
"dropdown": {
"top":"Top",
"right":"Right",
"bottom":"Bottom",
"left":"Left"
}
} }
}, },
"integration": { "integration": {

View File

@@ -211,7 +211,7 @@ export const EditAppModal = ({
<IntegrationTab form={form} /> <IntegrationTab form={form} />
</Tabs> </Tabs>
<Group position="right" mt="md"> <Group noWrap position="right" mt="md">
<Button onClick={closeModal} px={50} variant="light" color="gray"> <Button onClick={closeModal} px={50} variant="light" color="gray">
{t('common:cancel')} {t('common:cancel')}
</Button> </Button>

View File

@@ -1,6 +1,7 @@
import { Flex, Tabs } from '@mantine/core'; import { Flex, Select, Stack, Switch, Tabs } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { useTranslation } from 'next-i18next';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { AppType } from '../../../../../../types/app'; import { AppType } from '../../../../../../types/app';
@@ -19,6 +20,7 @@ export const AppearanceTab = ({
}: AppearanceTabProps) => { }: AppearanceTabProps) => {
const iconSelectorRef = useRef(); const iconSelectorRef = useRef();
const [debouncedValue] = useDebouncedValue(form.values.name, 500); const [debouncedValue] = useDebouncedValue(form.values.name, 500);
const { t } = useTranslation('layout/modals/add-app');
useEffect(() => { useEffect(() => {
if (allowAppNamePropagation !== true) { if (allowAppNamePropagation !== true) {
@@ -38,17 +40,54 @@ export const AppearanceTab = ({
return ( return (
<Tabs.Panel value="appearance" pt="lg"> <Tabs.Panel value="appearance" pt="lg">
<Flex gap={5}> <Stack spacing="xs">
<IconSelector <Flex gap={5} mb="xs">
defaultValue={form.values.appearance.iconUrl} <IconSelector
defaultValue={form.values.appearance.iconUrl}
onChange={(value) => {
form.setFieldValue('appearance.iconUrl', value);
disallowAppNameProgagation();
}}
value={form.values.appearance.iconUrl}
ref={iconSelectorRef}
/>
</Flex>
<Select
label={t('appearance.appNameStatus.label')}
description={t('appearance.appNameStatus.description')}
data={[
{ value: 'normal', label: t('appearance.appNameStatus.dropdown.normal') as string },
{ value: 'hover', label: t('appearance.appNameStatus.dropdown.hover') as string },
{ value: 'hidden', label: t('appearance.appNameStatus.dropdown.hidden') as string },
]}
{...form.getInputProps('appearance.appNameStatus')}
onChange={(value) => { onChange={(value) => {
form.setFieldValue('appearance.iconUrl', value); form.setFieldValue('appearance.appNameStatus', value);
disallowAppNameProgagation();
}} }}
value={form.values.appearance.iconUrl}
ref={iconSelectorRef}
/> />
</Flex> {form.values.appearance.appNameStatus === 'normal' && (
<Select
label={t('appearance.positionAppName.label')}
description={t('appearance.positionAppName.description')}
data={[
{ value: 'column', label: t('appearance.positionAppName.dropdown.top') as string },
{
value: 'row-reverse',
label: t('appearance.positionAppName.dropdown.right') as string,
},
{
value: 'column-reverse',
label: t('appearance.positionAppName.dropdown.bottom') as string,
},
{ value: 'row', label: t('appearance.positionAppName.dropdown.left') as string },
]}
{...form.getInputProps('appearance.positionAppName')}
onChange={(value) => {
form.setFieldValue('appearance.positionAppName', value);
}}
/>
)}
</Stack>
</Tabs.Panel> </Tabs.Panel>
); );
}; };

View File

@@ -1,8 +1,9 @@
import { Switch, Tabs } from '@mantine/core'; import { Text, TextInput, Tooltip, Stack, Switch, Tabs, Group, useMantineTheme, HoverCard } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { AppType } from '../../../../../../types/app'; import { AppType } from '../../../../../../types/app';
import { InfoCard } from '~/components/InfoCard/InfoCard'
interface BehaviourTabProps { interface BehaviourTabProps {
form: UseFormReturnType<AppType, (values: AppType) => AppType>; form: UseFormReturnType<AppType, (values: AppType) => AppType>;
@@ -10,14 +11,29 @@ interface BehaviourTabProps {
export const BehaviourTab = ({ form }: BehaviourTabProps) => { export const BehaviourTab = ({ form }: BehaviourTabProps) => {
const { t } = useTranslation('layout/modals/add-app'); const { t } = useTranslation('layout/modals/add-app');
const { primaryColor } = useMantineTheme();
return ( return (
<Tabs.Panel value="behaviour" pt="xs"> <Tabs.Panel value="behaviour" pt="xs">
<Switch <Stack spacing="xs">
label={t('behaviour.isOpeningNewTab.label')} <Switch
description={t('behaviour.isOpeningNewTab.description')} label={t('behaviour.isOpeningNewTab.label')}
{...form.getInputProps('behaviour.isOpeningNewTab', { type: 'checkbox' })} description={t('behaviour.isOpeningNewTab.description')}
/> styles={{ label: { fontWeight: 500, }, description: { marginTop: 0, }, }}
{...form.getInputProps('behaviour.isOpeningNewTab', { type: 'checkbox' })}
/>
<Stack spacing="0.25rem">
<Group>
<Text size="0.875rem" weight={500}>
{t('behaviour.tooltipDescription.label')}
</Text>
<InfoCard message={t('behaviour.tooltipDescription.description')}/>
</Group>
<TextInput
{...form.getInputProps('behaviour.tooltipDescription')}
/>
</Stack>
</Stack>
</Tabs.Panel> </Tabs.Panel>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { Tabs, Text, TextInput } from '@mantine/core'; import { Stack, Tabs, Text, TextInput } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { IconClick, IconCursorText, IconLink } from '@tabler/icons-react'; import { IconClick, IconCursorText, IconLink } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
@@ -15,41 +15,44 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
const { t } = useTranslation('layout/modals/add-app'); const { t } = useTranslation('layout/modals/add-app');
return ( return (
<Tabs.Panel value="general" pt="sm"> <Tabs.Panel value="general" pt="sm">
<TextInput <Stack spacing="xs">
icon={<IconCursorText size={16} />} <TextInput
label={t('general.appname.label')} icon={<IconCursorText size={16} />}
description={t('general.appname.description')} label={t('general.appname.label')}
placeholder="My example app" description={t('general.appname.description')}
variant="default" placeholder="My example app"
withAsterisk variant="default"
mb="md" withAsterisk
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<TextInput <TextInput
icon={<IconLink size={16} />} icon={<IconLink size={16} />}
label={t('general.internalAddress.label')} label={t('general.internalAddress.label')}
description={t('general.internalAddress.description')} description={t('general.internalAddress.description')}
placeholder="https://google.com" placeholder="https://google.com"
variant="default" variant="default"
withAsterisk withAsterisk
mb="md" {...form.getInputProps('url')}
{...form.getInputProps('url')} onChange={(e) => {
/> form.setFieldValue('url', e.target.value);
<TextInput }}
icon={<IconClick size={16} />} />
label={t('general.externalAddress.label')} <TextInput
description={t('general.externalAddress.description')} icon={<IconClick size={16} />}
placeholder="https://homarr.mywebsite.com/" label={t('general.externalAddress.label')}
variant="default" description={t('general.externalAddress.description')}
{...form.getInputProps('behaviour.externalUrl')} placeholder="https://homarr.mywebsite.com/"
/> variant="default"
{...form.getInputProps('behaviour.externalUrl')}
/>
{!form.values.behaviour.externalUrl.startsWith('https://') && {!form.values.behaviour.externalUrl.startsWith('https://') &&
!form.values.behaviour.externalUrl.startsWith('http://') && ( !form.values.behaviour.externalUrl.startsWith('http://') && (
<Text color="red" mt="sm" size="sm"> <Text color="red" mt="sm" size="sm">
{t('behaviour.customProtocolWarning')} {t('behaviour.customProtocolWarning')}
</Text> </Text>
)} )}
</Stack>
</Tabs.Panel> </Tabs.Panel>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { MultiSelect, Switch, Tabs } from '@mantine/core'; import { MultiSelect, Stack, Switch, Tabs } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
@@ -16,26 +16,28 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
); );
return ( return (
<Tabs.Panel value="network" pt="lg"> <Tabs.Panel value="network" pt="lg">
<Switch <Stack spacing="xs">
label={t('network.statusChecker.label')} <Switch
description={t('network.statusChecker.description')} label={t('network.statusChecker.label')}
mb="md" description={t('network.statusChecker.description')}
defaultChecked={form.values.network.enabledStatusChecker} styles={{ label: { fontWeight: 500, }, description: { marginTop: 0, }, }}
{...form.getInputProps('network.enabledStatusChecker')} defaultChecked={form.values.network.enabledStatusChecker}
/> {...form.getInputProps('network.enabledStatusChecker')}
{form.values.network.enabledStatusChecker && (
<MultiSelect
required
label={t('network.statusCodes.label')}
description={t('network.statusCodes.description')}
data={StatusCodes}
clearable
searchable
defaultValue={acceptableStatusCodes}
variant="default"
{...form.getInputProps('network.statusCodes')}
/> />
)} {form.values.network.enabledStatusChecker && (
<MultiSelect
required
label={t('network.statusCodes.label')}
description={t('network.statusCodes.description')}
data={StatusCodes}
clearable
searchable
defaultValue={acceptableStatusCodes}
variant="default"
{...form.getInputProps('network.statusCodes')}
/>
)}
</Stack>
</Tabs.Panel> </Tabs.Panel>
); );
}; };

View File

@@ -93,6 +93,8 @@ export const AvailableElementTypes = ({
url: 'https://homarr.dev', url: 'https://homarr.dev',
appearance: { appearance: {
iconUrl: '/imgs/logo/logo.png', iconUrl: '/imgs/logo/logo.png',
appNameStatus: 'normal',
positionAppName: 'column',
}, },
network: { network: {
enabledStatusChecker: true, enabledStatusChecker: true,
@@ -164,4 +166,4 @@ const ElementItem = ({ name, icon, onClick }: ElementItemProps) => {
</Stack> </Stack>
</UnstyledButton> </UnstyledButton>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { Box, Stack, Title, UnstyledButton } from '@mantine/core'; import { Box, Flex, Text, Tooltip, UnstyledButton } from '@mantine/core';
import { createStyles } from '@mantine/styles'; import { createStyles, useMantineTheme } from '@mantine/styles';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import Link from 'next/link'; import Link from 'next/link';
@@ -20,49 +20,76 @@ export const AppTile = ({ className, app }: AppTileProps) => {
const { cx, classes } = useStyles(); const { cx, classes } = useStyles();
const { colorScheme } = useMantineTheme();
const tooltipContent = [
app.appearance.appNameStatus === "hover" ? app.name : undefined,
app.behaviour.tooltipDescription
].filter( e => e ).join( ': ' );
const { const {
classes: { card: cardClass }, classes: { card: cardClass },
} = useCardStyles(false); } = useCardStyles(false);
function Inner() { function Inner() {
return ( return (
<> <Tooltip.Floating
<Stack label={tooltipContent}
position="right-start"
c={ colorScheme === 'light' ? "black" : "dark.0" }
color={ colorScheme === 'light' ? "gray.2" : "dark.4" }
multiline
disabled={tooltipContent === ''}
styles={{ tooltip: { '&': { maxWidth: 300, }, }, }}
>
<Flex
m={0} m={0}
p={0} p={0}
spacing="xs"
justify="space-around" justify="space-around"
align="center" align="center"
style={{ height: '100%', width: '100%' }} h="100%"
w="100%"
className="dashboard-tile-app" className="dashboard-tile-app"
direction={app.appearance.positionAppName ?? 'column'}
> >
<Box hidden={false}> <Box px={10} hidden={["hover", "hidden"].includes(app.appearance.appNameStatus)}>
<Title <Text
order={5} w="max-content"
size="md" size="md"
ta="center" ta="center"
lineClamp={1} weight={700}
className={cx(classes.appName, 'dashboard-tile-app-title')} className={cx(classes.appName, 'dashboard-tile-app-title')}
> >
{app.name} {app.name}
</Title> </Text>
</Box> </Box>
<motion.img <Box
className={classes.image} w="100%"
height="85%" h="100%"
width="85%" display="flex"
style={{ sx={{
objectFit: 'contain', alignContent: 'center',
justifyContent: 'center',
flex: '1 1 auto',
flexWrap: 'wrap',
}} }}
src={app.appearance.iconUrl} >
alt={app.name} <motion.img
whileHover={{ className={classes.image}
scale: 1.2, height="85%"
transition: { duration: 0.2 }, style={{
}} objectFit: 'contain',
/> }}
</Stack> src={app.appearance.iconUrl}
</> alt={app.name}
whileHover={{
scale: 1.2,
transition: { duration: 0.2 },
}}
/>
</Box>
</Flex>
</Tooltip.Floating>
); );
} }
@@ -101,7 +128,6 @@ const useStyles = createStyles((theme, _params, getRef) => ({
wordBreak: 'break-word', wordBreak: 'break-word',
}, },
button: { button: {
paddingBottom: 10,
height: '100%', height: '100%',
width: '100%', width: '100%',
display: 'flex', display: 'flex',

View File

@@ -129,6 +129,8 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
url: address, url: address,
appearance: { appearance: {
iconUrl: '/imgs/logo/logo.png', iconUrl: '/imgs/logo/logo.png',
appNameStatus: 'normal',
positionAppName: 'column'
}, },
network: { network: {
enabledStatusChecker: true, enabledStatusChecker: true,

View File

@@ -1,4 +1,5 @@
import { Icon, IconKey, IconPassword, IconUser } from '@tabler/icons-react'; import { Icon, IconKey, IconPassword, IconUser } from '@tabler/icons-react';
import { Property } from 'csstype'
import { TileBaseType } from './tile'; import { TileBaseType } from './tile';
@@ -19,6 +20,7 @@ export type ConfigAppType = Omit<AppType, 'integration'> & {
interface AppBehaviourType { interface AppBehaviourType {
externalUrl: string; externalUrl: string;
isOpeningNewTab: boolean; isOpeningNewTab: boolean;
tooltipDescription?: string;
} }
interface AppNetworkType { interface AppNetworkType {
@@ -32,6 +34,8 @@ interface AppNetworkType {
interface AppAppearanceType { interface AppAppearanceType {
iconUrl: string; iconUrl: string;
appNameStatus: "normal"|"hover"|"hidden";
positionAppName: Property.FlexDirection;
} }
export type IntegrationType = export type IntegrationType =