Browse Source

bug fixes

master
Silberengel 1 month ago
parent
commit
dc6c02d67f
  1. 9
      src/app.css
  2. 66
      src/lib/components/layout/Header.svelte
  3. 95
      src/lib/components/layout/ProfileBadge.svelte
  4. 7
      src/lib/components/relay/RelayInfo.svelte
  5. 16
      src/lib/modules/feed/FeedPage.svelte
  6. 47
      src/lib/modules/feed/FeedPost.svelte
  7. 304
      src/lib/modules/profiles/ProfilePage.svelte
  8. 150
      src/routes/bookmarks/+page.svelte
  9. 38
      src/routes/settings/+page.svelte

9
src/app.css

@ -472,12 +472,13 @@ img.emoji-inline {
} }
/* Responsive images and media - max 600px, scale down on smaller screens */ /* Responsive images and media - max 600px, scale down on smaller screens */
img:not(.profile-picture):not([alt*="profile" i]):not([alt*="avatar" i]):not([src*="avatar" i]):not([src*="profile" i]), img:not(.profile-picture):not([alt*="profile" i]):not([alt*="avatar" i]):not([src*="avatar" i]):not([src*="profile" i]):not(.relay-icon):not(.relay-favorite-pic),
video, video,
audio { audio {
max-width: 600px; max-width: 600px !important;
width: 100%; width: 100% !important;
height: auto; height: auto !important;
display: block;
} }
/* Ensure media in markdown content is responsive */ /* Ensure media in markdown content is responsive */

66
src/lib/components/layout/Header.svelte

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { sessionManager, type UserSession } from '../../services/auth/session-manager.js'; import { sessionManager, type UserSession } from '../../services/auth/session-manager.js';
import UserPreferences from '../preferences/UserPreferences.svelte';
import ProfileBadge from '../layout/ProfileBadge.svelte'; import ProfileBadge from '../layout/ProfileBadge.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -33,12 +32,13 @@
<header class="relative border-b border-fog-border dark:border-fog-dark-border"> <header class="relative border-b border-fog-border dark:border-fog-dark-border">
<!-- Banner image --> <!-- Banner image -->
<div class="h-24 sm:h-32 md:h-48 overflow-hidden bg-fog-surface dark:bg-fog-dark-surface"> <div class="h-24 sm:h-32 md:h-48 overflow-hidden bg-fog-surface dark:bg-fog-dark-surface w-full">
<div class="max-w-7xl mx-auto h-full relative"> <div class="max-w-7xl mx-auto h-full relative w-full flex items-center justify-center">
<img <img
src="/aither.png" src="/aither.png"
alt="aitherboard banner" alt="aitherboard banner"
class="w-full h-full object-cover object-center opacity-90 dark:opacity-70" class="w-full h-full object-cover object-center opacity-90 dark:opacity-70"
style="object-position: center;"
loading="eager" loading="eager"
/> />
<!-- Overlay gradient for text readability --> <!-- Overlay gradient for text readability -->
@ -64,22 +64,17 @@
<a href="/topics" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Topics</a> <a href="/topics" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Topics</a>
<a href="/repos" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Repos</a> <a href="/repos" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Repos</a>
<a href="/cache" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Cache</a> <a href="/cache" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Cache</a>
<a href="/bookmarks" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Bookmarks</a>
<a href="/settings" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Settings</a>
{#if isLoggedIn && currentPubkey}
<a href="/logout" onclick={(e) => { e.preventDefault(); handleLogout(); }} class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Logout</a>
{:else}
<a href="/login" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Login</a>
{/if}
</div> </div>
<div class="flex flex-wrap gap-1.5 sm:gap-2 md:gap-4 items-center min-w-0 flex-shrink-0 nav-links"> <div class="flex flex-wrap gap-1.5 sm:gap-2 md:gap-4 items-center min-w-0 flex-shrink-0 nav-links">
{#if isLoggedIn && currentPubkey} {#if isLoggedIn && currentPubkey}
<UserPreferences /> <ProfileBadge pubkey={currentPubkey} pictureOnly={true} />
<ProfileBadge pubkey={currentPubkey} />
<button
onclick={handleLogout}
class="px-2 sm:px-3 py-1 rounded border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight transition-colors flex-shrink-0"
title="Logout"
aria-label="Logout"
>
<span class="emoji emoji-grayscale">🚪</span>
</button>
{:else}
<a href="/login" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">Login</a>
<UserPreferences />
{/if} {/if}
</div> </div>
</div> </div>
@ -89,44 +84,51 @@
<style> <style>
header { header {
max-width: 100%; max-width: 100%;
width: 100%;
} }
nav { /* Banner container */
min-width: 0; /* Allow flex items to shrink */ header > div:first-child {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
} }
.emoji { header > div:first-child > div {
font-size: 1rem; width: 100%;
line-height: 1; max-width: 100%;
opacity: 0.7;
} }
.emoji-grayscale { header > div:first-child img {
filter: grayscale(100%); width: 100%;
height: 100%;
object-fit: cover;
object-position: center center;
} }
button:hover .emoji-grayscale { nav {
filter: grayscale(80%); min-width: 0; /* Allow flex items to shrink */
} }
/* Responsive navigation links */ /* Responsive navigation links */
.nav-links { .nav-links {
font-size: 0.75rem; font-size: 0.65rem;
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.nav-links { .nav-links {
font-size: 0.875rem; font-size: 0.75rem;
} }
} }
.nav-brand { .nav-brand {
font-size: 1rem; font-size: 0.875rem;
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.nav-brand { .nav-brand {
font-size: 1.25rem; font-size: 1rem;
} }
} }
@ -144,11 +146,11 @@
} }
.nav-links { .nav-links {
font-size: 0.7rem; font-size: 0.6rem;
} }
.nav-brand { .nav-brand {
font-size: 0.9rem; font-size: 0.8rem;
} }
} }
</style> </style>

95
src/lib/components/layout/ProfileBadge.svelte

