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. 7
      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. 19
      src/lib/a/nav/AFooter.svelte
  11. 113
      src/lib/a/nav/ANavbar.svelte
  12. 8
      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. 8
      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 @@ @@ -19,76 +19,76 @@
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* single color */
--color-highlight: #f9f6f1;
--color-border: var(--color-highlight);
--color-text-muted: var(--color-text-muted);
/* success */
--color-success-50: #e3f2e7;
--color-success-100: #c7e6cf;
--color-success-200: #a2d4ae;
--color-success-300: #7dbf8e;
--color-success-400: #5ea571;
--color-success-500: #4e8e5f;
--color-success-600: #3e744c;
--color-success-700: #305b3b;
--color-success-800: #22412a;
--color-success-900: #15281b;
/* info */
--color-info-50: #e7eff6;
--color-info-100: #c5d9ea;
--color-info-200: #9fbfdb;
--color-info-300: #7aa5cc;
--color-info-400: #5e90be;
--color-info-500: #4779a5;
--color-info-600: #365d80;
--color-info-700: #27445d;
--color-info-800: #192b3a;
--color-info-900: #0d161f;
/* warning */
--color-warning-50: #fef4e6;
--color-warning-100: #fde4bf;
--color-warning-200: #fcd18e;
--color-warning-300: #fbbc5c;
--color-warning-400: #f9aa33;
--color-warning-500: #f7971b;
--color-warning-600: #c97a14;
--color-warning-700: #9a5c0e;
--color-warning-800: #6c3e08;
--color-warning-900: #3e2404;
/* danger */
--color-danger-50: #fbeaea;
--color-danger-100: #f5cccc;
--color-danger-200: #eba5a5;
--color-danger-300: #e17e7e;
--color-danger-400: #d96060;
--color-danger-500: #c94848;
--color-danger-600: #a53939;
--color-danger-700: #7c2b2b;
--color-danger-800: #521c1c;
--color-danger-900: #2b0e0e;
/* single color */
--color-highlight: #f9f6f1;
--color-border: var(--color-highlight);
--color-text-muted: var(--color-text-muted);
/* success */
--color-success-50: #e3f2e7;
--color-success-100: #c7e6cf;
--color-success-200: #a2d4ae;
--color-success-300: #7dbf8e;
--color-success-400: #5ea571;
--color-success-500: #4e8e5f;
--color-success-600: #3e744c;
--color-success-700: #305b3b;
--color-success-800: #22412a;
--color-success-900: #15281b;
/* info */
--color-info-50: #e7eff6;
--color-info-100: #c5d9ea;
--color-info-200: #9fbfdb;
--color-info-300: #7aa5cc;
--color-info-400: #5e90be;
--color-info-500: #4779a5;
--color-info-600: #365d80;
--color-info-700: #27445d;
--color-info-800: #192b3a;
--color-info-900: #0d161f;
/* warning */
--color-warning-50: #fef4e6;
--color-warning-100: #fde4bf;
--color-warning-200: #fcd18e;
--color-warning-300: #fbbc5c;
--color-warning-400: #f9aa33;
--color-warning-500: #f7971b;
--color-warning-600: #c97a14;
--color-warning-700: #9a5c0e;
--color-warning-800: #6c3e08;
--color-warning-900: #3e2404;
/* danger */
--color-danger-50: #fbeaea;
--color-danger-100: #f5cccc;
--color-danger-200: #eba5a5;
--color-danger-300: #e17e7e;
--color-danger-400: #d96060;
--color-danger-500: #c94848;
--color-danger-600: #a53939;
--color-danger-700: #7c2b2b;
--color-danger-800: #521c1c;
--color-danger-900: #2b0e0e;
}
/* Map Tailwind utilities → theme tokens (PRIMARY ONLY) */
@theme inline {
--color-primary-0: var(--brand-primary-0);
--color-primary-50: var(--brand-primary-50);
--color-primary-100: var(--brand-primary-100);
--color-primary-200: var(--brand-primary-200);
--color-primary-300: var(--brand-primary-300);
--color-primary-400: var(--brand-primary-400);
--color-primary-500: var(--brand-primary-500);
--color-primary-600: var(--brand-primary-600);
--color-primary-700: var(--brand-primary-700);
--color-primary-800: var(--brand-primary-800);
--color-primary-900: var(--brand-primary-900);
--color-primary-950: var(--brand-primary-950);
--color-primary-1000: var(--brand-primary-1000);
--color-primary-0: var(--brand-primary-0);
--color-primary-50: var(--brand-primary-50);
--color-primary-100: var(--brand-primary-100);
--color-primary-200: var(--brand-primary-200);
--color-primary-300: var(--brand-primary-300);
--color-primary-400: var(--brand-primary-400);
--color-primary-500: var(--brand-primary-500);
--color-primary-600: var(--brand-primary-600);
--color-primary-700: var(--brand-primary-700);
--color-primary-800: var(--brand-primary-800);
--color-primary-900: var(--brand-primary-900);
--color-primary-950: var(--brand-primary-950);
--color-primary-1000: var(--brand-primary-1000);
}
@source "../node_modules/flowbite-svelte/dist";
@ -97,33 +97,47 @@ @@ -97,33 +97,47 @@
/* @utility and @layer rules… */
/* .content-visibility-auto */
@utility content-visibility-auto { content-visibility: auto; }
@utility content-visibility-auto {
content-visibility: auto;
}
/* .contain-size */
@utility contain-size { contain: size; }
@utility contain-size {
contain: size;
}
/* numbers -> px (e.g. contain-intrinsic-w-[320] => width: 320px) */
@utility contain-intrinsic-w-* {
--tw-ciw: --value(number);
width: calc(var(--tw-ciw) * 1px);
--tw-ciw: --value(number);
width: calc(var(--tw-ciw) * 1px);
}
@utility contain-intrinsic-h-* {
--tw-cih: --value(number);
height: calc(var(--tw-cih) * 1px);
--tw-cih: --value(number);
height: calc(var(--tw-cih) * 1px);
}
/* percentages (e.g. contain-intrinsic-wp-[65%] => width: 65%) */
@utility contain-intrinsic-wp-* { width: --value(percentage); }
@utility contain-intrinsic-hp-* { height: --value(percentage); }
@utility contain-intrinsic-wp-* {
width: --value(percentage);
}
@utility contain-intrinsic-hp-* {
height: --value(percentage);
}
/* 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. */
@utility list-upper-alpha { list-style-type: upper-alpha; }
@utility list-lower-alpha { list-style-type: lower-alpha; }
@utility list-upper-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) */
@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). */
@ -135,10 +149,11 @@ @@ -135,10 +149,11 @@
@layer base {
/* disable chrome 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;
}
@ -465,7 +480,6 @@ @@ -465,7 +480,6 @@
}
@layer components {
nav a {
text-decoration-line: none !important;
}
@ -506,7 +520,12 @@ @@ -506,7 +520,12 @@
/* 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;
}

16
src/app.html

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

7
src/lib/a/README.md

@ -1,11 +1,10 @@ @@ -1,11 +1,10 @@
# Component Library
This folder contains a component library.
The idea is to have project-scoped reusable components that centralize theming and style rules,
This folder contains a component library.
The idea is to have project-scoped reusable components that centralize theming and style rules,
so that main pages and layouts focus on the functionalities.
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.

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

@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
showContent = true,
actions,
onSelect,
onDeferralClick
onDeferralClick,
}: {
event: NDKEvent;
label?: string;
@ -29,7 +29,11 @@ @@ -29,7 +29,11 @@
showDeferralNaddr?: boolean;
showPublicationLink?: 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;
onDeferralClick?: (naddr: string, ev: NDKEvent) => void;
} = $props();
@ -98,12 +102,12 @@ @@ -98,12 +102,12 @@
const displayName: string | undefined =
profileData?.display_name || profileData?.name;
const avatarFallback: string =
(displayName || event.pubkey || "?").slice(0, 1).toUpperCase();
const createdDate: string =
event.created_at
? new Date(event.created_at * 1000).toLocaleDateString()
: "Unknown date";
const avatarFallback: string = (displayName || event.pubkey || "?")
.slice(0, 1)
.toUpperCase();
const createdDate: string = event.created_at
? new Date(event.created_at * 1000).toLocaleDateString()
: "Unknown date";
const computedActions =
actions && actions.length > 0
@ -112,8 +116,8 @@ @@ -112,8 +116,8 @@
{
label: "Open",
onClick: (ev: NDKEvent) => onSelect?.(ev),
variant: "light" as const
}
variant: "light" as const,
},
];
</script>
@ -131,31 +135,35 @@ @@ -131,31 +135,35 @@
<!-- Meta -->
<div class="flex flex-row w-full gap-3 items-center min-w-0">
{#if label}
<span class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
{label}
</span>
<span
class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400"
>
{label}
</span>
{/if}
{#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">
Kind {event.kind}
</span>
<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"
>
Kind {event.kind}
</span>
{/if}
{#if community}
<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"
title="Has posted to the community"
>
<svg class="w-3 h-3" 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>
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"
title="Has posted to the community"
>
<svg class="w-3 h-3" 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>
Community
</span>
{/if}
<span class="text-xs ml-auto mb-4">
{createdDate}
</span>
{createdDate}
</span>
</div>
<div class="flex flex-row">
@ -171,7 +179,9 @@ @@ -171,7 +179,9 @@
</div>
{:else}
{#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}
</div>
{/if}
@ -184,7 +194,7 @@ @@ -184,7 +194,7 @@
tabindex="0"
onclick={handleDeferralClick}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleDeferralClick(e as unknown as MouseEvent);
}
@ -196,7 +206,9 @@ @@ -196,7 +206,9 @@
{/if}
{#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)}
</div>
{/if}
@ -205,7 +217,9 @@ @@ -205,7 +217,9 @@
<!-- Footer / Actions -->
{#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} />
</div>
{/if}

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

@ -1,21 +1,36 @@ @@ -1,21 +1,36 @@
<script lang="ts">
import { Card, Heading, P, Button, Modal, Avatar, Dropdown, DropdownItem } from 'flowbite-svelte';
import { ChevronDownOutline } from 'flowbite-svelte-icons';
import AAlert from '$lib/a/primitives/AAlert.svelte';
import CopyToClipboard from '$lib/components/util/CopyToClipboard.svelte';
import { goto } from '$app/navigation';
import LazyImage from '$lib/components/util/LazyImage.svelte';
import { userBadge } from '$lib/snippets/UserSnippets.svelte';
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';
import {
Card,
Heading,
P,
Button,
Modal,
Avatar,
Dropdown,
DropdownItem,
} from "flowbite-svelte";
import { ChevronDownOutline } from "flowbite-svelte-icons";
import AAlert from "$lib/a/primitives/AAlert.svelte";
import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte";
import { goto } from "$app/navigation";
import LazyImage from "$lib/components/util/LazyImage.svelte";
import { userBadge } from "$lib/snippets/UserSnippets.svelte";
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 Profile = {
@ -52,39 +67,64 @@ @@ -52,39 +67,64 @@
function displayName() {
const p = props.profile;
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() {
const npub = props.user?.npub;
if (!npub) return '';
return npub.slice(0, 12) + '…' + npub.slice(-8);
if (!npub) return "";
return npub.slice(0, 12) + "…" + npub.slice(-8);
}
function hideOnError(e: Event) {
const img = e.currentTarget as HTMLImageElement | null;
if (img) {
img.style.display = 'none';
img.style.display = "none";
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 }[] = [];
if (event.kind === 0) {
const npub = toNpub(event.pubkey);
if (npub) ids.push({ label: 'npub', value: npub, link: `/events?id=${npub}` });
ids.push({ 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 });
if (npub)
ids.push({ label: "npub", value: npub, link: `/events?id=${npub}` });
ids.push({
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 {
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 {
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 {}
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;
}
@ -98,10 +138,10 @@ @@ -98,10 +138,10 @@
const p = props.profile;
if (p?.lud16) {
try {
const [name, domain] = p.lud16.split('@');
const [name, domain] = p.lud16.split("@");
const url = lnurlpWellKnownUrl(domain, name);
const words = bech32.toWords(new TextEncoder().encode(url));
lnurl = bech32.encode('lnurl', words);
lnurl = bech32.encode("lnurl", words);
} catch {
lnurl = null;
}
@ -120,46 +160,71 @@ @@ -120,46 +160,71 @@
}
// 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;
} else {
const cachedProfileData = (ev as any).profileData;
if (cachedProfileData && typeof cachedProfileData.isInUserLists === 'boolean') {
if (
cachedProfileData &&
typeof cachedProfileData.isInUserLists === "boolean"
) {
isInUserLists = cachedProfileData.isInUserLists;
} else {
fetchCurrentUserLists().then((lists) => {
isInUserLists = isPubkeyInUserLists(ev.pubkey, lists);
}).catch(() => {
isInUserLists = false;
});
fetchCurrentUserLists()
.then((lists) => {
isInUserLists = isPubkeyInUserLists(ev.pubkey, lists);
})
.catch(() => {
isInUserLists = false;
});
}
}
// 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];
} else {
checkCommunity(ev.pubkey).then((status) => {
communityStatus = status;
}).catch(() => {
communityStatus = false;
});
checkCommunity(ev.pubkey)
.then((status) => {
communityStatus = status;
})
.catch(() => {
communityStatus = false;
});
}
});
</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}
<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>
{: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}
<div class={`p-6 flex flex-col gap-4 relative`}>
<Avatar size="xl" src={props.profile?.picture ?? null} alt="Avatar" class="absolute w-fit top-[-56px]" />
<Avatar
size="xl"
src={props.profile?.picture ?? null}
alt="Avatar"
class="absolute w-fit top-[-56px]"
/>
<div class="flex flex-col gap-3 mt-14">
<Heading tag="h1" class="h-leather mb-2">{displayName()}</Heading>
@ -170,16 +235,39 @@ @@ -170,16 +235,39 @@
{#if props.event}
<div class="flex items-center gap-2 min-w-0">
{#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 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">
<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
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>
{/if}
{#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.)">
<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
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>
{/if}
</div>
@ -187,31 +275,57 @@ @@ -187,31 +275,57 @@
</div>
{#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)}
</div>
{/if}
<div class="flex flex-wrap gap-4 text-sm">
{#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}
</div>
<div class="flex flex-row flex-wrap justify-end gap-4 text-sm">
{#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}
<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>
{#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}
</Dropdown>
{#if props.isOwn}
<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>
<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}
</div>
@ -225,13 +339,23 @@ @@ -225,13 +339,23 @@
</Card>
{#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}
<div>
<div class="flex flex-col items-center">
{@render userBadge(
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,
)}
<P class="break-all">{props.profile.lud16}</P>
@ -240,7 +364,8 @@ @@ -240,7 +364,8 @@
<P>Scan the QR code or copy the address</P>
{#if lnurl}
<P class="break-all overflow-wrap-anywhere">
<CopyToClipboard icon={false} displayText={lnurl}></CopyToClipboard>
<CopyToClipboard icon={false} displayText={lnurl}
></CopyToClipboard>
</P>
<QrCode value={lnurl} />
{:else}

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

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

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

@ -82,7 +82,9 @@ @@ -82,7 +82,9 @@
</div>
<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}>
{#if isSubmitting}
Submitting...

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

@ -1,6 +1,25 @@ @@ -1,6 +1,25 @@
<script lang="ts">
import { Textarea, Toolbar, ToolbarGroup, ToolbarButton, Label, Button } from "flowbite-svelte";
import { Bold, Italic, Strikethrough, Quote, Link2, Image, Hash, List, ListOrdered, Eye, PencilLine } from "@lucide/svelte";
import {
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
let {
@ -36,29 +55,45 @@ @@ -36,29 +55,45 @@
const markupButtons = [
{ label: "Bold", icon: Bold, 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: "Image", icon: Image, action: () => insertMarkup("![", "](url)") },
{ label: "Quote", icon: Quote, 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("#", "") },
];
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;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
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
setTimeout(() => {
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);
}
@ -89,12 +124,12 @@ @@ -89,12 +124,12 @@
<div bind:this={wrapper} class="rounded-lg">
<div class="min-h-[180px] relative">
{#if activeTab === 'write'}
{#if activeTab === "write"}
<div class="inset-0">
<Textarea
id={id}
{id}
rows={isExpanded ? 30 : rows}
bind:value={value}
bind:value
classes={{
wrapper: "!m-0 p-0 h-full",
inner: "!m-0 !bg-transparent !dark:bg-transparent",
@ -103,21 +138,34 @@ @@ -103,21 +138,34 @@
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",
}}
placeholder={placeholder}
{placeholder}
>
{#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">
{#each markupButtons as button}
{@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} />
</ToolbarButton>
{/each}
{#if extensions}
{@render extensions()}
{/if}
<ToolbarButton title="Toggle preview" color="dark" size="md" onclick={togglePreview}>
<ToolbarButton
title="Toggle preview"
color="dark"
size="md"
onclick={togglePreview}
>
<Eye size={24} />
</ToolbarButton>
</ToolbarGroup>
@ -135,17 +183,29 @@ @@ -135,17 +183,29 @@
</Button>
</div>
{: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">
<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">
<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} />
</ToolbarButton>
</ToolbarGroup>
</Toolbar>
</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 previewSnippet}
{@render previewSnippet(preview, previewArg)}

18
src/lib/a/index.ts

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
export { default as AThemeToggleMini } from './primitives/AThemeToggleMini.svelte';
export { default as AAlert } from './primitives/AAlert.svelte';
export { default as APagination } from './primitives/APagination.svelte';
export { default as AThemeToggleMini } from "./primitives/AThemeToggleMini.svelte";
export { default as AAlert } from "./primitives/AAlert.svelte";
export { default as APagination } from "./primitives/APagination.svelte";
export { default as ANavbar } from './nav/ANavbar.svelte';
export { default as AFooter } from './nav/AFooter.svelte';
export { default as ANavbar } from "./nav/ANavbar.svelte";
export { default as AFooter } from "./nav/AFooter.svelte";
export { default as ACommentForm } from './forms/ACommentForm.svelte';
export { default as AMarkupForm } from './forms/AMarkupForm.svelte';
export { default as ATextareaWithPreview } from './forms/ATextareaWithPreview.svelte';
export { default as ACommentForm } from "./forms/ACommentForm.svelte";
export { default as AMarkupForm } from "./forms/AMarkupForm.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";

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

@ -1,11 +1,22 @@ @@ -1,11 +1,22 @@
<script>
import { Footer, FooterCopyright, FooterLink, FooterLinkGroup } from "flowbite-svelte";
import {
Footer,
FooterCopyright,
FooterLink,
FooterLinkGroup,
} from "flowbite-svelte";
</script>
<Footer class="m-2">
<FooterCopyright 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">
<FooterCopyright
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="/contact">Contact</FooterLink>
</FooterLinkGroup>
</Footer>
</Footer>

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

@ -6,52 +6,93 @@ @@ -6,52 +6,93 @@
NavUl,
NavHamburger,
NavBrand,
MegaMenu
MegaMenu,
} from "flowbite-svelte";
import Profile from "$components/util/Profile.svelte";
import { ChevronDownOutline } from "flowbite-svelte-icons";
let menu2 = [
{ name: 'Publications', href: '/', help: 'Browse publications' },
{ name: 'Events', href: '/events', help: 'Search and engage with events' },
{ name: 'Visualize', href: '/visualize', help: 'Visualize connections between publications and authors' },
{ name: "Publications", href: "/", help: "Browse publications" },
{ name: "Events", href: "/events", help: "Search and engage with events" },
{
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: 'About', href: '/about', help: 'About the project' },
{ name: 'Contact', href: '/contact', help: 'Contact us or submit a bug report' }
{
name: "Getting Started",
href: "/start",
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>
<Navbar id="navi" class="fixed start-0 top-0 z-50 flex flex-row bg-primary-50 dark:bg-primary-1000"
navContainerClass="flex-row items-center !p-0">
<NavBrand href="/">
<div class="flex flex-col">
<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>
</div>
</NavBrand>
<div class="flex md:order-2">
<Profile />
<NavHamburger />
<Navbar
id="navi"
class="fixed start-0 top-0 z-50 flex flex-row bg-primary-50 dark:bg-primary-1000"
navContainerClass="flex-row items-center !p-0"
>
<NavBrand href="/">
<div class="flex flex-col">
<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>
</div>
<NavUl class="order-1 ml-auto items-center" classes={{ ul: "items-center" }}>
<NavLi class="cursor-pointer">
Explore<ChevronDownOutline class="text-primary-800 ms-2 inline h-6 w-6 dark:text-white" />
</NavLi>
<MegaMenu full items={menu2}>
{#snippet children({ item })}
<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>
</NavBrand>
<div class="flex md:order-2">
<Profile />
<NavHamburger />
</div>
<NavUl class="order-1 ml-auto items-center" classes={{ ul: "items-center" }}>
<NavLi class="cursor-pointer">
Explore<ChevronDownOutline
class="text-primary-800 ms-2 inline h-6 w-6 dark:text-white"
/>
</NavLi>
<MegaMenu full items={menu2}>
{#snippet children({ item })}
<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>

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

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

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

@ -1,16 +1,52 @@ @@ -1,16 +1,52 @@
<script lang="ts">
import { showTech } from '$lib/stores/techStore';
let { summary = '', tech = false, defaultOpen = false, forceHide = false, class: className = '' } = $props();
import { showTech } from "$lib/stores/techStore";
let {
summary = "",
tech = false,
defaultOpen = false,
forceHide = false,
class: className = "",
} = $props();
let open = $derived(defaultOpen);
$effect(() => { if (tech && !$showTech) open = false; });
function onToggle(e: Event){ const el = e.currentTarget as HTMLDetailsElement; open = el.open; }
$effect(() => {
if (tech && !$showTech) open = false;
});
function onToggle(e: Event) {
const el = e.currentTarget as HTMLDetailsElement;
open = el.open;
}
</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">
<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>
<details
{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>
{#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}
<span class="ml-auto text-xs opacity-60 group-open:opacity-50">{open ? 'Hide':'Show'}</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}
<span class="ml-auto text-xs opacity-60 group-open:opacity-50"
>{open ? "Hide" : "Show"}</span
>
</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>

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

@ -1,6 +1,9 @@ @@ -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
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
placeholder={placeholder}
{placeholder}
/>

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

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
<script lang="ts">
import type { DisplayBadge } from '$lib/nostr/nip58';
import type { DisplayBadge } from "$lib/nostr/nip58";
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];
</script>

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

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
<script lang="ts">
import type { DisplayBadge } from '$lib/nostr/nip58';
import ANostrBadge from './ANostrBadge.svelte';
import type { DisplayBadge } from "$lib/nostr/nip58";
import ANostrBadge from "./ANostrBadge.svelte";
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;
const shown = () => badges.slice(0, limit);
</script>
@ -12,7 +12,9 @@ @@ -12,7 +12,9 @@
<ANostrBadge badge={b} {size} />
{/each}
{#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}
</span>
{/if}

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

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

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

@ -15,8 +15,8 @@ @@ -15,8 +15,8 @@
hasNextPage = false,
hasPreviousPage = false,
totalItems = 0,
itemsLabel = 'items',
className = ''
itemsLabel = "items",
className = "",
} = $props<{
currentPage: number;
totalPages: number;
@ -36,7 +36,9 @@ @@ -36,7 +36,9 @@
</script>
{#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">
Page {currentPage} of {totalPages} ({totalItems} total {itemsLabel})
</div>

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

@ -1,16 +1,21 @@ @@ -1,16 +1,21 @@
<script lang="ts">
import { showTech } from '$lib/stores/techStore.ts';
import { showTech } from "$lib/stores/techStore.ts";
let revealed = $state(false);
let { title = 'Technical details', className = '' , content} = $props();
let { title = "Technical details", className = "", content } = $props();
let hidden = $derived(!$showTech && !revealed);
</script>
{#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>
<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>
{:else}
{@render content()}
{@render content()}
{/if}

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

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

8
src/styles/a/primitives.css

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

8
src/styles/notifications.css

@ -107,11 +107,15 @@ @@ -107,11 +107,15 @@
}
.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 {
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 */

3
src/styles/scrollbar.css

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

90
src/theme-tokens.css

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

Loading…
Cancel
Save