mirror of
https://github.com/unraid/api.git
synced 2026-01-04 23:50:37 -06:00
refactor: enhance app configuration and notification components for improved UI consistency
- modified vite.config.ts to integrate app configuration into UI setup - updated app.config.ts to include new button, tabs, and slideover variants for better theming - cleaned up main.css by removing unused styles and ensuring proper imports - refactored notification components to streamline structure and improve readability
This commit is contained in:
@@ -1,8 +1,36 @@
|
||||
export default {
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'blue',
|
||||
neutral: 'gray',
|
||||
// overrided by tailwind-shared/css-variables.css
|
||||
// these shared tailwind styles and colors are imported in src/assets/main.css
|
||||
},
|
||||
|
||||
// https://ui.nuxt.com/docs/components/button#theme
|
||||
button: {
|
||||
//keep in mind, there is a "variant" AND a "variants" property
|
||||
variants: {
|
||||
variant: {
|
||||
ghost: '',
|
||||
link: 'hover:underline focus:underline',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// https://ui.nuxt.com/docs/components/tabs#theme
|
||||
tabs: {
|
||||
variants: {
|
||||
pill: {},
|
||||
},
|
||||
},
|
||||
|
||||
// https://ui.nuxt.com/docs/components/slideover#theme
|
||||
slideover: {
|
||||
slots: {
|
||||
// title: 'text-3xl font-normal',
|
||||
},
|
||||
variants: {
|
||||
right: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
toaster: {
|
||||
|
||||
@@ -92,4 +92,4 @@ iframe#progressFrame {
|
||||
.has-banner-gradient #header.image > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ const icon = computed<{ name: string; color: string } | null>(() => {
|
||||
|
||||
<template>
|
||||
<div class="relative flex items-center justify-center">
|
||||
<UIcon name="i-heroicons-bell-20-solid" class="text-header-text-primary h-6 w-6" />
|
||||
<UIcon name="i-heroicons-bell-20-solid" class="h-6 w-6" />
|
||||
<div
|
||||
v-if="!seen && indicatorLevel === 'UNREAD'"
|
||||
class="border-muted bg-unraid-green absolute top-0 right-0 size-2.5 rounded-full border"
|
||||
|
||||
@@ -107,35 +107,27 @@ const reformattedTimestamp = computed<string>(() => {
|
||||
<div class="" v-html="descriptionMarkup" />
|
||||
</div>
|
||||
|
||||
<p v-if="mutationError" class="text-red-600">{{ t('common.error') }}: {{ mutationError }}</p>
|
||||
<p v-if="mutationError" class="text-destructive">{{ t('common.error') }}: {{ mutationError }}</p>
|
||||
|
||||
<div class="flex items-baseline justify-end gap-4">
|
||||
<UButton
|
||||
v-if="link"
|
||||
:to="link"
|
||||
variant="link"
|
||||
class="text-primary inline-flex items-center justify-center p-0 text-sm font-medium hover:underline focus:underline"
|
||||
icon="i-heroicons-link-20-solid"
|
||||
>
|
||||
<span class="text-sm">{{ t('notifications.item.viewLink') }}</span>
|
||||
<UButton v-if="link" :to="link" variant="link" icon="i-heroicons-link-20-solid">
|
||||
{{ t('notifications.item.viewLink') }}
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="type === NotificationType.UNREAD"
|
||||
:loading="archive.loading"
|
||||
icon="i-heroicons-archive-box-20-solid"
|
||||
class="!bg-none"
|
||||
@click="() => void archive.mutate({ id: props.id })"
|
||||
>
|
||||
<span class="text-sm">{{ t('notifications.item.archive') }}</span>
|
||||
{{ t('notifications.item.archive') }}
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="type === NotificationType.ARCHIVE"
|
||||
:loading="deleteNotification.loading"
|
||||
icon="i-heroicons-trash-20-solid"
|
||||
class="!bg-none"
|
||||
@click="() => void deleteNotification.mutate({ id: props.id, type: props.type })"
|
||||
>
|
||||
<span class="text-sm">{{ t('notifications.item.delete') }}</span>
|
||||
{{ t('notifications.item.delete') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -221,9 +221,8 @@ const displayErrorMessage = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- nextui replacement for LoadingError -->
|
||||
<!-- USkeleton for loading and error states -->
|
||||
<div v-else class="flex h-full flex-col items-center justify-center gap-3 px-3">
|
||||
<!-- Loading (centered, like LoadingError) -->
|
||||
<div v-if="loading" class="w-full max-w-md space-y-4">
|
||||
<div v-for="n in 3" :key="n" class="py-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -240,7 +239,7 @@ const displayErrorMessage = computed(() => {
|
||||
<!-- Error (centered, icon + title + message + full-width button) -->
|
||||
<div v-else-if="offlineError || error" class="w-full max-w-sm space-y-3">
|
||||
<div class="flex justify-center">
|
||||
<UIcon name="i-heroicons-shield-exclamation-20-solid" class="size-10 text-red-600" />
|
||||
<UIcon name="i-heroicons-shield-exclamation-20-solid" class="text-destructive size-10" />
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h3 class="font-bold">Error</h3>
|
||||
@@ -251,7 +250,7 @@ const displayErrorMessage = computed(() => {
|
||||
|
||||
<!-- Default (empty state) -->
|
||||
<div v-else class="contents">
|
||||
<CheckIcon class="h-10 translate-y-3 text-green-600" />
|
||||
<CheckIcon class="text-unraid-green h-10 translate-y-3" />
|
||||
{{ noNotificationsMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,8 +30,15 @@ const importance = ref<Importance | undefined>(undefined);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const filterOptions = computed<Array<{ label: string; value?: Importance }>>(() => [
|
||||
{ label: t('notifications.sidebar.filters.all') },
|
||||
const activeFilter = computed({
|
||||
get: () => importance.value ?? 'all',
|
||||
set: (val) => {
|
||||
importance.value = val === 'all' ? undefined : (val as Importance);
|
||||
},
|
||||
});
|
||||
|
||||
const filterTabs = computed(() => [
|
||||
{ label: t('notifications.sidebar.filters.all'), value: 'all' as const },
|
||||
{ label: t('notifications.sidebar.filters.alert'), value: Importance.ALERT },
|
||||
{ label: t('notifications.sidebar.filters.info'), value: Importance.INFO },
|
||||
{ label: t('notifications.sidebar.filters.warning'), value: Importance.WARNING },
|
||||
@@ -131,25 +138,24 @@ const activeTab = ref<'unread' | 'archived'>('unread');
|
||||
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
id: 'unread',
|
||||
label: t('notifications.sidebar.unreadTab'),
|
||||
count: overview.value?.unread.total,
|
||||
value: 'unread' as const,
|
||||
badge: overview.value?.unread.total ?? 0,
|
||||
},
|
||||
{
|
||||
id: 'archived',
|
||||
label: t('notifications.sidebar.archivedTab'),
|
||||
count: readArchivedCount.value,
|
||||
value: 'archived' as const,
|
||||
badge: readArchivedCount.value ?? 0,
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<!-- totally scuffed but we use: !bg-transparent, !bg-none, hover:text-current to override conflicting webgui/api styles -->
|
||||
<template>
|
||||
<div>
|
||||
<UButton
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
class="!bg-transparent"
|
||||
class="text-inverted hover:text-current"
|
||||
@click="
|
||||
() => {
|
||||
isOpen = true;
|
||||
@@ -161,20 +167,7 @@ const tabs = computed(() => [
|
||||
<NotificationsIndicator :overview="overview" :seen="haveSeenNotifications" />
|
||||
</UButton>
|
||||
|
||||
<USlideover
|
||||
v-model:open="isOpen"
|
||||
side="right"
|
||||
:title="t('notifications.sidebar.title')"
|
||||
:close="{
|
||||
color: 'neutral',
|
||||
variant: 'ghost',
|
||||
class: 'rounded-md !bg-none hover:text-current',
|
||||
}"
|
||||
:ui="{
|
||||
content: 'w-screen max-w-screen sm:max-w-[540px]',
|
||||
title: 'text-3xl font-normal',
|
||||
}"
|
||||
>
|
||||
<USlideover v-model:open="isOpen" side="right" :title="t('notifications.sidebar.title')">
|
||||
<template #body>
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
@@ -182,34 +175,19 @@ const tabs = computed(() => [
|
||||
<div class="flex flex-col gap-3 px-0 py-3">
|
||||
<!-- Tabs & Action Button Row -->
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<!-- Custom Pill Tabs -->
|
||||
<div class="dark:bg-muted flex shrink-0 gap-1 rounded-lg bg-gray-100 p-2">
|
||||
<UButton
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id as 'unread' | 'archived'"
|
||||
:color="activeTab === tab.id ? 'primary' : 'neutral'"
|
||||
:variant="activeTab === tab.id ? 'solid' : 'ghost'"
|
||||
size="sm"
|
||||
class="!bg-none transition-colors"
|
||||
:class="[
|
||||
activeTab === tab.id
|
||||
? 'text-white'
|
||||
: 'text-gray-500 hover:bg-transparent hover:text-gray-700 dark:text-gray-400 dark:hover:bg-transparent dark:hover:text-gray-200',
|
||||
]"
|
||||
>
|
||||
<span>{{ tab.label }}</span>
|
||||
<span v-if="tab.count !== undefined" class="opacity-90">({{ tab.count }})</span>
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<UTabs
|
||||
v-model="activeTab"
|
||||
:items="tabs"
|
||||
:content="false"
|
||||
variant="pill"
|
||||
color="primary"
|
||||
/>
|
||||
<!-- Action Button -->
|
||||
<UButton
|
||||
v-if="activeTab === 'unread'"
|
||||
:disabled="loadingArchiveAll"
|
||||
variant="link"
|
||||
color="neutral"
|
||||
class="hover:text-primary h-auto !bg-none p-0 font-normal hover:underline"
|
||||
@click="confirmAndArchiveAll"
|
||||
>
|
||||
{{ t('notifications.sidebar.archiveAllAction') }}
|
||||
@@ -219,7 +197,6 @@ const tabs = computed(() => [
|
||||
:disabled="loadingDeleteAll"
|
||||
variant="link"
|
||||
color="neutral"
|
||||
class="text-foreground hover:text-destructive h-auto !bg-none p-0 font-normal transition-colors hover:underline"
|
||||
@click="confirmAndDeleteArchives"
|
||||
>
|
||||
{{ t('notifications.sidebar.deleteAllAction') }}
|
||||
@@ -229,26 +206,13 @@ const tabs = computed(() => [
|
||||
<!-- Filters & Settings Row -->
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<!-- Filter Button Group -->
|
||||
<div
|
||||
class="dark:bg-muted flex items-center gap-1 overflow-x-auto rounded-lg bg-gray-100 p-1"
|
||||
>
|
||||
<UButton
|
||||
v-for="option in filterOptions"
|
||||
:key="option.value ?? 'all'"
|
||||
@click="importance = option.value"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
class="!bg-none whitespace-nowrap transition-colors"
|
||||
:class="[
|
||||
importance === option.value
|
||||
? 'dark:bg-accented bg-white text-gray-900 shadow-sm ring-1 ring-gray-200 hover:bg-white hover:text-gray-900 dark:text-white dark:ring-gray-600 dark:hover:bg-gray-700 dark:hover:text-white'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-transparent hover:ring-1 hover:ring-gray-300 dark:hover:ring-gray-600',
|
||||
]"
|
||||
>
|
||||
{{ option.label }}
|
||||
</UButton>
|
||||
</div>
|
||||
<UTabs
|
||||
v-model="activeFilter"
|
||||
:items="filterTabs"
|
||||
:content="false"
|
||||
variant="pill"
|
||||
color="neutral"
|
||||
/>
|
||||
<!-- Settings Icon -->
|
||||
<UTooltip
|
||||
:delay-duration="0"
|
||||
@@ -263,7 +227,6 @@ const tabs = computed(() => [
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
icon="i-heroicons-cog-6-tooth-20-solid"
|
||||
class="h-8 w-8 !bg-none hover:text-current"
|
||||
@click="openSettings"
|
||||
/>
|
||||
</UTooltip>
|
||||
|
||||
@@ -8,6 +8,7 @@ import vue from '@vitejs/plugin-vue';
|
||||
import { defineConfig } from 'vite';
|
||||
import removeConsole from 'vite-plugin-remove-console';
|
||||
|
||||
import appConfig from './app.config';
|
||||
import scopeTailwindToUnapi from './postcss/scopeTailwindToUnapi';
|
||||
import { serveStaticHtml } from './vite-plugin-serve-static';
|
||||
|
||||
@@ -72,7 +73,9 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
}),
|
||||
ui(),
|
||||
ui({
|
||||
ui: appConfig.ui,
|
||||
}),
|
||||
serveStaticHtml(), // Serve static test pages
|
||||
// Remove console logs in production
|
||||
...(dropConsole
|
||||
|
||||
Reference in New Issue
Block a user