Files
veridian/app/components/Sidenav/NavHome.vue
T
zoeissleeping ab57af1f23 feat: show page titles on agent and topic pages
This commit moves the actions bar out of the default template and into the
individual pages so that they can display their titles at the top. this
commit also has pages set the meta title so tabs are labeled nicely.
2026-02-24 16:33:32 +00:00

153 lines
6.2 KiB
Vue

<script setup lang="ts">
const { agents, createAgent } = useAgents();
const agentsOpen = ref(true);
const creatingAgent = ref(false);
const { openDropdown, dropdownState, closeDropdown } = useDropdown();
const triplit = useTriplitClient();
const navRef = ref<HTMLElement | null>(null);
const toggleAgentsList = () => {
agentsOpen.value = !agentsOpen.value;
};
const newAgent = async () => {
creatingAgent.value = true;
try {
const agent = await createAgent();
return navigateTo(`/agent/${agent.id}`);
} finally {
creatingAgent.value = false;
}
};
const renameAgentId = ref<string | null>(null);
const newAgentName = ref('');
const startRename = (agentId: string, currentName: string) => {
renameAgentId.value = agentId;
newAgentName.value = currentName;
nextTick(() => {
const input = document.getElementById('agent-rename-input') as HTMLInputElement | null;
if (input) {
input.focus();
}
})
};
const saveRename = async () => {
if (renameAgentId.value && newAgentName.value.trim()) {
await triplit.update('agents', renameAgentId.value, {
name: newAgentName.value.trim()
});
}
cancelRename();
};
const cancelRename = () => {
renameAgentId.value = null;
newAgentName.value = '';
};
const deleteAgent = async (agentId: string) => {
await triplit.delete('agents', agentId);
}
const handleNavClick = (e: MouseEvent) => {
const trigger = (e.target as HTMLElement).closest('[data-action]') as HTMLElement | null;
if (!trigger) return;
const agentId = trigger.dataset.agentId || (trigger.closest('[data-agent-id]') as HTMLElement | null)?.dataset.agentId;
if (!agentId) return;
const action = trigger.dataset.action;
switch (action) {
case 'navigate':
if (e.metaKey || e.ctrlKey || e.shiftKey) return;
e.preventDefault();
return navigateTo(`/agent/${agentId}`);
case 'toggle-dropdown':
e.preventDefault();
e.stopPropagation();
if (dropdownState.open) {
closeDropdown();
return;
}
openDropdown(e, () => {
const agent = agents.value.find(a => a.id === agentId);
if (agent) {
return [
{ label: 'Rename', onClick: () => startRename(agentId, agent?.name || '') },
{ label: 'Delete', danger: true, onClick: () => deleteAgent(agentId) }
];
}
return [];
}, { minWidth: '120px', placement: 'right' });
break;
}
}
onMounted(() => {
// simply preload the topic page
preloadRouteComponents(`/agent/42`);
})
</script>
<template>
<nav ref="navRef" class="flex flex-col gap-1 overflow-auto">
<SidenavItem name="Search" icon="mynaui:search" />
<SidenavItem to="/" name="Home" icon="mynaui:home" />
<!-- Header Toggle -->
<button
class="flex items-center justify-between gap-2 px-2 py-2 rounded-lg hover:bg-[var(--color-hover)] transition-colors w-full"
@click="toggleAgentsList">
<span class="text-sm">Agents</span>
<Icon name="mynaui:chevron-down" class="w-4 h-4 transition-transform duration-200"
:class="{ '-rotate-90': !agentsOpen }" />
</button>
<!-- Animated Section -->
<Collapsible :is-open="agentsOpen">
<div class="mt-1 gap-1 flex flex-col transform-origin-top pt-1 pb-1 px-1">
<button @click="newAgent" :disabled="creatingAgent"
class="flex items-center gap-2 px-1 h-9 shrink-0 rounded-lg text-sm text-[var(--text-secondary)] hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50 w-full">
<div class="h-7 w-7 flex items-center justify-center">
<Icon v-if="creatingAgent" class="text-4.5" name="svg-spinners:ring-resize" />
<Icon v-else class="text-4.5" name="mynaui:plus" />
</div>
<span>New Agent</span>
</button>
<div @click="handleNavClick" class="flex flex-col">
<RowVirtualizerFixed :scroll-element="navRef" key-field="id" :items="agents" :prerender="50"
:item-size="40" :overscan="20">
<template v-slot="{ item: agent }">
<a data-action="navigate" :data-agent-id="agent.id" :href="`/agent/${agent.id}`"
:aria-label="agent.name"
class="mt-1 group px-2 decoration-none flex justify-between items-center shrink-0 rounded-lg transition-colors cursor-pointer h-9 text-[var(--text-secondary)] hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] focus:text-[var(--text-primary)]">
<input v-if="renameAgentId === agent.id" id="agent-rename-input" v-model="newAgentName"
@keydown.stop.enter="saveRename" @keydown.escape="cancelRename" @blur="saveRename"
class="flex-1 bg-transparent border-none outline-none text-sm font-medium text-[var(--text-primary)] px-0 min-w-0" />
<span v-else
class="text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap">
{{ agent.name }}
</span>
<div data-action="toggle-dropdown"
class="text-[var(--text-secondary)] shrink-0 opacity-0 group-hover:opacity-100 p-1 flex items-center justify-center rounded-md hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<span class="h-4.5 w-4.5 i-tabler:dots"></span>
</div>
</a>
</template>
</RowVirtualizerFixed>
</div>
</div>
</Collapsible>
</nav>
</template>