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

@ -15,9 +15,10 @@
type Props = {
expanded?: boolean;
onToggle: () => void;
mobileActive?: boolean;
};
let { expanded = false, onToggle }: Props = $props();
let { expanded = false, onToggle, mobileActive = false }: Props = $props();
let asideEl: HTMLElement;
let listEl = $state<HTMLDivElement | null>(null);
@ -144,14 +145,15 @@
<aside
bind:this={asideEl}
class="absolute top-2 right-0 z-10 flex h-[calc(100%-0.5rem)] flex-col rounded-tl-xl bg-white transition-all duration-200 min-[1540px]:rounded-tr-xl
{expanded ? 'w-150 shadow-2xl' : 'w-80 shadow-lg'} px-6 py-6"
class="flex-1 flex-col bg-white px-4 pt-4 pb-20 md:absolute md:top-2 md:right-0 md:z-10 md:h-[calc(100%-0.5rem)] md:flex-none md:rounded-tl-xl md:px-6 md:py-6 md:transition-all md:duration-200 min-[1540px]:rounded-tr-xl
{mobileActive ? 'flex' : 'hidden'} md:flex
{expanded ? 'md:w-150 md:shadow-2xl' : 'md:w-80 md:shadow-lg'}"
>
<div class="mb-6 flex shrink-0 items-center justify-between">
<span class="text-brand text-[1.5rem] leading-7">Chat</span>
<button
onclick={onToggle}
class="rounded bg-neutral-100 transition-colors hover:bg-neutral-200"
class="hidden rounded bg-neutral-100 transition-colors hover:bg-neutral-200 md:block"
aria-label={expanded ? "Collapse chat" : "Expand chat"}
>
<svg

View file

@ -15,7 +15,7 @@
</script>
<aside
class="flex max-w-52 min-w-48 shrink-0 flex-col justify-between py-2 pl-8 pr-1 pb-8"
class="hidden max-w-52 min-w-48 shrink-0 flex-col justify-between py-2 pl-8 pr-1 pb-8 md:flex"
>
<div>
<a
@ -59,6 +59,10 @@
>
More
</p>
<a
href="/about"
class="block py-1 text-neutral-500 hover:text-neutral-900">About</a
>
<a
href="/contacts"
class="block py-1 text-neutral-500 hover:text-neutral-900">Contacts</a

View file

