Browse Source

Lint components and styles

master
Nuša Pukšič 6 months ago committed by buttercat1791
parent
commit
fff91415dd
  1. 181
      src/app.css
  2. 16
      src/app.html
  3. 1
      src/lib/a/README.md
  4. 80
      src/lib/a/cards/AEventPreview.svelte
  5. 255
      src/lib/a/cards/AProfilePreview.svelte
  6. 11
      src/lib/a/forms/ACommentForm.svelte
  7. 4
      src/lib/a/forms/AMarkupForm.svelte
  8. 96
      src/lib/a/forms/ATextareaWithPreview.svelte
  9. 18
      src/lib/a/index.ts
  10. 17
      src/lib/a/nav/AFooter.svelte
  11. 113
      src/lib/a/nav/ANavbar.svelte
  12. 6
      src/lib/a/primitives/AAlert.svelte
  13. 56
      src/lib/a/primitives/ADetails.svelte
  14. 7
      src/lib/a/primitives/AInput.svelte
  15. 4
      src/lib/a/primitives/ANostrBadge.svelte
  16. 10
      src/lib/a/primitives/ANostrBadgeRow.svelte
  17. 74
      src/lib/a/primitives/ANostrUser.svelte
  18. 8
      src/lib/a/primitives/APagination.svelte
  19. 15
      src/lib/a/reader/ATechBlock.svelte
  20. 4
      src/lib/a/reader/ATechToggle.svelte
  21. 6
      src/styles/a/primitives.css
  22. 8
      src/styles/notifications.css
  23. 3
      src/styles/scrollbar.css
  24. 90
      src/theme-tokens.css

181
src/app.css

@ -19,76 +19,76 @@
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@theme { @theme {
/* single color */ /* single color */
--color-highlight: #f9f6f1; --color-highlight: #f9f6f1;
--color-border: var(--color-highlight); --color-border: var(--color-highlight);
--color-text-muted: var(--color-text-muted); --color-text-muted: var(--color-text-muted);
/* success */ /* success */
--color-success-50: #e3f2e7; --color-success-50: #e3f2e7;
--color-success-100: #c7e6cf; --color-success-100: #c7e6cf;
--color-success-200: #a2d4ae; --color-success-200: #a2d4ae;
--color-success-300: #7dbf8e; --color-success-300: #7dbf8e;
--color-success-400: #5ea571; --color-success-400: #5ea571;
--color-success-500: #4e8e5f; --color-success-500: #4e8e5f;
--color-success-600: #3e744c; --color-success-600: #3e744c;
--color-success-700: #305b3b; --color-success-700: #305b3b;
--color-success-800: #22412a; --color-success-800: #22412a;
--color-success-900: #15281b; --color-success-900: #15281b;
/* info */ /* info */
--color-info-50: #e7eff6; --color-info-50: #e7eff6;
--color-info-100: #c5d9ea; --color-info-100: #c5d9ea;
--color-info-200: #9fbfdb; --color-info-200: #9fbfdb;
--color-info-300: #7aa5cc; --color-info-300: #7aa5cc;
--color-info-400: #5e90be; --color-info-400: #5e90be;
--color-info-500: #4779a5; --color-info-500: #4779a5;
--color-info-600: #365d80; --color-info-600: #365d80;
--color-info-700: #27445d; --color-info-700: #27445d;
--color-info-800: #192b3a; --color-info-800: #192b3a;
--color-info-900: #0d161f; --color-info-900: #0d161f;
/* warning */ /* warning */
--color-warning-50: #fef4e6; --color-warning-50: #fef4e6;
--color-warning-100: #fde4bf; --color-warning-100: #fde4bf;
--color-warning-200: #fcd18e; --color-warning-200: #fcd18e;
--color-warning-300: #fbbc5c; --color-warning-300: #fbbc5c;
--color-warning-400: #f9aa33; --color-warning-400: #f9aa33;
--color-warning-500: #f7971b; --color-warning-500: #f7971b;
--color-warning-600: #c97a14; --color-warning-600: #c97a14;
--color-warning-700: #9a5c0e; --color-warning-700: #9a5c0e;
--color-warning-800: #6c3e08; --color-warning-800: #6c3e08;
--color-warning-900: #3e2404; --color-warning-900: #3e2404;
/* danger */ /* danger */
--color-danger-50: #fbeaea; --color-danger-50: #fbeaea;
--color-danger-100: #f5cccc; --color-danger-100: #f5cccc;
--color-danger-200: #eba5a5; --color-danger-200: #eba5a5;
--color-danger-300: #e17e7e; --color-danger-300: #e17e7e;
--color-danger-400: #d96060; --color-danger-400: #d96060;
--color-danger-500: #c94848; --color-danger-500: #c94848;
--color-danger-600: #a53939; --color-danger-600: #a53939;
--color-danger-700: #7c2b2b; --color-danger-700: #7c2b2b;
--color-danger-800: #521c1c; --color-danger-800: #521c1c;
--color-danger-900: #2b0e0e; --color-danger-900: #2b0e0e;
} }
/* Map Tailwind utilities → theme tokens (PRIMARY ONLY) */ /* Map Tailwind utilities → theme tokens (PRIMARY ONLY) */
@theme inline { @theme inline {
--color-primary-0: var(--brand-primary-0); --color-primary-0: var(--brand-primary-0);
--color-primary-50: var(--brand-primary-50); --color-primary-50: var(--brand-primary-50);
--color-primary-100: var(--brand-primary-100); --color-primary-100: var(--brand-primary-100);
--color-primary-200: var(--brand-primary-200); --color-primary-200: var(--brand-primary-200);
--color-primary-300: var(--brand-primary-300); --color-primary-300: var(--brand-primary-300);
--color-primary-400: var(--brand-primary-400); --color-primary-400: var(--brand-primary-400);
--color-primary-500: var(--brand-primary-500); --color-primary-500: var(--brand-primary-500);
--color-primary-600: var(--brand-primary-600); --color-primary-600: var(--brand-primary-600);
--color-primary-700: var(--brand-primary-700); --color-primary-700: var(--brand-primary-700);
--color-primary-800: var(--brand-primary-800); --color-primary-800: var(--brand-primary-800);
--color-primary-900: var(--brand-primary-900); --color-primary-900: var(--brand-primary-900);
--color-primary-950: var(--brand-primary-950); --color-primary-950: var(--brand-primary-950);
--color-primary-1000: var(--brand-primary-1000); --color-primary-1000: var(--brand-primary-1000);
} }
@source "../node_modules/flowbite-svelte/dist"; @source "../node_modules/flowbite-svelte/dist";
@ -97,33 +97,47 @@
/* @utility and @layer rules… */ /* @utility and @layer rules… */
/* .content-visibility-auto */ /* .content-visibility-auto */
@utility content-visibility-auto { content-visibility: auto; } @utility content-visibility-auto {
content-visibility: auto;
}
/* .contain-size */ /* .contain-size */
@utility contain-size { contain: size; } @utility contain-size {
contain: size;
}
/* numbers -> px (e.g. contain-intrinsic-w-[320] => width: 320px) */ /* numbers -> px (e.g. contain-intrinsic-w-[320] => width: 320px) */
@utility contain-intrinsic-w-* { @utility contain-intrinsic-w-* {
--tw-ciw: --value(number); --tw-ciw: --value(number);
width: calc(var(--tw-ciw) * 1px); width: calc(var(--tw-ciw) * 1px);
} }
@utility contain-intrinsic-h-* { @utility contain-intrinsic-h-* {
--tw-cih: --value(number); --tw-cih: --value(number);
height: calc(var(--tw-cih) * 1px); height: calc(var(--tw-cih) * 1px);
} }
/* percentages (e.g. contain-intrinsic-wp-[65%] => width: 65%) */ /* percentages (e.g. contain-intrinsic-wp-[65%] => width: 65%) */
@utility contain-intrinsic-wp-* { width: --value(percentage); } @utility contain-intrinsic-wp-* {
@utility contain-intrinsic-hp-* { height: --value(percentage); } width: --value(percentage);
}
@utility contain-intrinsic-hp-* {
height: --value(percentage);
}
/* list-upper-alpha, list-lower-alpha (keep your old class names) /* list-upper-alpha, list-lower-alpha (keep your old class names)
Note: in v4 you can also write list-[upper-alpha] / list-[lower-alpha] inline. */ Note: in v4 you can also write list-[upper-alpha] / list-[lower-alpha] inline. */
@utility list-upper-alpha { list-style-type: upper-alpha; } @utility list-upper-alpha {
@utility list-lower-alpha { list-style-type: lower-alpha; } list-style-type: upper-alpha;
}
@utility list-lower-alpha {
list-style-type: lower-alpha;
}
/* flexGrow 1/2/3 — unlock grow-2, grow-3 (and any number via brackets) */ /* flexGrow 1/2/3 — unlock grow-2, grow-3 (and any number via brackets) */
@utility grow-* { flex-grow: --value(integer); } @utility grow-* {
flex-grow: --value(integer);
}
/* Hue rotate: use arbitrary values directly, e.g. hue-rotate-[20deg] (no config needed). */ /* Hue rotate: use arbitrary values directly, e.g. hue-rotate-[20deg] (no config needed). */
@ -135,10 +149,11 @@
@layer base { @layer base {
/* disable chrome cancel button */ /* disable chrome cancel button */
input[type="search"]::-webkit-search-cancel-button { input[type="search"]::-webkit-search-cancel-button {
display: none; display: none;
} }
.leather, .publication-leather { .leather,
.publication-leather {
@apply bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100; @apply bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100;
} }
@ -465,7 +480,6 @@
} }
@layer components { @layer components {
nav a { nav a {
text-decoration-line: none !important; text-decoration-line: none !important;
} }
@ -506,7 +520,12 @@
/* common heading base styles */ /* common heading base styles */
h1, h2, h3, h4, h5, h6 { h1,
h2,
h3,
h4,
h5,
h6 {
@apply text-gray-900 dark:text-gray-100 pt-4; @apply text-gray-900 dark:text-gray-100 pt-4;
} }

16
src/app.html

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en" data-tech="off"> <html lang="en" data-tech="off">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
@ -8,7 +8,7 @@
<!-- Apply saved theme ASAP to avoid flash --> <!-- Apply saved theme ASAP to avoid flash -->
<script> <script>
try { try {
const t = localStorage.getItem('theme'); const t = localStorage.getItem("theme");
if (t) document.documentElement.dataset.theme = t; if (t) document.documentElement.dataset.theme = t;
} catch (_) { } catch (_) {
/* no-op */ /* no-op */
@ -18,8 +18,8 @@
<!-- Apply saved tech toggle ASAP; default is off --> <!-- Apply saved tech toggle ASAP; default is off -->
<script> <script>
try { try {
const v = localStorage.getItem('alexandria/showTech'); const v = localStorage.getItem("alexandria/showTech");
document.documentElement.dataset.tech = v === 'true' ? 'on' : 'off'; document.documentElement.dataset.tech = v === "true" ? "on" : "off";
} catch (_) { } catch (_) {
/* no-op */ /* no-op */
} }
@ -46,18 +46,14 @@
}, },
}; };
</script> </script>
<script <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
></script>
<!-- highlight.js for code highlighting --> <!-- highlight.js for code highlighting -->
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"
/> />
<script <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"
></script>
%sveltekit.head% %sveltekit.head%
</head> </head>

