Skip to content

Commit 5888b00

Browse files
author
mdatelle
committed
feat: use and customize NavigationMenu and update status badges
1 parent ab1bfa4 commit 5888b00

File tree

2 files changed

+190
-37
lines changed

2 files changed

+190
-37
lines changed

web/components/LayoutViews/Detail.vue

Lines changed: 139 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
11
<script setup lang="ts">
22
import { computed, ref } from 'vue';
3+
4+
import {
5+
UBadge,
6+
UButton,
7+
UCheckbox,
8+
UDropdownMenu,
9+
UIcon,
10+
UNavigationMenu,
11+
USwitch,
12+
UTabs,
13+
} from '#components';
14+
315
import type { Component } from 'vue';
4-
import { UIcon, UBadge, UTabs } from '#components';
516
617
interface NavigationItem {
718
id: string;
819
label: string;
920
icon?: string;
1021
badge?: string | number;
22+
slot?: string;
23+
status?: {
24+
label: string;
25+
dotColor: string; // Tailwind color class like 'bg-green-500'
26+
}[];
27+
children?: NavigationItem[]; // For grouped/nested items
28+
isGroup?: boolean; // Indicates if this is a group/folder
29+
}
30+
31+
interface NavigationMenuItem extends NavigationItem {
32+
to?: string;
33+
slot?: string;
34+
onClick?: () => void;
1135
}
1236
1337
interface TabItem {
@@ -34,11 +58,58 @@ const props = withDefaults(defineProps<Props>(), {
3458
3559
const selectedNavigationId = ref(props.defaultNavigationId || props.navigationItems[0]?.id || '');
3660
const selectedTab = ref(props.defaultTabKey || '0');
61+
const selectedItems = ref<string[]>([]);
62+
63+
const selectedNavigationItem = computed(() => {
64+
// First check top-level items
65+
const topLevel = props.navigationItems.find((item) => item.id === selectedNavigationId.value);
66+
if (topLevel) return topLevel;
3767
38-
const selectedNavigationItem = computed(() =>
39-
props.navigationItems.find((item) => item.id === selectedNavigationId.value)
68+
// Then check nested items
69+
for (const item of props.navigationItems) {
70+
if (item.children) {
71+
const nested = item.children.find((child) => child.id === selectedNavigationId.value);
72+
if (nested) return nested;
73+
}
74+
}
75+
76+
return undefined;
77+
});
78+
79+
const navigationMenuItems = computed((): NavigationMenuItem[] =>
80+
props.navigationItems.map((item) => ({
81+
label: item.label,
82+
icon: item.icon,
83+
id: item.id,
84+
badge: item.badge,
85+
slot: item.slot,
86+
onClick: () => selectNavigationItem(item.id),
87+
children: item.children?.map((child) => ({
88+
label: child.label,
89+
icon: child.icon,
90+
id: child.id,
91+
badge: child.badge,
92+
slot: child.slot,
93+
onClick: () => selectNavigationItem(child.id),
94+
status: child.status,
95+
})),
96+
isGroup: item.isGroup,
97+
}))
4098
);
4199
100+
const toggleItemSelection = (itemId: string) => {
101+
const index = selectedItems.value.indexOf(itemId);
102+
if (index > -1) {
103+
selectedItems.value.splice(index, 1);
104+
} else {
105+
selectedItems.value.push(itemId);
106+
}
107+
};
108+
109+
const isItemSelected = (itemId: string) => {
110+
return selectedItems.value.includes(itemId);
111+
};
112+
42113
const tabItems = computed(() =>
43114
props.tabs.map((tab) => ({
44115
label: tab.label,
@@ -74,28 +145,75 @@ const getCurrentTabProps = () => {
74145
<div class="flex h-full gap-6">
75146
<!-- Left Navigation Section -->
76147
<div class="w-64 flex-shrink-0">
77-
<nav class="space-y-1">
78-
<button
79-
v-for="item in navigationItems"
80-
:key="item.id"
81-
:class="[
82-
'group flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
83-
selectedNavigationId === item.id
84-
? 'bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white'
85-
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white',
86-
]"
87-
@click="selectNavigationItem(item.id)"
88-
>
89-
<UIcon v-if="item.icon" :name="item.icon" class="h-5 w-5 flex-shrink-0" />
90-
<span class="truncate">{{ item.label }}</span>
91-
<UBadge v-if="item.badge" size="xs" :label="String(item.badge)" class="ml-auto" />
92-
</button>
93-
</nav>
148+
<UNavigationMenu :items="navigationMenuItems" orientation="vertical">
149+
<template v-for="navItem in navigationMenuItems" :key="navItem.id" #[navItem.slot]>
150+
<div class="flex items-center gap-3">
151+
<UCheckbox
152+
:model-value="isItemSelected(navItem.id)"
153+
class="flex-shrink-0"
154+
@update:model-value="toggleItemSelection(navItem.id)"
155+
@click.stop
156+
/>
157+
<UIcon v-if="navItem.icon" :name="navItem.icon" class="h-5 w-5 flex-shrink-0" />
158+
<span class="truncate flex-1">{{ navItem.label }}</span>
159+
<UBadge v-if="navItem.badge" size="xs" :label="String(navItem.badge)" />
160+
</div>
161+
</template>
162+
</UNavigationMenu>
94163
</div>
95164

96165
<!-- Right Content Section -->
97166
<div class="flex-1 min-w-0">
98-
<UTabs v-model="selectedTab" :items="tabItems" class="w-full" />
167+
<div v-if="selectedNavigationItem" class="mb-6 flex items-center justify-between">
168+
<div class="flex items-center gap-3">
169+
<UIcon
170+
v-if="selectedNavigationItem.icon"
171+
:name="selectedNavigationItem.icon"
172+
class="h-8 w-8"
173+
/>
174+
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
175+
{{ selectedNavigationItem.label }}
176+
</h1>
177+
178+
<!-- Status Indicators -->
179+
<div v-if="selectedNavigationItem.status" class="flex items-center gap-4">
180+
<UBadge
181+
v-for="(statusItem, index) in selectedNavigationItem.status"
182+
:key="index"
183+
variant="subtle"
184+
color="neutral"
185+
size="sm"
186+
>
187+
<div :class="['h-2 w-2 rounded-full mr-2', statusItem.dotColor]"/>
188+
{{ statusItem.label }}
189+
</UBadge>
190+
</div>
191+
</div>
192+
193+
<!-- Right Side Controls -->
194+
<div class="flex items-center gap-4">
195+
<div class="flex items-center gap-3">
196+
<span class="text-sm font-medium">Autostart</span>
197+
<USwitch :model-value="true" />
198+
</div>
199+
200+
<!-- Manage Dropdown -->
201+
<UDropdownMenu
202+
:items="[
203+
[{ label: 'Edit', icon: 'i-lucide-edit' }],
204+
[{ label: 'Remove', icon: 'i-lucide-trash-2' }],
205+
[{ label: 'Restart', icon: 'i-lucide-refresh-cw' }],
206+
[{ label: 'Force Update', icon: 'i-lucide-download' }],
207+
]"
208+
>
209+
<UButton variant="outline" color="primary" trailing-icon="i-lucide-chevron-down">
210+
Manage
211+
</UButton>
212+
</UDropdownMenu>
213+
</div>
214+
</div>
215+
216+
<UTabs v-model="selectedTab" variant="link" :items="tabItems" class="w-full" />
99217

100218
<!-- Tab Content -->
101219
<div class="mt-6">

web/pages/docker.vue

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,69 @@ const dockerContainers = [
2828
id: 'immich',
2929
label: 'immich',
3030
icon: 'i-lucide-play-circle',
31+
slot: 'immich' as const,
32+
status: [
33+
{ label: 'Update available', dotColor: 'bg-orange-500' },
34+
{ label: 'Started', dotColor: 'bg-green-500' }
35+
],
3136
},
3237
{
3338
id: 'organizrv2',
3439
label: 'organizrv2',
3540
icon: 'i-lucide-layers',
41+
slot: 'organizrv2' as const,
42+
status: [
43+
{ label: 'Started', dotColor: 'bg-green-500' }
44+
],
3645
},
3746
{
3847
id: 'jellyfin',
3948
label: 'Jellyfin',
4049
icon: 'i-lucide-film',
50+
slot: 'jellyfin' as const,
51+
status: [
52+
{ label: 'Stopped', dotColor: 'bg-red-500' }
53+
],
4154
},
4255
{
43-
id: 'mongodb',
44-
label: 'MongoDB',
45-
icon: 'i-lucide-database',
46-
badge: 'DB',
47-
},
48-
{
49-
id: 'postgres17',
50-
label: 'postgres17',
51-
icon: 'i-lucide-database',
52-
badge: 'DB',
53-
},
54-
{
55-
id: 'redis',
56-
label: 'Redis',
57-
icon: 'i-lucide-database',
58-
badge: 'DB',
56+
id: 'databases',
57+
label: 'Databases',
58+
icon: 'i-lucide-folder-database',
59+
slot: 'databases' as const,
60+
isGroup: true,
61+
children: [
62+
{
63+
id: 'mongodb',
64+
label: 'MongoDB',
65+
icon: 'i-lucide-database',
66+
badge: 'DB',
67+
slot: 'mongodb' as const,
68+
status: [
69+
{ label: 'Started', dotColor: 'bg-green-500' }
70+
],
71+
},
72+
{
73+
id: 'postgres17',
74+
label: 'postgres17',
75+
icon: 'i-lucide-database',
76+
badge: 'DB',
77+
slot: 'postgres17' as const,
78+
status: [
79+
{ label: 'Update available', dotColor: 'bg-orange-500' },
80+
{ label: 'Paused', dotColor: 'bg-blue-500' }
81+
],
82+
},
83+
{
84+
id: 'redis',
85+
label: 'Redis',
86+
icon: 'i-lucide-database',
87+
badge: 'DB',
88+
slot: 'redis' as const,
89+
status: [
90+
{ label: 'Started', dotColor: 'bg-green-500' }
91+
],
92+
},
93+
]
5994
},
6095
];
6196

0 commit comments

Comments
 (0)