mirror of
https://github.com/unraid/api.git
synced 2026-01-05 16:09:49 -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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