Improve mobile version

This commit is contained in:
dtonon 2026-05-18 12:27:18 +01:00
parent fdab17c8e2
commit 2e6f453abe
14 changed files with 421 additions and 73 deletions

View file

@ -3,6 +3,7 @@
import favicon from "$lib/assets/favicon.svg";
import Navbar from "$lib/components/Navbar.svelte";
import LeftSidebar from "$lib/components/LeftSidebar.svelte";
import MobileMenu from "$lib/components/MobileMenu.svelte";
import ChatSidebar from "$lib/components/ChatSidebar.svelte";
import LoginModal from "$lib/components/LoginModal.svelte";
import JoinModal from "$lib/components/JoinModal.svelte";
@ -27,8 +28,17 @@
});
let chatExpanded = $state(false);
let menuOpen = $state(false);
let mobileView = $state<"forum" | "chat">("forum");
const activeRoom = $derived(page.params.slug ?? "");
// On mobile a route change should always land on the forum pane, so opening
// a thread or room from the menu never leaves the user stranded on chat.
$effect(() => {
page.url.pathname;
mobileView = "forum";
});
</script>
<svelte:head>
@ -36,35 +46,98 @@
</svelte:head>
<div
class="flex h-screen items-center justify-center px-8 text-center text-gray-700 md:hidden"
role="status"
class="mx-auto flex max-w-[1540px] flex-col md:h-screen md:overflow-hidden {mobileView ===
'chat'
? 'h-dvh overflow-hidden'
: ''}"
>
Mobile version is coming, for now use a desktop PC
</div>
<div
class="hidden h-screen flex-col overflow-hidden max-w-[1540px] mx-auto md:flex"
>
<Navbar />
<div class="relative flex flex-1 gap-5 overflow-hidden pt-2">
<Navbar onMenuToggle={() => (menuOpen = true)} />
<div
class="relative md:flex md:flex-1 md:overflow-hidden md:gap-5 md:pt-2 {mobileView ===
'chat'
? 'flex flex-1 overflow-hidden'
: ''}"
>
<LeftSidebar {mode} {activeRoom} />
<main
class="flex-1 overflow-y-auto rounded-t-xl bg-white px-10 pt-6 pb-20 shadow-lg"
class="min-h-[calc(100dvh_-_4rem)] bg-white px-6 pt-8 pb-20 shadow-lg md:min-h-0 md:flex-1 md:overflow-y-auto md:rounded-t-xl md:px-10 md:pt-6
{mobileView === 'chat' ? 'hidden md:block' : 'block'}"
>
{@render children()}
</main>
{#if chatEnabled}
<div class="shrink-0 w-80" aria-hidden="true"></div>
<div class="hidden w-80 shrink-0 md:block" aria-hidden="true"></div>
<ChatSidebar
expanded={chatExpanded}
onToggle={() => (chatExpanded = !chatExpanded)}
mobileActive={mobileView === "chat"}
/>
{/if}
</div>
{#if chatEnabled}
<nav
class="fixed inset-x-0 bottom-0 z-30 flex border-t border-neutral-200 bg-neutral-100 md:hidden"
aria-label="Switch view"
>
<button
type="button"
onclick={() => (mobileView = "forum")}
aria-pressed={mobileView === "forum"}
class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium
{mobileView === 'forum' ? 'text-brand' : 'text-neutral-500'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.8"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
Forum
</button>
<button
type="button"
onclick={() => (mobileView = "chat")}
aria-pressed={mobileView === "chat"}
class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium
{mobileView === 'chat' ? 'text-brand' : 'text-neutral-500'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.8"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z"
/>
</svg>
Chat
</button>
</nav>
{/if}
</div>
<div class="hidden md:contents">
<LoginModal />
<JoinModal />
<NewDiscussionModal />
</div>
<MobileMenu
open={menuOpen}
onClose={() => (menuOpen = false)}
{mode}
{activeRoom}
/>
<LoginModal />
<JoinModal />
<NewDiscussionModal />

View file

@ -90,14 +90,14 @@
</svelte:head>
<div class="mx-auto max-w-6xl">
<div class="flex items-center justify-between py-2">
<div class="flex flex-wrap items-center justify-between gap-2 py-2">
<h1 class="text-[1.65rem] text-brand">Discussions</h1>
<div class="flex items-center gap-2">
<button
onclick={onNewTopic}
class="rounded bg-brand px-6 py-1.5 text-sm font-medium text-white hover:bg-brand-hover"
class="rounded bg-brand px-4 py-1.5 md:text-sm font-medium text-white hover:bg-brand-hover md:px-6"
>
New Topic
New discussion
</button>
<SortToggle {sort} />
</div>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { groupStore } from "$lib/group.svelte";
import { GROUP_ID } from "$lib/config";
const name = $derived(groupStore.data?.name ?? GROUP_ID);
const about = $derived(groupStore.data?.about ?? "");
</script>
<svelte:head>
<title>About — {name}</title>
<meta name="description" content={about} />
</svelte:head>
<div class="mx-auto max-w-6xl">
<h1 class="py-2 text-[1.65rem] text-brand">About {name}</h1>
{#if about}
<p class="leading-relaxed whitespace-pre-wrap text-neutral-600">{about}</p>
{:else}
<p class="text-neutral-400">No description available.</p>
{/if}
</div>

View file

@ -41,7 +41,7 @@
<meta name="description" content="Admins and contacts for this community." />
</svelte:head>
<div class="mx-auto max-w-3xl">
<div class="mx-auto max-w-6xl">
<h1 class="py-2 text-[1.65rem] text-brand">Contacts</h1>
{#if !groupStore.data}

View file

@ -45,9 +45,9 @@
<h1 class="text-[1.65rem] text-brand leading-7">{room?.name ?? slug}</h1>
<button
onclick={onNewTopic}
class="rounded bg-brand px-6 py-1.5 text-sm font-medium text-white hover:bg-brand-hover"
class="rounded bg-brand px-6 py-1.5 md:text-sm font-medium text-white hover:bg-brand-hover"
>
New Topic
New discussion
</button>
</div>

View file

@ -125,6 +125,13 @@
};
main.addEventListener("scroll", onScroll, { passive: true });
// On mobile the document scrolls (not main), so clear the quote popup on
// window scroll too.
const onWinScroll = () => {
if (selectionTarget) selectionTarget = null;
};
window.addEventListener("scroll", onWinScroll, { passive: true });
const onSelectionChange = () => {
const sel = window.getSelection();
if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
@ -171,6 +178,7 @@
return () => {
main.classList.remove("no-scrollbar");
main.removeEventListener("scroll", onScroll);
window.removeEventListener("scroll", onWinScroll);
document.removeEventListener("selectionchange", onSelectionChange);
};
});
@ -218,13 +226,22 @@
bindEl: (el: HTMLElement | null) => void,
)}
{@const author = resolveAuthor(p.pubkey, profiles)}
<div use:bindEl class="flex gap-6 items-start">
<div class="flex-shrink-0">
<div use:bindEl class="md:flex md:gap-6 md:items-start">
<div class="hidden flex-shrink-0 md:block">
{@render avatar(author)}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-baseline justify-between mb-3">
<span class="font-medium text-neutral-400">{author.name}</span>
<div
class="mb-4 flex items-center justify-between md:mb-3 md:items-baseline"
>
<div class="flex min-w-0 items-center gap-3">
<div class="flex-shrink-0 md:hidden">
{@render avatar(author)}
</div>
<span class="truncate font-medium text-neutral-400"
>{author.name}</span
>
</div>
<span class="text-sm text-neutral-400 ml-4 flex-shrink-0"
>{formatDate(p.createdAt)}</span
>
@ -232,37 +249,39 @@
<div data-quote-post-index={index} id="post-{p.id}" class="scroll-mt-32">
<PostContent content={p.content} {profiles} {threadEventAuthors} />
</div>
<div class="flex items-center justify-start mt-6">
<button
onclick={() => quotePost(p)}
disabled={!auth.user}
class="flex items-center gap-1.5 text-neutral-300 cursor-pointer hover:text-brand transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<span>Quote post</span>
<svg
class="w-3"
viewBox="0 0 800 800"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;"
><path
d="M101.286,748.313l199.143,0c109.981,0 199.142,-89.161 199.142,-199.142l0,-497.856m0,-0l199.143,199.142m-199.143,-199.142l-199.142,199.142"
style="fill:none;fill-rule:nonzero;stroke:currentColor;stroke-width:99.57px;"
/></svg
{#if auth.user}
<div class="flex items-center justify-start mt-6">
<button
onclick={() => quotePost(p)}
disabled={!auth.user}
class="flex items-center gap-1.5 text-neutral-300 cursor-pointer hover:text-brand transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
</div>
<span>Quote post</span>
<svg
class="w-3"
viewBox="0 0 800 800"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;"
><path
d="M101.286,748.313l199.143,0c109.981,0 199.142,-89.161 199.142,-199.142l0,-497.856m0,-0l199.143,199.142m-199.143,-199.142l-199.142,199.142"
style="fill:none;fill-rule:nonzero;stroke:currentColor;stroke-width:99.57px;"
/></svg
>
</button>
</div>
{/if}
</div>
</div>
{/snippet}
{#if detail}
<div class="flex gap-6 items-start">
<div class="flex-1 min-w-0" class:pr-18={!scrubberVisible}>
<div class="flex-1 min-w-0 {!scrubberVisible ? 'md:pr-18' : ''}">
<div
class="sticky -top-6 bg-white z-10 pb-1 -mx-10 px-10 pt-6 -mt-6 relative"
class="relative z-10 bg-white pb-1 md:sticky md:-top-6 md:-mx-10 md:px-10 md:pt-6 md:-mt-6"
>
<h1 class="text-[1.65rem] text-brand leading-7">{detail.title}</h1>
{#if isScrolled}
@ -295,7 +314,7 @@
</div>
{/if}
<div class="mt-8 border-t border-neutral-200 pt-6 pl-18">
<div class="mt-8 border-t border-neutral-200 pt-6 md:pl-18">
{#if auth.user}
{#if replyError}
<div
@ -314,13 +333,13 @@
contextPubkeys={allPosts.map((p) => p.pubkey)}
{threadEventAuthors}
/>
<div class="mt-2 flex justify-end">
<div class="mt-4 flex justify-end">
<button
onclick={submitReply}
disabled={replying || !replyContent.trim()}
class="rounded bg-brand px-6 py-1.5 font-medium text-white hover:bg-brand-hover disabled:opacity-50 disabled:cursor-not-allowed"
>
{replying ? "Posting…" : "Reply"}
{replying ? "Posting…" : "post reply"}
</button>
</div>
{:else}