feat: replace settings with a full page

This commit is contained in:
Zoe
2026-10-08 08:06:06 -05:00
parent 2b9f8d64d7
commit ff33e9c573
21 changed files with 503 additions and 245 deletions
+8 -10
View File
@@ -1,6 +1,4 @@
<script setup lang="ts">
import { DialogType } from '~/composables/useDialog';
const { user, signOut } = useAuth();
// to prevent the user details from going blank for a
@@ -16,10 +14,14 @@ watch(user, () => {
cachedUser.value = user.value;
})
const { openDialog } = useDialog();
const { isHovered } = useSidenavContext();
const openSettings = async () => {
dropdownOpen.value = false;
await navigateTo('/settings');
};
const handleLogout = async () => {
await signOut();
@@ -31,11 +33,7 @@ const handleLogout = async () => {
<header class="flex items-center justify-between overflow-hidden">
<button @click="dropdownOpen = !dropdownOpen"
class="flex gap-1 pr-1 items-center @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<div
:class="['w-9 h-9 md:w-7 md:h-7 flex-shrink-0 rounded-lg overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center', user?.image ? '' : 'border border-[var(--color-border)]']">
<img v-if="cachedUser?.image" :src="cachedUser.image" class="w-full h-full object-cover" />
<span v-else class="w-5.5 h-5.5 md:h-4 md:w-4 i-mynaui-user text-[var(--text-secondary)]"></span>
</div>
<UserAvatar :user="cachedUser" class="w-9 h-9 md:w-7 md:h-7 rounded-lg md:text-sm" />
<span
class="md:text-sm font-medium text-ellipsis overflow-hidden text-[var(--text-primary)] whitespace-nowrap">
{{ cachedUser?.name }}
@@ -55,7 +53,7 @@ const handleLogout = async () => {
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
<div v-if="dropdownOpen" v-click-outside="() => dropdownOpen = false"
class="w-full top-full text-sm mt-1 absolute z-30 bg-[var(--bg-surface)] border border-[var(--color-border)] rounded-xl p-1.5 flex flex-col gap-2">
<button @click="dropdownOpen = false; openDialog(DialogType.Settings)"
<button @click="openSettings"
class="text-left px-3 py-1.5 items-center gap-1 @hover:bg-[var(--color-hover)] rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<span class="i-mynaui-cog-four text-4.5"></span>
<span>Settings</span>
+16
View File
@@ -0,0 +1,16 @@
<script setup lang="ts">
const returnTo = useState('settings:returnTo', () => '/');
</script>
<template>
<header class="flex items-center rounded-lg overflow-hidden flex-1 min-w-0">
<NuxtLink :to="returnTo" aria-label="Back"
class="flex items-center gap-1 p-1.5 pr-2.5 @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] rounded-lg decoration-none transition-inherit">
<span class="i-mynaui-chevron-left text-5.5 md:text-4.5 text-[var(--text-secondary)]"></span>
<span
class="md:text-sm font-medium text-ellipsis overflow-hidden text-[var(--text-primary)] whitespace-nowrap">
Settings
</span>
</NuxtLink>
</header>
</template>
+63
View File
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { providerIcons } from '~/utils/model-mapping';
const route = useRoute();
const { providers } = await useModels();
const groups = [
{
label: 'Account',
items: [
{ to: '/settings/profile', name: 'Profile', icon: 'i-mynaui-user' },
],
},
{
label: 'App',
items: [
{ to: '/settings/general', name: 'General', icon: 'i-mynaui-cog-four' },
{ to: '/settings/appearance', name: 'Appearance', icon: 'i-tabler-palette' },
{ to: '/settings/providers', name: 'AI Providers', icon: 'i-mynaui-api' },
{ to: '/settings/assistants', name: 'System Assistants', icon: 'i-mynaui-sparkles' },
],
},
];
const inProviders = computed(() => route.path.startsWith('/settings/providers'));
// enabled providers first, same order the provider list uses
const sortedProviders = computed(() => [
...providers.value.filter(p => p.enabled),
...providers.value.filter(p => !p.enabled),
]);
</script>
<template>
<nav aria-label="Settings" class="flex flex-col gap-1 overflow-auto">
<template v-for="group in groups" :key="group.label">
<div class="px-2 pt-3 pb-0.5 text-sm md:text-xs font-medium uppercase tracking-wider">
{{ group.label }}
</div>
<template v-for="item in group.items" :key="item.to">
<SidenavItem :to="item.to" :name="item.name" :icon="item.icon" :active="route.path === item.to" />
<!-- the providers are listed under their section while it is open -->
<div v-if="item.to === '/settings/providers' && inProviders" class="flex flex-col gap-1 pl-4">
<NuxtLink v-for="p in sortedProviders" :key="p.id" :to="`/settings/providers/${p.id}`" :class="[
'case-capital decoration-none flex items-center gap-2 shrink-0 px-2 h-9 rounded-lg transition-colors',
route.path === `/settings/providers/${p.id}`
? 'text-[var(--text-primary)] bg-[var(--color-hover)]'
: 'text-[var(--text-secondary)] @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)]',
p.enabled ? '' : 'opacity-50',
]">
<component v-if="providerIcons[p.type]" :color="true" :is="providerIcons[p.type]"
class="h-6 w-6 md:w-4 md:h-4 flex-shrink-0 text-[var(--text-primary)]" />
<span class="md:text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap">
{{ p.name }}
</span>
</NuxtLink>
</div>
</template>
</template>
</nav>
</template>
+17 -7
View File
@@ -1,6 +1,4 @@
<script setup lang="ts">
import { DialogType } from '~/composables/useDialog';
const { close: closeSidebar, open, sidebarWidth, resize, saveWidth } = useSidebar();
const route = useRoute();
@@ -37,8 +35,6 @@ const onFocusOut = (e: FocusEvent) => {
}
};
const { openDialog } = useDialog();
const onResizeStart = (event: MouseEvent) => {
isResizing.value = true;
startX.value = event.clientX;
@@ -97,8 +93,18 @@ const navKind = computed(() => {
if (route.path === '/') return 'home';
if (route.path.startsWith('/agent/')) return 'agent';
if (route.path === '/resources') return 'resources';
if (route.path.startsWith('/settings')) return 'settings';
return null;
});
// remember where the user was so the settings back button can return there
const settingsReturnTo = useState('settings:returnTo', () => '/');
watch(() => route.fullPath, (path) => {
if (!path.startsWith('/settings')) {
settingsReturnTo.value = path;
}
}, { immediate: true });
</script>
<template>
@@ -126,6 +132,7 @@ const navKind = computed(() => {
<SidenavHeader v-if="navKind === 'home'" />
<SidenavHeaderResources v-else-if="navKind === 'resources'" />
<SidenavHeaderAgent v-else-if="navKind === 'agent'" />
<SidenavHeaderSettings v-else-if="navKind === 'settings'" />
<div class="flex items-center justify-end text-[var(--text-secondary)] gap-0.5">
<div :class="[
@@ -158,15 +165,18 @@ const navKind = computed(() => {
<SidenavNavHome v-if="navKind === 'home'" />
<SidenavNavAgent v-else-if="navKind === 'agent'" />
<SidenavNavResources v-else-if="navKind === 'resources'" />
<SidenavNavSettings v-else-if="navKind === 'settings'" />
</div>
<div class="flex justify-between pt-2 z-25 bg-[var(--bg-base)] pb-2">
<div class="flex">
<Tooltip :hotkey="['ctrl', ',']">
<button @click="openDialog(DialogType.Settings)"
class="flex items-center justify-center h-9 w-9 md:h-7 md:w-7 cursor-pointer @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] rounded-lg transition-colors text-[var(--text-secondary)] active:text-[var(--text-primary)]">
<NuxtLink to="/settings" aria-label="Settings" :class="[
'flex items-center justify-center h-9 w-9 md:h-7 md:w-7 cursor-pointer @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] rounded-lg transition-colors active:text-[var(--text-primary)]',
navKind === 'settings' ? 'text-[var(--text-primary)] bg-[var(--color-hover)]' : 'text-[var(--text-secondary)]',
]">
<span class="i-mynaui-cog-four text-6 md:text-5"></span>
</button>
</NuxtLink>
</Tooltip>
</div>