@ -1,42 +1,51 @@
<script lang="ts"> <script lang="ts">
import { getActivityStatus, getActivityMessage } from '../../services/auth/activity-tracker.js'; import { getActivityStatus, getActivityMessage } from '../../services/auth/activity-tracker.js';
import { fetchProfile, fetchUserStatus } from '../../services/user-data.js'; import { fetchProfile } from '../../services/user-data.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
interface Props { interface Props {
pubkey: string; pubkey: string;
inline?: boolean; // If true, show only handle/name (no picture, status, etc.) inline?: boolean; // If true, show only handle/name (no picture, status, etc.)
pictureOnly?: boolean; // If true, show only the profile picture/avatar
} }
let { pubkey, inline = false }: Props = $props(); let { pubkey, inline = false, pictureOnly = false }: Props = $props();
let profile = $state<{ name?: string; picture?: string; nip05?: string[] } | null>(null); let profile = $state<{ name?: string; picture?: string; nip05?: string[] } | null>(null);
let status = $state<string | null>(null);
let activityStatus = $state<'red' | 'yellow' | 'green' | null>(null); let activityStatus = $state<'red' | 'yellow' | 'green' | null>(null);
let activityMessage = $state<string | null>(null); let activityMessage = $state<string | null>(null);
let imageError = $state(false); let imageError = $state(false);
let loadingProfile = $state(false); let loadingProfile = $state(false);
let loadingStatus = $state(false);
let loadingActivity = $state(false); let loadingActivity = $state(false);
let lastLoadedPubkey = $state<string | null>(null); let lastLoadedPubkey = $state<string | null>(null);
$effect(() => { $effect(() => {
// Track inline prop to ensure effect runs when it changes
const currentInline = inline;
// Only load if pubkey changed and we haven't loaded it yet // Only load if pubkey changed and we haven't loaded it yet
if (pubkey && pubkey !== lastLoadedPubkey) { if (pubkey && pubkey !== lastLoadedPubkey) {
imageError = false; // Reset image error when pubkey changes imageError = false; // Reset image error when pubkey changes
// Reset state for new pubkey // Reset state for new pubkey
profile = null; profile = null;
status = null;
activityStatus = null; activityStatus = null;
activityMessage = null; activityMessage = null;
// Load immediately - no debounce // Load immediately - no debounce
loadProfile(); loadProfile();
// Only load status if not inline (feed view doesn't need status) // Only load activity status if not inline (feed view doesn't need it)
if (!inline) { if (!currentInline) {
loadStatus();
updateActivityStatus(); updateActivityStatus();
} }
} }
// If inline is true, clear activity (don't load it)
if (currentInline) {
activityStatus = null;
activityMessage = null;
} else if (pubkey && pubkey === lastLoadedPubkey && !loadingActivity) {
// If inline changed from true to false and we have a loaded pubkey, load activity now
updateActivityStatus();
}
}); });
async function loadProfile() { async function loadProfile() {
@ -63,32 +72,16 @@
} }
} }
async function loadStatus() {
const currentPubkey = pubkey;
if (!currentPubkey || loadingStatus) return;
loadingStatus = true;
try {
const s = await fetchUserStatus(currentPubkey);
// Only update if pubkey hasn't changed during load
if (pubkey === currentPubkey) {
status = s;
}
} finally {
if (pubkey === currentPubkey) {
loadingStatus = false;
}
}
}
async function updateActivityStatus() { async function updateActivityStatus() {
const currentPubkey = pubkey; const currentPubkey = pubkey;
if (!currentPubkey || loadingActivity || lastLoadedPubkey !== currentPubkey) return; // Don't load activity status if inline is true (feed view)
if (!currentPubkey || loadingActivity || lastLoadedPubkey !== currentPubkey || inline) return;
loadingActivity = true; loadingActivity = true;
try { try {
const actStatus = await getActivityStatus(currentPubkey); const actStatus = await getActivityStatus(currentPubkey);
const actMessage = await getActivityMessage(currentPubkey); const actMessage = await getActivityMessage(currentPubkey);
// Only update if pubkey hasn't changed during load // Only update if pubkey hasn't changed during load and still not inline
if (pubkey === currentPubkey && lastLoadedPubkey === currentPubkey) { if (pubkey === currentPubkey && lastLoadedPubkey === currentPubkey && !inline) {
activityStatus = actStatus; activityStatus = actStatus;
activityMessage = actMessage; activityMessage = actMessage;
} }
@ -144,13 +137,14 @@
}); });
</script> </script>
<a href="/profile/{pubkey}" class="profile-badge inline-flex items-start gap-2 min-w-0 max-w-full"> <a href="/profile/{pubkey}" class="profile-badge inline-flex items-start gap-2 min-w-0 max-w-full" class:picture-only={pictureOnly}>
{#if !inline} {#if !inline || pictureOnly}
{#if profile?.picture && !imageError} {#if profile?.picture && !imageError}
<img <img
src={profile.picture} src={profile.picture}
alt={profile.name || pubkey} alt={profile.name || pubkey}
class="profile-picture w-6 h-6 rounded flex-shrink-0" class="profile-picture w-6 h-6 rounded flex-shrink-0"
class:nav-picture={pictureOnly}
loading="lazy" loading="lazy"
onerror={() => { onerror={() => {
imageError = true; imageError = true;
@ -159,28 +153,28 @@
{:else} {:else}
<div <div
class="profile-placeholder w-6 h-6 rounded flex-shrink-0 flex items-center justify-center text-xs font-semibold" class="profile-placeholder w-6 h-6 rounded flex-shrink-0 flex items-center justify-center text-xs font-semibold"
class:nav-picture={pictureOnly}
style="background: {avatarColor}; color: white;" style="background: {avatarColor}; color: white;"
title={pubkey} title={profile?.name || pubkey}
> >
{avatarInitials} {avatarInitials}
</div> </div>
{/if} {/if}
{/if} {/if}
<div class="flex flex-col min-w-0 flex-1"> {#if !pictureOnly}
<div class="flex items-center gap-2 flex-wrap"> <div class="flex flex-col min-w-0 flex-1 max-w-full">
<span class="truncate min-w-0"> <div class="flex items-center gap-2 flex-wrap min-w-0">
<span class="truncate min-w-0 max-w-full">
{profile?.name || shortenedNpub} {profile?.name || shortenedNpub}
</span> </span>
{#if profile?.nip05 && profile.nip05.length > 0} {#if profile?.nip05 && profile.nip05.length > 0}
<span class="nip05-text text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0 whitespace-nowrap"> <span class="nip05-text text-fog-text-light dark:text-fog-dark-text-light min-w-0 break-all">
{profile.nip05[0]} {profile.nip05[0]}
</span> </span>
{/if} {/if}
</div> </div>
{#if status && status.trim()}
<span class="status-text text-sm text-fog-text-light dark:text-fog-dark-text-light">{status}</span>
{/if}
</div> </div>
{/if}
</a> </a>
<style> <style>
@ -188,7 +182,9 @@
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
max-width: 100%; max-width: 100%;
width: 100%;
transition: opacity 0.2s; transition: opacity 0.2s;
overflow: hidden;
} }
.profile-badge:hover { .profile-badge:hover {
@ -196,6 +192,20 @@
opacity: 0.9; opacity: 0.9;
} }
.profile-badge.picture-only {
gap: 0;
}
.profile-badge.picture-only:hover {
text-decoration: none;
opacity: 0.8;
}
.nav-picture {
width: 2rem;
height: 2rem;
}
.profile-picture { .profile-picture {
object-fit: cover; object-fit: cover;
display: block; display: block;
@ -206,13 +216,10 @@
line-height: 1; line-height: 1;
} }
.status-text {
display: block;
font-size: 0.75em;
line-height: 1.2;
}
.nip05-text { .nip05-text {
font-size: 0.875em; font-size: 0.875em;
word-break: break-word;
overflow-wrap: break-word;
max-width: 100%;
} }
</style> </style>

7
src/lib/components/relay/RelayInfo.svelte

@ -483,11 +483,12 @@
} }
.relay-icon { .relay-icon {
width: 2rem; width: 1.5rem;
height: 2rem; height: 1.5rem;
border-radius: 0.375rem; border-radius: 0.25rem;
object-fit: cover; object-fit: cover;
border: 1px solid var(--fog-border, #e5e7eb); border: 1px solid var(--fog-border, #e5e7eb);
flex-shrink: 0;
} }
:global(.dark) .relay-icon { :global(.dark) .relay-icon {

16
src/lib/modules/feed/FeedPage.svelte

@ -31,6 +31,7 @@
let subscriptionId: string | null = $state(null); let subscriptionId: string | null = $state(null);
let isMounted = $state(true); let isMounted = $state(true);
let initialLoadComplete = $state(false); let initialLoadComplete = $state(false);
let loadInProgress = $state(false);
// Load waiting room events into feed // Load waiting room events into feed
function loadWaitingRoomEvents() { function loadWaitingRoomEvents() {
@ -102,8 +103,9 @@
// Initial feed load // Initial feed load
async function loadFeed() { async function loadFeed() {
if (!isMounted) return; if (!isMounted || loadInProgress) return;
loadInProgress = true;
loading = true; loading = true;
relayError = null; relayError = null;
@ -182,6 +184,7 @@
} finally { } finally {
loading = false; loading = false;
initialLoadComplete = true; initialLoadComplete = true;
loadInProgress = false;
} }
} }
@ -211,8 +214,14 @@
onMount(() => { onMount(() => {
isMounted = true; isMounted = true;
loadInProgress = false;
// Use a small delay to ensure previous page cleanup completes
const initTimeout = setTimeout(() => {
if (!isMounted) return;
nostrClient.initialize().then(() => { nostrClient.initialize().then(() => {
if (isMounted) { if (isMounted && !loadInProgress) {
loadFeed().then(() => { loadFeed().then(() => {
if (isMounted) { if (isMounted) {
setupSubscription(); setupSubscription();
@ -220,9 +229,12 @@
}); });
} }
}); });
}, 50);
return () => { return () => {
clearTimeout(initTimeout);
isMounted = false; isMounted = false;
loadInProgress = false;
if (subscriptionId) { if (subscriptionId) {
nostrClient.unsubscribe(subscriptionId); nostrClient.unsubscribe(subscriptionId);
subscriptionId = null; subscriptionId = null;

47
src/lib/modules/feed/FeedPost.svelte

@ -43,15 +43,23 @@
let showReplyForm = $state(false); let showReplyForm = $state(false);
// Check if card should be collapsed (only in feed view) // Check if card should be collapsed (only in feed view)
let isMounted = $state(true);
$effect(() => { $effect(() => {
if (fullView) { if (fullView) {
shouldCollapse = false; shouldCollapse = false;
return; return;
} }
isMounted = true;
// Wait for content to render, then check height // Wait for content to render, then check height
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let rafId1: number | null = null;
let rafId2: number | null = null;
const checkHeight = () => { const checkHeight = () => {
if (!cardElement) return; if (!isMounted || !cardElement) return;
// Measure the full card height (using scrollHeight to get full content height) // Measure the full card height (using scrollHeight to get full content height)
const cardHeight = cardElement.scrollHeight; const cardHeight = cardElement.scrollHeight;
@ -59,13 +67,20 @@
}; };
// Check after content is rendered // Check after content is rendered
const timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
requestAnimationFrame(() => { if (!isMounted) return;
requestAnimationFrame(checkHeight); rafId1 = requestAnimationFrame(() => {
if (!isMounted) return;
rafId2 = requestAnimationFrame(checkHeight);
}); });
}, 200); }, 200);
return () => clearTimeout(timeoutId); return () => {
isMounted = false;
if (timeoutId) clearTimeout(timeoutId);
if (rafId1) cancelAnimationFrame(rafId1);
if (rafId2) cancelAnimationFrame(rafId2);
};
}); });
function toggleExpand() { function toggleExpand() {
@ -73,13 +88,21 @@
} }
$effect(() => { $effect(() => {
let cancelled = false;
if (isLoggedIn) { if (isLoggedIn) {
isBookmarked(post.id).then(b => { isBookmarked(post.id).then(b => {
if (!cancelled) {
bookmarked = b; bookmarked = b;
}
}); });
} else { } else {
bookmarked = false; bookmarked = false;
} }
return () => {
cancelled = true;
};
}); });
function getRelativeTime(): string { function getRelativeTime(): string {
@ -583,9 +606,11 @@
<EventMenu event={post} showContentActions={true} /> <EventMenu event={post} showContentActions={true} />
</div> </div>
</div> </div>
<div class="flex items-center gap-2 flex-wrap"> <div class="flex items-center gap-2 flex-nowrap">
<div class="flex-shrink-1 min-w-0">
<ProfileBadge pubkey={post.pubkey} /> <ProfileBadge pubkey={post.pubkey} />
<span class="text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0" style="font-size: 0.75em;">{getRelativeTime()}</span> </div>
<span class="text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0 whitespace-nowrap" style="font-size: 0.75em;">{getRelativeTime()}</span>
{#if getClientName()} {#if getClientName()}
<span class="text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0" style="font-size: 0.75em;">via {getClientName()}</span> <span class="text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0" style="font-size: 0.75em;">via {getClientName()}</span>
{/if} {/if}
@ -614,9 +639,11 @@
{:else} {:else}
<!-- Feed view: plaintext only, no profile pics, media as URLs --> <!-- Feed view: plaintext only, no profile pics, media as URLs -->
<div class="post-header flex flex-col gap-2 mb-2"> <div class="post-header flex flex-col gap-2 mb-2">
<div class="flex items-center gap-2 flex-wrap"> <div class="flex items-center gap-2 flex-nowrap">
<div class="flex-shrink-1 min-w-0">
<ProfileBadge pubkey={post.pubkey} inline={true} /> <ProfileBadge pubkey={post.pubkey} inline={true} />
<span class="text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0" style="font-size: 0.75em;">{getRelativeTime()}</span> </div>
<span class="text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0 whitespace-nowrap" style="font-size: 0.75em;">{getRelativeTime()}</span>
{#if getClientName()} {#if getClientName()}
<span class="text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0" style="font-size: 0.75em;">via {getClientName()}</span> <span class="text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0" style="font-size: 0.75em;">via {getClientName()}</span>
{/if} {/if}
@ -964,6 +991,8 @@
align-items: center; align-items: center;
vertical-align: middle; vertical-align: middle;
line-height: 1.5; line-height: 1.5;
width: auto;
max-width: none;
} }

304
src/lib/modules/profiles/ProfilePage.svelte

@ -3,11 +3,9 @@
import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte'; import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte';
import PaymentAddresses from './PaymentAddresses.svelte'; import PaymentAddresses from './PaymentAddresses.svelte';
import FeedPost from '../feed/FeedPost.svelte'; import FeedPost from '../feed/FeedPost.svelte';
import ThreadDrawer from '../feed/ThreadDrawer.svelte';
import ProfileEventsPanel from '../../components/profile/ProfileEventsPanel.svelte'; import ProfileEventsPanel from '../../components/profile/ProfileEventsPanel.svelte';
import ProfileMenu from '../../components/profile/ProfileMenu.svelte'; import ProfileMenu from '../../components/profile/ProfileMenu.svelte';
import BookmarksPanel from '../../components/profile/BookmarksPanel.svelte'; import BookmarksPanel from '../../components/profile/BookmarksPanel.svelte';
import { getPinnedEvents } from '../../services/user-actions.js';
import { fetchProfile, fetchUserStatus, type ProfileData } from '../../services/user-data.js'; import { fetchProfile, fetchUserStatus, type ProfileData } from '../../services/user-data.js';
import { nostrClient } from '../../services/nostr/nostr-client.js'; import { nostrClient } from '../../services/nostr/nostr-client.js';
import { relayManager } from '../../services/nostr/relay-manager.js'; import { relayManager } from '../../services/nostr/relay-manager.js';
@ -17,27 +15,15 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import type { NostrEvent } from '../../types/nostr.js'; import type { NostrEvent } from '../../types/nostr.js';
import { KIND, getFeedKinds } from '../../types/kind-lookup.js'; import { KIND } from '../../types/kind-lookup.js';
// Progressive rendering for profile posts
const INITIAL_RENDER_LIMIT = 25;
const RENDER_INCREMENT = 25;
let visiblePostsCount = $state(INITIAL_RENDER_LIMIT);
let visibleResponsesCount = $state(INITIAL_RENDER_LIMIT);
let visibleInteractionsCount = $state(INITIAL_RENDER_LIMIT);
// Derived: visible items for each tab
const visiblePosts = $derived.by(() => posts.slice(0, visiblePostsCount));
const visibleResponses = $derived.by(() => responses.slice(0, visibleResponsesCount));
const visibleInteractions = $derived.by(() => interactionsWithMe.slice(0, visibleInteractionsCount));
let profile = $state<ProfileData | null>(null); let profile = $state<ProfileData | null>(null);
let userStatus = $state<string | null>(null); let userStatus = $state<string | null>(null);
let posts = $state<NostrEvent[]>([]); let notifications = $state<NostrEvent[]>([]);
let responses = $state<NostrEvent[]>([]);
let interactionsWithMe = $state<NostrEvent[]>([]); let interactionsWithMe = $state<NostrEvent[]>([]);
let loading = $state(true); let loading = $state(true);
let activeTab = $state<'posts' | 'responses' | 'interactions' | 'pins'>('posts'); let activeTab = $state<'pins' | 'notifications' | 'interactions'>('pins');
let nip05Validations = $state<Record<string, boolean | null>>({}); // null = checking, true = valid, false = invalid let nip05Validations = $state<Record<string, boolean | null>>({}); // null = checking, true = valid, false = invalid
// Compute pubkey from route params // Compute pubkey from route params
let profilePubkey = $derived.by(() => decodePubkey($page.params.pubkey)); let profilePubkey = $derived.by(() => decodePubkey($page.params.pubkey));
@ -49,10 +35,6 @@
// Get current logged-in user's pubkey // Get current logged-in user's pubkey
let currentUserPubkey = $state<string | null>(sessionManager.getCurrentPubkey()); let currentUserPubkey = $state<string | null>(sessionManager.getCurrentPubkey());
// Drawer state for viewing threads
let drawerOpen = $state(false);
let drawerEvent = $state<NostrEvent | null>(null);
// Profile events panel state // Profile events panel state
let profileEventsPanelOpen = $state(false); let profileEventsPanelOpen = $state(false);
@ -82,16 +64,6 @@
}; };
}); });
function openDrawer(event: NostrEvent) {
drawerEvent = event;
drawerOpen = true;
}
function closeDrawer() {
drawerOpen = false;
drawerEvent = null;
}
function openProfileEventsPanel() { function openProfileEventsPanel() {
profileEventsPanelOpen = true; profileEventsPanelOpen = true;
} }
@ -117,14 +89,17 @@
$effect(() => { $effect(() => {
const unsubscribe = sessionManager.session.subscribe((session) => { const unsubscribe = sessionManager.session.subscribe((session) => {
currentUserPubkey = session?.pubkey || null; currentUserPubkey = session?.pubkey || null;
// Reload interactions if session changes and we're viewing another user's profile // Reload notifications/interactions if session changes
if (profile) { if (profile) {
const pubkey = decodePubkey($page.params.pubkey); const pubkey = decodePubkey($page.params.pubkey);
if (pubkey && currentUserPubkey && currentUserPubkey !== pubkey) { if (pubkey && currentUserPubkey) {
// Reload interactions tab data if (currentUserPubkey === pubkey) {
loadInteractionsWithMe(pubkey, currentUserPubkey); // Reload notifications for own profile
loadNotifications(pubkey);
} else { } else {
interactionsWithMe = []; // Reload interactions for other user's profile
loadInteractionsWithMe(pubkey, currentUserPubkey);
}
} }
} }
}); });
@ -134,13 +109,33 @@
async function loadPins(pubkey: string) { async function loadPins(pubkey: string) {
if (!isMounted) return; if (!isMounted) return;
try { try {
const pinnedIds = await getPinnedEvents(); // Fetch the user's pin list (kind 10001)
if (!isMounted || pinnedIds.size === 0) { const profileRelays = relayManager.getProfileReadRelays();
const pinLists = await nostrClient.fetchEvents(
[{ kinds: [KIND.PIN_LIST], authors: [pubkey], limit: 1 }],
profileRelays,
{ useCache: true, cacheResults: true, timeout: config.mediumTimeout }
);
if (!isMounted || pinLists.length === 0) {
if (isMounted) pins = []; if (isMounted) pins = [];
return; return;
} }
const profileRelays = relayManager.getProfileReadRelays(); // Extract event IDs from pin list
const pinnedIds = new Set<string>();
for (const tag of pinLists[0].tags) {
if (tag[0] === 'e' && tag[1]) {
pinnedIds.add(tag[1]);
}
}
if (pinnedIds.size === 0) {
if (isMounted) pins = [];
return;
}
// Fetch the actual pinned events
const fetchPromise = nostrClient.fetchEvents( const fetchPromise = nostrClient.fetchEvents(
[{ ids: Array.from(pinnedIds), limit: config.feedLimit }], [{ ids: Array.from(pinnedIds), limit: config.feedLimit }],
profileRelays, profileRelays,
@ -160,6 +155,53 @@
} }
} }
async function loadNotifications(pubkey: string) {
if (!isMounted) return;
try {
const notificationRelays = relayManager.getFeedReadRelays();
// Fetch user's posts to find replies
const userPosts = await nostrClient.fetchEvents(
[{ kinds: [KIND.SHORT_TEXT_NOTE], authors: [pubkey], limit: 100 }],
notificationRelays,
{ useCache: true, cacheResults: true, timeout: config.mediumTimeout }
);
if (!isMounted) return;
const userPostIds = new Set(userPosts.map(p => p.id));
// Fetch notifications: replies, mentions, reactions, zaps
const notificationEvents = await nostrClient.fetchEvents(
[
{ kinds: [KIND.SHORT_TEXT_NOTE], '#e': Array.from(userPostIds).slice(0, 50), limit: 100 }, // Replies to user's posts
{ kinds: [KIND.SHORT_TEXT_NOTE], '#p': [pubkey], limit: 100 }, // Mentions
{ kinds: [KIND.REACTION], '#p': [pubkey], limit: 100 }, // Reactions
{ kinds: [KIND.ZAP_RECEIPT], '#p': [pubkey], limit: 100 } // Zaps
],
notificationRelays,
{ useCache: true, cacheResults: true, timeout: config.mediumTimeout }
);
if (!isMounted) return;
// Deduplicate and sort by created_at descending
const seenIds = new Set<string>();
notifications = notificationEvents
.filter(e => {
if (seenIds.has(e.id)) return false;
seenIds.add(e.id);
// Exclude user's own events
return e.pubkey !== pubkey;
})
.sort((a, b) => b.created_at - a.created_at)
.slice(0, 100); // Limit to 100 most recent
} catch (error) {
console.error('Error loading notifications:', error);
notifications = [];
}
}
async function loadInteractionsWithMe(profilePubkey: string, currentUserPubkey: string) { async function loadInteractionsWithMe(profilePubkey: string, currentUserPubkey: string) {
if (!isMounted || !currentUserPubkey || currentUserPubkey === profilePubkey) { if (!isMounted || !currentUserPubkey || currentUserPubkey === profilePubkey) {
if (isMounted) interactionsWithMe = []; if (isMounted) interactionsWithMe = [];
@ -231,6 +273,7 @@
} }
} }
onMount(async () => { onMount(async () => {
await nostrClient.initialize(); await nostrClient.initialize();
// Load profile after initialization // Load profile after initialization
@ -480,73 +523,31 @@
} }
} }
// Step 2: Load posts and responses in parallel (non-blocking, update when ready) // Step 2: Load pins for the profile being viewed
const profileRelays = relayManager.getProfileReadRelays(); await loadPins(pubkey);
const responseRelays = relayManager.getFeedResponseReadRelays();
// Check again before loading posts
if (abortController.signal.aborted || currentLoadPubkey !== pubkey) {
return;
}
// Load posts first (needed for response filtering)
// Fetch all feed kinds (not just kind 1)
const feedKinds = getFeedKinds();
const feedEvents = await nostrClient.fetchEvents(
[{ kinds: feedKinds, authors: [pubkey], limit: config.feedLimit }],
profileRelays,
{ useCache: true, cacheResults: true, timeout: config.mediumTimeout }
);
// Check again after posts load
if (abortController.signal.aborted || currentLoadPubkey !== pubkey) {
return;
}
posts = feedEvents.sort((a, b) => b.created_at - a.created_at);
// Reset visible counts when new data loads
visiblePostsCount = INITIAL_RENDER_LIMIT;
visibleResponsesCount = INITIAL_RENDER_LIMIT;
visibleInteractionsCount = INITIAL_RENDER_LIMIT;
// Load responses in parallel with posts (but filter after posts are loaded)
const userPostIds = new Set(posts.map(p => p.id));
const responseEvents = await nostrClient.fetchEvents(
[{ kinds: [KIND.SHORT_TEXT_NOTE], '#p': [pubkey], limit: config.feedLimit }], // Fetch more to account for filtering
responseRelays,
{ useCache: true, cacheResults: true, timeout: config.mediumTimeout }
);
// Check again after responses load // Step 3: Load notifications or interactions
if (abortController.signal.aborted || currentLoadPubkey !== pubkey) { if (isOwnProfile) {
return; await loadNotifications(pubkey);
interactionsWithMe = [];
// Set default tab: if no pins, default to notifications
if (pins.length === 0 && notifications.length > 0) {
activeTab = 'notifications';
} else if (pins.length > 0) {
activeTab = 'pins';
} }
} else {
// Filter responses (exclude self-replies, only include replies to user's posts) notifications = [];
responses = responseEvents // Load interactions if logged in and viewing another user's profile
.filter(e => { if (currentUserPubkey) {
if (e.pubkey === pubkey) return false; // Exclude self-replies await loadInteractionsWithMe(pubkey, currentUserPubkey);
const eTag = e.tags.find(t => t[0] === 'e');
return eTag && userPostIds.has(eTag[1]);
})
.sort((a, b) => b.created_at - a.created_at)
.slice(0, 20); // Limit to 20 after filtering
// Step 3: Load interactions in background (non-blocking)
if (currentUserPubkey && currentUserPubkey !== pubkey) {
loadInteractionsWithMe(pubkey, currentUserPubkey).catch(err => {
console.debug('Error loading interactions:', err);
});
} else { } else {
interactionsWithMe = []; interactionsWithMe = [];
} }
// Set default tab to pins if available
// Step 4: Load pins if viewing own profile if (pins.length > 0) {
if (isOwnProfile) { activeTab = 'pins';
loadPins(pubkey); }
} else {
pins = [];
} }
} catch (error) { } catch (error) {
// Only update state if this load wasn't aborted // Only update state if this load wasn't aborted
@ -581,7 +582,7 @@
{#if profile.about} {#if profile.about}
<p class="text-fog-text dark:text-fog-dark-text mb-2">{profile.about}</p> <p class="text-fog-text dark:text-fog-dark-text mb-2">{profile.about}</p>
{/if} {/if}
{#if userStatus} {#if userStatus && userStatus.trim()}
<p class="text-fog-text-light dark:text-fog-dark-text-light italic mb-2" style="font-size: 0.875em;"> <p class="text-fog-text-light dark:text-fog-dark-text-light italic mb-2" style="font-size: 0.875em;">
{userStatus} {userStatus}
</p> </p>
@ -656,18 +657,19 @@
<div class="profile-posts"> <div class="profile-posts">
<div class="tabs mb-4 flex gap-4 border-b border-fog-border dark:border-fog-dark-border"> <div class="tabs mb-4 flex gap-4 border-b border-fog-border dark:border-fog-dark-border">
<button <button
onclick={() => activeTab = 'posts'} onclick={() => activeTab = 'pins'}
class="px-4 py-2 font-semibold {activeTab === 'posts' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-4 py-2 font-semibold {activeTab === 'pins' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
> >
Posts ({posts.length}) Pins ({pins.length})
</button> </button>
{#if isOwnProfile}
<button <button
onclick={() => activeTab = 'responses'} onclick={() => activeTab = 'notifications'}
class="px-4 py-2 font-semibold {activeTab === 'responses' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-4 py-2 font-semibold {activeTab === 'notifications' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
> >
Responses ({responses.length}) Notifications ({notifications.length})
</button> </button>
{#if currentUserPubkey && currentUserPubkey !== decodePubkey($page.params.pubkey)} {:else if currentUserPubkey && currentUserPubkey !== profilePubkey}
<button <button
onclick={() => activeTab = 'interactions'} onclick={() => activeTab = 'interactions'}
class="px-4 py-2 font-semibold {activeTab === 'interactions' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-4 py-2 font-semibold {activeTab === 'interactions' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
@ -675,58 +677,26 @@
Interactions with me ({interactionsWithMe.length}) Interactions with me ({interactionsWithMe.length})
</button> </button>
{/if} {/if}
{#if isOwnProfile}
<button
onclick={() => activeTab = 'pins'}
class="px-4 py-2 font-semibold {activeTab === 'pins' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
>
Pins ({pins.length})
</button>
{/if}
</div> </div>
{#if activeTab === 'posts'} {#if activeTab === 'pins'}
{#if posts.length === 0} {#if pins.length === 0}
<p class="text-fog-text-light dark:text-fog-dark-text-light">No posts yet.</p> <p class="text-fog-text-light dark:text-fog-dark-text-light">No pinned posts yet.</p>
{:else} {:else}
<div class="posts-list"> <div class="pins-list">
{#each visiblePosts as post (post.id)} {#each pins as pin (pin.id)}
<FeedPost post={post} onOpenEvent={openDrawer} /> <FeedPost post={pin} />
{/each} {/each}
{#if posts.length > visiblePostsCount}
<div class="load-more-visible text-center py-4">
<button
class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-80"
onclick={() => {
visiblePostsCount = Math.min(visiblePostsCount + RENDER_INCREMENT, posts.length);
}}
>
Load {Math.min(RENDER_INCREMENT, posts.length - visiblePostsCount)} more posts
</button>
</div>
{/if}
</div> </div>
{/if} {/if}
{:else if activeTab === 'responses'} {:else if activeTab === 'notifications'}
{#if responses.length === 0} {#if notifications.length === 0}
<p class="text-fog-text-light dark:text-fog-dark-text-light">No responses yet.</p> <p class="text-fog-text-light dark:text-fog-dark-text-light">No notifications yet.</p>
{:else} {:else}
<div class="responses-list"> <div class="notifications-list">
{#each visibleResponses as response (response.id)} {#each notifications as notification (notification.id)}
<FeedPost post={response} onOpenEvent={openDrawer} /> <FeedPost post={notification} />
{/each} {/each}
{#if responses.length > visibleResponsesCount}
<div class="load-more-visible text-center py-4">
<button
class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-80"
onclick={() => {
visibleResponsesCount = Math.min(visibleResponsesCount + RENDER_INCREMENT, responses.length);
}}
>
Load {Math.min(RENDER_INCREMENT, responses.length - visibleResponsesCount)} more responses
</button>
</div>
{/if}
</div> </div>
{/if} {/if}
{:else if activeTab === 'interactions'} {:else if activeTab === 'interactions'}
@ -734,30 +704,8 @@
<p class="text-fog-text-light dark:text-fog-dark-text-light">No interactions with you yet.</p> <p class="text-fog-text-light dark:text-fog-dark-text-light">No interactions with you yet.</p>
{:else} {:else}
<div class="interactions-list"> <div class="interactions-list">
{#each visibleInteractions as interaction (interaction.id)} {#each interactionsWithMe as interaction (interaction.id)}
<FeedPost post={interaction} onOpenEvent={openDrawer} /> <FeedPost post={interaction} />
{/each}
{#if interactionsWithMe.length > visibleInteractionsCount}
<div class="load-more-visible text-center py-4">
<button
class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-80"
onclick={() => {
visibleInteractionsCount = Math.min(visibleInteractionsCount + RENDER_INCREMENT, interactionsWithMe.length);
}}
>
Load {Math.min(RENDER_INCREMENT, interactionsWithMe.length - visibleInteractionsCount)} more interactions
</button>
</div>
{/if}
</div>
{/if}
{:else if activeTab === 'pins'}
{#if pins.length === 0}
<p class="text-fog-text-light dark:text-fog-dark-text-light">No pinned posts yet.</p>
{:else}
<div class="pins-list">
{#each pins as pin (pin.id)}
<FeedPost post={pin} onOpenEvent={openDrawer} />
{/each} {/each}
</div> </div>
{/if} {/if}
@ -767,8 +715,6 @@
<p class="text-fog-text-light dark:text-fog-dark-text-light">Profile not found</p> <p class="text-fog-text-light dark:text-fog-dark-text-light">Profile not found</p>
{/if} {/if}
<ThreadDrawer opEvent={drawerEvent} isOpen={drawerOpen} onClose={closeDrawer} />
{#if isOwnProfile} {#if isOwnProfile}
<ProfileEventsPanel <ProfileEventsPanel
isOpen={profileEventsPanelOpen} isOpen={profileEventsPanelOpen}

150
src/routes/bookmarks/+page.svelte

@ -0,0 +1,150 @@
<script lang="ts">
import Header from '../../lib/components/layout/Header.svelte';
import FeedPost from '../../lib/modules/feed/FeedPost.svelte';
import { nostrClient } from '../../lib/services/nostr/nostr-client.js';
import { relayManager } from '../../lib/services/nostr/relay-manager.js';
import { config } from '../../lib/services/nostr/config.js';
import { onMount } from 'svelte';
import type { NostrEvent } from '../../lib/types/nostr.js';
import { KIND } from '../../lib/types/kind-lookup.js';
let events = $state<NostrEvent[]>([]);
let loading = $state(true);
let error = $state<string | null>(null);
async function loadBookmarks() {
loading = true;
error = null;
events = [];
try {
// Fetch all kind 10003 (bookmark) events from relays
const relays = relayManager.getFeedReadRelays();
const fetchedBookmarkLists = await nostrClient.fetchEvents(
[{ kinds: [KIND.BOOKMARKS], limit: config.feedLimit }],
relays,
{
useCache: true,
cacheResults: true,
timeout: config.standardTimeout
}
);
// Extract all event IDs from bookmark lists
const bookmarkedIds = new Set<string>();
for (const bookmarkList of fetchedBookmarkLists) {
for (const tag of bookmarkList.tags) {
if (tag[0] === 'e' && tag[1]) {
bookmarkedIds.add(tag[1]);
}
}
}
console.log(`[Bookmarks] Found ${bookmarkedIds.size} unique bookmarked event IDs from ${fetchedBookmarkLists.length} bookmark lists`);
if (bookmarkedIds.size === 0) {
loading = false;
return;
}
// Fetch the actual events - batch to avoid relay limits
const eventIds = Array.from(bookmarkedIds);
const batchSize = config.veryLargeBatchLimit; // Use config batch limit
const allFetchedEvents: NostrEvent[] = [];
console.log(`[Bookmarks] Fetching ${eventIds.length} events in batches of ${batchSize}`);
for (let i = 0; i < eventIds.length; i += batchSize) {
const batch = eventIds.slice(i, i + batchSize);
const filters = [{ ids: batch }];
console.log(`[Bookmarks] Fetching batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(eventIds.length / batchSize)} (${batch.length} events)`);
const batchEvents = await nostrClient.fetchEvents(
filters,
relays,
{
useCache: true,
cacheResults: true,
timeout: config.mediumTimeout
}
);
console.log(`[Bookmarks] Batch ${Math.floor(i / batchSize) + 1} returned ${batchEvents.length} events`);
allFetchedEvents.push(...batchEvents);
}
console.log(`[Bookmarks] Total fetched: ${allFetchedEvents.length} events`);
// Sort by created_at (newest first)
events = allFetchedEvents.sort((a, b) => b.created_at - a.created_at);
} catch (err) {
console.error('Error loading bookmarks:', err);
error = err instanceof Error ? err.message : 'Failed to load bookmarks';
} finally {
loading = false;
}
}
onMount(async () => {
await nostrClient.initialize();
await loadBookmarks();
});
</script>
<Header />
<main class="container mx-auto px-4 py-8">
<div class="bookmarks-page">
<h1 class="font-bold text-fog-text dark:text-fog-dark-text font-mono mb-6" style="font-size: 1.5em;">/Bookmarks</h1>
{#if loading}
<div class="loading-state">
<p class="text-fog-text dark:text-fog-dark-text">Loading bookmarks...</p>
</div>
{:else if error}
<div class="error-state">
<p class="text-fog-text dark:text-fog-dark-text error-message">{error}</p>
</div>
{:else if events.length === 0}
<div class="empty-state">
<p class="text-fog-text dark:text-fog-dark-text">No bookmarks found.</p>
</div>
{:else}
<div class="bookmarks-posts">
{#each events as event (event.id)}
<FeedPost post={event} />
{/each}
</div>
{/if}
</div>
</main>
<style>
.bookmarks-page {
max-width: 100%;
}
.loading-state,
.empty-state,
.error-state {
padding: 2rem;
text-align: center;
}
.error-message {
font-weight: 600;
color: var(--fog-accent, #64748b);
}
:global(.dark) .error-message {
color: var(--fog-dark-accent, #94a3b8);
}
.bookmarks-posts {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

38
src/routes/settings/+page.svelte

@ -16,28 +16,23 @@
let includeClientTag = $state(true); let includeClientTag = $state(true);
onMount(() => { onMount(() => {
// Load preferences from localStorage // Read current preferences from DOM/localStorage (don't apply, just read)
const storedTextSize = localStorage.getItem('textSize') as TextSize | null; const storedTextSize = localStorage.getItem('textSize') as TextSize | null;
const storedLineSpacing = localStorage.getItem('lineSpacing') as LineSpacing | null; const storedLineSpacing = localStorage.getItem('lineSpacing') as LineSpacing | null;
const storedContentWidth = localStorage.getItem('contentWidth') as ContentWidth | null; const storedContentWidth = localStorage.getItem('contentWidth') as ContentWidth | null;
const storedTheme = localStorage.getItem('theme');
textSize = storedTextSize || 'medium'; textSize = storedTextSize || 'medium';
lineSpacing = storedLineSpacing || 'normal'; lineSpacing = storedLineSpacing || 'normal';
contentWidth = storedContentWidth || 'medium'; contentWidth = storedContentWidth || 'medium';
// Check theme preference // Read current theme from DOM (don't change it)
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; isDark = document.documentElement.classList.contains('dark');
isDark = storedTheme === 'dark' || (!storedTheme && prefersDark);
// Load expiring events preference // Load expiring events preference
expiringEvents = hasExpiringEventsEnabled(); expiringEvents = hasExpiringEventsEnabled();
// Load client tag preference // Load client tag preference
includeClientTag = shouldIncludeClientTag(); includeClientTag = shouldIncludeClientTag();
// Apply preferences
applyPreferences();
}); });
function applyPreferences() { function applyPreferences() {
@ -96,8 +91,9 @@
<Header /> <Header />
<main class="container mx-auto px-4 py-8 max-w-2xl"> <main class="container mx-auto px-4 py-8">
<h1 class="font-bold mb-6 text-fog-text dark:text-fog-dark-text" style="font-size: 1.5em;">Settings</h1> <div class="settings-page">
<h1 class="font-bold text-fog-text dark:text-fog-dark-text font-mono mb-6" style="font-size: 1.5em;">/Settings</h1>
<div class="space-y-6"> <div class="space-y-6">
<!-- Theme Toggle --> <!-- Theme Toggle -->
@ -259,9 +255,15 @@
</p> </p>
</div> </div>
</div> </div>
</div>
</main> </main>
<style> <style>
.settings-page {
max-width: var(--content-width);
margin: 0 auto;
}
.preference-section { .preference-section {
margin-bottom: 0; margin-bottom: 0;
} }
@ -296,9 +298,9 @@
} }
.toggle-button.active { .toggle-button.active {
background: #3b82f6; background: var(--fog-accent, #64748b);
color: white; color: white;
border-color: #3b82f6; border-color: var(--fog-accent, #64748b);
} }
:global(.dark) .toggle-button { :global(.dark) .toggle-button {
@ -313,9 +315,9 @@
} }
:global(.dark) .toggle-button.active { :global(.dark) .toggle-button.active {
background: #3b82f6; background: var(--fog-dark-accent, #94a3b8);
color: white; color: white;
border-color: #3b82f6; border-color: var(--fog-dark-accent, #94a3b8);
} }
.option-button { .option-button {
@ -334,9 +336,9 @@
} }
.option-button.active { .option-button.active {
background: #3b82f6; background: var(--fog-accent, #64748b);
color: white; color: white;
border-color: #3b82f6; border-color: var(--fog-accent, #64748b);
} }
:global(.dark) .option-button { :global(.dark) .option-button {
@ -351,8 +353,8 @@
} }
:global(.dark) .option-button.active { :global(.dark) .option-button.active {
background: #3b82f6; background: var(--fog-dark-accent, #94a3b8);
color: white; color: white;
border-color: #3b82f6; border-color: var(--fog-dark-accent, #94a3b8);
} }
</style> </style>

Loading…
Cancel
Save