mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
fix(web): notification styles & alignment (#968)
* fix(web): notification icon & indicator colors * fix(web): notification item text size & weights * fix(web): notification button styles * fix(web): notification filter styles * fix(web): Tab List styles * fix(web): link button styles * fix(web): vertical spacing in notifications sidebar * fix(web): notification sidebar link styles * refactor(web): change default button border radius to rounded instead of rounded-md * fix(web): Notification Item alignment with other elements * refactor(web): add tw color palettes for unraid-green & unraid-red
This commit is contained in:
@@ -43,7 +43,7 @@ const icon = computed<{ component: Component; color: string } | null>(() => {
|
||||
case Importance.Alert:
|
||||
return {
|
||||
component: ShieldExclamationIcon,
|
||||
color: 'text-red-500',
|
||||
color: 'text-unraid-red',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -73,10 +73,10 @@ watch(overview, (newVal, oldVal) => {
|
||||
<div
|
||||
v-if="indicatorLevel"
|
||||
:class="
|
||||
cn('absolute top-0 right-0 size-2.5 rounded-full', {
|
||||
cn('absolute top-0 right-0 size-2.5 rounded-full border border-neutral-800', {
|
||||
'bg-unraid-red': indicatorLevel === Importance.Alert,
|
||||
'bg-yellow-500': indicatorLevel === Importance.Warning,
|
||||
'bg-green-500': indicatorLevel === 'UNREAD',
|
||||
'bg-yellow-accent': indicatorLevel === Importance.Warning,
|
||||
'bg-unraid-green': indicatorLevel === 'UNREAD',
|
||||
})
|
||||
"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Markdown } from '@/helpers/markdown';
|
||||
import {
|
||||
ArchiveBoxIcon,
|
||||
CheckBadgeIcon,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
archiveNotification as archiveMutation,
|
||||
deleteNotification as deleteMutation,
|
||||
} from './graphql/notification.query';
|
||||
import { Markdown } from '@/helpers/markdown';
|
||||
|
||||
const props = defineProps<NotificationFragmentFragment>();
|
||||
|
||||
@@ -22,7 +22,7 @@ const descriptionMarkup = computedAsync(async () => {
|
||||
try {
|
||||
return await Markdown.parse(props.description);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
return props.description;
|
||||
}
|
||||
}, '');
|
||||
@@ -32,17 +32,17 @@ const icon = computed<{ component: Component; color: string } | null>(() => {
|
||||
case 'INFO':
|
||||
return {
|
||||
component: CheckBadgeIcon,
|
||||
color: 'text-green-500',
|
||||
color: 'text-unraid-green',
|
||||
};
|
||||
case 'WARNING':
|
||||
return {
|
||||
component: ExclamationTriangleIcon,
|
||||
color: 'text-yellow-500',
|
||||
color: 'text-yellow-accent',
|
||||
};
|
||||
case 'ALERT':
|
||||
return {
|
||||
component: ShieldExclamationIcon,
|
||||
color: 'text-red-500',
|
||||
color: 'text-unraid-red',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -66,66 +66,57 @@ const mutationError = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="group/item relative w-full py-4 pl-1 flex flex-col gap-2">
|
||||
<header
|
||||
class="w-full flex flex-row items-baseline justify-between gap-2 -translate-y-1 group-hover/item:font-medium group-focus/item:font-medium"
|
||||
>
|
||||
<h3
|
||||
class="text-muted-foreground text-[0.875rem] tracking-wide flex flex-row items-baseline gap-2 uppercase"
|
||||
>
|
||||
<!-- fixed width hack ensures alignment with other elements regardless of scrollbar presence or width -->
|
||||
<div class="group/item relative py-5 flex flex-col gap-2 text-base w-[487px]">
|
||||
<header class="w-full flex flex-row items-baseline justify-between gap-2 -translate-y-1">
|
||||
<h3 class="tracking-normal flex flex-row items-baseline gap-2 uppercase font-bold">
|
||||
<!-- the `translate` compensates for extra space added by the `svg` element when rendered -->
|
||||
<component
|
||||
:is="icon.component"
|
||||
v-if="icon"
|
||||
class="size-5 shrink-0 translate-y-1.5"
|
||||
class="size-5 shrink-0 translate-y-1"
|
||||
:class="icon.color"
|
||||
/>
|
||||
<span>{{ title }}</span>
|
||||
</h3>
|
||||
|
||||
<div class="shrink-0 flex flex-row items-baseline justify-end gap-2 mt-1">
|
||||
<p class="text-12px opacity-75">{{ formattedTimestamp }}</p>
|
||||
<p class="text-gray-500 text-sm">{{ formattedTimestamp }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<h4 class="group-hover/item:font-medium group-focus/item:font-medium">
|
||||
<h4 class="font-bold">
|
||||
{{ subject }}
|
||||
</h4>
|
||||
|
||||
<div
|
||||
class="w-full flex flex-row items-center justify-between gap-2 opacity-75 group-hover/item:opacity-100 group-focus/item:opacity-100"
|
||||
>
|
||||
<div class="text-secondary-foreground" v-html="descriptionMarkup" />
|
||||
<div class="w-full flex flex-row items-center justify-between gap-2">
|
||||
<div class="" v-html="descriptionMarkup" />
|
||||
</div>
|
||||
|
||||
<p v-if="mutationError" class="text-red-600">Error: {{ mutationError }}</p>
|
||||
|
||||
<div class="flex justify-end items-baseline gap-2">
|
||||
<div class="flex justify-end items-baseline gap-4">
|
||||
<a v-if="link" :href="link">
|
||||
<Button type="button" variant="outline" size="xs">
|
||||
<LinkIcon class="size-3 mr-1 text-muted-foreground/80" />
|
||||
<span class="text-sm text-muted-foreground mt-0.5">View</span>
|
||||
<Button type="button" variant="outline">
|
||||
<LinkIcon class="size-4 mr-2" />
|
||||
<span class="text-sm">View</span>
|
||||
</Button>
|
||||
</a>
|
||||
<Button
|
||||
v-if="type === NotificationType.Unread"
|
||||
:disabled="archive.loading"
|
||||
class="relative z-20 rounded"
|
||||
size="xs"
|
||||
@click="archive.mutate"
|
||||
>
|
||||
<ArchiveBoxIcon class="size-3 mr-1" />
|
||||
<span class="text-sm mt-0.5">Archive</span>
|
||||
<ArchiveBoxIcon class="size-4 mr-2" />
|
||||
<span class="text-sm">Archive</span>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="type === NotificationType.Archive"
|
||||
:disabled="deleteNotification.loading"
|
||||
class="relative z-20 rounded"
|
||||
size="xs"
|
||||
@click="deleteNotification.mutate"
|
||||
>
|
||||
<TrashIcon class="size-3 mr-1" />
|
||||
<span class="text-sm mt-0.5">Delete</span>
|
||||
<TrashIcon class="size-4 mr-2" />
|
||||
<span class="text-sm">Delete</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +77,7 @@ async function onLoadMore() {
|
||||
<div
|
||||
v-if="notifications?.length > 0"
|
||||
v-infinite-scroll="[onLoadMore, { canLoadMore: () => canLoadMore }]"
|
||||
class="divide-y divide-gray-200 overflow-y-auto pl-7 pr-4 h-full"
|
||||
class="divide-y divide-gray-200 overflow-y-auto h-full pl-7"
|
||||
>
|
||||
<NotificationsItem
|
||||
v-for="notification in notifications"
|
||||
|
||||
@@ -33,14 +33,15 @@ const confirmAndDeleteAll = async () => {
|
||||
</SheetTrigger>
|
||||
|
||||
<!-- We remove the horizontal padding from the container to keep the NotificationList's scrollbar in the right place -->
|
||||
<SheetContent :to="teleportTarget" class="w-full sm:max-w-[540px] h-screen px-0">
|
||||
<div class="flex flex-col h-full gap-3">
|
||||
<SheetHeader class="ml-1 px-6 flex items-baseline gap-0">
|
||||
<SheetContent
|
||||
:to="teleportTarget"
|
||||
class="w-full max-w-[100vw] sm:max-w-[540px] h-screen px-0 bg-[#f2f2f2]"
|
||||
>
|
||||
<div class="flex flex-col h-full gap-5">
|
||||
<SheetHeader class="ml-1 px-6 flex items-baseline gap-1">
|
||||
<SheetTitle class="text-2xl">Notifications</SheetTitle>
|
||||
<a href="/Settings/Notifications">
|
||||
<Button variant="link" size="sm" class="text-muted-foreground text-base p-0">
|
||||
Edit Settings
|
||||
</Button>
|
||||
<Button variant="link" size="sm" class="p-0 h-auto"> Edit Settings </Button>
|
||||
</a>
|
||||
</SheetHeader>
|
||||
|
||||
@@ -48,7 +49,7 @@ const confirmAndDeleteAll = async () => {
|
||||
<!-- this is necessary because flex items have a default min-height: auto, -->
|
||||
<!-- which means they won't shrink below the height of their content, even if you use flex-1 or other flex properties. -->
|
||||
<Tabs default-value="unread" class="flex-1 flex flex-col min-h-0" activation-mode="manual">
|
||||
<div class="flex flex-row justify-between items-center flex-wrap gap-2 px-6">
|
||||
<div class="flex flex-row justify-between items-center flex-wrap gap-5 px-6">
|
||||
<TabsList class="ml-[1px]">
|
||||
<TabsTrigger value="unread"> Unread </TabsTrigger>
|
||||
<TabsTrigger value="archived"> Archived </TabsTrigger>
|
||||
@@ -58,7 +59,7 @@ const confirmAndDeleteAll = async () => {
|
||||
:disabled="loadingArchiveAll"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="text-muted-foreground text-base p-0"
|
||||
class="text-foreground hover:text-destructive transition-none"
|
||||
@click="confirmAndArchiveAll"
|
||||
>
|
||||
Archive All
|
||||
@@ -69,7 +70,7 @@ const confirmAndDeleteAll = async () => {
|
||||
:disabled="loadingDeleteAll"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="text-muted-foreground text-base p-0"
|
||||
class="text-foreground hover:text-destructive transition-none"
|
||||
@click="confirmAndDeleteAll"
|
||||
>
|
||||
Delete All
|
||||
@@ -83,8 +84,8 @@ const confirmAndDeleteAll = async () => {
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger class="bg-secondary border-0 h-auto">
|
||||
<SelectValue class="text-muted-foreground" placeholder="Filter" />
|
||||
<SelectTrigger class="h-auto">
|
||||
<SelectValue class="text-gray-400 leading-6" placeholder="Filter By" />
|
||||
</SelectTrigger>
|
||||
<SelectContent :to="teleportTarget">
|
||||
<SelectGroup>
|
||||
@@ -98,11 +99,11 @@ const confirmAndDeleteAll = async () => {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<TabsContent value="unread" class="flex-1 min-h-0 mt-3">
|
||||
<TabsContent value="unread" class="flex-1 min-h-0 mt-5">
|
||||
<NotificationsList :importance="importance" :type="NotificationType.Unread" />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="archived" class="flex-1 min-h-0 mt-3">
|
||||
<TabsContent value="archived" class="flex-1 min-h-0 mt-5">
|
||||
<NotificationsList :importance="importance" :type="NotificationType.Archive" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
export { default as Button } from './Button.vue'
|
||||
export { default as Button } from './Button.vue';
|
||||
|
||||
export const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
xs: 'h-7 rounded px-2',
|
||||
default: 'px-4 py-2',
|
||||
xs: 'h-7 px-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
@@ -29,7 +26,7 @@ export const buttonVariants = cva(
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>;
|
||||
|
||||
@@ -19,7 +19,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
<SelectTrigger
|
||||
v-bind="forwardedProps"
|
||||
:class="cn(
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-4.5 py-3 text-base ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ const delegatedProps = computed(() => {
|
||||
<TabsList
|
||||
v-bind="delegatedProps"
|
||||
:class="cn(
|
||||
'inline-flex items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
|
||||
'inline-flex items-center justify-center rounded-md bg-input p-1.5 text-foreground',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -18,7 +18,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
<TabsTrigger
|
||||
v-bind="forwardedProps"
|
||||
:class="cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-base font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded px-4.5 py-2.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dotenv/config';
|
||||
import type { Config } from 'tailwindcss';
|
||||
import type { PluginAPI } from 'tailwindcss/types/config';
|
||||
|
||||
|
||||
// @ts-expect-error - just trying to get this to build @fixme
|
||||
export default <Partial<Config>>{
|
||||
darkMode: ['class'],
|
||||
@@ -45,9 +44,40 @@ export default <Partial<Config>>{
|
||||
'grey-lightest': '#f2f2f2',
|
||||
white: '#ffffff',
|
||||
|
||||
// unraid colors
|
||||
'yellow-accent': '#E9BF41',
|
||||
'orange-dark': '#f15a2c',
|
||||
orange: '#ff8c2f',
|
||||
'unraid-red': '#E22828',
|
||||
// palettes generated from https://uicolors.app/create
|
||||
'unraid-red': {
|
||||
DEFAULT: '#E22828',
|
||||
'50': '#fef2f2',
|
||||
'100': '#ffe1e1',
|
||||
'200': '#ffc9c9',
|
||||
'300': '#fea3a3',
|
||||
'400': '#fc6d6d',
|
||||
'500': '#f43f3f',
|
||||
'600': '#e22828',
|
||||
'700': '#bd1818',
|
||||
'800': '#9c1818',
|
||||
'900': '#821a1a',
|
||||
'950': '#470808',
|
||||
},
|
||||
|
||||
'unraid-green': {
|
||||
DEFAULT: '#63A659',
|
||||
'50': '#f5f9f4',
|
||||
'100': '#e7f3e5',
|
||||
'200': '#d0e6cc',
|
||||
'300': '#aad1a4',
|
||||
'400': '#7db474',
|
||||
'500': '#63a659',
|
||||
'600': '#457b3e',
|
||||
'700': '#396134',
|
||||
'800': '#314e2d',
|
||||
'900': '#284126',
|
||||
'950': '#122211',
|
||||
},
|
||||
|
||||
alpha: 'var(--color-alpha)',
|
||||
beta: 'var(--color-beta)',
|
||||
@@ -101,6 +131,7 @@ export default <Partial<Config>>{
|
||||
'30px': '30px',
|
||||
},
|
||||
spacing: {
|
||||
'4.5': '1.125rem',
|
||||
'-8px': '-8px',
|
||||
'2px': '2px',
|
||||
'4px': '4px',
|
||||
|
||||
Reference in New Issue
Block a user