Enable dark mode
This commit is contained in:
parent
b3e6533748
commit
dcd3857c80
26 changed files with 410 additions and 220 deletions
13
src/app.html
13
src/app.html
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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()}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
65
src/lib/components/ThemeToggle.svelte
Normal file
65
src/lib/components/ThemeToggle.svelte
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
61
src/lib/theme.svelte.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue