add collapsible tab pane
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</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>
|
||||
@@ -1,5 +1,5 @@
|
||||
import {type Source, source} from "sveltekit-sse";
|
||||
import {channelOptions, isChannelLocked} from "$lib/shared.svelte";
|
||||
import { channelOptions, isChannelLocked, selectedTab, knownTabs } from "$lib/shared.svelte";
|
||||
import { source, type Source } from "sveltekit-sse";
|
||||
|
||||
interface ChatElements {
|
||||
messagesContainer: Element | null,
|
||||
@@ -43,7 +43,7 @@ interface ChannelList {
|
||||
}
|
||||
|
||||
// ref `DataStructure.ChatTab`
|
||||
interface ChatTab {
|
||||
export interface ChatTab {
|
||||
name: string;
|
||||
index: number;
|
||||
}
|
||||
@@ -170,9 +170,9 @@ export class ChatTwoWeb {
|
||||
}
|
||||
}
|
||||
|
||||
// calculate timestamp width
|
||||
// to ensure that all timestamps have the same width. some typefaces have the same width across
|
||||
// all number glyphs, others do not. then there's AM/PM vs 24 hour, and so on
|
||||
// calculate timestamp width to ensure that all timestamps have the same width.
|
||||
// some typefaces have the same width across all number glyphs, others do not.
|
||||
// then there's AM/PM vs 24 hour, and so on
|
||||
calculateTimestampWidth(timestamp: string) {
|
||||
if (this.elements.timestampWidthProbe === null)
|
||||
return;
|
||||
@@ -368,10 +368,11 @@ export class ChatTwoWeb {
|
||||
|
||||
// tab switched
|
||||
this.connection.select('tab-switched').subscribe((data: string) => {
|
||||
console.log(`Data received: ${data}`)
|
||||
console.log(`tab-switched: ${data}`)
|
||||
if (data) {
|
||||
try {
|
||||
let chatTab: ChatTab = JSON.parse(data);
|
||||
const chatTab: ChatTab = JSON.parse(data);
|
||||
selectedTab.index = chatTab.index;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -380,10 +381,14 @@ export class ChatTwoWeb {
|
||||
|
||||
// list of all tabs
|
||||
this.connection.select('tab-list').subscribe((data: string) => {
|
||||
console.log(`Data received: ${data}`)
|
||||
console.log(`tab-list: ${data}`)
|
||||
if (data) {
|
||||
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) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ChatTab } from "./chat.svelte";
|
||||
|
||||
export const isChannelLocked: { locked: boolean } = $state({ locked: false });
|
||||
export const channelOptions: ChannelOption[] = $state([ { text: 'Invalid', value: 0, preview: true } ]);
|
||||
|
||||
@@ -6,3 +8,7 @@ export interface ChannelOption {
|
||||
value: number;
|
||||
preview: boolean;
|
||||
}
|
||||
|
||||
export const selectedTab: { index: number } = $state({ index: 0 });
|
||||
export const knownTabs: ChatTab[] = $state([]);
|
||||
export const tabBarState: { visible: boolean } = $state({ visible: false });
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { addGfdStylesheet } from "$lib/gfd";
|
||||
import DynamicTextArea from "../../components/DynamicTextArea.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: '' });
|
||||
$effect.pre(() => {
|
||||
@@ -34,6 +36,11 @@
|
||||
</script>
|
||||
|
||||
<main class="chat">
|
||||
<TabPane />
|
||||
|
||||
<div class="main-content">
|
||||
<TabPaneOpener />
|
||||
|
||||
<section id="messages">
|
||||
<div class="scroll-container">
|
||||
<ol id="messages-list"></ol>
|
||||
@@ -61,6 +68,7 @@
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="timestamp-width-probe"></div>
|
||||
@@ -18,6 +18,7 @@
|
||||
--fg-faint: #a0a0a0;
|
||||
--fg-scrollbar: #404040;
|
||||
--bg: #101010;
|
||||
--bg-sidebar: #080808;
|
||||
--bg-input: #202020;
|
||||
--bg-input-hover: #282828;
|
||||
--focus-color: #4060a0;
|
||||
@@ -51,22 +52,29 @@ span > a {
|
||||
|
||||
/* layout and global styles */
|
||||
body {
|
||||
padding: 50px;
|
||||
padding: 25px;
|
||||
height: 100dvh;
|
||||
background-color: var(--bg-input-hover);
|
||||
}
|
||||
|
||||
body > div > main.chat {
|
||||
main.chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg);
|
||||
border-radius: 20px;
|
||||
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;
|
||||
flex-direction: column;
|
||||
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 */
|
||||
section#messages {
|
||||
position: relative;
|
||||
@@ -122,7 +190,9 @@ section#messages {
|
||||
scrollbar-color: var(--fg-scrollbar) var(--bg);
|
||||
}
|
||||
|
||||
ol {
|
||||
ol#messages-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
@@ -271,7 +341,6 @@ section#input {
|
||||
&:focus {
|
||||
outline: 2px solid var(--focus-color);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,12 +384,12 @@ section#input {
|
||||
}
|
||||
|
||||
/*** mobile ***/
|
||||
@media ((max-width: 600px) and (orientation: portrait)) or (max-width: 400px) {
|
||||
@media ((max-width: 600px) and (orientation: portrait)) or (max-height: 400px) {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body > main.chat {
|
||||
main.chat {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user