Improve the timeline UI and spacing

This commit is contained in:
dtonon 2026-04-07 12:36:37 +01:00
parent 7c6bded69e
commit fa56d0252d
2 changed files with 71 additions and 27 deletions

View file

@ -1,7 +1,12 @@
<script lang="ts">
import { onMount } from "svelte";
import { page } from "$app/state";
import { threadDetailStore, loadThread, sendReply, type PostData } from "$lib/thread.svelte";
import {
threadDetailStore,
loadThread,
sendReply,
type PostData,
} from "$lib/thread.svelte";
import { auth, login } from "$lib/auth.svelte";
import Reactions from "$lib/components/Reactions.svelte";
import ThreadScrubber from "$lib/components/ThreadScrubber.svelte";
@ -9,7 +14,10 @@
type Author = { pubkey: string; name: string; picture?: string };
function resolveAuthor(pubkey: string, profiles: Record<string, NostrUser>): Author {
function resolveAuthor(
pubkey: string,
profiles: Record<string, NostrUser>,
): Author {
const user = profiles[pubkey];
return {
pubkey,
@ -47,13 +55,12 @@
}
}
let opTopOffset = $state(0);
let scrubberVisible = $state(false);
const detail = $derived(threadDetailStore.detail);
const profiles = $derived(threadDetailStore.profiles);
const allPosts = $derived(
detail ? [detail.op, ...detail.replies] : []
);
const allPosts = $derived(detail ? [detail.op, ...detail.replies] : []);
const postEls = $derived([opEl, ...replyEls]);
// Reload when navigating between threads
@ -65,7 +72,9 @@
const main = document.querySelector("main");
if (!main) return;
main.classList.add("no-scrollbar");
const onScroll = () => { isScrolled = main.scrollTop > 0; };
const onScroll = () => {
isScrolled = main.scrollTop > 0;
};
main.addEventListener("scroll", onScroll, { passive: true });
return () => {
main.classList.remove("no-scrollbar");
@ -88,9 +97,15 @@
{#snippet avatar(author: Author)}
{#if author.picture}
<img src={author.picture} alt="" class="w-12 h-12 rounded-full object-cover" />
<img
src={author.picture}
alt=""
class="w-12 h-12 rounded-full object-cover"
/>
{:else}
<span class="w-12 h-12 flex items-center justify-center rounded-full bg-gray-200 text-lg font-semibold text-gray-500">
<span
class="w-12 h-12 flex items-center justify-center rounded-full bg-gray-200 text-lg font-semibold text-gray-500"
>
{author.name[0].toUpperCase()}
</span>
{/if}
@ -105,7 +120,9 @@
<div class="flex-1 min-w-0">
<div class="flex items-baseline justify-between mb-2">
<span class="font-medium text-gray-600">{author.name}</span>
<span class="text-sm text-gray-400 ml-4 flex-shrink-0">{formatDate(p.createdAt)}</span>
<span class="text-sm text-gray-400 ml-4 flex-shrink-0"
>{formatDate(p.createdAt)}</span
>
</div>
<div class="prose leading-5 max-w-none text-gray-700">
{#each p.content.split("\n\n") as para}
@ -114,7 +131,9 @@
</div>
<div class="flex items-center justify-between mt-3">
<Reactions reactions={[]} zaps={0} />
<div class="flex items-center gap-4 text-sm text-gray-400 flex-shrink-0">
<div
class="flex items-center gap-4 text-sm text-gray-400 flex-shrink-0"
>
<button class="hover:text-brand transition-colors">Quote</button>
<button class="hover:text-brand transition-colors">React</button>
<button class="hover:text-amber-500 transition-colors">⚡ Zap</button>
@ -126,21 +145,25 @@
{#if detail}
<div class="flex gap-6 items-start">
<div class="flex-1 min-w-0">
<div class="sticky -top-6 bg-white z-10 pb-4 -mx-10 px-10 pt-6 -mt-6 relative">
<div class="flex-1 min-w-0" class:pr-18={!scrubberVisible}>
<div
class="sticky -top-6 bg-white z-10 pb-4 -mx-10 px-10 pt-6 -mt-6 relative"
>
<h1 class="text-[1.65rem] text-brand leading-7">{detail.title}</h1>
<div
class="absolute left-0 right-0 h-8 pointer-events-none transition-opacity duration-200"
style="top: 100%; opacity: {isScrolled ? 1 : 0}; background: linear-gradient(to bottom, white, transparent);"
style="top: 100%; opacity: {isScrolled
? 1
: 0}; background: linear-gradient(to bottom, white, transparent);"
></div>
</div>
<div class="pt-6 pb-6 border-b border-gray-100" bind:this={opEl}>
<div class="pt-6 pb-6" bind:this={opEl}>
{@render post(detail.op, () => {})}
</div>
{#if detail.replies.length > 0}
<div class="divide-y divide-gray-100">
<div class="divide-y divide-gray-100 border-t border-gray-100">
{#each detail.replies as reply, i}
<div class="py-6" bind:this={replyEls[i]}>
{@render post(reply, () => {})}
@ -149,10 +172,12 @@
</div>
{/if}
<div class="mt-8 border-t border-gray-200 pt-6">
<div class="mt-8 border-t border-gray-200 pt-6 pl-18">
{#if auth.user}
{#if replyError}
<div class="mb-4 rounded bg-red-50 px-4 py-3 text-sm text-red-700 border border-red-200">
<div
class="mb-4 rounded bg-red-50 px-4 py-3 text-sm text-red-700 border border-red-200"
>
{replyError}
</div>
{/if}
@ -173,15 +198,17 @@
</button>
</div>
{:else}
<p class="text-sm text-gray-500">
To participate and reply,
<button onclick={login} class="text-brand hover:underline">login now</button>
<p class=" text-gray-500 text-center">
To participate and reply, please
<button onclick={login} class="text-brand hover:underline"
>login now</button
>
</p>
{/if}
</div>
</div>
<ThreadScrubber posts={allPosts} {postEls} topOffset={opTopOffset} />
<ThreadScrubber posts={allPosts} {postEls} topOffset={opTopOffset} bind:visible={scrubberVisible} />
</div>
{:else}
<div class="py-12 text-center text-gray-400">Loading…</div>