add collapsible tab pane

This commit is contained in:
Ennea
2025-09-30 15:08:55 +02:00
parent 11311316fd
commit 2cdc5bfcd9
7 changed files with 208 additions and 47 deletions
+1
View File
@@ -6,6 +6,7 @@
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
@@ -0,0 +1,42 @@
<script lang="ts">
import { selectedTab, knownTabs, tabBarState } from "$lib/shared.svelte";
async function selectTab(index: number) {
const rawResponse = await fetch('/tab', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ index })
});
// const content = await rawResponse.json();
// TODO: use the response
}
function closeTabBar() {
tabBarState.visible = false;
}
</script>
<aside id="tabs" class:visible={tabBarState.visible}>
<div class="inner">
<header>
<span>Tabs</span>
<button type="button" onclick={() => closeTabBar()}>
<!-- "x" icon from https://github.com/feathericons/feather, under MIT license -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</header>
<ol id="tabs-list">
{#each knownTabs as tab}
<li class:active={selectedTab.index == tab.index}>
<button type="button" onclick={() => selectTab(tab.index)}>
{ tab.name }
</button>
</li>
{/each}
</ol>
</div>
</aside>
@@ -0,0 +1,30 @@
<script lang="ts">
import { tabBarState } from "$lib/shared.svelte";
function onclick() {
tabBarState.visible = true;
}
</script>
<button type="button" aria-label="Open tab pane" class:visible={!tabBarState.visible} {onclick}>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</button>
<style>
button {
position: absolute;
top: 50%;
left: 0;
padding: 25px 0;
border: none;
background-color: transparent;
transform: translateY(-50%);
z-index: 100;
opacity: 0;
transition: opacity 250ms ease;
}
button.visible {
opacity: 1;
}
</style>
+15 -10
View File
@@ -1,5 +1,5 @@
import {type Source, source} from "sveltekit-sse"; import { channelOptions, isChannelLocked, selectedTab, knownTabs } from "$lib/shared.svelte";
import {channelOptions, isChannelLocked} from "$lib/shared.svelte"; import { source, type Source } from "sveltekit-sse";
interface ChatElements { interface ChatElements {
messagesContainer: Element | null, messagesContainer: Element | null,
@@ -43,7 +43,7 @@ interface ChannelList {
} }
// ref `DataStructure.ChatTab` // ref `DataStructure.ChatTab`
interface ChatTab { export interface ChatTab {
name: string; name: string;
index: number; index: number;
} }
@@ -170,9 +170,9 @@ export class ChatTwoWeb {
} }
} }
// calculate timestamp width // calculate timestamp width to ensure that all timestamps have the same width.
// to ensure that all timestamps have the same width. some typefaces have the same width across // some typefaces have the same width across all number glyphs, others do not.
// all number glyphs, others do not. then there's AM/PM vs 24 hour, and so on // then there's AM/PM vs 24 hour, and so on
calculateTimestampWidth(timestamp: string) { calculateTimestampWidth(timestamp: string) {
if (this.elements.timestampWidthProbe === null) if (this.elements.timestampWidthProbe === null)
return; return;
@@ -368,10 +368,11 @@ export class ChatTwoWeb {
// tab switched // tab switched
this.connection.select('tab-switched').subscribe((data: string) => { this.connection.select('tab-switched').subscribe((data: string) => {
console.log(`Data received: ${data}`) console.log(`tab-switched: ${data}`)
if (data) { if (data) {
try { try {
let chatTab: ChatTab = JSON.parse(data); const chatTab: ChatTab = JSON.parse(data);
selectedTab.index = chatTab.index;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@@ -380,10 +381,14 @@ export class ChatTwoWeb {
// list of all tabs // list of all tabs
this.connection.select('tab-list').subscribe((data: string) => { this.connection.select('tab-list').subscribe((data: string) => {
console.log(`Data received: ${data}`) console.log(`tab-list: ${data}`)
if (data) { if (data) {
try { try {
let chatTabLit: ChatTabList = JSON.parse(data); const chatTabList: ChatTabList = JSON.parse(data);
knownTabs.length = 0;
for (const tab of chatTabList.tabs) {
knownTabs.push(tab);
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@@ -1,3 +1,5 @@
import type { ChatTab } from "./chat.svelte";
export const isChannelLocked: { locked: boolean } = $state({ locked: false }); export const isChannelLocked: { locked: boolean } = $state({ locked: false });
export const channelOptions: ChannelOption[] = $state([ { text: 'Invalid', value: 0, preview: true } ]); export const channelOptions: ChannelOption[] = $state([ { text: 'Invalid', value: 0, preview: true } ]);
@@ -6,3 +8,7 @@ export interface ChannelOption {
value: number; value: number;
preview: boolean; preview: boolean;
} }
export const selectedTab: { index: number } = $state({ index: 0 });
export const knownTabs: ChatTab[] = $state([]);
export const tabBarState: { visible: boolean } = $state({ visible: false });
@@ -1,11 +1,13 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state' import { page } from '$app/state'
import {Alert} from "@sveltestrap/sveltestrap"; import { Alert } from "@sveltestrap/sveltestrap";
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import {ChatTwoWeb} from '$lib/chat.svelte' import { ChatTwoWeb } from '$lib/chat.svelte'
import {addGfdStylesheet} from "$lib/gfd"; import { addGfdStylesheet } from "$lib/gfd";
import DynamicTextArea from "../../components/DynamicTextArea.svelte"; import DynamicTextArea from "../../components/DynamicTextArea.svelte";
import ChannelSelector from "../../components/ChannelSelector.svelte"; import ChannelSelector from "../../components/ChannelSelector.svelte";
import TabPane from "../../components/TabPane.svelte";
import TabPaneOpener from "../../components/TabPaneOpener.svelte";
let data: App.Warning = $state({ hasWarning: false, content: '' }); let data: App.Warning = $state({ hasWarning: false, content: '' });
$effect.pre(() => { $effect.pre(() => {
@@ -34,6 +36,11 @@
</script> </script>
<main class="chat"> <main class="chat">
<TabPane />
<div class="main-content">
<TabPaneOpener />
<section id="messages"> <section id="messages">
<div class="scroll-container"> <div class="scroll-container">
<ol id="messages-list"></ol> <ol id="messages-list"></ol>
@@ -54,13 +61,14 @@
<section id="input"> <section id="input">
<form> <form>
<div class="input-container"> <div class="input-container">
<DynamicTextArea/> <DynamicTextArea />
<ChannelSelector/> <ChannelSelector />
</div> </div>
<button type="submit">Send</button> <button type="submit">Send</button>
</form> </form>
</section> </section>
</div>
</main> </main>
<div id="timestamp-width-probe"></div> <div id="timestamp-width-probe"></div>
+77 -8
View File
@@ -18,6 +18,7 @@
--fg-faint: #a0a0a0; --fg-faint: #a0a0a0;
--fg-scrollbar: #404040; --fg-scrollbar: #404040;
--bg: #101010; --bg: #101010;
--bg-sidebar: #080808;
--bg-input: #202020; --bg-input: #202020;
--bg-input-hover: #282828; --bg-input-hover: #282828;
--focus-color: #4060a0; --focus-color: #4060a0;
@@ -51,22 +52,29 @@ span > a {
/* layout and global styles */ /* layout and global styles */
body { body {
padding: 50px; padding: 25px;
height: 100dvh; height: 100dvh;
background-color: var(--bg-input-hover); background-color: var(--bg-input-hover);
} }
body > div > main.chat { main.chat {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden;
background-color: var(--bg); background-color: var(--bg);
border-radius: 20px; border-radius: 20px;
box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.5); box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.5);
& > .main-content {
display: flex;
flex-direction: column;
flex-grow: 1;
}
} }
body > div > main.auth { main.auth {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -108,6 +116,66 @@ body > div > main.auth {
} }
} }
/* tab list */
aside#tabs {
flex: 0 0 auto;
overflow: hidden;
background-color: var(--bg-sidebar);
transition: width 250ms ease;
width: 0px;
&.visible { width: 200px; }
div.inner {
width: 200px;
padding: 20px;
}
header {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 20px;
font-size: 1.1rem;
font-weight: 550;
button {
margin-bottom: 2px;
border: none;
background-color: transparent;
}
}
ol#tabs-list {
margin: 0 -5px;
padding: 0;
list-style-type: none;
}
li {
padding: 3px 5px;
color: var(--fg-faint);
button {
width: 100%;
text-align: left;
color: inherit;
border: none;
background-color: transparent;
}
}
li + li {
margin-top: 3px;
}
li.active {
color: var(--fg);
border-radius: 3px;
background-color: var(--bg-input);
}
}
/* message list */ /* message list */
section#messages { section#messages {
position: relative; position: relative;
@@ -122,7 +190,9 @@ section#messages {
scrollbar-color: var(--fg-scrollbar) var(--bg); scrollbar-color: var(--fg-scrollbar) var(--bg);
} }
ol { ol#messages-list {
margin: 0;
padding: 0;
list-style-type: none; list-style-type: none;
} }
@@ -271,7 +341,6 @@ section#input {
&:focus { &:focus {
outline: 2px solid var(--focus-color); outline: 2px solid var(--focus-color);
} }
} }
} }
} }
@@ -315,12 +384,12 @@ section#input {
} }
/*** mobile ***/ /*** mobile ***/
@media ((max-width: 600px) and (orientation: portrait)) or (max-width: 400px) { @media ((max-width: 600px) and (orientation: portrait)) or (max-height: 400px) {
body { body {
padding: 0; padding: 0;
} }
body > main.chat { main.chat {
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
} }