Collapsable+resizable sidebar. Standard view header component.

This commit is contained in:
Shivering Ocelot committed 2025-07-20 21:44:20 -05:00
1 parent 69e8614a98
commit 9b30473c40
9 files changed
+202 -41

No files matched your search

+1 -2
View File
@@ -6,11 +6,10 @@
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0"> <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
<!--link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css"-->
<!--link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.6.0/css/fontawesome.min.css" integrity="sha384-NvKbDTEnL+A8F/AA5Tc5kmMLSJHUO868P+lDtTpJIeQdGYaUIuLr4lVGOEA1OcMy" crossorigin="anonymous"--> <!--link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.6.0/css/fontawesome.min.css" integrity="sha384-NvKbDTEnL+A8F/AA5Tc5kmMLSJHUO868P+lDtTpJIeQdGYaUIuLr4lVGOEA1OcMy" crossorigin="anonymous"-->
<!-- Checkout https://icons.getbootstrap.com, may be a good alternative -->
<title>Horde Chat</title> <title>Horde Chat</title>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
+22 -9
View File
@@ -1,5 +1,5 @@
/* color palette from <https://github.com/vuejs/theme> */
:root { :root {
/* color palette from <https://github.com/vuejs/theme> */
--vt-c-white: #ffffff; --vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8; --vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2; --vt-c-white-mute: #f2f2f2;
@@ -19,33 +19,46 @@
--vt-c-text-light-2: rgba(60, 60, 60, 0.66); --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white); --vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64); --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
--vt-c-text-dark-3: rgba(180, 180, 180, 0.64);
/* Horde colors */
--color-green: #00bd7e;
--color-green-dark: #003825;
--color-blue: #3d8de1;
} }
/* semantic color variables for this project */ /* semantic color and size variables for this project */
:root { :root {
--color-background: var(--vt-c-black); --color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft); --color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute); --color-background-mute: var(--vt-c-black-mute);
--color-background-hover: var(--vt-c-black-mute);
--color-background-active: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2); --color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1); --color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1); --color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
--color-text: var(--vt-c-text-dark-2);
--color-text-mute: var(--vt-c-text-dark-3);
--color-active: var(--color-heading);
--border-radius: 6px;
--section-gap: 160px; --section-gap: 160px;
} }
/* Horde Colors */ /* Z-Index hierarchy */
:root { :root {
--color-green: #00bd7e; --z-index-sidebar: 100;
--color-green-dark: #003825; --z-index-alert: 150;
--color-blue: #3d8de1; --z-index-context-menu: 160;
--border-radius: 6px;
} }
/* Dark theme by default, Light theme via browser settings */
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root { :root {
--color-background: var(--vt-c-white); --color-background: var(--vt-c-white);
+4
View File
@@ -29,4 +29,8 @@ main {
height: 100vh; height: 100vh;
flex-grow: 1; flex-grow: 1;
overflow: auto; overflow: auto;
}
.with-sidebar-expanded .when-sidebar-collapsed {
display: none;
} }
+36
View File
@@ -0,0 +1,36 @@
<script setup>
import * as ui from '../ui';
function sidebarExpand () {
ui.emit('sidebar-expand');
}
</script>
<template>
<header>
<button class="when-sidebar-collapsed" @click="sidebarExpand()">
>>
</button>
<slot>
<h1>Title Bar</h1>
</slot>
</header>
</template>
<style scoped>
header {
height: 3em;
border-bottom: 1px solid var(--color-border);
display: flex;
flex-direction: row;
align-items: center;
}
:slotted(h1) {
font-size: 160%;
font-weight: bold;
padding: 0 0.4em;
}
</style>
+118 -26
View File
@@ -1,19 +1,40 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import { import {
ref, computed, reactive, ref, computed, reactive, watch,
onMounted, onUnmounted, onMounted, onUnmounted,
} from "vue"; } from "vue";
import * as backend from '../../backend'; import * as backend from '../../backend';
import * as ui from '../../ui'; import * as ui from '../../ui';
const collapsed = ref(false);
const mode = ref('chats');
const chats = ref([]); const chats = ref([]);
const agents = ref([]); const agents = ref([]);
const mode = ref('chats');
const sidebarExpanded = ref(true);
const MIN_WIDTH = 150;
const MAX_WIDTH = 400;
const sidebarWidth = ref(200);
let dragState = null;
watch(sidebarExpanded, newValue => {
if (newValue) {
document.body.classList.add('with-sidebar-expanded');
} else {
document.body.classList.remove('with-sidebar-expanded');
}
}, {immediate: true});
ui.listen('sidebar-expand', ev => {
sidebarExpanded.value = true;
});
onMounted(async () => { onMounted(async () => {
document.body.addEventListener('mousemove', documentMouseMove);
document.body.addEventListener('mouseup', documentMouseUp);
chats.value = (await backend.getChats()).map(it => { chats.value = (await backend.getChats()).map(it => {
return {id: it.id, name: it.name}; return {id: it.id, name: it.name};
}); });
@@ -21,23 +42,61 @@
return {id: it.id, name: it.name}; return {id: it.id, name: it.name};
}); });
}); });
onUnmounted(() => {
document.body.removeEventListener('mousemove', documentMouseMove);
document.body.removeEventListener('mouseup', documentMouseUp);
});
function dragBarMouseDown (event) {
dragState = {x: event.clientX, y: event.clientY, width: sidebarWidth.value};
event.preventDefault();
event.stopPropagation();
console.log("Set Drag State", dragState);
}
function documentMouseMove (event) {
if (dragState) {
let widthDiff = event.clientX - dragState.x;
let newWidth = dragState.width + widthDiff;
newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, newWidth));
sidebarWidth.value = newWidth;
}
}
function documentMouseUp (event) {
if (dragState) {
dragState = null;
}
}
function sidebarStyle () {
return {
flex: `0 0 ${sidebarWidth.value}px`
}
}
</script> </script>
<template> <template>
<button class="nav-button expand-navbar" v-if="collapsed" @click="collapsed=false;"> <aside class="sidebar" :style="sidebarStyle()" v-if="sidebarExpanded">
</button> <div class="drag-bar" @mousedown="dragBarMouseDown($event)"></div>
<div class="tab-bar">
<aside class="sidebar" style="min-width:8em" v-else> <div class="tab-group">
<div class="drag-bar"></div> <div @click="mode='chats'" class="tab" :class="{active: mode=='chats'}">
<ul class="tab-bar"> <i class="fa-solid fa-message"></i>
<li @click="mode='chats'" :class="{active: mode=='chats'}"> </div>
<i class="fa-solid fa-message"></i> <div @click="mode='agents'" class="tab" :class="{active: mode=='agents'}">
</li> <i class="fa-solid fa-user-group"></i>
<li @click="mode='agents'" :class="{active: mode=='agents'}"> </div>
<i class="fa-solid fa-user-group"></i> </div>
</li> <div class="tab-group">
<li><i class="fa-solid fa-angles-left"></i></li> <button class="header-button">
</ul> <i class="fa-solid fa-angles-left" @click="sidebarExpanded=false;"></i>
</button>
</div>
</div>
<nav v-if="mode=='chats'"> <nav v-if="mode=='chats'">
<RouterLink v-for="it in chats" :to="'/chat/' + it.id" class="nav-item">{{it.name}}</RouterLink> <RouterLink v-for="it in chats" :to="'/chat/' + it.id" class="nav-item">{{it.name}}</RouterLink>
<RouterLink to="/chat/create" class="nav-item">Create Chat</RouterLink> <RouterLink to="/chat/create" class="nav-item">Create Chat</RouterLink>
@@ -54,33 +113,43 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
z-index: var(--z-index-sidebar);
background-color: var(--color-background);
} }
.tab-bar { .tab-bar, .tab-group {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-around;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
.tab-bar {
justify-content: space-between;
}
.tab-bar li { .tab {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
align-items: center; align-items: center;
list-style-type: none; list-style-type: none;
border-radius: var(--border-radius);
width: 3em;
height: 3em; height: 3em;
transition: background-color 0.5s; width: 3em;
padding-top: 3px;
color: var(--color-text-mute);
/*flex-grow: 1;*/
} }
.tab-bar li:hover { .tab:hover {
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
} }
.tab-bar li.active { .tab.active {
background-color: var(--color-background-soft); color: var(--color-text-heading);
background-color: var(--color-background);
padding-top: 0;
border-top: 3px solid var(--color-active);
} }
.nav-item { .nav-item {
@@ -118,4 +187,27 @@
right: 0; right: 0;
cursor: ew-resize; cursor: ew-resize;
} }
/* Adapt sidebar to small screens */
@media (max-width: 480px) {
.sidebar {
position: absolute;
width: 100%;
height: 100%;
}
.drag-bar {
display: none;
}
}
.header-button {
height: 3em;
width: 3em;
border-radius: 1.5em;
background-color: none;
border: none;
}
.header-button:hover {
background-color: var(--color-background-hover);
}
</style> </style>
+6 -1
View File
@@ -4,6 +4,8 @@
onMounted, onUnmounted, onMounted, onUnmounted,
} from "vue"; } from "vue";
import TitleBar from '../components/TitleBar.vue';
import * as backend from '../backend'; import * as backend from '../backend';
import * as ui from '../ui'; import * as ui from '../ui';
@@ -188,7 +190,10 @@
<template> <template>
<main> <main>
<h1>{{ agent.name || `Agent-${id}` }}</h1> <TitleBar>
<h1>{{ agent.name || `Agent-${id}` }}</h1>
</TitleBar>
<section class="top-section"> <section class="top-section">
<p style="white-space: pre-wrap; padding-bottom: 1em; border-bottom: 1px solid var(--color-border)">{{ kindInfo.description }}</p> <p style="white-space: pre-wrap; padding-bottom: 1em; border-bottom: 1px solid var(--color-border)">{{ kindInfo.description }}</p>
+5 -1
View File
@@ -4,6 +4,8 @@
onMounted, onUnmounted, onMounted, onUnmounted,
} from "vue"; } from "vue";
import TitleBar from '../components/TitleBar.vue';
import * as backend from '../backend'; import * as backend from '../backend';
import * as ui from '../ui'; import * as ui from '../ui';
@@ -48,7 +50,9 @@
<template> <template>
<main> <main>
<h1>Create New Chat</h1> <TitleBar>
<h1>Create Agent</h1>
</TitleBar>
<div class="main-container"> <div class="main-container">
<input v-model="agent.name" type="text" placeholder="name"> <input v-model="agent.name" type="text" placeholder="name">
+5 -1
View File
@@ -4,6 +4,8 @@
onMounted, onUnmounted, onMounted, onUnmounted,
} from "vue"; } from "vue";
import TitleBar from '../components/TitleBar.vue';
import * as backend from '../backend'; import * as backend from '../backend';
import * as ui from '../ui'; import * as ui from '../ui';
@@ -390,7 +392,9 @@
<template> <template>
<main> <main>
<h1>{{ chat.name || 'Chat #' + chat.id }}</h1> <TitleBar>
<h1>{{ chat.name || 'Chat #' + chat.id }}</h1>
</TitleBar>
<div class="messages-container" ref="chatContainerElem"> <div class="messages-container" ref="chatContainerElem">
<div v-for="msg in chatEntries" :class="chatEntryClass(msg)" @contextmenu.prevent="chatEntryContextMenu(msg)"> <div v-for="msg in chatEntries" :class="chatEntryClass(msg)" @contextmenu.prevent="chatEntryContextMenu(msg)">
+5 -1
View File
@@ -4,6 +4,8 @@
onMounted, onUnmounted, onMounted, onUnmounted,
} from "vue"; } from "vue";
import TitleBar from '../components/TitleBar.vue';
import * as backend from '../backend'; import * as backend from '../backend';
import * as ui from '../ui'; import * as ui from '../ui';
@@ -39,7 +41,9 @@
<template> <template>
<main> <main>
<h1>Create New Chat</h1> <TitleBar>
<h1>Create Chat</h1>
</TitleBar>
<div class="main-container"> <div class="main-container">
<input v-model="title" type="text" placeholder="title"> <input v-model="title" type="text" placeholder="title">