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

@ -120,7 +120,7 @@
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="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()}
</div>
@ -140,7 +140,7 @@
>
<div class="hidden md:block md:h-6" aria-hidden="true"></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 />
</div>
@ -149,7 +149,7 @@
</div>
{#if showChat}
<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"
>
<button
@ -157,7 +157,7 @@
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'}"
{mobileView === 'forum' ? 'text-brand' : 'text-neutral-500 dark:text-neutral-400'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -181,7 +181,7 @@
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'}"
{mobileView === 'chat' ? 'text-brand' : 'text-neutral-500 dark:text-neutral-400'}"
>
<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" />
{:else}
<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"
>
{a.name[0].toUpperCase()}
@ -60,18 +60,18 @@
<DiscussionsFeed groupId={GROUP_ID} title="Discussions" />
{:else}
{#if partial}
<hr class="text-neutral-300" />
<hr class="text-neutral-300 dark:text-neutral-600" />
{:else}
<h1 class="text-brand py-2 text-[1.65rem]">Forum rooms</h1>
{/if}
{#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…"}
</p>
{/if}
<div class="divide-y divide-neutral-100">
<div class="divide-y divide-neutral-100 dark:divide-neutral-800">
{#each groupsStore.list as room}
{@const act = overviewStore.activity[room.id]}
{@const adminPk = overviewStore.admins[room.id]}
@ -82,14 +82,14 @@
class="group flex items-start justify-between gap-4 py-5"
>
<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}
</h2>
{#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 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>
{@render pic(admin)}
</div>
@ -99,9 +99,9 @@
<div class="flex shrink-0 flex-col items-center gap-0.5">
<div class="flex items-center gap-1">
{@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>
<span class="text-sm text-neutral-400">activity</span>
<span class="text-sm text-neutral-400 dark:text-neutral-500">activity</span>
</div>
{/if}
</a>

View file

@ -81,14 +81,14 @@
{/if}
{#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}
<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}
<ul class="mt-2 space-y-3">
{#each contacts as c (c.pubkey)}
<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}
<img
@ -98,7 +98,7 @@
/>
{:else}
<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()}
</span>
@ -107,11 +107,11 @@
<div class="min-w-0 flex-1">
<div class="flex items-start justify-between gap-3">
<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)}
</p>
{#if c.entry?.nip05}
<p class="truncate text-neutral-400">
<p class="truncate text-neutral-400 dark:text-neutral-500">
{c.entry.nip05}
</p>
{/if}
@ -127,7 +127,7 @@
</div>
{#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}
</p>
{/if}
@ -145,7 +145,7 @@
</a>
{/if}
{#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}
</span>
{/if}
@ -155,8 +155,8 @@
{#if MODE === "full"}
{@const rooms = roomsOf(c.pubkey)}
{#if rooms.length > 0}
<p class="mt-2 text-neutral-500">
<span class="text-neutral-400">Manages:</span>
<p class="mt-2 text-neutral-500 dark:text-neutral-400">
<span class="text-neutral-400 dark:text-neutral-500">Manages:</span>
{#each rooms as r, i}<a
href="/room/{r.id}"
class="text-brand hover:underline">{r.name}</a

View file

@ -2,6 +2,10 @@
@plugin "@tailwindcss/forms";
@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 {
font-size: 16px;
}
@ -9,6 +13,9 @@ html {
body {
background-color: var(--color-neutral-100);
}
:where(.dark) body {
background-color: var(--color-neutral-800);
}
.no-scrollbar {
scrollbar-width: none;
@ -17,6 +24,20 @@ body {
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
typography plugin's em-based heading margins; the first block stays flush. */
.prose :is(h1, h2, h3, h4, h5, h6) {
@ -35,6 +56,21 @@ body {
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 {
--color-brand: #e32a6d;
--color-brand-hover: #c4205a;

View file

@ -31,7 +31,7 @@
<div class="mx-auto max-w-6xl">
<a
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
xmlns="http://www.w3.org/2000/svg"
@ -56,8 +56,8 @@
<PostContent content={resource.content} headingOffset={0} />
</div>
{: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}
<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}
</div>

View file

@ -268,7 +268,7 @@
/>
{:else}
<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()}
</span>
@ -293,7 +293,7 @@
<div class="flex-shrink-0 md:hidden">
{@render avatar(author)}
</div>
<span class="truncate font-medium text-neutral-500"
<span class="truncate font-medium text-neutral-500 dark:text-neutral-400"
>{author.name}</span
>
</div>
@ -302,7 +302,7 @@
<div class="relative">
<button
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-haspopup="menu"
aria-expanded={openMenuId === p.id}
@ -321,7 +321,7 @@
{#if openMenuId === p.id}
<div
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
role="menuitem"
@ -336,7 +336,7 @@
{/if}
</div>
{/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>
@ -348,7 +348,7 @@
<button
onclick={() => quotePost(p)}
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>
<svg
@ -375,11 +375,11 @@
<div class="flex items-start gap-6">
<div class="min-w-0 flex-1 {!scrubberVisible ? 'md:pr-18' : ''}">
<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
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
xmlns="http://www.w3.org/2000/svg"
@ -401,8 +401,8 @@
<h1 class="text-brand text-[1.65rem] leading-7">{detail.title}</h1>
{#if isScrolled}
<div
class="pointer-events-none absolute right-0 left-0 h-8 transition-opacity duration-200"
style="top: 100%; background: linear-gradient(to bottom, white, transparent);"
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%;"
></div>
{/if}
</div>
@ -420,7 +420,7 @@
</div>
{#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}
<div class="py-6" bind:this={replyEls[i]}>
{@render post(reply, i + 1, () => {})}
@ -429,7 +429,7 @@
</div>
{/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 replyError}
<div
@ -458,7 +458,7 @@
</button>
</div>
{:else}
<p class=" text-center text-neutral-500">
<p class=" text-center text-neutral-500 dark:text-neutral-400">
To participate and reply, please
<button onclick={openLogin} class="text-brand hover:underline"
>login now</button
@ -476,7 +476,7 @@
/>
</div>
{: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 selectionTarget && auth.user}
@ -485,7 +485,7 @@
onmousedown={(e) => e.preventDefault()}
onclick={quoteFromSelection}
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
</button>