Enable dark mode

This commit is contained in:
dtonon 2026-05-27 22:13:48 +01:00
parent b3e6533748
commit dcd3857c80
26 changed files with 410 additions and 220 deletions

View file

@ -4,6 +4,19 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" /> <meta name="text-scale" content="scale" />
<script>
// Apply theme before first paint to avoid a light-on-dark flash.
(function () {
try {
var t = localStorage.getItem("squalk:theme");
var dark =
t === "dark" ||
(t !== "light" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
if (dark) document.documentElement.classList.add("dark");
} catch (e) {}
})();
</script>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">

View file

@ -190,7 +190,7 @@
<aside <aside
bind:this={asideEl} bind:this={asideEl}
class="flex-1 flex-col bg-white px-4 pt-4 pb-20 min-[1540px]:rounded-tr-xl md:absolute md:top-6 md:right-0 md:z-10 md:h-[calc(100%-1.5rem)] md:flex-none md:rounded-tl-xl md:px-6 md:py-6 md:transition-all md:duration-200 class="flex-1 flex-col bg-white dark:bg-neutral-900 px-4 pt-4 pb-20 min-[1540px]:rounded-tr-xl md:absolute md:top-6 md:right-0 md:z-10 md:h-[calc(100%-1.5rem)] md:flex-none md:rounded-tl-xl md:px-6 md:py-6 md:transition-all md:duration-200
{mobileActive ? 'flex' : 'hidden'} md:flex {mobileActive ? 'flex' : 'hidden'} md:flex
{expanded ? 'md:w-150 md:shadow-2xl' : 'md:w-80 md:shadow-lg'}" {expanded ? 'md:w-150 md:shadow-2xl' : 'md:w-80 md:shadow-lg'}"
> >
@ -198,7 +198,7 @@
<span class="text-brand text-[1.5rem] leading-7">Chat</span> <span class="text-brand text-[1.5rem] leading-7">Chat</span>
<button <button
onclick={onToggle} onclick={onToggle}
class="hidden rounded bg-neutral-100 transition-colors hover:bg-neutral-200 md:block" class="hidden rounded bg-neutral-100 dark:bg-neutral-800 transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-700 md:block"
aria-label={expanded ? "Collapse chat" : "Expand chat"} aria-label={expanded ? "Collapse chat" : "Expand chat"}
> >
<svg <svg
@ -228,7 +228,7 @@
class="no-scrollbar -mr-6 flex flex-1 flex-col overflow-y-auto pr-6" class="no-scrollbar -mr-6 flex flex-1 flex-col overflow-y-auto pr-6"
> >
{#if messages.length === 0} {#if messages.length === 0}
<div class="m-auto py-8 text-center text-sm text-neutral-400"> <div class="m-auto py-8 text-center text-sm text-neutral-400 dark:text-neutral-500">
No messages yet. No messages yet.
</div> </div>
{:else} {:else}
@ -247,17 +247,17 @@
/> />
{:else} {:else}
<span <span
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-xs font-semibold text-neutral-500" class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-neutral-200 dark:bg-neutral-700 text-xs font-semibold text-neutral-500 dark:text-neutral-400"
> >
{author.name[0].toUpperCase()} {author.name[0].toUpperCase()}
</span> </span>
{/if} {/if}
<span class="font-medium text-neutral-500">{author.name}</span> <span class="font-medium text-neutral-500 dark:text-neutral-400">{author.name}</span>
<div class="ml-auto flex items-center gap-1"> <div class="ml-auto flex items-center gap-1">
<div class="relative"> <div class="relative">
<button <button
onclick={(e) => toggleMenu(msg.id, e)} onclick={(e) => toggleMenu(msg.id, e)}
class="flex items-center justify-center rounded p-0.5 text-neutral-300 transition-colors hover:bg-neutral-100 hover:text-neutral-500" class="flex items-center justify-center rounded p-0.5 text-neutral-300 dark:text-neutral-600 transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:text-neutral-500 dark:hover:text-neutral-400"
aria-label="Message actions" aria-label="Message actions"
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={openMenuId === msg.id} aria-expanded={openMenuId === msg.id}
@ -277,7 +277,7 @@
<div <div
role="menu" role="menu"
style={`${menuPos.top !== undefined ? `top:${menuPos.top}px` : `bottom:${menuPos.bottom}px`};right:${menuPos.right}px`} style={`${menuPos.top !== undefined ? `top:${menuPos.top}px` : `bottom:${menuPos.bottom}px`};right:${menuPos.right}px`}
class="fixed z-50 w-36 rounded-lg border border-neutral-100 bg-white py-1 text-sm shadow-lg" class="fixed z-50 w-36 rounded-lg border border-neutral-100 dark:border-neutral-800 bg-white dark:bg-neutral-900 py-1 text-sm shadow-lg"
> >
<button <button
role="menuitem" role="menuitem"
@ -285,7 +285,7 @@
e.stopPropagation(); e.stopPropagation();
startReply(msg); startReply(msg);
}} }}
class="w-full px-3 py-1.5 text-left hover:bg-neutral-50" class="w-full px-3 py-1.5 text-left hover:bg-neutral-50 dark:hover:bg-neutral-800"
>Reply</button >Reply</button
> >
{#if canModerate} {#if canModerate}
@ -302,7 +302,7 @@
</div> </div>
{/if} {/if}
</div> </div>
<span class="text-xs text-neutral-400" <span class="text-xs text-neutral-400 dark:text-neutral-500"
>{formatTime(msg.createdAt)}</span >{formatTime(msg.createdAt)}</span
> >
</div> </div>
@ -310,7 +310,7 @@
<div class="mt-0.5"> <div class="mt-0.5">
{#if msg.replyToId} {#if msg.replyToId}
<div <div
class="mb-1 border-l-2 border-neutral-300 pl-2 text-xs text-neutral-500" class="mb-1 border-l-2 border-neutral-300 dark:border-neutral-600 pl-2 text-xs text-neutral-500 dark:text-neutral-400"
> >
{#if parent} {#if parent}
<span class="font-medium">{parentAuthor?.name}</span>: <span class="font-medium">{parentAuthor?.name}</span>:
@ -320,7 +320,7 @@
{/if} {/if}
</div> </div>
{/if} {/if}
<p class="leading-5 text-neutral-700"> <p class="leading-5 text-neutral-700 dark:text-neutral-300">
<ChatContent content={msg.content} {profiles} /> <ChatContent content={msg.content} {profiles} />
</p> </p>
</div> </div>
@ -330,22 +330,22 @@
{/if} {/if}
</div> </div>
<div class="border-t border-neutral-200 pt-4"> <div class="border-t border-neutral-200 dark:border-neutral-700 pt-4">
{#if replyTarget} {#if replyTarget}
{@const replyAuthor = resolveAuthor(replyTarget.pubkey)} {@const replyAuthor = resolveAuthor(replyTarget.pubkey)}
<div <div
class="mb-2 flex items-start gap-2 rounded bg-neutral-50 px-2 py-1.5 text-xs text-neutral-600" class="mb-2 flex items-start gap-2 rounded bg-neutral-50 dark:bg-neutral-800 px-2 py-1.5 text-xs text-neutral-600 dark:text-neutral-400"
> >
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<span class="text-neutral-400">↳ Reply to </span> <span class="text-neutral-400 dark:text-neutral-500">↳ Reply to </span>
<span class="font-medium">{replyAuthor.name}</span>: <span class="font-medium">{replyAuthor.name}</span>:
<span class="text-neutral-500" <span class="text-neutral-500 dark:text-neutral-400"
>{truncate(replyTarget.content, 80)}</span >{truncate(replyTarget.content, 80)}</span
> >
</div> </div>
<button <button
onclick={cancelReply} onclick={cancelReply}
class="shrink-0 text-neutral-400 hover:text-neutral-600" class="shrink-0 text-neutral-400 dark:text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-400"
aria-label="Cancel reply" aria-label="Cancel reply"
> >
@ -367,7 +367,7 @@
disabled={sending} disabled={sending}
placeholder={auth.user ? "Message..." : "Login to send messages"} placeholder={auth.user ? "Message..." : "Login to send messages"}
{contextPubkeys} {contextPubkeys}
textareaClass="w-full resize-none rounded border border-neutral-200 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand disabled:opacity-50" textareaClass="w-full resize-none rounded border border-neutral-200 dark:border-neutral-700 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-brand disabled:opacity-50"
/> />
</div> </div>
</aside> </aside>

View file

@ -38,15 +38,15 @@
onclick={cancelDelete} onclick={cancelDelete}
></button> ></button>
<div <div
class="relative w-full max-w-md rounded-lg bg-white p-6 shadow-xl" class="relative w-full max-w-md rounded-lg bg-white dark:bg-neutral-900 p-6 shadow-xl"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="delete-title" aria-labelledby="delete-title"
> >
<h2 id="delete-title" class="mb-2 text-lg font-semibold text-neutral-900"> <h2 id="delete-title" class="mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Delete {deleteState.target.label}? Delete {deleteState.target.label}?
</h2> </h2>
<p class="mb-4 text-sm text-neutral-600"> <p class="mb-4 text-sm text-neutral-600 dark:text-neutral-400">
This asks the relay to remove the {deleteState.target.label} for everyone. This asks the relay to remove the {deleteState.target.label} for everyone.
It can't be undone. It can't be undone.
</p> </p>
@ -60,7 +60,7 @@
</div> </div>
{/if} {/if}
<label for="delete-reason" class="mb-1 block text-sm text-neutral-600" <label for="delete-reason" class="mb-1 block text-sm text-neutral-600 dark:text-neutral-400"
>Reason (optional)</label >Reason (optional)</label
> >
<textarea <textarea
@ -70,7 +70,7 @@
disabled={deleteState.busy} disabled={deleteState.busy}
rows="2" rows="2"
placeholder="Recorded with the deletion" placeholder="Recorded with the deletion"
class="focus:ring-brand mb-4 w-full resize-none rounded border border-neutral-200 px-3 py-2 text-sm focus:ring-1 focus:outline-none disabled:opacity-50" class="focus:ring-brand mb-4 w-full resize-none rounded border border-neutral-200 dark:border-neutral-700 px-3 py-2 text-sm focus:ring-1 focus:outline-none disabled:opacity-50"
></textarea> ></textarea>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
@ -78,7 +78,7 @@
type="button" type="button"
onclick={cancelDelete} onclick={cancelDelete}
disabled={deleteState.busy} disabled={deleteState.busy}
class="rounded px-3 py-2 text-sm font-medium text-neutral-600 hover:bg-neutral-100 disabled:opacity-50" class="rounded px-3 py-2 text-sm font-medium text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800 disabled:opacity-50"
> >
Cancel Cancel
</button> </button>

View file

@ -116,7 +116,7 @@
</div> </div>
{#if threadStore.loading && rows.length === 0} {#if threadStore.loading && rows.length === 0}
<p class="py-6 text-center text-sm text-neutral-400"> <p class="py-6 text-center text-sm text-neutral-400 dark:text-neutral-500">
Loading discussions… Loading discussions…
</p> </p>
{:else if !threadStore.exhausted} {:else if !threadStore.exhausted}
@ -125,12 +125,12 @@
onclick={() => loadMore(groupId)} onclick={() => loadMore(groupId)}
disabled={threadStore.loadingMore} disabled={threadStore.loadingMore}
aria-busy={threadStore.loadingMore} aria-busy={threadStore.loadingMore}
class="rounded border border-neutral-200 px-6 py-1.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50 disabled:opacity-50" class="rounded border border-neutral-200 dark:border-neutral-700 px-6 py-1.5 text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800 disabled:opacity-50"
> >
{threadStore.loadingMore ? "Loading…" : "Show more"} {threadStore.loadingMore ? "Loading…" : "Show more"}
</button> </button>
</div> </div>
{:else if rows.length > 0} {:else if rows.length > 0}
<p class="py-6 text-center text-sm text-neutral-400">No more discussions</p> <p class="py-6 text-center text-sm text-neutral-400 dark:text-neutral-500">No more discussions</p>
{/if} {/if}
</div> </div>

View file

@ -37,7 +37,7 @@
onclick={onClose} onclick={onClose}
></button> ></button>
<div <div
class="relative w-full max-w-md rounded-lg bg-white p-6 shadow-xl" class="relative w-full max-w-md rounded-lg bg-white dark:bg-neutral-900 p-6 shadow-xl"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="join-title" aria-labelledby="join-title"
@ -47,15 +47,15 @@
onclick={onClose} onclick={onClose}
aria-label="Close" aria-label="Close"
disabled={joinState.busy} disabled={joinState.busy}
class="absolute top-3 right-3 text-2xl leading-none text-neutral-400 hover:text-neutral-700 disabled:opacity-50" class="absolute top-3 right-3 text-2xl leading-none text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300 disabled:opacity-50"
> >
× ×
</button> </button>
<h2 id="join-title" class="mb-2 text-lg font-semibold text-neutral-900"> <h2 id="join-title" class="mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Join this group Join this group
</h2> </h2>
<p class="mb-4 text-sm text-neutral-600"> <p class="mb-4 text-sm text-neutral-600 dark:text-neutral-400">
{#if joinState.codeRequired} {#if joinState.codeRequired}
This community requires an invite code. Enter your code below to This community requires an invite code. Enter your code below to
request access. If you don't have one, contact the admin. request access. If you don't have one, contact the admin.
@ -87,7 +87,7 @@
autocapitalize="off" autocapitalize="off"
spellcheck="false" spellcheck="false"
onkeydown={(e) => e.key === "Enter" && onRetry()} onkeydown={(e) => e.key === "Enter" && onRetry()}
class="focus:ring-brand mb-3 w-full rounded border border-neutral-200 px-3 py-2 text-sm focus:ring-1 focus:outline-none disabled:opacity-50" class="focus:ring-brand mb-3 w-full rounded border border-neutral-200 dark:border-neutral-700 px-3 py-2 text-sm focus:ring-1 focus:outline-none disabled:opacity-50"
/> />
{/if} {/if}
@ -96,7 +96,7 @@
type="button" type="button"
onclick={onClose} onclick={onClose}
disabled={joinState.busy} disabled={joinState.busy}
class="rounded px-3 py-2 text-sm font-medium text-neutral-600 hover:bg-neutral-100 disabled:opacity-50" class="rounded px-3 py-2 text-sm font-medium text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800 disabled:opacity-50"
> >
Close Close
</button> </button>

View file

@ -30,22 +30,22 @@
<aside aria-label="Latest discussions"> <aside aria-label="Latest discussions">
<h2 class="text-brand pb-2 text-[1.5rem] leading-7">Latest discussions</h2> <h2 class="text-brand pb-2 text-[1.5rem] leading-7">Latest discussions</h2>
{#if overviewStore.recent.length === 0} {#if overviewStore.recent.length === 0}
<p class="text-sm text-neutral-400"> <p class="text-sm text-neutral-400 dark:text-neutral-500">
{overviewStore.loading ? "Loading…" : "Nothing yet."} {overviewStore.loading ? "Loading…" : "Nothing yet."}
</p> </p>
{:else} {:else}
<ul class="divide-y divide-neutral-100"> <ul class="divide-y divide-neutral-100 dark:divide-neutral-800">
{#each overviewStore.recent as t} {#each overviewStore.recent as t}
{@const author = authorOf(t.authorPubkey)} {@const author = authorOf(t.authorPubkey)}
<li> <li>
<a href="/thread/{t.id}" class="group block py-3"> <a href="/thread/{t.id}" class="group block py-3">
<p <p
class="group-hover:text-brand line-clamp-2 text-lg leading-5 text-neutral-700" class="group-hover:text-brand line-clamp-2 text-lg leading-5 text-neutral-700 dark:text-neutral-300"
> >
{t.title} {t.title}
</p> </p>
<div <div
class="mt-1.5 flex items-center gap-1.5 text-xs text-neutral-400" class="mt-1.5 flex items-center gap-1.5 text-xs text-neutral-400 dark:text-neutral-500"
> >
<span>by</span> <span>by</span>
{#if author.picture} {#if author.picture}
@ -56,7 +56,7 @@
/> />
{:else} {:else}
<span <span
class="flex h-5 w-5 items-center justify-center rounded-full bg-neutral-200 text-[10px] font-semibold text-neutral-500" class="flex h-5 w-5 items-center justify-center rounded-full bg-neutral-200 dark:bg-neutral-700 text-[10px] font-semibold text-neutral-500 dark:text-neutral-400"
aria-hidden="true" aria-hidden="true"
> >
{author.name[0].toUpperCase()} {author.name[0].toUpperCase()}

View file

@ -5,6 +5,7 @@
import { resourcesStore } from "$lib/resources.svelte"; import { resourcesStore } from "$lib/resources.svelte";
import { draftState, resumeDraft } from "$lib/draft.svelte"; import { draftState, resumeDraft } from "$lib/draft.svelte";
import { GROUP_ID, TITLE } from "$lib/config"; import { GROUP_ID, TITLE } from "$lib/config";
import ThemeToggle from "$lib/components/ThemeToggle.svelte";
type Props = { type Props = {
mode: "simple" | "full"; mode: "simple" | "full";
@ -45,37 +46,37 @@
class="max-w-[90%] shrink-0 object-cover" class="max-w-[90%] shrink-0 object-cover"
/> />
{/if} {/if}
<span class="mt-2 text-[1.3rem] leading-6 font-medium text-neutral-900" <span class="mt-2 text-[1.3rem] leading-6 font-medium text-neutral-900 dark:text-neutral-100"
>{name}</span >{name}</span
> >
</a> </a>
<a <a
href="/" href="/"
class="flex items-center gap-2 py-1 text-neutral-700 hover:bg-neutral-100" class="flex items-center gap-2 py-1 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800"
> >
{mode === "simple" ? "Discussions" : "Home"} {mode === "simple" ? "Discussions" : "Home"}
</a> </a>
{#if mode === "simple"} {#if mode === "simple"}
<div class="mt-6 flex"> <div class="mt-6 flex">
<p class="text-sm text-neutral-500">{groupStore.data?.about ?? ""}</p> <p class="text-sm text-neutral-500 dark:text-neutral-400">{groupStore.data?.about ?? ""}</p>
</div> </div>
{:else} {:else}
<nav class="mt-6 flex-auto" aria-label="Rooms"> <nav class="mt-6 flex-auto" aria-label="Rooms">
<p <p
class="pb-1 text-xs font-semibold tracking-wider text-neutral-400 uppercase" class="pb-1 text-xs font-semibold tracking-wider text-neutral-400 dark:text-neutral-500 uppercase"
> >
Rooms Rooms
</p> </p>
{#if groupsStore.list.length === 0 && !groupsStore.loaded} {#if groupsStore.list.length === 0 && !groupsStore.loaded}
<p class="py-1 text-sm text-neutral-400">Loading rooms…</p> <p class="py-1 text-sm text-neutral-400 dark:text-neutral-500">Loading rooms…</p>
{/if} {/if}
{#each groupsStore.list as room} {#each groupsStore.list as room}
<a <a
href="/room/{room.id}" href="/room/{room.id}"
class="flex items-center gap-2 py-1 hover:bg-neutral-100 class="flex items-center gap-2 py-1 hover:bg-neutral-100 dark:hover:bg-neutral-800
{activeRoom === room.id ? ' text-brand' : 'text-neutral-700'}" {activeRoom === room.id ? ' text-brand' : 'text-neutral-700 dark:text-neutral-300'}"
> >
{room.name} {room.name}
</a> </a>
@ -84,18 +85,18 @@
{#if activeAbout} {#if activeAbout}
<div class="mt-6"> <div class="mt-6">
<p <p
class="pb-1 text-xs font-semibold tracking-wider text-neutral-400 uppercase" class="pb-1 text-xs font-semibold tracking-wider text-neutral-400 dark:text-neutral-500 uppercase"
> >
About About
</p> </p>
<p class="text-sm text-neutral-500">{activeAbout}</p> <p class="text-sm text-neutral-500 dark:text-neutral-400">{activeAbout}</p>
</div> </div>
{/if} {/if}
{/if} {/if}
<nav class="mt-6 flex-auto" aria-label="Resources"> <nav class="mt-6 flex-auto" aria-label="Resources">
<p <p
class="pb-1 text-xs font-semibold tracking-wider text-neutral-400 uppercase" class="pb-1 text-xs font-semibold tracking-wider text-neutral-400 dark:text-neutral-500 uppercase"
> >
Resources Resources
</p> </p>
@ -103,16 +104,16 @@
<a <a
href="/resource/{r.slug}" href="/resource/{r.slug}"
aria-current={activeResource === r.slug ? "page" : undefined} aria-current={activeResource === r.slug ? "page" : undefined}
class="block py-1 hover:text-neutral-900 class="block py-1 hover:text-neutral-900 dark:hover:text-neutral-100
{activeResource === r.slug ? 'text-brand' : 'text-neutral-500'}" {activeResource === r.slug ? 'text-brand' : 'text-neutral-500 dark:text-neutral-400'}"
>{r.title}</a >{r.title}</a
> >
{/each} {/each}
<a <a
href="/contacts" href="/contacts"
aria-current={contactsActive ? "page" : undefined} aria-current={contactsActive ? "page" : undefined}
class="block py-1 hover:text-neutral-900 class="block py-1 hover:text-neutral-900 dark:hover:text-neutral-100
{contactsActive ? 'text-brand' : 'text-neutral-500'}">Contacts</a {contactsActive ? 'text-brand' : 'text-neutral-500 dark:text-neutral-400'}">Contacts</a
> >
</nav> </nav>
</div> </div>
@ -128,36 +129,42 @@
</button> </button>
{/if} {/if}
{#if auth.user} {#if auth.user}
<button <div class="mt-3 flex items-center gap-1">
onclick={() => { <button
if (confirm("Log out?")) logout(); onclick={() => {
}} if (confirm("Log out?")) logout();
class="mt-3 flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm text-neutral-700 hover:bg-neutral-200" }}
aria-label="Account options" class="flex min-w-0 flex-1 items-center gap-2 rounded px-2 py-1.5 text-left text-sm text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-700"
> aria-label="Account options"
{#if auth.user.metadata.picture} >
<img {#if auth.user.metadata.picture}
src={auth.user.metadata.picture} <img
alt="" src={auth.user.metadata.picture}
class="h-7 w-7 rounded-full object-cover" alt=""
/> class="h-7 w-7 rounded-full object-cover"
{:else} />
<span {:else}
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-neutral-300 text-xs font-semibold text-neutral-600" <span
aria-hidden="true" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-neutral-300 text-xs font-semibold text-neutral-600 dark:text-neutral-400 dark:bg-neutral-600"
> aria-hidden="true"
{auth.user.shortName.slice(0, 1).toUpperCase()} >
</span> {auth.user.shortName.slice(0, 1).toUpperCase()}
{/if} </span>
<span class="truncate font-medium">{auth.user.shortName}</span> {/if}
</button> <span class="truncate font-medium">{auth.user.shortName}</span>
</button>
<ThemeToggle />
</div>
{:else} {:else}
<button <div class="mt-3 flex items-center gap-1">
onclick={openLogin} <button
class="bg-brand hover:bg-brand-hover mt-3 w-full rounded px-3 py-1.5 text-sm font-medium text-white" onclick={openLogin}
> class="bg-brand hover:bg-brand-hover min-w-0 flex-1 rounded px-3 py-1.5 text-sm font-medium text-white"
Login >
</button> Login
</button>
<ThemeToggle />
</div>
{/if} {/if}
</div> </div>
</aside> </aside>

View file

@ -88,7 +88,7 @@
onclick={onClose} onclick={onClose}
></button> ></button>
<div <div
class="relative w-full max-w-sm rounded-lg bg-white p-6 shadow-xl" class="relative w-full max-w-sm rounded-lg bg-white dark:bg-neutral-900 p-6 shadow-xl"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="login-title" aria-labelledby="login-title"
@ -97,12 +97,12 @@
type="button" type="button"
onclick={onClose} onclick={onClose}
aria-label="Close" aria-label="Close"
class="absolute top-3 right-3 text-2xl leading-none text-neutral-400 hover:text-neutral-700" class="absolute top-3 right-3 text-2xl leading-none text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300"
> >
× ×
</button> </button>
<h2 id="login-title" class="mb-4 text-lg font-semibold text-neutral-900"> <h2 id="login-title" class="mb-4 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{view === "extension" ? "Log in" : "Log in with nsec"} {view === "extension" ? "Log in" : "Log in with nsec"}
</h2> </h2>
@ -124,7 +124,7 @@
{busy ? "Connecting…" : "Log in with extension"} {busy ? "Connecting…" : "Log in with extension"}
</button> </button>
{#if !hasExtension} {#if !hasExtension}
<p class="mt-2 text-xs text-neutral-500"> <p class="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
No Nostr extension detected in this browser. No Nostr extension detected in this browser.
</p> </p>
{/if} {/if}
@ -149,7 +149,7 @@
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
onkeydown={(e) => e.key === "Enter" && handleNsec()} onkeydown={(e) => e.key === "Enter" && handleNsec()}
class="focus:ring-brand w-full rounded border border-neutral-200 px-3 py-2 font-mono text-sm focus:ring-1 focus:outline-none disabled:opacity-50" class="focus:ring-brand w-full rounded border border-neutral-200 dark:border-neutral-700 px-3 py-2 font-mono text-sm focus:ring-1 focus:outline-none disabled:opacity-50"
/> />
<button <button
onclick={handleNsec} onclick={handleNsec}
@ -158,7 +158,7 @@
> >
{busy ? "Logging in…" : "Log in"} {busy ? "Logging in…" : "Log in"}
</button> </button>
<p class="mt-2 text-xs text-neutral-500"> <p class="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
Your key is kept in this browser. Use only for testing. Your key is kept in this browser. Use only for testing.
</p> </p>
<button <button

View file

@ -315,7 +315,7 @@
<div <div
id="mention-listbox" id="mention-listbox"
bind:this={listboxEl} bind:this={listboxEl}
class="absolute right-0 left-0 z-30 max-h-72 overflow-auto rounded border border-neutral-200 bg-white shadow-lg" class="absolute right-0 left-0 z-30 max-h-72 overflow-auto rounded border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-lg"
class:bottom-full={anchorAbove} class:bottom-full={anchorAbove}
class:mb-1={anchorAbove} class:mb-1={anchorAbove}
class:top-full={!anchorAbove} class:top-full={!anchorAbove}
@ -323,7 +323,7 @@
> >
{#if mentionQuery === ""} {#if mentionQuery === ""}
<div <div
class="px-3 py-1.5 text-xs text-neutral-400" class="px-3 py-1.5 text-xs text-neutral-400 dark:text-neutral-500"
class:border-b={mergedResults.length > 0} class:border-b={mergedResults.length > 0}
class:border-neutral-100={mergedResults.length > 0} class:border-neutral-100={mergedResults.length > 0}
> >
@ -331,21 +331,21 @@
</div> </div>
{:else if remoteSearching && mergedResults.length > 0} {:else if remoteSearching && mergedResults.length > 0}
<div <div
class="flex items-center gap-2 border-b border-neutral-100 px-3 py-1.5 text-xs text-neutral-400" class="flex items-center gap-2 border-b border-neutral-100 dark:border-neutral-800 px-3 py-1.5 text-xs text-neutral-400 dark:text-neutral-500"
aria-live="polite" aria-live="polite"
> >
<span <span
class="inline-block h-3 w-3 animate-spin rounded-full border border-neutral-300 border-t-neutral-600" class="inline-block h-3 w-3 animate-spin rounded-full border border-neutral-300 dark:border-neutral-600 border-t-neutral-600"
aria-hidden="true" aria-hidden="true"
></span> ></span>
<span>Searching…</span> <span>Searching…</span>
</div> </div>
{/if} {/if}
{#if mergedResults.length === 0 && mentionQuery !== ""} {#if mergedResults.length === 0 && mentionQuery !== ""}
<div class="flex items-center gap-2 px-3 py-2 text-xs text-neutral-400"> <div class="flex items-center gap-2 px-3 py-2 text-xs text-neutral-400 dark:text-neutral-500">
{#if remoteSearching} {#if remoteSearching}
<span <span
class="inline-block h-3 w-3 animate-spin rounded-full border border-neutral-300 border-t-neutral-600" class="inline-block h-3 w-3 animate-spin rounded-full border border-neutral-300 dark:border-neutral-600 border-t-neutral-600"
aria-hidden="true" aria-hidden="true"
></span> ></span>
<span>Searching…</span> <span>Searching…</span>
@ -378,17 +378,17 @@
/> />
{:else} {:else}
<span <span
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-neutral-200 text-xs text-neutral-500" class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-neutral-200 dark:bg-neutral-700 text-xs text-neutral-500 dark:text-neutral-400"
aria-hidden="true" aria-hidden="true"
> >
{profileLabel(entry)[0]?.toUpperCase() ?? "?"} {profileLabel(entry)[0]?.toUpperCase() ?? "?"}
</span> </span>
{/if} {/if}
<span class="truncate font-medium text-neutral-700"> <span class="truncate font-medium text-neutral-700 dark:text-neutral-300">
{profileLabel(entry)} {profileLabel(entry)}
</span> </span>
{#if profileSubLabel(entry)} {#if profileSubLabel(entry)}
<span class="truncate text-xs text-neutral-400"> <span class="truncate text-xs text-neutral-400 dark:text-neutral-500">
{profileSubLabel(entry)} {profileSubLabel(entry)}
</span> </span>
{/if} {/if}

View file

@ -80,13 +80,13 @@
<div class="flex flex-col {previewing ? 'min-h-0 flex-1' : ''}"> <div class="flex flex-col {previewing ? 'min-h-0 flex-1' : ''}">
{#if previewing} {#if previewing}
<div <div
class="max-h-[70vh] min-h-0 w-full flex-1 overflow-auto rounded-t border border-neutral-200 px-3 py-2 {minHeightClass}" class="max-h-[70vh] min-h-0 w-full flex-1 overflow-auto rounded-t border border-neutral-200 dark:border-neutral-700 px-3 py-2 {minHeightClass}"
aria-label="Preview" aria-label="Preview"
> >
{#if value.trim()} {#if value.trim()}
<PostContent content={value} {threadEventAuthors} /> <PostContent content={value} {threadEventAuthors} />
{:else} {:else}
<p class="text-neutral-400 italic">Nothing to preview</p> <p class="text-neutral-400 dark:text-neutral-500 italic">Nothing to preview</p>
{/if} {/if}
</div> </div>
{:else} {:else}
@ -97,18 +97,18 @@
{rows} {rows}
{placeholder} {placeholder}
{contextPubkeys} {contextPubkeys}
textareaClass="block w-full rounded-t border border-neutral-200 px-3 py-2 focus:outline-none focus:ring-1 focus:ring-brand disabled:opacity-50 resize-y {minHeightClass}" textareaClass="block w-full rounded-t border border-neutral-200 dark:border-neutral-700 px-3 py-2 focus:outline-none focus:ring-1 focus:ring-brand disabled:opacity-50 resize-y {minHeightClass}"
/> />
{/if} {/if}
<div <div
class="flex flex-shrink-0 items-center gap-4 rounded-b border border-t-0 border-neutral-200 bg-neutral-50 px-3 py-2 text-sm" class="flex flex-shrink-0 items-center gap-4 rounded-b border border-t-0 border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800 px-3 py-2 text-sm"
> >
<button <button
type="button" type="button"
onclick={onUploadClick} onclick={onUploadClick}
disabled={uploading || disabled || previewing || !BLOSSOM_URL} disabled={uploading || disabled || previewing || !BLOSSOM_URL}
title={!BLOSSOM_URL ? "Blossom server not configured" : ""} title={!BLOSSOM_URL ? "Blossom server not configured" : ""}
class="inline-flex items-center gap-1.5 text-neutral-600 hover:text-neutral-900 disabled:cursor-not-allowed disabled:opacity-50" class="inline-flex items-center gap-1.5 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 disabled:cursor-not-allowed disabled:opacity-50"
> >
<svg <svg
width="16" width="16"
@ -135,7 +135,7 @@
onclick={togglePreview} onclick={togglePreview}
disabled={disabled || uploading} disabled={disabled || uploading}
aria-pressed={previewing} aria-pressed={previewing}
class="ml-auto inline-flex items-center gap-1.5 text-neutral-600 hover:text-neutral-900 disabled:cursor-not-allowed disabled:opacity-50" class="ml-auto inline-flex items-center gap-1.5 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 disabled:cursor-not-allowed disabled:opacity-50"
> >
<svg <svg
width="16" width="16"

View file

@ -4,6 +4,7 @@
import { resourcesStore } from "$lib/resources.svelte"; import { resourcesStore } from "$lib/resources.svelte";
import { auth, openLogin, logout } from "$lib/auth.svelte"; import { auth, openLogin, logout } from "$lib/auth.svelte";
import { draftState, resumeDraft } from "$lib/draft.svelte"; import { draftState, resumeDraft } from "$lib/draft.svelte";
import ThemeToggle from "$lib/components/ThemeToggle.svelte";
type Props = { type Props = {
open: boolean; open: boolean;
@ -80,7 +81,7 @@
transition:fade={{ duration: 150 }} transition:fade={{ duration: 150 }}
></button> ></button>
<div <div
class="absolute inset-y-0 right-0 flex w-72 max-w-[80%] flex-col bg-white px-6 py-4 shadow-xl" class="absolute inset-y-0 right-0 flex w-72 max-w-[80%] flex-col bg-white dark:bg-neutral-900 px-6 py-4 shadow-xl"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Menu" aria-label="Menu"
@ -94,7 +95,7 @@
<button <button
type="button" type="button"
onclick={onClose} onclick={onClose}
class="-mr-1 rounded p-1 text-neutral-500 hover:bg-neutral-100" class="-mr-1 rounded p-1 text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800"
aria-label="Close menu" aria-label="Close menu"
> >
<svg <svg
@ -119,26 +120,26 @@
<a <a
href="/" href="/"
onclick={onClose} onclick={onClose}
class="hover:text-brand py-2 text-xl text-neutral-700" class="hover:text-brand py-2 text-xl text-neutral-700 dark:text-neutral-300"
>{mode === "simple" ? "Discussions" : "Home"}</a >{mode === "simple" ? "Discussions" : "Home"}</a
> >
{#if mode === "full"} {#if mode === "full"}
<nav class="mt-4" aria-label="Rooms"> <nav class="mt-4" aria-label="Rooms">
<p <p
class="pb-1 font-semibold tracking-wider text-neutral-400 uppercase" class="pb-1 font-semibold tracking-wider text-neutral-400 dark:text-neutral-500 uppercase"
> >
Rooms Rooms
</p> </p>
{#if groupsStore.list.length === 0 && !groupsStore.loaded} {#if groupsStore.list.length === 0 && !groupsStore.loaded}
<p class="py-1.5 text-base text-neutral-400">Loading rooms…</p> <p class="py-1.5 text-base text-neutral-400 dark:text-neutral-500">Loading rooms…</p>
{/if} {/if}
{#each groupsStore.list as room} {#each groupsStore.list as room}
<a <a
href="/room/{room.id}" href="/room/{room.id}"
onclick={onClose} onclick={onClose}
class="block py-1.5 text-xl class="block py-1.5 text-xl
{activeRoom === room.id ? 'text-brand' : 'hover:text-brand text-neutral-700'}" {activeRoom === room.id ? 'text-brand' : 'hover:text-brand text-neutral-700 dark:text-neutral-300'}"
> >
{room.name} {room.name}
</a> </a>
@ -148,7 +149,7 @@
<nav class="mt-4" aria-label="Resources"> <nav class="mt-4" aria-label="Resources">
<p <p
class="pb-1 font-semibold tracking-wider text-neutral-400 uppercase" class="pb-1 font-semibold tracking-wider text-neutral-400 dark:text-neutral-500 uppercase"
> >
Resources Resources
</p> </p>
@ -158,7 +159,7 @@
onclick={onClose} onclick={onClose}
aria-current={activeResource === r.slug ? "page" : undefined} aria-current={activeResource === r.slug ? "page" : undefined}
class="hover:text-brand block py-1.5 text-xl class="hover:text-brand block py-1.5 text-xl
{activeResource === r.slug ? 'text-brand' : 'text-neutral-700'}" {activeResource === r.slug ? 'text-brand' : 'text-neutral-700 dark:text-neutral-300'}"
>{r.title}</a >{r.title}</a
> >
{/each} {/each}
@ -167,11 +168,11 @@
onclick={onClose} onclick={onClose}
aria-current={contactsActive ? "page" : undefined} aria-current={contactsActive ? "page" : undefined}
class="hover:text-brand block py-1.5 text-lg class="hover:text-brand block py-1.5 text-lg
{contactsActive ? 'text-brand' : 'text-neutral-700'}">Contacts</a {contactsActive ? 'text-brand' : 'text-neutral-700 dark:text-neutral-300'}">Contacts</a
> >
</nav> </nav>
<div class="mt-4 flex flex-col gap-2 border-t border-neutral-100 pt-4"> <div class="mt-4 flex flex-col gap-2 border-t border-neutral-100 dark:border-neutral-800 pt-4">
{#if draftState.iconized} {#if draftState.iconized}
<button <button
type="button" type="button"
@ -182,41 +183,48 @@
</button> </button>
{/if} {/if}
{#if auth.user} {#if auth.user}
<button <div class="flex items-center gap-2">
type="button" <button
onclick={onLogout} type="button"
class="flex w-full items-center gap-2 rounded px-2 py-2 text-left text-neutral-700 hover:bg-neutral-100" onclick={onLogout}
aria-label="Log out" class="flex min-w-0 flex-1 items-center gap-2 rounded px-2 py-2 text-left text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-800"
> aria-label="Log out"
{#if auth.user.metadata.picture} >
<img {#if auth.user.metadata.picture}
src={auth.user.metadata.picture} <img
alt="" src={auth.user.metadata.picture}
class="h-8 w-8 rounded-full object-cover" alt=""
/> class="h-8 w-8 rounded-full object-cover"
{:else} />
<span {:else}
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-300 text-sm font-semibold text-neutral-600" <span
aria-hidden="true" class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-300 text-sm font-semibold text-neutral-600 dark:text-neutral-400 dark:bg-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
> >
{auth.user.shortName.slice(0, 1).toUpperCase()} <span
</span> class="ml-auto shrink-0 text-sm text-neutral-400 dark:text-neutral-500"
{/if} >Log out</span
<span class="truncate text-lg font-medium" >
>{auth.user.shortName}</span </button>
> <ThemeToggle size="md" />
<span class="ml-auto shrink-0 text-sm text-neutral-400" </div>
>Log out</span
>
</button>
{:else} {:else}
<button <div class="flex items-center gap-2">
type="button" <button
onclick={onLogin} type="button"
class="bg-brand hover:bg-brand-hover w-full rounded px-3 py-2 text-lg font-medium text-white" onclick={onLogin}
> class="bg-brand hover:bg-brand-hover min-w-0 flex-1 rounded px-3 py-2 text-lg font-medium text-white"
Login >
</button> Login
</button>
<ThemeToggle size="md" />
</div>
{/if} {/if}
</div> </div>
</div> </div>

View file

@ -15,7 +15,7 @@
<!-- Mobile only: on desktop the logo lives at the top of the left sidebar. --> <!-- Mobile only: on desktop the logo lives at the top of the left sidebar. -->
<header <header
class="sticky top-0 z-20 flex h-16 shrink-0 items-center justify-between gap-2 bg-neutral-100 px-4 md:hidden" class="sticky top-0 z-20 flex h-16 shrink-0 items-center justify-between gap-2 bg-neutral-100 dark:bg-neutral-800 px-4 md:hidden"
> >
<a <a
href="/" href="/"
@ -36,14 +36,14 @@
</div> </div>
{/if} {/if}
<span <span
class="truncate text-2xl font-medium text-neutral-900 md:text-[1.7rem]" class="truncate text-2xl font-medium text-neutral-900 dark:text-neutral-100 md:text-[1.7rem]"
>{name}</span >{name}</span
> >
</a> </a>
<button <button
type="button" type="button"
onclick={onMenuToggle} onclick={onMenuToggle}
class="-mr-1 shrink-0 rounded p-1.5 text-neutral-700 hover:bg-neutral-200 md:hidden" class="-mr-1 shrink-0 rounded p-1.5 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-700 md:hidden"
aria-label="Open menu" aria-label="Open menu"
> >
<svg <svg

View file

@ -140,7 +140,7 @@
onclick={iconizeDraft} onclick={iconizeDraft}
></button> ></button>
<div <div
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" class="relative flex h-full max-h-full w-full flex-col gap-4 overflow-y-auto bg-white dark:bg-neutral-900 p-6 shadow-xl md:h-auto md:max-h-[90vh] md:max-w-2xl md:overflow-hidden md:rounded-lg"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="newdisc-title" aria-labelledby="newdisc-title"
@ -149,7 +149,7 @@
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div> <div>
{#if targetName} {#if targetName}
<p class="text-sm text-neutral-700">{targetName}</p> <p class="text-sm text-neutral-700 dark:text-neutral-300">{targetName}</p>
{/if} {/if}
<h2 id="newdisc-title" class="text-brand text-2xl">New discussion</h2> <h2 id="newdisc-title" class="text-brand text-2xl">New discussion</h2>
</div> </div>
@ -157,7 +157,7 @@
type="button" type="button"
onclick={iconizeDraft} onclick={iconizeDraft}
aria-label="Minimize draft" aria-label="Minimize draft"
class="rounded bg-neutral-50 p-1.5 text-neutral-700 hover:bg-neutral-100" class="rounded bg-neutral-50 dark:bg-neutral-800 p-1.5 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800"
> >
<svg <svg
width="20" width="20"
@ -179,7 +179,7 @@
<div> <div>
<label <label
for="newdisc-title-input" for="newdisc-title-input"
class="mb-1 block text-xs font-semibold tracking-wider text-neutral-400 uppercase" class="mb-1 block text-xs font-semibold tracking-wider text-neutral-400 dark:text-neutral-500 uppercase"
> >
Title Title
</label> </label>
@ -190,7 +190,7 @@
bind:value={draftState.title} bind:value={draftState.title}
disabled={draftState.publishing} disabled={draftState.publishing}
maxlength="72" maxlength="72"
class="focus:ring-brand w-full rounded border border-neutral-200 px-3 py-2 focus:ring-1 focus:outline-none disabled:opacity-50" class="focus:ring-brand w-full rounded border border-neutral-200 dark:border-neutral-700 px-3 py-2 focus:ring-1 focus:outline-none disabled:opacity-50"
/> />
</div> </div>
@ -199,12 +199,12 @@
<div class="relative"> <div class="relative">
<label <label
for="newdisc-labels-input" for="newdisc-labels-input"
class="mb-1 block text-xs font-semibold tracking-wider text-neutral-400 uppercase" class="mb-1 block text-xs font-semibold tracking-wider text-neutral-400 dark:text-neutral-500 uppercase"
> >
Labels Labels
</label> </label>
<div <div
class="focus-within:ring-brand flex min-h-[2.5rem] flex-wrap items-center gap-1.5 rounded border border-neutral-200 px-3 py-1.5 focus-within:ring-1" class="focus-within:ring-brand flex min-h-[2.5rem] flex-wrap items-center gap-1.5 rounded border border-neutral-200 dark:border-neutral-700 px-3 py-1.5 focus-within:ring-1"
> >
{#each draftState.labels as l} {#each draftState.labels as l}
<span <span
@ -238,7 +238,7 @@
/> />
{#if suggestion} {#if suggestion}
<span <span
class="pointer-events-none absolute inset-0 flex items-center text-sm text-neutral-400" class="pointer-events-none absolute inset-0 flex items-center text-sm text-neutral-400 dark:text-neutral-500"
aria-hidden="true" aria-hidden="true"
> >
<span class="invisible">{labelInput}</span><span <span class="invisible">{labelInput}</span><span
@ -250,7 +250,7 @@
</div> </div>
{#if suggestOpen && filtered.length > 0} {#if suggestOpen && filtered.length > 0}
<div <div
class="absolute right-0 left-0 z-20 mt-1 flex flex-wrap gap-1.5 rounded border border-neutral-200 bg-white p-3 shadow-lg" class="absolute right-0 left-0 z-20 mt-1 flex flex-wrap gap-1.5 rounded border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 p-3 shadow-lg"
> >
{#each filtered as l} {#each filtered as l}
<button <button
@ -265,7 +265,7 @@
</div> </div>
{/if} {/if}
{#if suggestion} {#if suggestion}
<p class="mt-1 text-xs text-neutral-400"> <p class="mt-1 text-xs text-neutral-400 dark:text-neutral-500">
Press Tab or Enter to add “{suggestion} Press Tab or Enter to add “{suggestion}
</p> </p>
{/if} {/if}

View file

@ -607,7 +607,7 @@
{@const u = profiles[inline.pubkey] ?? resolvedUsers[inline.pubkey]} {@const u = profiles[inline.pubkey] ?? resolvedUsers[inline.pubkey]}
<a <a
href="#post-{inline.eventId}" href="#post-{inline.eventId}"
class="-ml-3 block bg-neutral-100 py-1 pl-3 leading-4 font-normal no-underline hover:bg-neutral-200" class="-ml-3 block bg-neutral-100 dark:bg-neutral-800 py-1 pl-3 leading-4 font-normal text-neutral-700 dark:text-neutral-300 no-underline hover:bg-neutral-200 dark:hover:bg-neutral-700"
>{u?.shortName ?? inline.pubkey.slice(0, 8)} said >{u?.shortName ?? inline.pubkey.slice(0, 8)} said
<svg <svg
class="mb-0.5 inline w-3" class="mb-0.5 inline w-3"
@ -619,7 +619,7 @@
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;"
><path ><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" 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:#000;stroke-width:99.57px;" style="fill:none;fill-rule:nonzero;stroke:currentColor;stroke-width:99.57px;"
/></svg /></svg
> >
</a> </a>
@ -683,7 +683,7 @@
/> />
{:else if block.type === "blockquote"} {:else if block.type === "blockquote"}
<blockquote <blockquote
class="my-3 mb-3 border-l-3 border-neutral-200 pb-1 pl-3 text-neutral-500" class="my-3 mb-3 border-l-3 border-neutral-200 dark:border-neutral-700 pb-1 pl-3 text-neutral-500 dark:text-neutral-400"
> >
{@render renderBlocks(block.blocks)} {@render renderBlocks(block.blocks)}
</blockquote> </blockquote>
@ -752,7 +752,7 @@
{/snippet} {/snippet}
<div <div
class="prose max-w-none leading-5 text-neutral-700 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-neutral-100 [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 [&_blockquote_img]:mx-0 [&_blockquote_img]:w-full [&_blockquote_img]:max-w-full [&_blockquote_img]:rounded [&_blockquote_p]:before:content-none [&_blockquote_p]:after:content-none [&_code]:before:content-none [&_code]:after:content-none [&_img]:my-5 [&_p]:my-3 [&_pre]:bg-neutral-100 [&_pre]:text-neutral-800 [&_table]:my-0" class="prose max-w-none leading-5 text-neutral-700 dark:text-neutral-300 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-neutral-100 [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 dark:[&_:not(pre)>code]:bg-neutral-800 dark:[&_:not(pre)>code]:text-neutral-200 [&_blockquote_img]:mx-0 [&_blockquote_img]:w-full [&_blockquote_img]:max-w-full [&_blockquote_img]:rounded [&_blockquote_p]:before:content-none [&_blockquote_p]:after:content-none [&_code]:before:content-none [&_code]:after:content-none [&_img]:my-5 [&_p]:my-3 [&_pre]:bg-neutral-100 [&_pre]:text-neutral-800 dark:[&_pre]:bg-neutral-800 dark:[&_pre]:text-neutral-200 [&_table]:my-0"
> >
{@render renderBlocks(blocks)} {@render renderBlocks(blocks)}
</div> </div>

View file

@ -93,7 +93,7 @@
bind:this={menuEl} bind:this={menuEl}
role="menu" role="menu"
aria-label="Sort discussions" aria-label="Sort discussions"
class="absolute right-0 z-10 mt-1 min-w-48 rounded border border-neutral-200 bg-white py-1 shadow-lg {open class="absolute right-0 z-10 mt-1 min-w-48 rounded border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 py-1 shadow-lg {open
? 'block' ? 'block'
: 'hidden'}" : 'hidden'}"
> >
@ -103,10 +103,10 @@
role="menuitem" role="menuitem"
aria-current={o.value === sort ? "true" : undefined} aria-current={o.value === sort ? "true" : undefined}
onclick={() => closeMenu()} onclick={() => closeMenu()}
class="flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 {o.value === class="flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 dark:hover:bg-neutral-800 {o.value ===
sort sort
? 'font-semibold text-neutral-900' ? 'font-semibold text-neutral-900 dark:text-neutral-100'
: 'text-neutral-600'}" : 'text-neutral-600 dark:text-neutral-400'}"
> >
<span class="w-4 text-center" aria-hidden="true" <span class="w-4 text-center" aria-hidden="true"
>{o.value === sort ? "✓" : ""}</span >{o.value === sort ? "✓" : ""}</span

View file

@ -0,0 +1,65 @@
<script lang="ts">
import { themeState, toggleTheme } from "$lib/theme.svelte";
type Props = { size?: "sm" | "md" };
let { size = "sm" }: Props = $props();
const isDark = $derived(themeState.theme === "dark");
const iconSize = $derived(size === "md" ? "h-6 w-6" : "h-5 w-5");
const padding = $derived(size === "md" ? "p-3" : "p-2.5");
const label = $derived(
isDark ? "Switch to light theme" : "Switch to dark theme",
);
// The tooltip clarifies whether the current state follows the OS or is pinned,
// so users can tell at a glance if their pick will outlive an OS theme change.
const title = $derived(
themeState.linked
? `Following system (${themeState.theme})`
: `${themeState.theme.charAt(0).toUpperCase()}${themeState.theme.slice(1)} theme`,
);
</script>
<button
type="button"
onclick={toggleTheme}
aria-label={label}
aria-pressed={isDark}
{title}
class="inline-flex items-center justify-center rounded {padding} text-neutral-500 transition-colors hover:bg-neutral-200 hover:text-neutral-700 dark:hover:text-neutral-300 dark:text-neutral-400 dark:hover:bg-neutral-700"
>
{#if isDark}
<!-- Moon -->
<svg
xmlns="http://www.w3.org/2000/svg"
class={iconSize}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.8"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.752 15.002A9.718 9.718 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"
/>
</svg>
{:else}
<!-- Sun -->
<svg
xmlns="http://www.w3.org/2000/svg"
class={iconSize}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.8"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
/>
</svg>
{/if}
</button>

View file

@ -27,7 +27,7 @@
<img src={a.picture} alt="" class={cls} /> <img src={a.picture} alt="" class={cls} />
{:else} {:else}
<span <span
class="{cls} flex items-center justify-center rounded-full bg-neutral-200 text-[10px] font-semibold text-neutral-500" class="{cls} flex items-center justify-center rounded-full bg-neutral-200 dark:bg-neutral-700 text-[10px] font-semibold text-neutral-500 dark:text-neutral-400"
> >
{a.name[0].toUpperCase()} {a.name[0].toUpperCase()}
</span> </span>
@ -36,12 +36,12 @@
<a <a
href="/thread/{thread.id}" href="/thread/{thread.id}"
class="flex items-center gap-3 border-b border-neutral-100 py-4 hover:bg-neutral-50 sm:gap-6" class="flex items-center gap-3 border-b border-neutral-100 dark:border-neutral-800 py-4 hover:bg-neutral-50 dark:hover:bg-neutral-800 sm:gap-6"
> >
<!-- Col 1: title + byline --> <!-- Col 1: title + byline -->
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="mb-1.5 text-lg leading-5 text-neutral-900">{thread.title}</div> <div class="mb-1.5 text-lg leading-5 text-neutral-900 dark:text-neutral-100">{thread.title}</div>
<div class="flex items-center gap-1.5 text-sm text-neutral-500"> <div class="flex items-center gap-1.5 text-sm text-neutral-500 dark:text-neutral-400">
<span>by</span> <span>by</span>
{@render avatar(thread.author, "h-5 w-5 rounded-full object-cover")} {@render avatar(thread.author, "h-5 w-5 rounded-full object-cover")}
{#if thread.repliers.length > 0} {#if thread.repliers.length > 0}
@ -73,8 +73,8 @@
<!-- Col 3: replies | activity --> <!-- Col 3: replies | activity -->
<div class="flex shrink-0 items-center gap-4 sm:gap-6"> <div class="flex shrink-0 items-center gap-4 sm:gap-6">
<div class="flex flex-col items-center gap-0.5"> <div class="flex flex-col items-center gap-0.5">
<span class="text-neutral-800">{thread.replyCount}</span> <span class="text-neutral-800 dark:text-neutral-200">{thread.replyCount}</span>
<span class="text-sm text-neutral-400">replies</span> <span class="text-sm text-neutral-400 dark:text-neutral-500">replies</span>
</div> </div>
<div class="flex flex-col items-center gap-0.5"> <div class="flex flex-col items-center gap-0.5">
@ -83,9 +83,9 @@
thread.lastActiveAuthor, thread.lastActiveAuthor,
"h-5 w-5 rounded-full object-cover", "h-5 w-5 rounded-full object-cover",
)} )}
<span class="text-neutral-800">{thread.lastActivity}</span> <span class="text-neutral-800 dark:text-neutral-200">{thread.lastActivity}</span>
</div> </div>
<span class="text-sm text-neutral-400">activity</span> <span class="text-sm text-neutral-400 dark:text-neutral-500">activity</span>
</div> </div>
</div> </div>
</a> </a>

View file

@ -149,7 +149,7 @@
> >
<!-- First post date --> <!-- First post date -->
<div <div
class="mb-1 text-right text-xs leading-tight whitespace-pre text-neutral-300" class="mb-1 text-right text-xs leading-tight whitespace-pre text-neutral-300 dark:text-neutral-600"
> >
{firstPost ? formatDate(firstPost.createdAt) : ""} {firstPost ? formatDate(firstPost.createdAt) : ""}
</div> </div>
@ -163,7 +163,7 @@
> >
<!-- Track line --> <!-- Track line -->
<div <div
class="absolute top-0 bottom-0 rounded-full bg-neutral-400" class="absolute top-0 bottom-0 rounded-full bg-neutral-400 dark:bg-neutral-600"
style="width: 2px; right: 4px;" style="width: 2px; right: 4px;"
></div> ></div>
@ -180,7 +180,7 @@
<!-- Current date label --> <!-- Current date label -->
<div <div
class="pointer-events-none absolute text-xs leading-tight whitespace-nowrap text-neutral-400" class="pointer-events-none absolute text-xs leading-tight whitespace-nowrap text-neutral-400 dark:text-neutral-500"
style="right: 18px; top: {thumbTop + style="right: 18px; top: {thumbTop +
thumbHeight / 2}px; transform: translateY(-50%);" thumbHeight / 2}px; transform: translateY(-50%);"
> >
@ -190,7 +190,7 @@
<!-- Last post date --> <!-- Last post date -->
<div <div
class="mt-1 text-right text-xs leading-tight whitespace-pre text-neutral-300" class="mt-1 text-right text-xs leading-tight whitespace-pre text-neutral-300 dark:text-neutral-600"
> >
{lastPost ? formatDate(lastPost.createdAt) : ""} {lastPost ? formatDate(lastPost.createdAt) : ""}
</div> </div>

View file

@ -18,7 +18,7 @@
type="button" type="button"
onclick={dismissToast} onclick={dismissToast}
aria-label="Dismiss" aria-label="Dismiss"
class="text-lg leading-none text-neutral-400 hover:text-white" class="text-lg leading-none text-neutral-400 dark:text-neutral-500 hover:text-white"
> >
× ×
</button> </button>

61
src/lib/theme.svelte.ts Normal file
View file

@ -0,0 +1,61 @@
import { browser } from "$app/environment";
export type Theme = "light" | "dark";
const KEY = "squalk:theme";
function osTheme(): Theme {
if (!browser) return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function readStored(): Theme | null {
if (!browser) return null;
const v = localStorage.getItem(KEY);
return v === "light" || v === "dark" ? v : null;
}
function apply(theme: Theme) {
if (!browser) return;
document.documentElement.classList.toggle("dark", theme === "dark");
}
const initialStored = readStored();
const initialTheme: Theme = initialStored ?? osTheme();
// The theme is "linked" to the OS preference when no explicit choice is stored.
// In that mode, OS changes propagate. Once the user picks a value that diverges
// from the OS, the link is broken and OS changes are ignored — only another
// user toggle can re-link (when the new pick happens to match the OS again).
export const themeState = $state({
theme: initialTheme,
linked: initialStored === null,
});
if (browser) {
apply(initialTheme);
const mq = window.matchMedia("(prefers-color-scheme: dark)");
mq.addEventListener("change", (e) => {
if (!themeState.linked) return;
const next: Theme = e.matches ? "dark" : "light";
themeState.theme = next;
apply(next);
});
}
export function toggleTheme() {
const next: Theme = themeState.theme === "dark" ? "light" : "dark";
const os = osTheme();
if (next === os) {
// Toggling to the current OS value re-links and clears the explicit pick.
if (browser) localStorage.removeItem(KEY);
themeState.linked = true;
} else {
if (browser) localStorage.setItem(KEY, next);
themeState.linked = false;
}
themeState.theme = next;
apply(next);
}

View file

@ -120,7 +120,7 @@
it first and it reappears once the top is reached. Desktop only. --> it first and it reappears once the top is reached. Desktop only. -->
<div class="hidden md:block md:h-6" aria-hidden="true"></div> <div class="hidden md:block md:h-6" aria-hidden="true"></div>
<div <div
class="min-h-[calc(100dvh_-_4rem)] bg-white px-6 pt-4 pb-20 shadow-lg md:min-h-full md:rounded-t-xl md:px-10 md:pt-6 md:pt-8" class="min-h-[calc(100dvh_-_4rem)] bg-white dark:bg-neutral-900 px-6 pt-4 pb-20 shadow-lg md:min-h-full md:rounded-t-xl md:px-10 md:pt-6 md:pt-8"
> >
{@render children()} {@render children()}
</div> </div>
@ -140,7 +140,7 @@
> >
<div class="hidden md:block md:h-6" aria-hidden="true"></div> <div class="hidden md:block md:h-6" aria-hidden="true"></div>
<div <div
class="bg-white px-6 pt-8 pb-20 shadow-lg md:min-h-full md:rounded-t-xl md:px-8 md:pt-6" class="bg-white dark:bg-neutral-900 px-6 pt-8 pb-20 shadow-lg md:min-h-full md:rounded-t-xl md:px-8 md:pt-6"
> >
<LatestDiscussions /> <LatestDiscussions />
</div> </div>
@ -149,7 +149,7 @@
</div> </div>
{#if showChat} {#if showChat}
<nav <nav
class="fixed inset-x-0 bottom-0 z-30 flex border-t border-neutral-200 bg-neutral-100 md:hidden" class="fixed inset-x-0 bottom-0 z-30 flex border-t border-neutral-200 dark:border-neutral-700 bg-neutral-100 dark:bg-neutral-800 md:hidden"
aria-label="Switch view" aria-label="Switch view"
> >
<button <button
@ -157,7 +157,7 @@
onclick={() => (mobileView = "forum")} onclick={() => (mobileView = "forum")}
aria-pressed={mobileView === "forum"} aria-pressed={mobileView === "forum"}
class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium
{mobileView === 'forum' ? 'text-brand' : 'text-neutral-500'}" {mobileView === 'forum' ? 'text-brand' : 'text-neutral-500 dark:text-neutral-400'}"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -181,7 +181,7 @@
onclick={() => (mobileView = "chat")} onclick={() => (mobileView = "chat")}
aria-pressed={mobileView === "chat"} aria-pressed={mobileView === "chat"}
class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium
{mobileView === 'chat' ? 'text-brand' : 'text-neutral-500'}" {mobileView === 'chat' ? 'text-brand' : 'text-neutral-500 dark:text-neutral-400'}"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View file

@ -42,7 +42,7 @@
<img src={a.picture} alt="" class="h-5 w-5 rounded-full object-cover" /> <img src={a.picture} alt="" class="h-5 w-5 rounded-full object-cover" />
{:else} {:else}
<span <span
class="flex h-5 w-5 items-center justify-center rounded-full bg-neutral-200 text-[10px] font-semibold text-neutral-500" class="flex h-5 w-5 items-center justify-center rounded-full bg-neutral-200 dark:bg-neutral-700 text-[10px] font-semibold text-neutral-500 dark:text-neutral-400"
aria-hidden="true" aria-hidden="true"
> >
{a.name[0].toUpperCase()} {a.name[0].toUpperCase()}
@ -60,18 +60,18 @@
<DiscussionsFeed groupId={GROUP_ID} title="Discussions" /> <DiscussionsFeed groupId={GROUP_ID} title="Discussions" />
{:else} {:else}
{#if partial} {#if partial}
<hr class="text-neutral-300" /> <hr class="text-neutral-300 dark:text-neutral-600" />
{:else} {:else}
<h1 class="text-brand py-2 text-[1.65rem]">Forum rooms</h1> <h1 class="text-brand py-2 text-[1.65rem]">Forum rooms</h1>
{/if} {/if}
{#if groupsStore.list.length === 0} {#if groupsStore.list.length === 0}
<p class="py-6 text-sm text-neutral-400"> <p class="py-6 text-sm text-neutral-400 dark:text-neutral-500">
{groupsStore.loaded ? "No rooms available yet." : "Loading rooms…"} {groupsStore.loaded ? "No rooms available yet." : "Loading rooms…"}
</p> </p>
{/if} {/if}
<div class="divide-y divide-neutral-100"> <div class="divide-y divide-neutral-100 dark:divide-neutral-800">
{#each groupsStore.list as room} {#each groupsStore.list as room}
{@const act = overviewStore.activity[room.id]} {@const act = overviewStore.activity[room.id]}
{@const adminPk = overviewStore.admins[room.id]} {@const adminPk = overviewStore.admins[room.id]}
@ -82,14 +82,14 @@
class="group flex items-start justify-between gap-4 py-5" class="group flex items-start justify-between gap-4 py-5"
> >
<div class="min-w-0"> <div class="min-w-0">
<h2 class="group-hover:text-brand text-2xl text-neutral-800"> <h2 class="group-hover:text-brand text-2xl text-neutral-800 dark:text-neutral-200">
{room.name} {room.name}
</h2> </h2>
{#if room.about} {#if room.about}
<p class="mt-1 leading-5 text-neutral-600">{room.about}</p> <p class="mt-1 leading-5 text-neutral-600 dark:text-neutral-400">{room.about}</p>
{/if} {/if}
{#if admin} {#if admin}
<div class="mt-2 flex items-center gap-2 text-sm text-neutral-500"> <div class="mt-2 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
<span>Admin</span> <span>Admin</span>
{@render pic(admin)} {@render pic(admin)}
</div> </div>
@ -99,9 +99,9 @@
<div class="flex shrink-0 flex-col items-center gap-0.5"> <div class="flex shrink-0 flex-col items-center gap-0.5">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
{@render pic(last)} {@render pic(last)}
<span class="text-neutral-800">{relativeTime(act.latestAt)}</span> <span class="text-neutral-800 dark:text-neutral-200">{relativeTime(act.latestAt)}</span>
</div> </div>
<span class="text-sm text-neutral-400">activity</span> <span class="text-sm text-neutral-400 dark:text-neutral-500">activity</span>
</div> </div>
{/if} {/if}
</a> </a>

View file

@ -81,14 +81,14 @@
{/if} {/if}
{#if loading} {#if loading}
<p class="py-6 text-center text-neutral-400">Loading…</p> <p class="py-6 text-center text-neutral-400 dark:text-neutral-500">Loading…</p>
{:else if contacts.length === 0} {:else if contacts.length === 0}
<p class="py-6 text-center text-neutral-400">No admins listed.</p> <p class="py-6 text-center text-neutral-400 dark:text-neutral-500">No admins listed.</p>
{:else} {:else}
<ul class="mt-2 space-y-3"> <ul class="mt-2 space-y-3">
{#each contacts as c (c.pubkey)} {#each contacts as c (c.pubkey)}
<li <li
class="flex gap-4 rounded-lg border border-neutral-100 p-4 shadow-sm" class="flex gap-4 rounded-lg border border-neutral-100 dark:border-neutral-800 p-4 shadow-sm"
> >
{#if c.entry?.picture} {#if c.entry?.picture}
<img <img
@ -98,7 +98,7 @@
/> />
{:else} {:else}
<span <span
class="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-lg font-semibold text-neutral-500" class="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-neutral-200 dark:bg-neutral-700 text-lg font-semibold text-neutral-500 dark:text-neutral-400"
> >
{displayName(c)[0].toUpperCase()} {displayName(c)[0].toUpperCase()}
</span> </span>
@ -107,11 +107,11 @@
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div class="min-w-0"> <div class="min-w-0">
<p class="truncate text-2xl font-medium text-neutral-900"> <p class="truncate text-2xl font-medium text-neutral-900 dark:text-neutral-100">
{displayName(c)} {displayName(c)}
</p> </p>
{#if c.entry?.nip05} {#if c.entry?.nip05}
<p class="truncate text-neutral-400"> <p class="truncate text-neutral-400 dark:text-neutral-500">
{c.entry.nip05} {c.entry.nip05}
</p> </p>
{/if} {/if}
@ -127,7 +127,7 @@
</div> </div>
{#if c.entry?.about} {#if c.entry?.about}
<p class="mt-2 whitespace-pre-line text-neutral-600"> <p class="mt-2 whitespace-pre-line text-neutral-600 dark:text-neutral-400">
{c.entry.about} {c.entry.about}
</p> </p>
{/if} {/if}
@ -145,7 +145,7 @@
</a> </a>
{/if} {/if}
{#if c.entry?.lud16} {#if c.entry?.lud16}
<span class="text-neutral-500" title="Lightning address"> <span class="text-neutral-500 dark:text-neutral-400" title="Lightning address">
{c.entry.lud16} {c.entry.lud16}
</span> </span>
{/if} {/if}
@ -155,8 +155,8 @@
{#if MODE === "full"} {#if MODE === "full"}
{@const rooms = roomsOf(c.pubkey)} {@const rooms = roomsOf(c.pubkey)}
{#if rooms.length > 0} {#if rooms.length > 0}
<p class="mt-2 text-neutral-500"> <p class="mt-2 text-neutral-500 dark:text-neutral-400">
<span class="text-neutral-400">Manages:</span> <span class="text-neutral-400 dark:text-neutral-500">Manages:</span>
{#each rooms as r, i}<a {#each rooms as r, i}<a
href="/room/{r.id}" href="/room/{r.id}"
class="text-brand hover:underline">{r.name}</a class="text-brand hover:underline">{r.name}</a

View file

@ -2,6 +2,10 @@
@plugin "@tailwindcss/forms"; @plugin "@tailwindcss/forms";
@plugin "@tailwindcss/typography"; @plugin "@tailwindcss/typography";
/* Class-based dark mode: the FOUC script in app.html and the theme store add
`dark` to <html> so `dark:` utilities apply without relying on prefers-color-scheme. */
@custom-variant dark (&:where(.dark, .dark *));
html { html {
font-size: 16px; font-size: 16px;
} }
@ -9,6 +13,9 @@ html {
body { body {
background-color: var(--color-neutral-100); background-color: var(--color-neutral-100);
} }
:where(.dark) body {
background-color: var(--color-neutral-800);
}
.no-scrollbar { .no-scrollbar {
scrollbar-width: none; scrollbar-width: none;
@ -17,6 +24,20 @@ body {
display: none; display: none;
} }
/* Form inputs in dark mode the forms plugin defaults to a white background,
which would punch holes through panels. Override at the element level so we
don't have to add `dark:bg-neutral-800` to every <input>/<textarea>. */
:where(.dark) input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):not([type="color"]):not([type="submit"]):not([type="reset"]):not([type="button"]),
:where(.dark) textarea,
:where(.dark) select {
background-color: var(--color-neutral-800);
color: var(--color-neutral-100);
}
:where(.dark) input::placeholder,
:where(.dark) textarea::placeholder {
color: var(--color-neutral-500);
}
/* Tighter heading rhythm for all rendered (prose) content. Overrides the /* Tighter heading rhythm for all rendered (prose) content. Overrides the
typography plugin's em-based heading margins; the first block stays flush. */ typography plugin's em-based heading margins; the first block stays flush. */
.prose :is(h1, h2, h3, h4, h5, h6) { .prose :is(h1, h2, h3, h4, h5, h6) {
@ -35,6 +56,21 @@ body {
margin-top: 0; margin-top: 0;
} }
/* Prose headings/strong/links default to dark text colors; flip them in dark mode. */
:where(.dark) .prose :is(h1, h2, h3, h4, h5, h6, strong, th) {
color: var(--color-neutral-100);
}
:where(.dark) .prose :is(h1) {
color: var(--color-brand);
}
:where(.dark) .prose :is(blockquote) {
color: var(--color-neutral-400);
border-left-color: var(--color-neutral-700);
}
:where(.dark) .prose :is(hr, thead, tbody tr) {
border-color: var(--color-neutral-700);
}
@theme { @theme {
--color-brand: #e32a6d; --color-brand: #e32a6d;
--color-brand-hover: #c4205a; --color-brand-hover: #c4205a;

View file

@ -31,7 +31,7 @@
<div class="mx-auto max-w-6xl"> <div class="mx-auto max-w-6xl">
<a <a
href="/" href="/"
class="hover:text-brand mb-1 inline-flex items-center gap-1 text-sm text-neutral-400" class="hover:text-brand mb-1 inline-flex items-center gap-1 text-sm text-neutral-400 dark:text-neutral-500"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -56,8 +56,8 @@
<PostContent content={resource.content} headingOffset={0} /> <PostContent content={resource.content} headingOffset={0} />
</div> </div>
{:else if notFound} {:else if notFound}
<p class="py-12 text-center text-neutral-400">Resource not found.</p> <p class="py-12 text-center text-neutral-400 dark:text-neutral-500">Resource not found.</p>
{:else} {:else}
<p class="py-12 text-center text-neutral-400">Loading…</p> <p class="py-12 text-center text-neutral-400 dark:text-neutral-500">Loading…</p>
{/if} {/if}
</div> </div>

View file

@ -268,7 +268,7 @@
/> />
{:else} {:else}
<span <span
class="flex h-12 w-12 items-center justify-center rounded-full bg-neutral-200 text-lg font-semibold text-neutral-500" class="flex h-12 w-12 items-center justify-center rounded-full bg-neutral-200 dark:bg-neutral-700 text-lg font-semibold text-neutral-500 dark:text-neutral-400"
> >
{author.name[0].toUpperCase()} {author.name[0].toUpperCase()}
</span> </span>
@ -293,7 +293,7 @@
<div class="flex-shrink-0 md:hidden"> <div class="flex-shrink-0 md:hidden">
{@render avatar(author)} {@render avatar(author)}
</div> </div>
<span class="truncate font-medium text-neutral-500" <span class="truncate font-medium text-neutral-500 dark:text-neutral-400"
>{author.name}</span >{author.name}</span
> >
</div> </div>
@ -302,7 +302,7 @@
<div class="relative"> <div class="relative">
<button <button
onclick={(e) => toggleMenu(p.id, e)} onclick={(e) => toggleMenu(p.id, e)}
class="flex items-center justify-center rounded p-1 text-neutral-300 transition-colors hover:bg-neutral-100 hover:text-neutral-500" class="flex items-center justify-center rounded p-1 text-neutral-300 dark:text-neutral-600 transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:text-neutral-500 dark:hover:text-neutral-400"
aria-label="Post actions" aria-label="Post actions"
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={openMenuId === p.id} aria-expanded={openMenuId === p.id}
@ -321,7 +321,7 @@
{#if openMenuId === p.id} {#if openMenuId === p.id}
<div <div
role="menu" role="menu"
class="absolute top-7 right-0 z-20 w-36 rounded-lg border border-neutral-100 bg-white py-1 text-sm shadow-lg" class="absolute top-7 right-0 z-20 w-36 rounded-lg border border-neutral-100 dark:border-neutral-800 bg-white dark:bg-neutral-900 py-1 text-sm shadow-lg"
> >
<button <button
role="menuitem" role="menuitem"
@ -336,7 +336,7 @@
{/if} {/if}
</div> </div>
{/if} {/if}
<span class="text-sm text-neutral-400">{formatDate(p.createdAt)}</span <span class="text-sm text-neutral-400 dark:text-neutral-500">{formatDate(p.createdAt)}</span
> >
</div> </div>
</div> </div>
@ -348,7 +348,7 @@
<button <button
onclick={() => quotePost(p)} onclick={() => quotePost(p)}
disabled={!auth.user} disabled={!auth.user}
class="hover:text-brand flex cursor-pointer items-center gap-1.5 text-neutral-300 transition-colors disabled:cursor-not-allowed disabled:opacity-50" class="hover:text-brand flex cursor-pointer items-center gap-1.5 text-neutral-300 dark:text-neutral-600 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
> >
<span>Quote post</span> <span>Quote post</span>
<svg <svg
@ -375,11 +375,11 @@
<div class="flex items-start gap-6"> <div class="flex items-start gap-6">
<div class="min-w-0 flex-1 {!scrubberVisible ? 'md:pr-18' : ''}"> <div class="min-w-0 flex-1 {!scrubberVisible ? 'md:pr-18' : ''}">
<div <div
class="relative z-10 bg-white pb-1 md:sticky md:-top-6 md:-mx-10 md:-mt-6 md:px-10 md:pt-6" class="relative z-10 bg-white dark:bg-neutral-900 pb-1 md:sticky md:-top-6 md:-mx-10 md:-mt-6 md:px-10 md:pt-6"
> >
<a <a
href={MODE === "full" ? `/room/${detail.groupId}` : "/"} href={MODE === "full" ? `/room/${detail.groupId}` : "/"}
class="hover:text-brand mb-1 inline-flex items-center gap-1 text-sm text-neutral-400" class="hover:text-brand mb-1 inline-flex items-center gap-1 text-sm text-neutral-400 dark:text-neutral-500"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -401,8 +401,8 @@
<h1 class="text-brand text-[1.65rem] leading-7">{detail.title}</h1> <h1 class="text-brand text-[1.65rem] leading-7">{detail.title}</h1>
{#if isScrolled} {#if isScrolled}
<div <div
class="pointer-events-none absolute right-0 left-0 h-8 transition-opacity duration-200" class="pointer-events-none absolute right-0 left-0 h-8 bg-gradient-to-b from-white to-transparent transition-opacity duration-200 dark:from-neutral-900"
style="top: 100%; background: linear-gradient(to bottom, white, transparent);" style="top: 100%;"
></div> ></div>
{/if} {/if}
</div> </div>
@ -420,7 +420,7 @@
</div> </div>
{#if detail.replies.length > 0} {#if detail.replies.length > 0}
<div class="divide-y divide-neutral-100 border-t border-neutral-100"> <div class="divide-y divide-neutral-100 dark:divide-neutral-800 border-t border-neutral-100 dark:border-neutral-800">
{#each detail.replies as reply, i} {#each detail.replies as reply, i}
<div class="py-6" bind:this={replyEls[i]}> <div class="py-6" bind:this={replyEls[i]}>
{@render post(reply, i + 1, () => {})} {@render post(reply, i + 1, () => {})}
@ -429,7 +429,7 @@
</div> </div>
{/if} {/if}
<div class="mt-8 border-t border-neutral-200 pt-6 md:pl-18"> <div class="mt-8 border-t border-neutral-200 dark:border-neutral-700 pt-6 md:pl-18">
{#if auth.user} {#if auth.user}
{#if replyError} {#if replyError}
<div <div
@ -458,7 +458,7 @@
</button> </button>
</div> </div>
{:else} {:else}
<p class=" text-center text-neutral-500"> <p class=" text-center text-neutral-500 dark:text-neutral-400">
To participate and reply, please To participate and reply, please
<button onclick={openLogin} class="text-brand hover:underline" <button onclick={openLogin} class="text-brand hover:underline"
>login now</button >login now</button
@ -476,7 +476,7 @@
/> />
</div> </div>
{:else} {:else}
<div class="py-12 text-center text-neutral-400">Loading…</div> <div class="py-12 text-center text-neutral-400 dark:text-neutral-500">Loading…</div>
{/if} {/if}
{#if selectionTarget && auth.user} {#if selectionTarget && auth.user}
@ -485,7 +485,7 @@
onmousedown={(e) => e.preventDefault()} onmousedown={(e) => e.preventDefault()}
onclick={quoteFromSelection} onclick={quoteFromSelection}
style="top: {selectionTarget.top}px; left: {selectionTarget.left}px;" style="top: {selectionTarget.top}px; left: {selectionTarget.left}px;"
class="fixed z-50 -translate-x-1/2 -translate-y-full rounded bg-neutral-900 px-3 py-1 text-xs font-medium text-white shadow-md hover:bg-neutral-700" class="bg-brand hover:bg-brand-hover fixed z-50 -translate-x-1/2 -translate-y-full rounded px-3 py-1 text-xs font-medium text-white shadow-md"
> >
Quote Quote
</button> </button>