@ -0,0 +1,200 @@
<script lang="ts">
import { fly, fade } from "svelte/transition";
import { rooms } from "$lib/mock";
import { auth, openLogin, logout } from "$lib/auth.svelte";
import { draftState, resumeDraft } from "$lib/draft.svelte";
type Props = {
open: boolean;
onClose: () => void;
mode: "simple" | "full";
activeRoom?: string;
};
let { open, onClose, mode, activeRoom }: Props = $props();
const groups = [...new Set(rooms.map((r) => r.group))];
function onLogin() {
onClose();
openLogin();
}
function onLogout() {
if (confirm("Log out?")) {
logout();
onClose();
}
}
function onResume() {
onClose();
resumeDraft();
}
function onKeydown(e: KeyboardEvent) {
if (!open) return;
if (e.key === "Escape") onClose();
}
// Swipe-to-close: the drawer sits on the right, so a rightward swipe dismisses it.
let touchStartX = 0;
let touchStartY = 0;
let swiping = false;
function onTouchStart(e: TouchEvent) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
swiping = false;
}
function onTouchMove(e: TouchEvent) {
const dx = e.touches[0].clientX - touchStartX;
const dy = e.touches[0].clientY - touchStartY;
if (Math.abs(dx) > Math.abs(dy)) swiping = true;
}
function onTouchEnd(e: TouchEvent) {
if (!swiping) return;
if (e.changedTouches[0].clientX - touchStartX > 60) onClose();
}
</script>
<svelte:window onkeydown={onKeydown} />
{#if open}
<div class="fixed inset-0 z-40 md:hidden">
<button
type="button"
aria-label="Close menu"
class="absolute inset-0 bg-black/40"
onclick={onClose}
transition:fade={{ duration: 150 }}
></button>
<div
class="absolute inset-y-0 right-0 flex w-72 max-w-[80%] flex-col bg-white px-6 py-4 shadow-xl"
role="dialog"
aria-modal="true"
aria-label="Menu"
tabindex="-1"
ontouchstart={onTouchStart}
ontouchmove={onTouchMove}
ontouchend={onTouchEnd}
transition:fly={{ x: 300, duration: 200 }}
>
<div class="flex justify-end">
<button
type="button"
onclick={onClose}
class="-mr-1 rounded p-1 text-neutral-500 hover:bg-neutral-100"
aria-label="Close menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-7 w-7"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.8"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="mt-auto flex flex-col overflow-y-auto">
<a
href="/"
onclick={onClose}
class="py-2 text-xl text-neutral-700 hover:text-brand">Home</a
>
{#if mode === "full"}
<nav class="mt-4">
{#each groups as group}
<div class="mb-5">
<p
class="pb-1 text-xs font-semibold uppercase tracking-wider text-neutral-400"
>
{group}
</p>
{#each rooms.filter((r) => r.group === group) as room}
<a
href="/room/{room.slug}"
onclick={onClose}
class="block py-1.5 text-lg
{activeRoom === room.slug ? 'text-brand' : 'text-neutral-700 hover:text-brand'}"
>
{room.name}
</a>
{/each}
</div>
{/each}
</nav>
{/if}
<a
href="/about"
onclick={onClose}
class="py-2 text-xl text-neutral-700 hover:text-brand">About</a
>
<a
href="/contacts"
onclick={onClose}
class="py-2 text-xl text-neutral-700 hover:text-brand">Contacts</a
>
<div class="mt-4 flex flex-col gap-2 border-t border-neutral-100 pt-4">
{#if draftState.iconized}
<button
type="button"
onclick={onResume}
class="w-full rounded bg-brand px-3 py-2 font-medium text-white hover:bg-brand-hover"
>
Resume draft
</button>
{/if}
{#if auth.user}
<button
type="button"
onclick={onLogout}
class="flex w-full items-center gap-2 rounded px-2 py-2 text-left text-neutral-700 hover:bg-neutral-100"
aria-label="Log out"
>
{#if auth.user.metadata.picture}
<img
src={auth.user.metadata.picture}
alt=""
class="h-8 w-8 rounded-full object-cover"
/>
{:else}
<span
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-300 text-sm font-semibold text-neutral-600"
aria-hidden="true"
>
{auth.user.shortName.slice(0, 1).toUpperCase()}
</span>
{/if}
<span class="truncate text-lg font-medium">{auth.user.shortName}</span
>
<span class="ml-auto shrink-0 text-sm text-neutral-400">Log out</span>
</button>
{:else}
<button
type="button"
onclick={onLogin}
class="w-full rounded bg-brand px-3 py-2 text-lg font-medium text-white hover:bg-brand-hover"
>
Login
</button>
{/if}
</div>
</div>
</div>
</div>
{/if}

View file

@ -2,24 +2,53 @@
import { groupStore } from "$lib/group.svelte";
import { GROUP_ID } from "$lib/config";
type Props = { onMenuToggle: () => void };
let { onMenuToggle }: Props = $props();
const name = $derived(groupStore.data?.name ?? GROUP_ID);
</script>
<header class="flex h-16 shrink-0 items-center justify-between px-8">
<div class="flex items-center gap-2">
<header
class="sticky top-0 z-20 flex h-16 shrink-0 items-center justify-between gap-2 bg-neutral-100 px-4 md:static md:px-8"
>
<div class="flex min-w-0 items-center gap-2">
{#if groupStore.data?.picture}
<img
src={groupStore.data.picture}
alt=""
class="h-9 w-9 rounded-full object-cover"
class="h-9 w-9 shrink-0 rounded-full object-cover"
/>
{:else}
<div
class="flex h-9 w-9 items-center justify-center rounded-full bg-red-500 font-bold text-white"
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-red-500 font-bold text-white"
>
{name[0].toUpperCase()}
</div>
{/if}
<span class="text-[1.7rem] font-medium text-neutral-900">{name}</span>
<span class="truncate text-2xl font-medium text-neutral-900 md:text-[1.7rem]"
>{name}</span
>
</div>
<button
type="button"
onclick={onMenuToggle}
class="-mr-1 shrink-0 rounded p-1.5 text-neutral-700 hover:bg-neutral-200 md:hidden"
aria-label="Open menu"
>
<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>
</button>
</header>

View file

@ -121,7 +121,7 @@
<svelte:window onkeydown={onKeydown} />
{#if draftState.modalOpen}
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="fixed inset-0 z-50 flex items-center justify-center p-0 md:p-4">
<button
type="button"
aria-label="Minimize draft"
@ -130,7 +130,7 @@
onclick={iconizeDraft}
></button>
<div
class="relative w-full max-w-2xl rounded-lg bg-white p-6 shadow-xl flex flex-col gap-4 max-h-[90vh] overflow-hidden"
class="relative flex h-full max-h-full w-full flex-col gap-4 overflow-y-auto bg-white p-6 shadow-xl md:h-auto md:max-h-[90vh] md:max-w-2xl md:overflow-hidden md:rounded-lg"
role="dialog"
aria-modal="true"
aria-labelledby="newdisc-title"
@ -284,7 +284,7 @@
type="button"
onclick={onDiscard}
disabled={draftState.publishing}
class="rounded bg-neutral-700 px-5 py-1.5 text-sm font-medium text-white hover:bg-neutral-800 disabled:opacity-50"
class="rounded bg-neutral-700 px-5 py-1.5 font-medium text-white hover:bg-neutral-800 disabled:opacity-50"
>
Discard
</button>
@ -294,7 +294,7 @@
disabled={draftState.publishing ||
!draftState.title.trim() ||
!draftState.content.trim()}
class="rounded bg-brand px-5 py-1.5 text-sm font-medium text-white hover:bg-brand-hover disabled:cursor-not-allowed disabled:opacity-50"
class="rounded bg-brand px-5 py-1.5 font-medium text-white hover:bg-brand-hover disabled:cursor-not-allowed disabled:opacity-50"
>
{draftState.publishing ? "Publishing…" : "Publish discussion"}
</button>

View file

@ -61,7 +61,7 @@
aria-haspopup="menu"
aria-expanded={open}
aria-label="Sort discussions, current: {currentLabel}"
class="flex items-center gap-2 rounded bg-neutral-800 px-4 py-1.5 text-sm font-medium text-white hover:bg-neutral-700"
class="flex items-center gap-2 rounded bg-neutral-800 px-4 py-1.5 md:text-sm font-medium text-white hover:bg-neutral-700"
>
<svg
class="h-4 w-4"

View file

@ -36,7 +36,7 @@
<a
href="/thread/{thread.id}"
class="flex items-center gap-6 border-b border-neutral-100 py-4 hover:bg-neutral-50"
class="flex items-center gap-3 border-b border-neutral-100 py-4 hover:bg-neutral-50 sm:gap-6"
>
<!-- Col 1: title + byline -->
<div class="flex-1 min-w-0">
@ -71,7 +71,7 @@
</div>
<!-- Col 3: replies | activity -->
<div class="flex shrink-0 items-center gap-6">
<div class="flex shrink-0 items-center gap-4 sm:gap-6">
<div class="flex flex-col items-center gap-0.5">
<span class="text-neutral-800">{thread.replyCount}</span>
<span class="text-sm text-neutral-400">replies</span>

View file

@ -141,10 +141,10 @@
</script>
<div
class="sticky self-start flex-shrink-0 flex flex-col items-end select-none pt-1"
style="top: calc({topOffset}px - 1.5rem); height: 50vh; width: 72px; display: {isOverflowing
class="sticky self-start flex-shrink-0 flex-col items-end select-none pt-1 max-md:hidden {isOverflowing
? 'flex'
: 'none'};"
: 'hidden'}"
style="top: calc({topOffset}px - 1.5rem); height: 50vh; width: 72px;"
aria-hidden="true"
>
<!-- First post date -->