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
+198 -37

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">
<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"-->
<!-- Checkout https://icons.getbootstrap.com, may be a good alternative -->
<title>Horde Chat</title>
<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 {
/* color palette from <https://github.com/vuejs/theme> */
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
@@ -19,33 +19,46 @@
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--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 {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--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-hover: var(--vt-c-divider-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;
}
/* Horde Colors */
/* Z-Index hierarchy */
:root {
--color-green: #00bd7e;
--color-green-dark: #003825;
--color-blue: #3d8de1;
--border-radius: 6px;
--z-index-sidebar: 100;
--z-index-alert: 150;
--z-index-context-menu: 160;
}
/* Dark theme by default, Light theme via browser settings */
@media (prefers-color-scheme: light) {
:root {
--color-background: var(--vt-c-white);
+4
View File
@@ -30,3 +30,7 @@ main {
flex-grow: 1;
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>
+116 -24
View File
@@ -1,19 +1,40 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import {
ref, computed, reactive,
ref, computed, reactive, watch,
onMounted, onUnmounted,
} from "vue";
import * as backend from '../../backend';
import * as ui from '../../ui';
const collapsed = ref(false);
const mode = ref('chats');
const chats = 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 () => {
document.body.addEventListener('mousemove', documentMouseMove);
document.body.addEventListener('mouseup', documentMouseUp);
chats.value = (await backend.getChats()).map(it => {
return {id: it.id, name: it.name};
});
@@ -21,23 +42,61 @@
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>
<template>
<button class="nav-button expand-navbar" v-if="collapsed" @click="collapsed=false;">
</button>
<aside class="sidebar" style="min-width:8em" v-else>
<div class="drag-bar"></div>
<ul class="tab-bar">
<li @click="mode='chats'" :class="{active: mode=='chats'}">
<aside class="sidebar" :style="sidebarStyle()" v-if="sidebarExpanded">
<div class="drag-bar" @mousedown="dragBarMouseDown($event)"></div>
<div class="tab-bar">
<div class="tab-group">
<div @click="mode='chats'" class="tab" :class="{active: mode=='chats'}">
<i class="fa-solid fa-message"></i>
</li>
<li @click="mode='agents'" :class="{active: mode=='agents'}">
</div>
<div @click="mode='agents'" class="tab" :class="{active: mode=='agents'}">
<i class="fa-solid fa-user-group"></i>
</li>
<li><i class="fa-solid fa-angles-left"></i></li>
</ul>
</div>
</div>
<div class="tab-group">
<button class="header-button">
<i class="fa-solid fa-angles-left" @click="sidebarExpanded=false;"></i>
</button>
</div>
</div>
<nav v-if="mode=='chats'">
<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>
@@ -54,33 +113,43 @@
display: flex;
flex-direction: column;
position: relative;
z-index: var(--z-index-sidebar);
background-color: var(--color-background);
}
.tab-bar {
.tab-bar, .tab-group {
display: flex;
flex-direction: row;
justify-content: space-around;
padding: 0;
margin: 0;
}
.tab-bar {
justify-content: space-between;
}
.tab-bar li {
.tab {
display: flex;
justify-content: space-around;
align-items: center;
list-style-type: none;
border-radius: var(--border-radius);
width: 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);
}
.tab-bar li.active {
background-color: var(--color-background-soft);
.tab.active {
color: var(--color-text-heading);
background-color: var(--color-background);
padding-top: 0;
border-top: 3px solid var(--color-active);
}
.nav-item {
@@ -118,4 +187,27 @@
right: 0;
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>
+5
View File
@@ -4,6 +4,8 @@
onMounted, onUnmounted,
} from "vue";
import TitleBar from '../components/TitleBar.vue';
import * as backend from '../backend';
import * as ui from '../ui';
@@ -188,7 +190,10 @@
<template>
<main>
<TitleBar>
<h1>{{ agent.name || `Agent-${id}` }}</h1>
</TitleBar>
<section class="top-section">
<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,
} from "vue";
import TitleBar from '../components/TitleBar.vue';
import * as backend from '../backend';
import * as ui from '../ui';
@@ -48,7 +50,9 @@
<template>
<main>
<h1>Create New Chat</h1>
<TitleBar>
<h1>Create Agent</h1>
</TitleBar>
<div class="main-container">
<input v-model="agent.name" type="text" placeholder="name">
+4
View File
@@ -4,6 +4,8 @@
onMounted, onUnmounted,
} from "vue";
import TitleBar from '../components/TitleBar.vue';
import * as backend from '../backend';
import * as ui from '../ui';
@@ -390,7 +392,9 @@
<template>
<main>
<TitleBar>
<h1>{{ chat.name || 'Chat #' + chat.id }}</h1>
</TitleBar>
<div class="messages-container" ref="chatContainerElem">
<div v-for="msg in chatEntries" :class="chatEntryClass(msg)" @contextmenu.prevent="chatEntryContextMenu(msg)">
+5 -1
View File
@@ -4,6 +4,8 @@
onMounted, onUnmounted,
} from "vue";
import TitleBar from '../components/TitleBar.vue';
import * as backend from '../backend';
import * as ui from '../ui';
@@ -39,7 +41,9 @@
<template>
<main>
<h1>Create New Chat</h1>
<TitleBar>
<h1>Create Chat</h1>
</TitleBar>
<div class="main-container">
<input v-model="title" type="text" placeholder="title">