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:
Ajit Mehrotra
2025-12-01 21:20:26 -05:00
parent 51d7b05858
commit 6488191184
7 changed files with 76 additions and 242 deletions

View File

@@ -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: {

View File

@@ -1,4 +1,4 @@
/*
/*
* Tailwind v4 configuration with Nuxt UI v3
* Using scoped selectors to prevent breaking Unraid WebGUI
*/
@@ -9,7 +9,7 @@
/* Import theme and utilities only - no global preflight */
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
@import "@nuxt/ui";
@import "@nuxt/ui";
@import 'tw-animate-css';
@import '../../../@tailwind-shared/index.css';
@@ -17,7 +17,7 @@
@source "../**/*.{vue,ts,js,tsx,jsx}";
@source "../../../unraid-ui/src/**/*.{vue,ts,js,tsx,jsx}";
/*
/*
* Scoped base styles for .unapi elements only
* Import Tailwind's preflight into our custom layer and scope it
*/
@@ -28,164 +28,13 @@
@import "tailwindcss/preflight.css";
}
/* Override Unraid's button styles for Nuxt UI components */
.unapi button {
/* Reset Unraid's button styles */
margin: 0 !important;
padding: 0;
border: none;
background: none;
}
/* Reset button-like controls back to UA defaults so Nuxt UI styles win */
.unapi button,
.unapi button[type='button'],
.unapi button:hover,
.unapi button:hover[disabled],
.unapi input[type='button'],
.unapi input[type='button']:hover,
.unapi input[type='button']:hover[disabled],
.unapi input[type='reset'],
.unapi input[type='submit'],
.unapi a.button {
box-sizing: border-box;
font: inherit;
min-width: revert;
text-transform: revert;
/* background: revert; */
/* color: revert; */
}
/* Reset text inputs so Nuxt UI field styles render correctly */
.unapi input[type='text'],
.unapi input[type='password'],
.unapi input[type='number'],
.unapi input[type='url'],
.unapi input[type='email'],
.unapi input[type='date'],
.unapi input[type='file'],
.unapi textarea,
.unapi .textarea {
box-sizing: border-box;
font: inherit;
color: inherit;
}
/* Clear global table row sizing that fights the tree table layout */
.unapi table thead td,
.unapi table thead th,
.unapi table tbody td,
.unapi table tbody th {
all: revert;
box-sizing: border-box;
}
/* Accessible focus styles for keyboard navigation */
.unapi button:focus-visible {
outline: 2px solid #ff8c2f;
outline-offset: 2px;
}
/* Restore button functionality while removing Unraid's forced styles */
.unapi button:not([role="switch"]) {
/* display: inline-flex; */
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
/* Ensure Nuxt UI modal/slideover close buttons work properly */
.unapi [role="dialog"] button,
.unapi [data-radix-collection-item] button {
margin: 0 !important;
/* background: transparent !important; */
border: none !important;
}
/* Focus styles for dialog buttons */
.unapi [role="dialog"] button:focus-visible,
.unapi [data-radix-collection-item] button:focus-visible {
outline: 2px solid #ff8c2f;
outline-offset: 2px;
}
/* Reset figure element for logo */
.unapi figure {
margin: 0;
padding: 0;
}
/* Reset heading elements - only margin/padding */
.unapi h1,
.unapi h2,
.unapi h3,
.unapi h4,
.unapi h5 {
margin: 0;
padding: 0;
}
/* Reset paragraph element */
.unapi p {
margin: 0;
padding: 0;
text-align: unset;
}
/* Reset UL styles to prevent default browser styling */
.unapi ul {
padding-inline-start: 0;
list-style-type: none;
}
/* Reset toggle/switch button backgrounds */
.unapi button[role="switch"],
.unapi button[role="switch"][data-state="checked"],
.unapi button[role="switch"][data-state="unchecked"] {
background-color: transparent;
background: transparent;
}
/* Style for checked state */
.unapi button[role="switch"][data-state="checked"] {
background-color: #ff8c2f; /* Unraid orange */
}
/* Style for unchecked state */
.unapi button[role="switch"][data-state="unchecked"] {
background-color: #e5e5e5;
}
/* Dark mode toggle styles */
.unapi.dark button[role="switch"][data-state="unchecked"],
.dark .unapi button[role="switch"][data-state="unchecked"] {
background-color: #333;
border-color: #555;
}
/* Toggle thumb/handle */
.unapi button[role="switch"] span {
background-color: white;
}
}
/* Override link styles inside .unapi */
.unapi a,
.unapi a:link,
.unapi a:visited {
color: inherit;
text-decoration: none;
}
.unapi a:hover,
.unapi a:focus {
text-decoration: underline;
color: inherit;
}
/* Note: Tailwind utilities will apply globally but should be used with .unapi prefix in HTML */
/* Ensure unraid-modals container has extremely high z-index */
unraid-modals.unapi {
position: relative;
@@ -236,4 +85,4 @@ iframe#progressFrame {
.has-banner-gradient #header.image > * {
position: relative;
z-index: 1;
}
}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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