Skip to content

Commit 48c75f5

Browse files
satellite/admin-ui: minor UI changes
- Fix missing favicons - System theme - Persistent side nav Change-Id: I94227f97a69fb3c333782c83ee57708d7ea81803
1 parent 4b3cc5e commit 48c75f5

File tree

8 files changed

+188
-51
lines changed

8 files changed

+188
-51
lines changed

satellite/admin/back-office/ui/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
<head>
55
<meta charset="UTF-8" />
6-
<link rel="icon" href="/favicon.ico" />
76
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<link rel="apple-touch-icon" sizes="180x180" href="/src/assets/favicon/apple-touch-icon.png">
8+
<link rel="icon" type="image/png" sizes="32x32" href="/src/assets/favicon/favicon-32x32.png">
9+
<link rel="icon" type="image/png" sizes="16x16" href="/src/assets/favicon/favicon-16x16.png">
810
<title>Storj - Admin</title>
911
</head>
1012

satellite/admin/back-office/ui/src/App.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,35 @@
2121
</template>
2222

2323
<script setup lang="ts">
24-
import { onMounted } from 'vue';
24+
import { onBeforeUnmount, onMounted, watch } from 'vue';
2525
import { VSkeletonLoader } from 'vuetify/components';
2626
2727
import { useAppStore } from '@/store/app';
2828
import { useNotify } from '@/composables/useNotify';
2929
import { useUsersStore } from '@/store/users';
30+
import { DARK_THEME_QUERY, useThemeStore } from '@/store/theme';
3031
3132
import Notifications from '@/layouts/default/Notifications.vue';
3233
3334
const appStore = useAppStore();
35+
const themeStore = useThemeStore();
3436
const usersStore = useUsersStore();
3537
const notify = useNotify();
3638
39+
const darkThemeMediaQuery = window.matchMedia(DARK_THEME_QUERY);
40+
41+
function onThemeChange(e: MediaQueryListEvent) {
42+
themeStore.setThemeLightness(!e.matches);
43+
}
44+
45+
watch(() => themeStore.state.name, (theme) => {
46+
if (theme === 'auto') {
47+
darkThemeMediaQuery.addEventListener('change', onThemeChange);
48+
return;
49+
}
50+
darkThemeMediaQuery.removeEventListener('change', onThemeChange);
51+
}, { immediate: true });
52+
3753
onMounted(async () => {
3854
try {
3955
await Promise.all([
@@ -44,4 +60,8 @@ onMounted(async () => {
4460
notify.error(`Failed to initialise app. ${error.message}`);
4561
}
4662
});
63+
64+
onBeforeUnmount(() => {
65+
darkThemeMediaQuery.removeEventListener('change', onThemeChange);
66+
});
4767
</script>
8.6 KB
Loading
624 Bytes
Loading
1.34 KB
Loading
15 KB
Binary file not shown.

satellite/admin/back-office/ui/src/layouts/default/AppBar.vue

Lines changed: 94 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,77 @@
55
<v-app-bar :elevation="0">
66
<v-app-bar-nav-icon
77
variant="text" color="default" class="mr-1" size="small" density="comfortable"
8-
@click.stop="drawer = !drawer"
8+
@click.stop="!mdAndUp ? drawer = !drawer : rail = !rail"
99
/>
1010

1111
<v-app-bar-title class="mx-1">
1212
<router-link v-if="featureFlags.dashboard" to="/dashboard">
13-
<v-img v-if="theme.global.current.value.dark" src="@/assets/logo-dark.svg" width="172" alt="Storj Logo" />
13+
<v-img v-if="themeStore.globalTheme?.dark" src="@/assets/logo-dark.svg" width="172" alt="Storj Logo" />
1414
<v-img v-else src="@/assets/logo.svg" width="172" alt="Storj Logo" />
1515
</router-link>
1616
<div v-else>
17-
<v-img v-if="theme.global.current.value.dark" src="@/assets/logo-dark.svg" width="172" alt="Storj Logo" />
17+
<v-img v-if="themeStore.globalTheme?.dark" src="@/assets/logo-dark.svg" width="172" alt="Storj Logo" />
1818
<v-img v-else src="@/assets/logo.svg" width="172" alt="Storj Logo" />
1919
</div>
2020
</v-app-bar-title>
2121

2222
<template #append>
23-
<!-- Theme Toggle Light/Dark Mode -->
24-
<v-btn-toggle v-model="activeTheme" mandatory border inset rounded="lg" density="compact">
25-
<v-tooltip text="Light Theme" location="bottom">
26-
<template #activator="{ props }">
27-
<v-btn
28-
v-bind="props" :icon="Sun" size="x-small" class="px-4" aria-label="Toggle Light Theme"
29-
@click="toggleTheme('light')"
30-
/>
31-
</template>
32-
</v-tooltip>
33-
34-
<v-tooltip text="Dark Theme" location="bottom">
35-
<template #activator="{ props }">
36-
<v-btn
37-
v-bind="props" :icon="MoonStar" size="x-small" class="px-4" aria-label="Toggle Dark Theme"
38-
@click="toggleTheme('dark')"
39-
/>
40-
</template>
41-
</v-tooltip>
42-
</v-btn-toggle>
23+
<v-menu offset-y width="200" class="rounded-xl">
24+
<template #activator="{ props: activatorProps }">
25+
<v-btn
26+
class="mr-2"
27+
v-bind="activatorProps"
28+
variant="outlined"
29+
color="default"
30+
rounded="lg"
31+
:icon="activeThemeIcon"
32+
/>
33+
</template>
34+
35+
<v-list class="px-2 rounded-lg">
36+
<v-list-item :active="activeTheme === 0" class="px-2" @click="themeStore.setTheme('light')">
37+
<v-list-item-title class="text-body-2">
38+
<v-btn
39+
class="mr-2"
40+
variant="outlined"
41+
color="default"
42+
size="x-small"
43+
rounded="lg"
44+
:icon="Sun"
45+
/>
46+
Light
47+
</v-list-item-title>
48+
</v-list-item>
49+
50+
<v-list-item :active="activeTheme === 1" class="px-2" @click="themeStore.setTheme('dark')">
51+
<v-list-item-title class="text-body-2">
52+
<v-btn
53+
class="mr-2"
54+
variant="outlined"
55+
color="default"
56+
size="x-small"
57+
rounded="lg"
58+
:icon="MoonStar"
59+
/>
60+
Dark
61+
</v-list-item-title>
62+
</v-list-item>
63+
64+
<v-list-item :active="activeTheme === 2" class="px-2" @click="themeStore.setTheme('auto')">
65+
<v-list-item-title class="text-body-2">
66+
<v-btn
67+
class="mr-2"
68+
variant="outlined"
69+
color="default"
70+
size="x-small"
71+
rounded="lg"
72+
:icon="smAndDown ? Smartphone : Monitor"
73+
/>
74+
System
75+
</v-list-item-title>
76+
</v-list-item>
77+
</v-list>
78+
</v-menu>
4379

4480
<v-menu offset-y class="rounded-xl">
4581
<template v-if="featureFlags.switchSatellite && featureFlags.operator &&featureFlags.signOut" #activator="{ props }">
@@ -86,9 +122,9 @@
86122
</template>
87123
</v-app-bar>
88124

89-
<v-navigation-drawer v-model="drawer" color="surface">
125+
<v-navigation-drawer v-model="drawer" :rail="mdAndUp && rail" :permanent="mdAndUp" color="surface">
90126
<v-sheet>
91-
<v-list class="px-2" variant="flat">
127+
<v-list density="compact" nav>
92128
<v-list-item v-if="featureFlags.switchSatellite" link class="pa-4 rounded-lg">
93129
<v-menu activator="parent" location="end" transition="scale-transition">
94130
<v-list class="pa-2">
@@ -166,7 +202,7 @@
166202
<!-- This view is temporary until we implement list with search -->
167203
<v-list-item v-if="featureFlags.account.search" link router-link to="/account-search" class="my-1" rounded="lg">
168204
<template #prepend>
169-
<img src="@/assets/icon-team.svg" alt="Accounts">
205+
<v-icon :icon="UserRoundSearch" />
170206
</template>
171207
<v-list-item-title class="text-body-2 ml-3">
172208
Search account
@@ -187,49 +223,58 @@
187223
</template>
188224

189225
<script setup lang="ts">
190-
import { ref, watch } from 'vue';
226+
import { computed, ref } from 'vue';
191227
import {
192228
VAppBar,
193229
VAppBarNavIcon,
194230
VAppBarTitle,
195-
VImg,
196-
VMenu,
197-
VBtnToggle,
198231
VBtn,
199-
VTooltip,
232+
VDivider,
200233
VIcon,
234+
VImg,
201235
VList,
202236
VListItem,
203-
VListItemTitle,
204237
VListItemSubtitle,
205-
VDivider,
238+
VListItemTitle,
239+
VMenu,
206240
VNavigationDrawer,
207241
VSheet,
208242
} from 'vuetify/components';
209-
import { useTheme } from 'vuetify';
210-
import { MoonStar, Sun } from 'lucide-vue-next';
243+
import { useDisplay } from 'vuetify';
244+
import { Monitor, MoonStar, Smartphone, Sun, UserRoundSearch } from 'lucide-vue-next';
211245
212246
import { FeatureFlags } from '@/api/client.gen';
213247
import { useAppStore } from '@/store/app';
248+
import { useThemeStore } from '@/store/theme';
249+
250+
const appStore = useAppStore();
251+
const themeStore = useThemeStore();
252+
const { mdAndUp, smAndDown } = useDisplay();
214253
215-
const theme = useTheme();
216254
const drawer = ref<boolean>(true);
217-
const activeTheme = ref<number>(0);
218-
const featureFlags = useAppStore().state.settings.admin.features as FeatureFlags;
255+
const rail = ref<boolean>(true);
219256
220-
function toggleTheme(newTheme: string) {
221-
if ((newTheme === 'dark' && theme.global.current.value.dark) || (newTheme === 'light' && !theme.global.current.value.dark)) {
222-
return;
257+
const activeTheme = computed<number>(() => {
258+
switch (themeStore.state.name) {
259+
case 'light':
260+
return 0;
261+
case 'dark':
262+
return 1;
263+
default:
264+
return 2;
223265
}
224-
theme.global.name.value = newTheme;
225-
localStorage.setItem('theme', newTheme); // Store the selected theme in localStorage
226-
}
266+
});
227267
228-
watch(() => theme.global.current.value.dark, newVal => {
229-
activeTheme.value = newVal ? 1 : 0;
268+
const activeThemeIcon = computed(() => {
269+
switch (themeStore.state.name) {
270+
case 'light':
271+
return Sun;
272+
case 'dark':
273+
return MoonStar;
274+
default:
275+
return themeStore.globalTheme?.dark ? MoonStar : Sun;
276+
}
230277
});
231278
232-
// Check for stored theme in localStorage. If none, default to 'light'
233-
toggleTheme(localStorage.getItem('theme') || 'light');
234-
activeTheme.value = theme.global.current.value.dark ? 1 : 0;
279+
const featureFlags = computed(() => appStore.state.settings.admin.features as FeatureFlags);
235280
</script>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (C) 2025 Storj Labs, Inc.
2+
// See LICENSE for copying information.
3+
4+
import { defineStore } from 'pinia';
5+
import { computed, reactive } from 'vue';
6+
import { useTheme } from 'vuetify';
7+
8+
export const DARK_THEME_QUERY = '(prefers-color-scheme: dark)';
9+
10+
export type ThemeName = 'light' | 'dark' | 'auto';
11+
12+
export class ThemeState {
13+
private _name: ThemeName = 'auto';
14+
15+
constructor() {
16+
this._name = localStorage.getItem('theme') as ThemeName || 'auto';
17+
}
18+
19+
public get name(): ThemeName {
20+
return this._name;
21+
}
22+
23+
set name(name: ThemeName) {
24+
localStorage.setItem('theme', name);
25+
this._name = name;
26+
}
27+
}
28+
29+
export const useThemeStore = defineStore('theme', () => {
30+
const theme = useTheme();
31+
32+
const state = reactive<ThemeState>((() => {
33+
const themeState = new ThemeState();
34+
35+
if (themeState.name === 'auto') {
36+
theme.global.name.value = window.matchMedia(DARK_THEME_QUERY).matches ? 'dark' : 'light';
37+
} else {
38+
theme.global.name.value = themeState.name;
39+
}
40+
return themeState;
41+
})());
42+
43+
const globalTheme = computed(() => theme.global.current.value);
44+
45+
function setTheme(name: ThemeName): void {
46+
if (name === state.name) {
47+
return;
48+
}
49+
state.name = name;
50+
if (name === 'auto') {
51+
theme.global.name.value = window.matchMedia(DARK_THEME_QUERY).matches ? 'dark' : 'light';
52+
} else {
53+
theme.global.name.value = name;
54+
}
55+
}
56+
57+
function setThemeLightness(isLight: boolean): void {
58+
if (state.name !== 'auto') {
59+
return;
60+
}
61+
theme.global.name.value = isLight ? 'light' : 'dark';
62+
}
63+
64+
return {
65+
state,
66+
globalTheme,
67+
setTheme,
68+
setThemeLightness,
69+
};
70+
});

0 commit comments

Comments
 (0)