1
src/lib/a/README.md

@ -8,4 +8,3 @@ All components are based on Flowbite Svelte components,
which are built on top of Tailwind CSS. which are built on top of Tailwind CSS.
Keeping all the styles in one place allows us to easily change the look and feel of the application by switching themes. Keeping all the styles in one place allows us to easily change the look and feel of the application by switching themes.

80
src/lib/a/cards/AEventPreview.svelte

@ -18,7 +18,7 @@
showContent = true, showContent = true,
actions, actions,
onSelect, onSelect,
onDeferralClick onDeferralClick,
}: { }: {
event: NDKEvent; event: NDKEvent;
label?: string; label?: string;
@ -29,7 +29,11 @@
showDeferralNaddr?: boolean; showDeferralNaddr?: boolean;
showPublicationLink?: boolean; showPublicationLink?: boolean;
showContent?: boolean; showContent?: boolean;
actions?: { label: string; onClick: (ev: NDKEvent) => void; variant?: "primary" | "light" | "alternative" }[]; actions?: {
label: string;
onClick: (ev: NDKEvent) => void;
variant?: "primary" | "light" | "alternative";
}[];
onSelect?: (ev: NDKEvent) => void; onSelect?: (ev: NDKEvent) => void;
onDeferralClick?: (naddr: string, ev: NDKEvent) => void; onDeferralClick?: (naddr: string, ev: NDKEvent) => void;
} = $props(); } = $props();
@ -98,12 +102,12 @@
const displayName: string | undefined = const displayName: string | undefined =
profileData?.display_name || profileData?.name; profileData?.display_name || profileData?.name;
const avatarFallback: string = const avatarFallback: string = (displayName || event.pubkey || "?")
(displayName || event.pubkey || "?").slice(0, 1).toUpperCase(); .slice(0, 1)
const createdDate: string = .toUpperCase();
event.created_at const createdDate: string = event.created_at
? new Date(event.created_at * 1000).toLocaleDateString() ? new Date(event.created_at * 1000).toLocaleDateString()
: "Unknown date"; : "Unknown date";
const computedActions = const computedActions =
actions && actions.length > 0 actions && actions.length > 0
@ -112,8 +116,8 @@
{ {
label: "Open", label: "Open",
onClick: (ev: NDKEvent) => onSelect?.(ev), onClick: (ev: NDKEvent) => onSelect?.(ev),
variant: "light" as const variant: "light" as const,
} },
]; ];
</script> </script>
@ -131,31 +135,35 @@
<!-- Meta --> <!-- Meta -->
<div class="flex flex-row w-full gap-3 items-center min-w-0"> <div class="flex flex-row w-full gap-3 items-center min-w-0">
{#if label} {#if label}
<span class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400"> <span
{label} class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400"
</span> >
{label}
</span>
{/if} {/if}
{#if showKind} {#if showKind}
<span class="text-[10px] px-1.5 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300"> <span
Kind {event.kind} class="text-[10px] px-1.5 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300"
</span> >
Kind {event.kind}
</span>
{/if} {/if}
{#if community} {#if community}
<span <span
class="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300" class="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300"
title="Has posted to the community" title="Has posted to the community"
> >
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"> <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
<path <path
d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"
/> />
</svg> </svg>
Community Community
</span> </span>
{/if} {/if}
<span class="text-xs ml-auto mb-4"> <span class="text-xs ml-auto mb-4">
{createdDate} {createdDate}
</span> </span>
</div> </div>
<div class="flex flex-row"> <div class="flex flex-row">
@ -171,7 +179,9 @@
</div> </div>
{:else} {:else}
{#if summary} {#if summary}
<div class="text-sm text-primary-900 dark:text-primary-200 line-clamp-2"> <div
class="text-sm text-primary-900 dark:text-primary-200 line-clamp-2"
>
{summary} {summary}
</div> </div>
{/if} {/if}
@ -184,7 +194,7 @@
tabindex="0" tabindex="0"
onclick={handleDeferralClick} onclick={handleDeferralClick}
onkeydown={(e) => { onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === "Enter" || e.key === " ") {
e.preventDefault(); e.preventDefault();
handleDeferralClick(e as unknown as MouseEvent); handleDeferralClick(e as unknown as MouseEvent);
} }
@ -196,7 +206,9 @@
{/if} {/if}
{#if showContent && event.content} {#if showContent && event.content}
<div class="text-sm text-gray-800 dark:text-gray-200 line-clamp-3 break-words mb-4"> <div
class="text-sm text-gray-800 dark:text-gray-200 line-clamp-3 break-words mb-4"
>
{clippedContent(event.content)} {clippedContent(event.content)}
</div> </div>
{/if} {/if}
@ -205,7 +217,9 @@
<!-- Footer / Actions --> <!-- Footer / Actions -->
{#if showPublicationLink && event.kind !== 0} {#if showPublicationLink && event.kind !== 0}
<div class="px-4 pt-2 pb-3 border-t border-primary-200 dark:border-primary-700 flex items-center gap-2 flex-wrap"> <div
class="px-4 pt-2 pb-3 border-t border-primary-200 dark:border-primary-700 flex items-center gap-2 flex-wrap"
>
<ViewPublicationLink {event} /> <ViewPublicationLink {event} />
</div> </div>
{/if} {/if}

255
src/lib/a/cards/AProfilePreview.svelte

@ -1,21 +1,36 @@
<script lang="ts"> <script lang="ts">
import { Card, Heading, P, Button, Modal, Avatar, Dropdown, DropdownItem } from 'flowbite-svelte'; import {
import { ChevronDownOutline } from 'flowbite-svelte-icons'; Card,
import AAlert from '$lib/a/primitives/AAlert.svelte'; Heading,
import CopyToClipboard from '$lib/components/util/CopyToClipboard.svelte'; P,
import { goto } from '$app/navigation'; Button,
import LazyImage from '$lib/components/util/LazyImage.svelte'; Modal,
import { userBadge } from '$lib/snippets/UserSnippets.svelte'; Avatar,
import { basicMarkup } from '$lib/snippets/MarkupSnippets.svelte'; Dropdown,
import QrCode from '$lib/components/util/QrCode.svelte'; DropdownItem,
import { generateDarkPastelColor } from '$lib/utils/image_utils'; } from "flowbite-svelte";
import { lnurlpWellKnownUrl, checkCommunity } from '$lib/utils/search_utility'; import { ChevronDownOutline } from "flowbite-svelte-icons";
import { bech32 } from 'bech32'; import AAlert from "$lib/a/primitives/AAlert.svelte";
import { getNdkContext, activeInboxRelays } from '$lib/ndk'; import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte";
import { toNpub } from '$lib/utils/nostrUtils'; import { goto } from "$app/navigation";
import { neventEncode, naddrEncode, nprofileEncode } from '$lib/utils'; import LazyImage from "$lib/components/util/LazyImage.svelte";
import { isPubkeyInUserLists, fetchCurrentUserLists } from '$lib/utils/user_lists'; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import type { NDKEvent } from '@nostr-dev-kit/ndk'; import { basicMarkup } from "$lib/snippets/MarkupSnippets.svelte";
import QrCode from "$lib/components/util/QrCode.svelte";
import { generateDarkPastelColor } from "$lib/utils/image_utils";
import {
lnurlpWellKnownUrl,
checkCommunity,
} from "$lib/utils/search_utility";
import { bech32 } from "bech32";
import { getNdkContext, activeInboxRelays } from "$lib/ndk";
import { toNpub } from "$lib/utils/nostrUtils";
import { neventEncode, naddrEncode, nprofileEncode } from "$lib/utils";
import {
isPubkeyInUserLists,
fetchCurrentUserLists,
} from "$lib/utils/user_lists";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
type UserLite = { npub?: string | null }; type UserLite = { npub?: string | null };
type Profile = { type Profile = {
@ -52,39 +67,64 @@
function displayName() { function displayName() {
const p = props.profile; const p = props.profile;
const u = props.user; const u = props.user;
return p?.display_name || p?.displayName || p?.name || (u?.npub ? u.npub.slice(0, 10) + '…' : ''); return (
p?.display_name ||
p?.displayName ||
p?.name ||
(u?.npub ? u.npub.slice(0, 10) + "…" : "")
);
} }
function shortNpub() { function shortNpub() {
const npub = props.user?.npub; const npub = props.user?.npub;
if (!npub) return ''; if (!npub) return "";
return npub.slice(0, 12) + '…' + npub.slice(-8); return npub.slice(0, 12) + "…" + npub.slice(-8);
} }
function hideOnError(e: Event) { function hideOnError(e: Event) {
const img = e.currentTarget as HTMLImageElement | null; const img = e.currentTarget as HTMLImageElement | null;
if (img) { if (img) {
img.style.display = 'none'; img.style.display = "none";
const next = img.nextElementSibling as HTMLElement | null; const next = img.nextElementSibling as HTMLElement | null;
if (next) next.classList.remove('hidden'); if (next) next.classList.remove("hidden");
} }
} }
function getIdentifiers(event: NDKEvent, profile: any): { label: string; value: string; link?: string }[] { function getIdentifiers(
event: NDKEvent,
profile: any,
): { label: string; value: string; link?: string }[] {
const ids: { label: string; value: string; link?: string }[] = []; const ids: { label: string; value: string; link?: string }[] = [];
if (event.kind === 0) { if (event.kind === 0) {
const npub = toNpub(event.pubkey); const npub = toNpub(event.pubkey);
if (npub) ids.push({ label: 'npub', value: npub, link: `/events?id=${npub}` }); if (npub)
ids.push({ label: 'nprofile', value: nprofileEncode(event.pubkey, $activeInboxRelays), link: `/events?id=${nprofileEncode(event.pubkey, $activeInboxRelays)}` }); ids.push({ label: "npub", value: npub, link: `/events?id=${npub}` });
ids.push({ label: 'nevent', value: neventEncode(event, $activeInboxRelays), link: `/events?id=${neventEncode(event, $activeInboxRelays)}` }); ids.push({
ids.push({ label: 'pubkey', value: event.pubkey }); label: "nprofile",
value: nprofileEncode(event.pubkey, $activeInboxRelays),
link: `/events?id=${nprofileEncode(event.pubkey, $activeInboxRelays)}`,
});
ids.push({
label: "nevent",
value: neventEncode(event, $activeInboxRelays),
link: `/events?id=${neventEncode(event, $activeInboxRelays)}`,
});
ids.push({ label: "pubkey", value: event.pubkey });
} else { } else {
ids.push({ label: 'nevent', value: neventEncode(event, $activeInboxRelays), link: `/events?id=${neventEncode(event, $activeInboxRelays)}` }); ids.push({
label: "nevent",
value: neventEncode(event, $activeInboxRelays),
link: `/events?id=${neventEncode(event, $activeInboxRelays)}`,
});
try { try {
const naddr = naddrEncode(event, $activeInboxRelays); const naddr = naddrEncode(event, $activeInboxRelays);
ids.push({ label: 'naddr', value: naddr, link: `/events?id=${naddr}` }); ids.push({ label: "naddr", value: naddr, link: `/events?id=${naddr}` });
} catch {} } catch {}
ids.push({ label: 'id', value: event.id, link: `/events?id=${event.id}` }); ids.push({
label: "id",
value: event.id,
link: `/events?id=${event.id}`,
});
} }
return ids; return ids;
} }
@ -98,10 +138,10 @@
const p = props.profile; const p = props.profile;
if (p?.lud16) { if (p?.lud16) {
try { try {
const [name, domain] = p.lud16.split('@'); const [name, domain] = p.lud16.split("@");
const url = lnurlpWellKnownUrl(domain, name); const url = lnurlpWellKnownUrl(domain, name);
const words = bech32.toWords(new TextEncoder().encode(url)); const words = bech32.toWords(new TextEncoder().encode(url));
lnurl = bech32.encode('lnurl', words); lnurl = bech32.encode("lnurl", words);
} catch { } catch {
lnurl = null; lnurl = null;
} }
@ -120,46 +160,71 @@
} }
// isInUserLists: prefer prop.profile hint, else cached profileData, else fetch // isInUserLists: prefer prop.profile hint, else cached profileData, else fetch
if (props.profile && typeof props.profile.isInUserLists === 'boolean') { if (props.profile && typeof props.profile.isInUserLists === "boolean") {
isInUserLists = props.profile.isInUserLists; isInUserLists = props.profile.isInUserLists;
} else { } else {
const cachedProfileData = (ev as any).profileData; const cachedProfileData = (ev as any).profileData;
if (cachedProfileData && typeof cachedProfileData.isInUserLists === 'boolean') { if (
cachedProfileData &&
typeof cachedProfileData.isInUserLists === "boolean"
) {
isInUserLists = cachedProfileData.isInUserLists; isInUserLists = cachedProfileData.isInUserLists;
} else { } else {
fetchCurrentUserLists().then((lists) => { fetchCurrentUserLists()
isInUserLists = isPubkeyInUserLists(ev.pubkey, lists); .then((lists) => {
}).catch(() => { isInUserLists = isPubkeyInUserLists(ev.pubkey, lists);
isInUserLists = false; })
}); .catch(() => {
isInUserLists = false;
});
} }
} }
// community status: prefer map if provided, else check // community status: prefer map if provided, else check
if (props.communityStatusMap && props.communityStatusMap[ev.pubkey] !== undefined) { if (
props.communityStatusMap &&
props.communityStatusMap[ev.pubkey] !== undefined
) {
communityStatus = props.communityStatusMap[ev.pubkey]; communityStatus = props.communityStatusMap[ev.pubkey];
} else { } else {
checkCommunity(ev.pubkey).then((status) => { checkCommunity(ev.pubkey)
communityStatus = status; .then((status) => {
}).catch(() => { communityStatus = status;
communityStatus = false; })
}); .catch(() => {
communityStatus = false;
});
} }
}); });
</script> </script>
<Card size="xl" class="main-leather p-0 overflow-hidden rounded-lg border border-primary-200 dark:border-primary-700"> <Card
size="xl"
class="main-leather p-0 overflow-hidden rounded-lg border border-primary-200 dark:border-primary-700"
>
{#if props.profile?.banner} {#if props.profile?.banner}
<div class="w-full bg-primary-200 dark:bg-primary-800 relative"> <div class="w-full bg-primary-200 dark:bg-primary-800 relative">
<LazyImage src={props.profile.banner} alt="Profile banner" eventId={props.event.id} className="w-full h-60 object-cover" /> <LazyImage
src={props.profile.banner}
alt="Profile banner"
eventId={props.event.id}
className="w-full h-60 object-cover"
/>
</div> </div>
{:else} {:else}
<div class="w-full h-60" style={`background-color: ${generateDarkPastelColor(props.event.id)};`}></div> <div
class="w-full h-60"
style={`background-color: ${generateDarkPastelColor(props.event.id)};`}
></div>
{/if} {/if}
<div class={`p-6 flex flex-col gap-4 relative`}> <div class={`p-6 flex flex-col gap-4 relative`}>
<Avatar
<Avatar size="xl" src={props.profile?.picture ?? null} alt="Avatar" class="absolute w-fit top-[-56px]" /> size="xl"
src={props.profile?.picture ?? null}
alt="Avatar"
class="absolute w-fit top-[-56px]"
/>
<div class="flex flex-col gap-3 mt-14"> <div class="flex flex-col gap-3 mt-14">
<Heading tag="h1" class="h-leather mb-2">{displayName()}</Heading> <Heading tag="h1" class="h-leather mb-2">{displayName()}</Heading>
@ -170,16 +235,39 @@
{#if props.event} {#if props.event}
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-2 min-w-0">
{#if props.profile?.nip05} {#if props.profile?.nip05}
<span class="px-2 py-0.5 !mb-0 rounded bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 text-xs">{props.profile.nip05}</span> <span
class="px-2 py-0.5 !mb-0 rounded bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 text-xs"
>{props.profile.nip05}</span
>
{/if} {/if}
{#if communityStatus === true} {#if communityStatus === true}
<div class="flex-shrink-0 w-4 h-4 bg-yellow-100 dark:bg-yellow-900 rounded-full flex items-center justify-center" title="Has posted to the community"> <div
<svg class="w-3 h-3 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg> class="flex-shrink-0 w-4 h-4 bg-yellow-100 dark:bg-yellow-900 rounded-full flex items-center justify-center"
title="Has posted to the community"
>
<svg
class="w-3 h-3 text-yellow-600 dark:text-yellow-400"
fill="currentColor"
viewBox="0 0 24 24"
><path
d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"
/></svg
>
</div> </div>
{/if} {/if}
{#if isInUserLists === true} {#if isInUserLists === true}
<div class="flex-shrink-0 w-4 h-4 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center" title="In your lists (follows, etc.)"> <div
<svg class="w-3 h-3 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg> class="flex-shrink-0 w-4 h-4 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center"
title="In your lists (follows, etc.)"
>
<svg
class="w-3 h-3 text-red-600 dark:text-red-400"
fill="currentColor"
viewBox="0 0 24 24"
><path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
/></svg
>
</div> </div>
{/if} {/if}
</div> </div>
@ -187,31 +275,57 @@
</div> </div>
{#if props.profile?.about} {#if props.profile?.about}
<div class="prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 break-words overflow-wrap-anywhere min-w-0"> <div
class="prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 break-words overflow-wrap-anywhere min-w-0"
>
{@render basicMarkup(props.profile.about, ndk)} {@render basicMarkup(props.profile.about, ndk)}
</div> </div>
{/if} {/if}
<div class="flex flex-wrap gap-4 text-sm"> <div class="flex flex-wrap gap-4 text-sm">
{#if props.profile?.website} {#if props.profile?.website}
<a href={props.profile.website} rel="noopener" class="text-primary-600 dark:text-primary-400 hover:underline break-all" target="_blank">{props.profile.website}</a> <a
href={props.profile.website}
rel="noopener"
class="text-primary-600 dark:text-primary-400 hover:underline break-all"
target="_blank">{props.profile.website}</a
>
{/if} {/if}
</div> </div>
<div class="flex flex-row flex-wrap justify-end gap-4 text-sm"> <div class="flex flex-row flex-wrap justify-end gap-4 text-sm">
{#if props.profile?.lud16} {#if props.profile?.lud16}
<Button color="alternative" size="xs" onclick={() => (lnModalOpen = true)}> {props.profile.lud16}</Button> <Button
color="alternative"
size="xs"
onclick={() => (lnModalOpen = true)}>⚡ {props.profile.lud16}</Button
>
{/if} {/if}
<Button size="xs" color="alternative">Identifiers <ChevronDownOutline class="ms-2 h-6 w-6" /></Button> <Button size="xs" color="alternative"
>Identifiers <ChevronDownOutline class="ms-2 h-6 w-6" /></Button
>
<Dropdown simple> <Dropdown simple>
{#each getIdentifiers(props.event, props.profile) as identifier} {#each getIdentifiers(props.event, props.profile) as identifier}
<DropdownItem><CopyToClipboard displayText={identifier.label} copyText={identifier.value} /></DropdownItem> <DropdownItem
><CopyToClipboard
displayText={identifier.label}
copyText={identifier.value}
/></DropdownItem
>
{/each} {/each}
</Dropdown> </Dropdown>
{#if props.isOwn} {#if props.isOwn}
<Button class="!mb-0" size="xs" onclick={() => goto('/profile/notifications')}>Notifications</Button> <Button
<Button class="!mb-0" size="xs" onclick={() => goto('/profile/my-notes')}>My notes</Button> class="!mb-0"
size="xs"
onclick={() => goto("/profile/notifications")}>Notifications</Button
>
<Button
class="!mb-0"
size="xs"
onclick={() => goto("/profile/my-notes")}>My notes</Button
>
{/if} {/if}
</div> </div>
@ -225,13 +339,23 @@
</Card> </Card>
{#if lnModalOpen} {#if lnModalOpen}
<Modal class="modal-leather" title="Lightning Address" bind:open={lnModalOpen} outsideclose size="sm"> <Modal
class="modal-leather"
title="Lightning Address"
bind:open={lnModalOpen}
outsideclose
size="sm"
>
{#if props.profile?.lud16} {#if props.profile?.lud16}
<div> <div>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
{@render userBadge( {@render userBadge(
props.user?.npub ?? toNpub(props.event.pubkey), props.user?.npub ?? toNpub(props.event.pubkey),
props.profile?.displayName || props.profile?.display_name || props.profile?.name || (props.event?.pubkey || ''), props.profile?.displayName ||
props.profile?.display_name ||
props.profile?.name ||
props.event?.pubkey ||
"",
ndk, ndk,
)} )}
<P class="break-all">{props.profile.lud16}</P> <P class="break-all">{props.profile.lud16}</P>
@ -240,7 +364,8 @@
<P>Scan the QR code or copy the address</P> <P>Scan the QR code or copy the address</P>
{#if lnurl} {#if lnurl}
<P class="break-all overflow-wrap-anywhere"> <P class="break-all overflow-wrap-anywhere">
<CopyToClipboard icon={false} displayText={lnurl}></CopyToClipboard> <CopyToClipboard icon={false} displayText={lnurl}
></CopyToClipboard>
</P> </P>
<QrCode value={lnurl} /> <QrCode value={lnurl} />
{:else} {:else}

11
src/lib/a/forms/ACommentForm.svelte

@ -61,8 +61,15 @@
<div class="flex flex-row justify-between mt-2"> <div class="flex flex-row justify-between mt-2">
<div class="flex flex-row flex-wrap gap-3 !m-0"> <div class="flex flex-row flex-wrap gap-3 !m-0">
<Button size="xs" color="alternative" onclick={removeFormatting} class="!m-0">Remove Formatting</Button> <Button
<Button size="xs" color="alternative" class="!m-0" onclick={clearForm}>Clear</Button> size="xs"
color="alternative"
onclick={removeFormatting}
class="!m-0">Remove Formatting</Button
>
<Button size="xs" color="alternative" class="!m-0" onclick={clearForm}
>Clear</Button
>
</div> </div>
<Button <Button
disabled={isSubmitting || !content.trim() || !$userStore.signedIn} disabled={isSubmitting || !content.trim() || !$userStore.signedIn}

4
src/lib/a/forms/AMarkupForm.svelte

@ -82,7 +82,9 @@
</div> </div>
<div class="flex justify-end space-x-4"> <div class="flex justify-end space-x-4">
<Button type="button" color="alternative" onclick={clearForm}>Clear Form</Button> <Button type="button" color="alternative" onclick={clearForm}
>Clear Form</Button
>
<Button type="submit" tabindex={0} disabled={isSubmitting}> <Button type="submit" tabindex={0} disabled={isSubmitting}>
{#if isSubmitting} {#if isSubmitting}
Submitting... Submitting...

96
src/lib/a/forms/ATextareaWithPreview.svelte

@ -1,6 +1,25 @@
<script lang="ts"> <script lang="ts">
import { Textarea, Toolbar, ToolbarGroup, ToolbarButton, Label, Button } from "flowbite-svelte"; import {
import { Bold, Italic, Strikethrough, Quote, Link2, Image, Hash, List, ListOrdered, Eye, PencilLine } from "@lucide/svelte"; Textarea,
Toolbar,
ToolbarGroup,
ToolbarButton,
Label,
Button,
} from "flowbite-svelte";
import {
Bold,
Italic,
Strikethrough,
Quote,
Link2,
Image,
Hash,
List,
ListOrdered,
Eye,
PencilLine,
} from "@lucide/svelte";
// Reusable editor with toolbar (from ACommentForm) and toolbar-only Preview // Reusable editor with toolbar (from ACommentForm) and toolbar-only Preview
let { let {
@ -36,29 +55,45 @@
const markupButtons = [ const markupButtons = [
{ label: "Bold", icon: Bold, action: () => insertMarkup("**", "**") }, { label: "Bold", icon: Bold, action: () => insertMarkup("**", "**") },
{ label: "Italic", icon: Italic, action: () => insertMarkup("_", "_") }, { label: "Italic", icon: Italic, action: () => insertMarkup("_", "_") },
{ label: "Strike", icon: Strikethrough, action: () => insertMarkup("~~", "~~") }, {
label: "Strike",
icon: Strikethrough,
action: () => insertMarkup("~~", "~~"),
},
{ label: "Link", icon: Link2, action: () => insertMarkup("[", "](url)") }, { label: "Link", icon: Link2, action: () => insertMarkup("[", "](url)") },
{ label: "Image", icon: Image, action: () => insertMarkup("![", "](url)") }, { label: "Image", icon: Image, action: () => insertMarkup("![", "](url)") },
{ label: "Quote", icon: Quote, action: () => insertMarkup("> ", "") }, { label: "Quote", icon: Quote, action: () => insertMarkup("> ", "") },
{ label: "List", icon: List, action: () => insertMarkup("* ", "") }, { label: "List", icon: List, action: () => insertMarkup("* ", "") },
{ label: "Numbered List", icon: ListOrdered, action: () => insertMarkup("1. ", "") }, {
label: "Numbered List",
icon: ListOrdered,
action: () => insertMarkup("1. ", ""),
},
{ label: "Hashtag", icon: Hash, action: () => insertMarkup("#", "") }, { label: "Hashtag", icon: Hash, action: () => insertMarkup("#", "") },
]; ];
function insertMarkup(prefix: string, suffix: string) { function insertMarkup(prefix: string, suffix: string) {
const textarea = wrapper?.querySelector("textarea") as HTMLTextAreaElement | null; const textarea = wrapper?.querySelector(
"textarea",
) as HTMLTextAreaElement | null;
if (!textarea) return; if (!textarea) return;
const start = textarea.selectionStart; const start = textarea.selectionStart;
const end = textarea.selectionEnd; const end = textarea.selectionEnd;
const selectedText = value.substring(start, end); const selectedText = value.substring(start, end);
value = value.substring(0, start) + prefix + selectedText + suffix + value.substring(end); value =
value.substring(0, start) +
prefix +
selectedText +
suffix +
value.substring(end);
// Set cursor position after the inserted markup // Set cursor position after the inserted markup
setTimeout(() => { setTimeout(() => {
textarea.focus(); textarea.focus();
textarea.selectionStart = textarea.selectionEnd = start + prefix.length + selectedText.length + suffix.length; textarea.selectionStart = textarea.selectionEnd =
start + prefix.length + selectedText.length + suffix.length;
}, 0); }, 0);
} }
@ -89,12 +124,12 @@
<div bind:this={wrapper} class="rounded-lg"> <div bind:this={wrapper} class="rounded-lg">
<div class="min-h-[180px] relative"> <div class="min-h-[180px] relative">
{#if activeTab === 'write'} {#if activeTab === "write"}
<div class="inset-0"> <div class="inset-0">
<Textarea <Textarea
id={id} {id}
rows={isExpanded ? 30 : rows} rows={isExpanded ? 30 : rows}
bind:value={value} bind:value
classes={{ classes={{
wrapper: "!m-0 p-0 h-full", wrapper: "!m-0 p-0 h-full",
inner: "!m-0 !bg-transparent !dark:bg-transparent", inner: "!m-0 !bg-transparent !dark:bg-transparent",
@ -103,21 +138,34 @@
addon: "!m-0 top-3 hidden md:flex", addon: "!m-0 top-3 hidden md:flex",
div: "!m-0 !bg-transparent !dark:bg-transparent !border-0 !rounded-none !shadow-none !focus:ring-0", div: "!m-0 !bg-transparent !dark:bg-transparent !border-0 !rounded-none !shadow-none !focus:ring-0",
}} }}
placeholder={placeholder} {placeholder}
> >
{#snippet header()} {#snippet header()}
<Toolbar embedded class="flex-row !m-0 !dark:bg-transparent !bg-transparent"> <Toolbar
embedded
class="flex-row !m-0 !dark:bg-transparent !bg-transparent"
>
<ToolbarGroup class="flex-row flex-wrap !m-0"> <ToolbarGroup class="flex-row flex-wrap !m-0">
{#each markupButtons as button} {#each markupButtons as button}
{@const TheIcon = button.icon} {@const TheIcon = button.icon}
<ToolbarButton title={button.label} color="dark" size="md" onclick={button.action}> <ToolbarButton
title={button.label}
color="dark"
size="md"
onclick={button.action}
>
<TheIcon size={24} /> <TheIcon size={24} />
</ToolbarButton> </ToolbarButton>
{/each} {/each}
{#if extensions} {#if extensions}
{@render extensions()} {@render extensions()}
{/if} {/if}
<ToolbarButton title="Toggle preview" color="dark" size="md" onclick={togglePreview}> <ToolbarButton
title="Toggle preview"
color="dark"
size="md"
onclick={togglePreview}
>
<Eye size={24} /> <Eye size={24} />
</ToolbarButton> </ToolbarButton>
</ToolbarGroup> </ToolbarGroup>
@ -135,17 +183,29 @@
</Button> </Button>
</div> </div>
{:else} {:else}
<div class="absolute rounded-lg inset-0 flex flex-col bg-white dark:bg-gray-900"> <div
class="absolute rounded-lg inset-0 flex flex-col bg-white dark:bg-gray-900"
>
<div class="py-2 px-3 border-gray-200 dark:border-gray-700 border-b"> <div class="py-2 px-3 border-gray-200 dark:border-gray-700 border-b">
<Toolbar embedded class="flex-row !m-0 !dark:bg-transparent !bg-transparent"> <Toolbar
embedded
class="flex-row !m-0 !dark:bg-transparent !bg-transparent"
>
<ToolbarGroup class="flex-row flex-wrap !m-0"> <ToolbarGroup class="flex-row flex-wrap !m-0">
<ToolbarButton title="Back to editor" color="dark" size="md" onclick={togglePreview}> <ToolbarButton
title="Back to editor"
color="dark"
size="md"
onclick={togglePreview}
>
<PencilLine size={24} /> <PencilLine size={24} />
</ToolbarButton> </ToolbarButton>
</ToolbarGroup> </ToolbarGroup>
</Toolbar> </Toolbar>
</div> </div>
<div class="flex-1 overflow-auto px-4 py-2 max-w-none bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-100 prose-content markup-content rounded-b-lg"> <div
class="flex-1 overflow-auto px-4 py-2 max-w-none bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-100 prose-content markup-content rounded-b-lg"
>
{#if preview} {#if preview}
{#if previewSnippet} {#if previewSnippet}
{@render previewSnippet(preview, previewArg)} {@render previewSnippet(preview, previewArg)}

18
src/lib/a/index.ts

@ -1,12 +1,12 @@
export { default as AThemeToggleMini } from './primitives/AThemeToggleMini.svelte'; export { default as AThemeToggleMini } from "./primitives/AThemeToggleMini.svelte";
export { default as AAlert } from './primitives/AAlert.svelte'; export { default as AAlert } from "./primitives/AAlert.svelte";
export { default as APagination } from './primitives/APagination.svelte'; export { default as APagination } from "./primitives/APagination.svelte";
export { default as ANavbar } from './nav/ANavbar.svelte'; export { default as ANavbar } from "./nav/ANavbar.svelte";
export { default as AFooter } from './nav/AFooter.svelte'; export { default as AFooter } from "./nav/AFooter.svelte";
export { default as ACommentForm } from './forms/ACommentForm.svelte'; export { default as ACommentForm } from "./forms/ACommentForm.svelte";
export { default as AMarkupForm } from './forms/AMarkupForm.svelte'; export { default as AMarkupForm } from "./forms/AMarkupForm.svelte";
export { default as ATextareaWithPreview } from './forms/ATextareaWithPreview.svelte'; export { default as ATextareaWithPreview } from "./forms/ATextareaWithPreview.svelte";
export { default as AProfilePreview } from './cards/AProfilePreview.svelte'; export { default as AProfilePreview } from "./cards/AProfilePreview.svelte";

17
src/lib/a/nav/AFooter.svelte

@ -1,10 +1,21 @@
<script> <script>
import { Footer, FooterCopyright, FooterLink, FooterLinkGroup } from "flowbite-svelte"; import {
Footer,
FooterCopyright,
FooterLink,
FooterLinkGroup,
} from "flowbite-svelte";
</script> </script>
<Footer class="m-2"> <Footer class="m-2">
<FooterCopyright href="/events?id=npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz" by="GitCitadel" year={2025} /> <FooterCopyright
<FooterLinkGroup class="mt-3 flex flex-wrap items-center text-sm text-gray-500 sm:mt-0 dark:text-gray-400"> href="/events?id=npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz"
by="GitCitadel"
year={2025}
/>
<FooterLinkGroup
class="mt-3 flex flex-wrap items-center text-sm text-gray-500 sm:mt-0 dark:text-gray-400"
>
<FooterLink href="/about">About</FooterLink> <FooterLink href="/about">About</FooterLink>
<FooterLink href="/contact">Contact</FooterLink> <FooterLink href="/contact">Contact</FooterLink>
</FooterLinkGroup> </FooterLinkGroup>

113
src/lib/a/nav/ANavbar.svelte

@ -6,52 +6,93 @@
NavUl, NavUl,
NavHamburger, NavHamburger,
NavBrand, NavBrand,
MegaMenu MegaMenu,
} from "flowbite-svelte"; } from "flowbite-svelte";
import Profile from "$components/util/Profile.svelte"; import Profile from "$components/util/Profile.svelte";
import { ChevronDownOutline } from "flowbite-svelte-icons"; import { ChevronDownOutline } from "flowbite-svelte-icons";
let menu2 = [ let menu2 = [
{ name: 'Publications', href: '/', help: 'Browse publications' }, { name: "Publications", href: "/", help: "Browse publications" },
{ name: 'Events', href: '/events', help: 'Search and engage with events' }, { name: "Events", href: "/events", help: "Search and engage with events" },
{ name: 'Visualize', href: '/visualize', help: 'Visualize connections between publications and authors' }, {
name: "Visualize",
href: "/visualize",
help: "Visualize connections between publications and authors",
},
{ name: 'Compose notes', href: '/new/compose', help: 'Create individual notes (30041 events)'}, {
{ name: 'Publish events', href: '/events/compose', help: 'Create any kind' }, name: "Compose notes",
href: "/new/compose",
help: "Create individual notes (30041 events)",
},
{
name: "Publish events",
href: "/events/compose",
help: "Create any kind",
},
{ name: 'Getting Started', href: '/start', help: 'A quick overview and tutorial' }, {
{ name: 'Relay Status', href: '/about/relay-stats', help: 'Relay status and monitoring' }, name: "Getting Started",
{ name: 'About', href: '/about', help: 'About the project' }, href: "/start",
{ name: 'Contact', href: '/contact', help: 'Contact us or submit a bug report' } help: "A quick overview and tutorial",
},
{
name: "Relay Status",
href: "/about/relay-stats",
help: "Relay status and monitoring",
},
{ name: "About", href: "/about", help: "About the project" },
{
name: "Contact",
href: "/contact",
help: "Contact us or submit a bug report",
},
]; ];
</script> </script>
<Navbar id="navi" class="fixed start-0 top-0 z-50 flex flex-row bg-primary-50 dark:bg-primary-1000" <Navbar
navContainerClass="flex-row items-center !p-0"> id="navi"
<NavBrand href="/"> class="fixed start-0 top-0 z-50 flex flex-row bg-primary-50 dark:bg-primary-1000"
<div class="flex flex-col"> navContainerClass="flex-row items-center !p-0"
<h1 class="text-2xl font-bold mb-0 dark:text-primary-100 hover:dark:text-highlight">Alexandria</h1> >
<p class="text-xs font-semibold tracking-wide max-sm:max-w-[11rem] mb-0 dark:text-primary-200 ">READ THE ORIGINAL. MAKE CONNECTIONS. CULTIVATE KNOWLEDGE.</p> <NavBrand href="/">
</div> <div class="flex flex-col">
</NavBrand> <h1
<div class="flex md:order-2"> class="text-2xl font-bold mb-0 dark:text-primary-100 hover:dark:text-highlight"
<Profile /> >
<NavHamburger /> Alexandria
</h1>
<p
class="text-xs font-semibold tracking-wide max-sm:max-w-[11rem] mb-0 dark:text-primary-200"
>
READ THE ORIGINAL. MAKE CONNECTIONS. CULTIVATE KNOWLEDGE.
</p>
</div> </div>
<NavUl class="order-1 ml-auto items-center" classes={{ ul: "items-center" }}> </NavBrand>
<NavLi class="cursor-pointer"> <div class="flex md:order-2">
Explore<ChevronDownOutline class="text-primary-800 ms-2 inline h-6 w-6 dark:text-white" /> <Profile />
</NavLi> <NavHamburger />
<MegaMenu full items={menu2}> </div>
{#snippet children({ item })} <NavUl class="order-1 ml-auto items-center" classes={{ ul: "items-center" }}>
<a href={item.href} class="block h-full rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700"> <NavLi class="cursor-pointer">
<div class="font-semibold dark:text-white">{item.name}</div> Explore<ChevronDownOutline
<span class="text-sm font-light text-gray-500 dark:text-gray-400">{item.help}</span> class="text-primary-800 ms-2 inline h-6 w-6 dark:text-white"
</a> />
{/snippet} </NavLi>
</MegaMenu> <MegaMenu full items={menu2}>
<DarkMode /> {#snippet children({ item })}
</NavUl> <a
href={item.href}
class="block h-full rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<div class="font-semibold dark:text-white">{item.name}</div>
<span class="text-sm font-light text-gray-500 dark:text-gray-400"
>{item.help}</span
>
</a>
{/snippet}
</MegaMenu>
<DarkMode />
</NavUl>
</Navbar> </Navbar>

6
src/lib/a/primitives/AAlert.svelte

@ -12,9 +12,9 @@
<Alert {color} {dismissable} class="alert-leather mb-4 {classes}"> <Alert {color} {dismissable} class="alert-leather mb-4 {classes}">
{#if title} {#if title}
<div class="flex"> <div class="flex">
<span class="text-lg font-medium">{@render title()}</span> <span class="text-lg font-medium">{@render title()}</span>
</div> </div>
{/if} {/if}
{@render children()} {@render children()}
</Alert> </Alert>

56
src/lib/a/primitives/ADetails.svelte

@ -1,16 +1,52 @@
<script lang="ts"> <script lang="ts">
import { showTech } from '$lib/stores/techStore'; import { showTech } from "$lib/stores/techStore";
let { summary = '', tech = false, defaultOpen = false, forceHide = false, class: className = '' } = $props(); let {
summary = "",
tech = false,
defaultOpen = false,
forceHide = false,
class: className = "",
} = $props();
let open = $derived(defaultOpen); let open = $derived(defaultOpen);
$effect(() => { if (tech && !$showTech) open = false; }); $effect(() => {
function onToggle(e: Event){ const el = e.currentTarget as HTMLDetailsElement; open = el.open; } if (tech && !$showTech) open = false;
});
function onToggle(e: Event) {
const el = e.currentTarget as HTMLDetailsElement;
open = el.open;
}
</script> </script>
<details open={open} ontoggle={onToggle} class={`group rounded-lg border border-muted/20 bg-surface ${className}`} data-kind={tech ? 'tech':'general'}>
<summary class="flex items-center gap-2 cursor-pointer list-none px-3 py-2 rounded-lg select-none hover:bg-primary/10"> <details
<svg class={`h-4 w-4 transition-transform ${open ? 'rotate-90':''}`} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg> {open}
ontoggle={onToggle}
class={`group rounded-lg border border-muted/20 bg-surface ${className}`}
data-kind={tech ? "tech" : "general"}
>
<summary
class="flex items-center gap-2 cursor-pointer list-none px-3 py-2 rounded-lg select-none hover:bg-primary/10"
>
<svg
class={`h-4 w-4 transition-transform ${open ? "rotate-90" : ""}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><path d="M9 18l6-6-6-6" /></svg
>
<span class="font-medium">{summary}</span> <span class="font-medium">{summary}</span>
{#if tech}<span class="ml-2 text-[10px] uppercase tracking-wide rounded px-1.5 py-0.5 border border-primary/30 text-primary bg-primary/5">Technical</span>{/if} {#if tech}<span
<span class="ml-auto text-xs opacity-60 group-open:opacity-50">{open ? 'Hide':'Show'}</span> class="ml-2 text-[10px] uppercase tracking-wide rounded px-1.5 py-0.5 border border-primary/30 text-primary bg-primary/5"
>Technical</span
>{/if}
<span class="ml-auto text-xs opacity-60 group-open:opacity-50"
>{open ? "Hide" : "Show"}</span
>
</summary> </summary>
{#if !(tech && !$showTech && forceHide)}<div class="px-3 pb-3 pt-1 text-[0.95rem] leading-6"><slot /></div>{/if} {#if !(tech && !$showTech && forceHide)}<div
class="px-3 pb-3 pt-1 text-[0.95rem] leading-6"
>
<slot />
</div>{/if}
</details> </details>

7
src/lib/a/primitives/AInput.svelte

@ -1,6 +1,9 @@
<script lang="ts"> let { value = '', class: className = '', placeholder = '' } = $props(); </script> <script lang="ts">
let { value = "", class: className = "", placeholder = "" } = $props();
</script>
<input <input
class={`w-full h-10 px-3 rounded-md border border-muted/30 bg-surface text-text placeholder:opacity-60 focus:outline-none focus:ring-2 focus:ring-primary/40 ${className}`} class={`w-full h-10 px-3 rounded-md border border-muted/30 bg-surface text-text placeholder:opacity-60 focus:outline-none focus:ring-2 focus:ring-primary/40 ${className}`}
bind:value bind:value
placeholder={placeholder} {placeholder}
/> />

4
src/lib/a/primitives/ANostrBadge.svelte

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { DisplayBadge } from '$lib/nostr/nip58'; import type { DisplayBadge } from "$lib/nostr/nip58";
export let badge: DisplayBadge; export let badge: DisplayBadge;
export let size: 'xs' | 's' | 'm' | 'l' = 's'; export let size: "xs" | "s" | "m" | "l" = "s";
const px = { xs: 16, s: 24, m: 32, l: 48 }[size]; const px = { xs: 16, s: 24, m: 32, l: 48 }[size];
</script> </script>

10
src/lib/a/primitives/ANostrBadgeRow.svelte

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { DisplayBadge } from '$lib/nostr/nip58'; import type { DisplayBadge } from "$lib/nostr/nip58";
import ANostrBadge from './ANostrBadge.svelte'; import ANostrBadge from "./ANostrBadge.svelte";
export let badges: DisplayBadge[] = []; export let badges: DisplayBadge[] = [];
export let size: 'xs' | 's' | 'm' | 'l' = 's'; export let size: "xs" | "s" | "m" | "l" = "s";
export let limit: number = 6; export let limit: number = 6;
const shown = () => badges.slice(0, limit); const shown = () => badges.slice(0, limit);
</script> </script>
@ -12,7 +12,9 @@
<ANostrBadge badge={b} {size} /> <ANostrBadge badge={b} {size} />
{/each} {/each}
{#if badges.length > limit} {#if badges.length > limit}
<span class="text-[10px] px-1.5 py-0.5 rounded-md border border-muted/30 bg-surface/70"> <span
class="text-[10px] px-1.5 py-0.5 rounded-md border border-muted/30 bg-surface/70"
>
+{badges.length - limit} +{badges.length - limit}
</span> </span>
{/if} {/if}

74
src/lib/a/primitives/ANostrUser.svelte

@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import type { NostrProfile } from '$lib/nostr/types'; import type { NostrProfile } from "$lib/nostr/types";
import type { DisplayBadge } from '$lib/nostr/nip58'; import type { DisplayBadge } from "$lib/nostr/nip58";
import ANostrBadgeRow from './ANostrBadgeRow.svelte'; import ANostrBadgeRow from "./ANostrBadgeRow.svelte";
import { shortenBech32, displayNameFrom } from '$lib/nostr/format'; import { shortenBech32, displayNameFrom } from "$lib/nostr/format";
import { verifyNip05 } from '$lib/nostr/nip05'; import { verifyNip05 } from "$lib/nostr/nip05";
import { onMount } from 'svelte'; import { onMount } from "svelte";
let { let {
npub, // required npub, // required
pubkey = undefined as string | undefined, pubkey = undefined as string | undefined,
profile = undefined as NostrProfile | undefined, profile = undefined as NostrProfile | undefined,
size = 'md' as 'sm' | 'md' | 'lg', size = "md" as "sm" | "md" | "lg",
showNpub = true, showNpub = true,
showBadges = true, showBadges = true,
verifyNip05: doVerify = true, verifyNip05: doVerify = true,
@ -18,14 +18,14 @@
nativeBadges = null as DisplayBadge[] | null, nativeBadges = null as DisplayBadge[] | null,
badgeLimit = 6, badgeLimit = 6,
href = undefined as string | undefined, href = undefined as string | undefined,
class: className = '' class: className = "",
} = $props(); } = $props();
// Derived view-model // Derived view-model
let displayName = displayNameFrom(npub, profile); let displayName = displayNameFrom(npub, profile);
let shortNpub = shortenBech32(npub, true); let shortNpub = shortenBech32(npub, true);
let avatarUrl = profile?.picture ?? ''; let avatarUrl = profile?.picture ?? "";
let nip05 = profile?.nip05 ?? ''; let nip05 = profile?.nip05 ?? "";
// NIP-05 verify // NIP-05 verify
let computedVerified = $state(false); let computedVerified = $state(false);
@ -44,14 +44,24 @@
// Sizing map // Sizing map
const sizes = { const sizes = {
sm: { avatar: 'h-6 w-6', gap: 'gap-2', name: 'text-sm', meta: 'text-[11px]' }, sm: {
md: { avatar: 'h-8 w-8', gap: 'gap-2.5', name: 'text-base',meta: 'text-xs' }, avatar: "h-6 w-6",
lg: { avatar: 'h-10 w-10',gap: 'gap-3', name: 'text-lg', meta: 'text-sm' } gap: "gap-2",
name: "text-sm",
meta: "text-[11px]",
},
md: {
avatar: "h-8 w-8",
gap: "gap-2.5",
name: "text-base",
meta: "text-xs",
},
lg: { avatar: "h-10 w-10", gap: "gap-3", name: "text-lg", meta: "text-sm" },
}[size]; }[size];
</script> </script>
{#if href} {#if href}
<a href={href} class={`inline-flex items-center ${sizes.gap} ${className}`}> <a {href} class={`inline-flex items-center ${sizes.gap} ${className}`}>
<Content /> <Content />
</a> </a>
{:else} {:else}
@ -62,7 +72,9 @@
<!-- component content as a fragment (no extra <script> blocks, no JSX) --> <!-- component content as a fragment (no extra <script> blocks, no JSX) -->
{#snippet Content()} {#snippet Content()}
<span class={`shrink-0 rounded-full overflow-hidden bg-muted/20 border border-muted/30 ${sizes.avatar}`}> <span
class={`shrink-0 rounded-full overflow-hidden bg-muted/20 border border-muted/30 ${sizes.avatar}`}
>
{#if avatarUrl} {#if avatarUrl}
<img src={avatarUrl} alt="" class="h-full w-full object-cover" /> <img src={avatarUrl} alt="" class="h-full w-full object-cover" />
{:else} {:else}
@ -76,18 +88,32 @@
<span class={`flex items-center gap-1 font-medium ${sizes.name}`}> <span class={`flex items-center gap-1 font-medium ${sizes.name}`}>
<span class="truncate">{displayName}</span> <span class="truncate">{displayName}</span>
{#if nip05 && (computedVerified || loadingVerify)} {#if nip05 && (computedVerified || loadingVerify)}
<span class="inline-flex items-center" <span
title={computedVerified ? `NIP-05 verified: ${nip05}` : 'Verifying…'}> class="inline-flex items-center"
title={computedVerified ? `NIP-05 verified: ${nip05}` : "Verifying…"}
>
{#if computedVerified} {#if computedVerified}
<!-- Verified check --> <!-- Verified check -->
<svg class="h-4 w-4 text-primary" viewBox="0 0 24 24" fill="none" <svg
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> class="h-4 w-4 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 6L9 17l-5-5" /> <path d="M20 6L9 17l-5-5" />
</svg> </svg>
{:else} {:else}
<!-- Loading ring --> <!-- Loading ring -->
<svg class="h-4 w-4 animate-pulse opacity-70" viewBox="0 0 24 24" fill="none" <svg
stroke="currentColor" stroke-width="2"> class="h-4 w-4 animate-pulse opacity-70"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
</svg> </svg>
{/if} {/if}
@ -97,7 +123,9 @@
<span class={`flex items-center gap-2 text-muted/80 ${sizes.meta}`}> <span class={`flex items-center gap-2 text-muted/80 ${sizes.meta}`}>
{#if nip05}<span class="truncate" title={nip05}>{nip05}</span>{/if} {#if nip05}<span class="truncate" title={nip05}>{nip05}</span>{/if}
{#if showNpub}<span class="truncate opacity-80" title={npub}>{shortNpub}</span>{/if} {#if showNpub}<span class="truncate opacity-80" title={npub}
>{shortNpub}</span
>{/if}
</span> </span>
{#if showBadges} {#if showBadges}

8
src/lib/a/primitives/APagination.svelte

@ -15,8 +15,8 @@
hasNextPage = false, hasNextPage = false,
hasPreviousPage = false, hasPreviousPage = false,
totalItems = 0, totalItems = 0,
itemsLabel = 'items', itemsLabel = "items",
className = '' className = "",
} = $props<{ } = $props<{
currentPage: number; currentPage: number;
totalPages: number; totalPages: number;
@ -36,7 +36,9 @@
</script> </script>
{#if totalPages > 1} {#if totalPages > 1}
<div class={`mt-4 flex flex-row items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg ${className}`}> <div
class={`mt-4 flex flex-row items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg ${className}`}
>
<div class="text-sm !mb-0 text-gray-600 dark:text-gray-400"> <div class="text-sm !mb-0 text-gray-600 dark:text-gray-400">
Page {currentPage} of {totalPages} ({totalItems} total {itemsLabel}) Page {currentPage} of {totalPages} ({totalItems} total {itemsLabel})
</div> </div>

15
src/lib/a/reader/ATechBlock.svelte

@ -1,16 +1,21 @@
<script lang="ts"> <script lang="ts">
import { showTech } from '$lib/stores/techStore.ts'; import { showTech } from "$lib/stores/techStore.ts";
let revealed = $state(false); let revealed = $state(false);
let { title = 'Technical details', className = '' , content} = $props(); let { title = "Technical details", className = "", content } = $props();
let hidden = $derived(!$showTech && !revealed); let hidden = $derived(!$showTech && !revealed);
</script> </script>
{#if hidden} {#if hidden}
<div class="rounded-md border border-dashed border-muted/40 bg-surface/60 px-3 py-2 my-6 flex items-center gap-3 {className}"> <div
class="rounded-md border border-dashed border-muted/40 bg-surface/60 px-3 py-2 my-6 flex items-center gap-3 {className}"
>
<span class="text-xs opacity-70">{title} hidden</span> <span class="text-xs opacity-70">{title} hidden</span>
<button class="ml-auto text-sm underline hover:no-underline" onclick={() => revealed = true}>Reveal this block</button> <button
class="ml-auto text-sm underline hover:no-underline"
onclick={() => (revealed = true)}>Reveal this block</button
>
</div> </div>
{:else} {:else}
{@render content()} {@render content()}
{/if} {/if}

4
src/lib/a/reader/ATechToggle.svelte

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { showTech } from '$lib/stores/techStore.ts'; import { showTech } from "$lib/stores/techStore.ts";
import { Toggle, P } from "flowbite-svelte"; import { Toggle, P } from "flowbite-svelte";
let label = 'Show technical details'; let label = "Show technical details";
</script> </script>
<div class="inline-flex items-center gap-2 select-none my-3"> <div class="inline-flex items-center gap-2 select-none my-3">

6
src/styles/a/primitives.css

@ -1,5 +1,5 @@
@layer components { @layer components {
.alert-leather { .alert-leather {
@apply border border-s-4; @apply border border-s-4;
} }
} }

8
src/styles/notifications.css

@ -107,11 +107,15 @@
} }
.message-container:hover { .message-container:hover {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); box-shadow:
0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
} }
.dark .message-container:hover { .dark .message-container:hover {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.2); box-shadow:
0 4px 6px -1px rgb(0 0 0 / 0.3),
0 2px 4px -2px rgb(0 0 0 / 0.2);
} }
/* Filter indicator styling */ /* Filter indicator styling */

3
src/styles/scrollbar.css

@ -1,8 +1,7 @@
@layer components { @layer components {
/* Global scrollbar styles */ /* Global scrollbar styles */
* { * {
scrollbar-color: rgba(87, 66, 41, 0.8) scrollbar-color: rgba(87, 66, 41, 0.8) transparent; /* Transparent track, default scrollbar thumb */
transparent; /* Transparent track, default scrollbar thumb */
} }
/* Webkit Browsers (Chrome, Safari, Edge) */ /* Webkit Browsers (Chrome, Safari, Edge) */

90
src/theme-tokens.css

@ -1,64 +1,64 @@
/* Default theme (your current palette) */ /* Default theme (your current palette) */
:root { :root {
--brand-primary-0: #efe6dc; --brand-primary-0: #efe6dc;
--brand-primary-50: #decdb9; --brand-primary-50: #decdb9;
--brand-primary-100: #d6c1a8; --brand-primary-100: #d6c1a8;
--brand-primary-200: #c6a885; --brand-primary-200: #c6a885;
--brand-primary-300: #b58f62; --brand-primary-300: #b58f62;
--brand-primary-400: #ad8351; --brand-primary-400: #ad8351;
--brand-primary-500: #c6a885; --brand-primary-500: #c6a885;
--brand-primary-600: #795c39; --brand-primary-600: #795c39;
--brand-primary-700: #564a3e; --brand-primary-700: #564a3e;
--brand-primary-800: #3c352c; --brand-primary-800: #3c352c;
--brand-primary-900: #2a241c; --brand-primary-900: #2a241c;
--brand-primary-950: #1d1812; --brand-primary-950: #1d1812;
--brand-primary-1000: #15110d; --brand-primary-1000: #15110d;
} }
/* Example alternative theme: ocean */ /* Example alternative theme: ocean */
:root[data-theme="ocean"] { :root[data-theme="ocean"] {
--brand-primary-0: #ecf8ff; --brand-primary-0: #ecf8ff;
--brand-primary-50: #e6f3ff; --brand-primary-50: #e6f3ff;
--brand-primary-100: #d9ecff; --brand-primary-100: #d9ecff;
--brand-primary-200: #b9ddff; --brand-primary-200: #b9ddff;
--brand-primary-300: #90cbff; --brand-primary-300: #90cbff;
--brand-primary-400: #61b6fb; --brand-primary-400: #61b6fb;
--brand-primary-500: #0ea5e9; /* sky-500-ish */ --brand-primary-500: #0ea5e9; /* sky-500-ish */
--brand-primary-600: #0284c7; --brand-primary-600: #0284c7;
--brand-primary-700: #0369a1; --brand-primary-700: #0369a1;
--brand-primary-800: #075985; --brand-primary-800: #075985;
--brand-primary-900: #0c4a6e; --brand-primary-900: #0c4a6e;
--brand-primary-950: #082f49; --brand-primary-950: #082f49;
--brand-primary-1000: #062233; --brand-primary-1000: #062233;
} }
/* (Optional) per-theme dark tweaks — applied when <html class="dark"> is set */ /* (Optional) per-theme dark tweaks — applied when <html class="dark"> is set */
:root.dark[data-theme="ocean"] { :root.dark[data-theme="ocean"] {
/* nudge the mid tones brighter for contrast */ /* nudge the mid tones brighter for contrast */
--brand-primary-400: #7ccdfc; --brand-primary-400: #7ccdfc;
--brand-primary-500: #38bdf8; --brand-primary-500: #38bdf8;
} }
/* Example alternative theme: forrest */ /* Example alternative theme: forrest */
:root[data-theme="forrest"] { :root[data-theme="forrest"] {
--brand-primary-0: #eaf7ea; --brand-primary-0: #eaf7ea;
--brand-primary-50: #d6eed6; --brand-primary-50: #d6eed6;
--brand-primary-100: #bfe3bf; --brand-primary-100: #bfe3bf;
--brand-primary-200: #9fd49f; --brand-primary-200: #9fd49f;
--brand-primary-300: #7fc57f; --brand-primary-300: #7fc57f;
--brand-primary-400: #5fa65f; --brand-primary-400: #5fa65f;
--brand-primary-500: #3f863f; /* forest green */ --brand-primary-500: #3f863f; /* forest green */
--brand-primary-600: #2e6b2e; --brand-primary-600: #2e6b2e;
--brand-primary-700: #205120; --brand-primary-700: #205120;
--brand-primary-800: #153a15; --brand-primary-800: #153a15;
--brand-primary-900: #0c230c; --brand-primary-900: #0c230c;
--brand-primary-950: #071507; --brand-primary-950: #071507;
--brand-primary-1000: #041004; --brand-primary-1000: #041004;
} }
/* (Optional) per-theme dark tweaks — applied when <html class="dark"> is set */ /* (Optional) per-theme dark tweaks — applied when <html class="dark"> is set */
:root.dark[data-theme="forrest"] { :root.dark[data-theme="forrest"] {
/* nudge the mid tones brighter for contrast */ /* nudge the mid tones brighter for contrast */
--brand-primary-400: #7fc97f; --brand-primary-400: #7fc97f;
--brand-primary-500: #4caf50; --brand-primary-500: #4caf50;
} }

Loading…
Cancel
Save