add collapsible tab pane
This commit is contained in:
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user