feat: use and customize NavigationMenu and update status badges

This commit is contained in:
mdatelle
2025-07-23 14:02:32 -04:00
committed by Eli Bosley
parent 78ce64e357
commit f32493e728
2 changed files with 190 additions and 37 deletions

View File

@@ -1,13 +1,37 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import {
UBadge,
UButton,
UCheckbox,
UDropdownMenu,
UIcon,
UNavigationMenu,
USwitch,
UTabs,
} from '#components';
import type { Component } from 'vue';
import { UIcon, UBadge, UTabs } from '#components';
interface NavigationItem {
id: string;
label: string;
icon?: string;
badge?: string | number;
slot?: string;
status?: {
label: string;
dotColor: string; // Tailwind color class like 'bg-green-500'
}[];
children?: NavigationItem[]; // For grouped/nested items
isGroup?: boolean; // Indicates if this is a group/folder
}
interface NavigationMenuItem extends NavigationItem {
to?: string;
slot?: string;
onClick?: () => void;
}
interface TabItem {
@@ -34,11 +58,58 @@ const props = withDefaults(defineProps<Props>(), {
const selectedNavigationId = ref(props.defaultNavigationId || props.navigationItems[0]?.id || '');
const selectedTab = ref(props.defaultTabKey || '0');
const selectedItems = ref<string[]>([]);
const selectedNavigationItem = computed(() =>
props.navigationItems.find((item) => item.id === selectedNavigationId.value)
const selectedNavigationItem = computed(() => {
// First check top-level items
const topLevel = props.navigationItems.find((item) => item.id === selectedNavigationId.value);
if (topLevel) return topLevel;
// Then check nested items
for (const item of props.navigationItems) {
if (item.children) {
const nested = item.children.find((child) => child.id === selectedNavigationId.value);
if (nested) return nested;
}
}
return undefined;
});
const navigationMenuItems = computed((): NavigationMenuItem[] =>
props.navigationItems.map((item) => ({
label: item.label,
icon: item.icon,
id: item.id,
badge: item.badge,
slot: item.slot,
onClick: () => selectNavigationItem(item.id),
children: item.children?.map((child) => ({
label: child.label,
icon: child.icon,
id: child.id,
badge: child.badge,
slot: child.slot,
onClick: () => selectNavigationItem(child.id),
status: child.status,
})),
isGroup: item.isGroup,
}))
);
const toggleItemSelection = (itemId: string) => {
const index = selectedItems.value.indexOf(itemId);
if (index > -1) {
selectedItems.value.splice(index, 1);
} else {
selectedItems.value.push(itemId);
}
};
const isItemSelected = (itemId: string) => {
return selectedItems.value.includes(itemId);
};
const tabItems = computed(() =>
props.tabs.map((tab) => ({
label: tab.label,
@@ -74,28 +145,75 @@ const getCurrentTabProps = () => {
<div class="flex h-full gap-6">
<!-- Left Navigation Section -->
<div class="w-64 flex-shrink-0">
<nav class="space-y-1">
<button
v-for="item in navigationItems"
:key="item.id"
:class="[
'group flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
selectedNavigationId === item.id
? 'bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white'
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white',
]"
@click="selectNavigationItem(item.id)"
>
<UIcon v-if="item.icon" :name="item.icon" class="h-5 w-5 flex-shrink-0" />
<span class="truncate">{{ item.label }}</span>
<UBadge v-if="item.badge" size="xs" :label="String(item.badge)" class="ml-auto" />
</button>
</nav>
<UNavigationMenu :items="navigationMenuItems" orientation="vertical">
<template v-for="navItem in navigationMenuItems" :key="navItem.id" #[navItem.slot]>
<div class="flex items-center gap-3">
<UCheckbox
:model-value="isItemSelected(navItem.id)"
class="flex-shrink-0"
@update:model-value="toggleItemSelection(navItem.id)"
@click.stop
/>
<UIcon v-if="navItem.icon" :name="navItem.icon" class="h-5 w-5 flex-shrink-0" />
<span class="truncate flex-1">{{ navItem.label }}</span>
<UBadge v-if="navItem.badge" size="xs" :label="String(navItem.badge)" />
</div>
</template>
</UNavigationMenu>
</div>
<!-- Right Content Section -->
<div class="flex-1 min-w-0">
<UTabs v-model="selectedTab" :items="tabItems" class="w-full" />
<div v-if="selectedNavigationItem" class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<UIcon
v-if="selectedNavigationItem.icon"
:name="selectedNavigationItem.icon"
class="h-8 w-8"
/>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
{{ selectedNavigationItem.label }}
</h1>
<!-- Status Indicators -->
<div v-if="selectedNavigationItem.status" class="flex items-center gap-4">
<UBadge
v-for="(statusItem, index) in selectedNavigationItem.status"
:key="index"
variant="subtle"
color="neutral"
size="sm"
>
<div :class="['h-2 w-2 rounded-full mr-2', statusItem.dotColor]"/>
{{ statusItem.label }}
</UBadge>
</div>
</div>
<!-- Right Side Controls -->
<div class="flex items-center gap-4">
<div class="flex items-center gap-3">
<span class="text-sm font-medium">Autostart</span>
<USwitch :model-value="true" />
</div>
<!-- Manage Dropdown -->
<UDropdownMenu
:items="[
[{ label: 'Edit', icon: 'i-lucide-edit' }],
[{ label: 'Remove', icon: 'i-lucide-trash-2' }],
[{ label: 'Restart', icon: 'i-lucide-refresh-cw' }],
[{ label: 'Force Update', icon: 'i-lucide-download' }],
]"
>
<UButton variant="outline" color="primary" trailing-icon="i-lucide-chevron-down">
Manage
</UButton>
</UDropdownMenu>
</div>
</div>
<UTabs v-model="selectedTab" variant="link" :items="tabItems" class="w-full" />
<!-- Tab Content -->
<div class="mt-6">

View File

@@ -28,34 +28,69 @@ const dockerContainers = [
id: 'immich',
label: 'immich',
icon: 'i-lucide-play-circle',
slot: 'immich' as const,
status: [
{ label: 'Update available', dotColor: 'bg-orange-500' },
{ label: 'Started', dotColor: 'bg-green-500' }
],
},
{
id: 'organizrv2',
label: 'organizrv2',
icon: 'i-lucide-layers',
slot: 'organizrv2' as const,
status: [
{ label: 'Started', dotColor: 'bg-green-500' }
],
},
{
id: 'jellyfin',
label: 'Jellyfin',
icon: 'i-lucide-film',
slot: 'jellyfin' as const,
status: [
{ label: 'Stopped', dotColor: 'bg-red-500' }
],
},
{
id: 'mongodb',
label: 'MongoDB',
icon: 'i-lucide-database',
badge: 'DB',
},
{
id: 'postgres17',
label: 'postgres17',
icon: 'i-lucide-database',
badge: 'DB',
},
{
id: 'redis',
label: 'Redis',
icon: 'i-lucide-database',
badge: 'DB',
id: 'databases',
label: 'Databases',
icon: 'i-lucide-folder-database',
slot: 'databases' as const,
isGroup: true,
children: [
{
id: 'mongodb',
label: 'MongoDB',
icon: 'i-lucide-database',
badge: 'DB',
slot: 'mongodb' as const,
status: [
{ label: 'Started', dotColor: 'bg-green-500' }
],
},
{
id: 'postgres17',
label: 'postgres17',
icon: 'i-lucide-database',
badge: 'DB',
slot: 'postgres17' as const,
status: [
{ label: 'Update available', dotColor: 'bg-orange-500' },
{ label: 'Paused', dotColor: 'bg-blue-500' }
],
},
{
id: 'redis',
label: 'Redis',
icon: 'i-lucide-database',
badge: 'DB',
slot: 'redis' as const,
status: [
{ label: 'Started', dotColor: 'bg-green-500' }
],
},
]
},
];