diff --git a/src/app.css b/src/app.css index dc4ccc5..4988e6a 100644 --- a/src/app.css +++ b/src/app.css @@ -1,4 +1,5 @@ @import './styles/base.css'; +@import './styles/scrollbar.css'; @import './styles/publications.css'; @import './styles/visualize.css'; @@ -51,9 +52,18 @@ } main { - @apply max-w-full; + @apply max-w-full flex; } + main.publication { + @apply mt-[70px]; + } + + /* To scroll columns independently */ + main.publication.blog { + @apply w-full sm:w-auto min-h-full; + } + main.main-leather, article.article-leather { @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300; @@ -65,9 +75,9 @@ @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300 p-2 rounded; } - div.note-leather:hover:not(:has(.note-leather:hover)), - p.note-leather:hover:not(:has(.note-leather:hover)), - section.note-leather:hover:not(:has(.note-leather:hover)) { + .edit div.note-leather:hover:not(:has(.note-leather:hover)), + .edit p.note-leather:hover:not(:has(.note-leather:hover)), + section.edit.note-leather:hover:not(:has(.note-leather:hover)) { @apply hover:bg-primary-100 dark:hover:bg-primary-800; } @@ -121,6 +131,11 @@ @apply bg-primary-0 hover:bg-primary-0 dark:bg-primary-950 dark:hover:bg-primary-950 text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500; } + /* Navbar */ + nav.Navbar.navbar-main { + @apply z-30; + } + nav.navbar-leather { @apply bg-primary-0 dark:bg-primary-1000 z-10; } @@ -138,12 +153,18 @@ @apply text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500; } - aside.sidebar-leather>div { - @apply bg-primary-0 dark:bg-primary-1000; + /* Sidebar */ + aside.sidebar-leather { + @apply fixed md:sticky top-[130px] sm:top-[146px] h-[calc(100vh-130px)] sm:h-[calc(100vh-146px)] z-10; + @apply bg-primary-0 dark:bg-primary-1000 px-5 w-full sm:w-auto sm:max-w-xl; + } + + aside.sidebar-leather > div { + @apply bg-primary-50 dark:bg-gray-800 h-full px-5 py-0; } a.sidebar-item-leather { - @apply hover:bg-primary-100 dark:hover:bg-primary-800; + @apply hover:bg-primary-100 dark:hover:bg-gray-800; } div.skeleton-leather div { @@ -229,6 +250,21 @@ .link { @apply underline cursor-pointer hover:text-primary-400 dark:hover:text-primary-500; } + + /* Card with transition */ + .ArticleBox.grid .ArticleBoxImage { + @apply max-h-0; + transition: max-height 0.5s ease; + } + + .ArticleBox.grid.active .ArticleBoxImage { + @apply max-h-72; + } + + .tags span { + @apply bg-primary-50 text-primary-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-primary-900 dark:text-primary-200; + } + } @layer components { @@ -381,6 +417,10 @@ padding-left: 1rem; } +.line-ellipsis { + overflow: hidden; + text-overflow: ellipsis; +} .footnotes li { margin-bottom: 0.5rem; } diff --git a/src/lib/components/Login.svelte b/src/lib/components/Login.svelte index 13d9c93..afa83a5 100644 --- a/src/lib/components/Login.svelte +++ b/src/lib/components/Login.svelte @@ -50,7 +50,7 @@ {#if $ndkSignedIn} {:else} - + - +

Alexandria

diff --git a/src/lib/components/Preview.svelte b/src/lib/components/Preview.svelte index 0c85484..d25ac71 100644 --- a/src/lib/components/Preview.svelte +++ b/src/lib/components/Preview.svelte @@ -4,6 +4,7 @@ import { CaretDownSolid, CaretUpSolid, EditOutline } from 'flowbite-svelte-icons'; import Self from './Preview.svelte'; import { contentParagraph, sectionHeading } from '$lib/snippets/PublicationSnippets.svelte'; + import BlogHeader from "./blog/BlogHeader.svelte"; // TODO: Fix move between parents. @@ -16,8 +17,10 @@ oncursorrelease, parentId, rootId, + index, sectionClass, publicationType, + onBlogUpdate } = $props<{ allowEditing?: boolean; depth?: number; @@ -27,14 +30,19 @@ oncursorrelease?: (e: MouseEvent) => void; parentId?: string | null | undefined; rootId: string; + index: number; sectionClass?: string; publicationType?: string; + onBlogUpdate?: any; }>(); let currentContent: string = $state($pharosInstance.getContent(rootId)); let title: string | undefined = $state($pharosInstance.getIndexTitle(rootId)); let orderedChildren: string[] = $state($pharosInstance.getOrderedChildIds(rootId)); + let blogEntries = $state(Array.from($pharosInstance.getBlogEntries())); + let metadata = $state($pharosInstance.getIndexMetadata()); + let isEditing: boolean = $state(false); let hasCursor: boolean = $state(false); let childHasCursor: boolean = $state(false); @@ -86,6 +94,48 @@ } }); + function getBlogEvent(index: number) { + return blogEntries[index][1]; + } + + function byline(rootId: string, index: number) { + console.log(rootId, index, blogEntries); + const event = blogEntries[index][1]; + const author = event ? event.getMatchingTags("author")[0][1] : ''; + return author ?? ""; + } + + function hasCoverImage(rootId: string, index: number) { + console.log(rootId); + const event = blogEntries[index][1]; + const image = event && event.getMatchingTags("image")[0] ? event.getMatchingTags("image")[0][1] : ''; + return image ?? ''; + } + + function publishedAt(rootId: string, index: number) { + console.log(rootId, index); + console.log(blogEntries[index]); + const event = blogEntries[index][1]; + const date = event.created_at ? new Date(event.created_at * 1000) : ''; + if (date !== '') { + const formattedDate = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "2-digit", + }).format(date); + return formattedDate ?? ""; + } + return ''; + } + + function readBlog(rootId:string) { + onBlogUpdate?.(rootId); + } + + function propagateBlogUpdate(rootId:string) { + onBlogUpdate?.(rootId); + } + function handleMouseEnter(e: MouseEvent) { hasCursor = true; if (oncursorcapture) { @@ -153,17 +203,38 @@ {#snippet sectionHeading(title: string, depth: number)} {@const headingLevel = Math.min(depth + 1, 6)} {@const className = $pharosInstance.isFloatingTitle(rootId) ? 'discrete' : 'h-leather'} - + {title} {/snippet} +{#snippet coverImage(rootId: string, index: number, depth: number)} + {#if hasCoverImage(rootId, index)} +
+ {title} +
+ {/if} +{/snippet} + +{#snippet blogMetadata(rootId: string, index: number)} +

+ by {byline(rootId, index)} +

+

+ {publishedAt(rootId, index)} +

+{/snippet} + {#snippet contentParagraph(content: string, publicationType: string)} {#if publicationType === 'novel'}

{@html content}

+ {:else if publicationType === 'blog'} +

+ {@html content} +

{:else}

{@html content} @@ -222,25 +293,33 @@ {:else} - {@render sectionHeading(title!, depth)} + {#if !(publicationType === 'blog' && depth === 1)} + {@render sectionHeading(title!, depth)} + {/if} {/if} - {#key subtreeUpdateCount} - {#each orderedChildren as id, index} - - {/each} - {/key} + {#if publicationType === 'blog' && depth === 1} + + {:else } + {#key subtreeUpdateCount} + {#each orderedChildren as id, index} + + {/each} + {/key} + {/if}

{/if} {#if allowEditing && depth > 0} diff --git a/src/lib/components/Publication.svelte b/src/lib/components/Publication.svelte index b6f254d..5df2fe4 100644 --- a/src/lib/components/Publication.svelte +++ b/src/lib/components/Publication.svelte @@ -2,28 +2,34 @@ import { Alert, Button, + Card, Sidebar, SidebarGroup, - SidebarItem, SidebarWrapper, - Skeleton, - TextPlaceholder, - Tooltip, + Heading, } from "flowbite-svelte"; - import { getContext, onMount } from "svelte"; - import { BookOutline, ExclamationCircleOutline } from "flowbite-svelte-icons"; - import { page } from "$app/state"; + import { getContext, onDestroy, onMount } from "svelte"; + import { + CloseOutline, + ExclamationCircleOutline, + } from "flowbite-svelte-icons"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import PublicationSection from "./PublicationSection.svelte"; import type { PublicationTree } from "$lib/data_structures/publication_tree"; - - let { rootAddress, publicationType, indexEvent } = $props<{ - rootAddress: string, - publicationType: string, - indexEvent: NDKEvent + import Details from "$components/util/Details.svelte"; + import { publicationColumnVisibility } from "$lib/stores"; + import BlogHeader from "$components/blog/BlogHeader.svelte"; + import Interactions from "$components/util/Interactions.svelte"; + import TocToggle from "$components/util/TocToggle.svelte"; + import { pharosInstance } from '$lib/parser'; + + let { rootAddress, publicationType, indexEvent } = $props<{ + rootAddress: string; + publicationType: string; + indexEvent: NDKEvent; }>(); - const publicationTree = getContext('publicationTree') as PublicationTree; + const publicationTree = getContext("publicationTree") as PublicationTree; // #region Loading @@ -76,158 +82,221 @@ // #endregion - // #region ToC - - const tocBreakpoint = 1140; + // region Columns visibility + let currentBlog: null | string = $state(null); + let currentBlogEvent: null | NDKEvent = $state(null); + const isLeaf = $derived(indexEvent.kind === 30041); - let activeHash = $state(page.url.hash); - let showToc: boolean = $state(true); - let showTocButton: boolean = $state(false); + function isInnerActive() { + return currentBlog !== null && $publicationColumnVisibility.inner; + } - function normalizeHashPath(str: string): string { - return str - .toLowerCase() - .replace(/\s+/g, "-") - .replace(/[^\w-]/g, ""); + function closeDiscussion() { + publicationColumnVisibility.update((v) => ({ ...v, discussion: false })); } - function scrollToElementWithOffset() { - const hash = window.location.hash; - if (hash) { - const targetElement = document.querySelector(hash); - if (targetElement) { - const headerOffset = 80; - const elementPosition = targetElement.getBoundingClientRect().top; - const offsetPosition = elementPosition + window.scrollY - headerOffset; - - window.scrollTo({ - top: offsetPosition, - behavior: "auto", - }); + function loadBlog(rootId: string) { + // depending on the size of the screen, also toggle blog list & discussion visibility + publicationColumnVisibility.update((current) => { + const updated = current; + if (window.innerWidth < 1024) { + updated.blog = false; + updated.discussion = false; } + updated.inner = true; + return updated; + }); + + currentBlog = rootId; + // set current blog values for publication render + if (leaves.length > 0) { + currentBlogEvent = + leaves.find((i) => i && i.tagAddress() === currentBlog) ?? null; } } - /** - * Hides the table of contents sidebar when the window shrinks below a certain size. This - * prevents the sidebar from occluding the article content. - */ - function setTocVisibilityOnResize() { - showToc = window.innerWidth >= tocBreakpoint; - showTocButton = window.innerWidth < tocBreakpoint; + function showBlogHeader() { + return currentBlog && currentBlogEvent && window.innerWidth < 1140; } - /** - * Hides the table of contents sidebar when the user clicks outside of it. - */ - function hideTocOnClick(ev: MouseEvent) { - const target = ev.target as HTMLElement; - - if (target.closest(".sidebar-leather") || target.closest(".btn-leather")) { - return; - } - - if (showToc) { - showToc = false; - } - } - - // #endregion + onDestroy(() => { + // reset visibility + publicationColumnVisibility.reset(); + }); onMount(() => { - // Always check whether the TOC sidebar should be visible. - setTocVisibilityOnResize(); - window.addEventListener("hashchange", scrollToElementWithOffset); - // Also handle the case where the user lands on the page with a hash in the URL - scrollToElementWithOffset(); - window.addEventListener("resize", setTocVisibilityOnResize); - window.addEventListener("click", hideTocOnClick); + // Set current columns depending on the publication type + const isBlog = publicationType === "blog"; + publicationColumnVisibility.update((v) => ({ + ...v, + main: !isBlog, + blog: isBlog, + })); + if (isLeaf || isBlog) { + publicationColumnVisibility.update((v) => ({ ...v, toc: false })); + } // Set up the intersection observer. - observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting && !isLoading && !isDone) { - loadMore(1); - } - }); - }, { threshold: 0.5 }); + observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting && !isLoading && !isDone) { + loadMore(1); + } + }); + }, + { threshold: 0.5 }, + ); loadMore(8); return () => { - window.removeEventListener("hashchange", scrollToElementWithOffset); - window.removeEventListener("resize", setTocVisibilityOnResize); - window.removeEventListener("click", hideTocOnClick); - observer.disconnect(); }; }); + + // Whenever the publication changes, update rootId + let rootId = $derived($pharosInstance.getRootIndexId()); - - - -{#if showTocButton && !showToc} - +{#if publicationType !== "blog" || !isLeaf} + +{/if} + + +{#if $publicationColumnVisibility.main} +
+
+
+
+ + {#each leaves as leaf, i} + {#if leaf == null} + + + Error loading content. One or more events could not be loaded. + + {:else} + setLastElementRef(el, i)} + /> + {/if} + {/each} +
+ {#if isLoading} + + {:else if !isDone} + + {:else} +

+ You've reached the end of the publication. +

+ {/if} +
+
+{/if} + + +{#if $publicationColumnVisibility.blog} +
- - - Show Table of Contents --> +
+
+
+ + {#each leaves as leaf, i} + {#if leaf} + + {/if} + {/each} +
{/if} - - + {#if showBlogHeader() && currentBlog && currentBlogEvent} + + {/if} +
+ +
+ Unknown + 1.1.1970 +
+
+ This is a very intelligent comment placeholder that applies to + all the content equally well. +
+
+
+ -{/if} --> -
- {#each leaves as leaf, i} - {#if leaf == null} - - - Error loading content. One or more events could not be loaded. - - {:else} - setLastElementRef(el, i)} - /> - {/if} - {/each} -
- {#if isLoading} - - {:else if !isDone} - - {:else} -

You've reached the end of the publication.

- {/if} -
-
- - +{/if} diff --git a/src/lib/components/PublicationSection.svelte b/src/lib/components/PublicationSection.svelte index e9829c9..de23463 100644 --- a/src/lib/components/PublicationSection.svelte +++ b/src/lib/components/PublicationSection.svelte @@ -104,7 +104,7 @@ }); -
+
{#await Promise.all([leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches])} {:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches]} diff --git a/src/lib/components/blog/BlogHeader.svelte b/src/lib/components/blog/BlogHeader.svelte new file mode 100644 index 0000000..f7fc25b --- /dev/null +++ b/src/lib/components/blog/BlogHeader.svelte @@ -0,0 +1,70 @@ + + +{#if title != null} + +
+
+
+ + {publishedAt()} +
+ +
+ {#if image && active} +
+ +
+ {/if} +
+ + {#if hashtags} +
+ {#each hashtags as tag} + {tag} + {/each} +
+ {/if} +
+ {#if active} + + {/if} +
+
+{/if} diff --git a/src/lib/components/util/ArticleNav.svelte b/src/lib/components/util/ArticleNav.svelte new file mode 100644 index 0000000..8665ece --- /dev/null +++ b/src/lib/components/util/ArticleNav.svelte @@ -0,0 +1,149 @@ + + + \ No newline at end of file diff --git a/src/lib/components/util/CardActions.svelte b/src/lib/components/util/CardActions.svelte index a1e3a3a..eee1415 100644 --- a/src/lib/components/util/CardActions.svelte +++ b/src/lib/components/util/CardActions.svelte @@ -2,7 +2,6 @@ import { ClipboardCheckOutline, ClipboardCleanOutline, - CodeOutline, DotsVerticalOutline, EyeOutline, ShareNodesOutline @@ -11,25 +10,26 @@ import { standardRelays } from "$lib/consts"; import { neventEncode, naddrEncode } from "$lib/utils"; import InlineProfile from "$components/util/InlineProfile.svelte"; - import { goto } from "$app/navigation"; let { event } = $props(); + // Derive metadata from event + let title = $derived(event.tags.find((t: string[]) => t[0] === 'title')?.[1] ?? ''); + let summary = $derived(event.tags.find((t: string[]) => t[0] === 'summary')?.[1] ?? ''); + let image = $derived(event.tags.find((t: string[]) => t[0] === 'image')?.[1] ?? null); + let author = $derived(event.tags.find((t: string[]) => t[0] === 'author')?.[1] ?? ''); + let originalAuthor = $derived(event.tags.find((t: string[]) => t[0] === 'original_author')?.[1] ?? null); + let version = $derived(event.tags.find((t: string[]) => t[0] === 'version')?.[1] ?? ''); + let source = $derived(event.tags.find((t: string[]) => t[0] === 'source')?.[1] ?? null); + let type = $derived(event.tags.find((t: string[]) => t[0] === 'type')?.[1] ?? null); + let language = $derived(event.tags.find((t: string[]) => t[0] === 'language')?.[1] ?? null); + let publisher = $derived(event.tags.find((t: string[]) => t[0] === 'publisher')?.[1] ?? null); + let identifier = $derived(event.tags.find((t: string[]) => t[0] === 'identifier')?.[1] ?? null); + let jsonModalOpen: boolean = $state(false); let detailsModalOpen: boolean = $state(false); let eventIdCopied: boolean = $state(false); let shareLinkCopied: boolean = $state(false); - let title: string = $derived(event.getMatchingTags('title')[0]?.[1]); - let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown'); - let version: string = $derived(event.getMatchingTags('version')[0]?.[1] ?? '1'); - let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null); - let originalAuthor: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null); - let summary: string = $derived(event.getMatchingTags('summary')[0]?.[1] ?? null); - let type: string = $derived(event.getMatchingTags('type')[0]?.[1] ?? null); - let language: string = $derived(event.getMatchingTags('l')[0]?.[1] ?? null); - let source: string = $derived(event.getMatchingTags('source')[0]?.[1] ?? null); - let publisher: string = $derived(event.getMatchingTags('published_by')[0]?.[1] ?? null); - let identifier: string = $derived(event.getMatchingTags('i')[0]?.[1] ?? null); let isOpen = $state(false); @@ -116,7 +116,7 @@ } -
+
+ + + +
+ + +

Can't like, zap or highlight yet.

+

You should totally check out the discussion though.

+
\ No newline at end of file diff --git a/src/lib/components/util/TocToggle.svelte b/src/lib/components/util/TocToggle.svelte new file mode 100644 index 0000000..8fe9626 --- /dev/null +++ b/src/lib/components/util/TocToggle.svelte @@ -0,0 +1,143 @@ + + + +{#if $publicationColumnVisibility.toc} + + + + Table of contents +

(This ToC is only for demo purposes, and is not fully-functional.)

+ {#each tocItems as item} + + {/each} +
+
+
+{/if} \ No newline at end of file diff --git a/src/lib/components/util/ZapOutline.svelte b/src/lib/components/util/ZapOutline.svelte new file mode 100644 index 0000000..b5588c0 --- /dev/null +++ b/src/lib/components/util/ZapOutline.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/lib/parser.ts b/src/lib/parser.ts index 45475c5..d877985 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -123,6 +123,11 @@ export default class Pharos { */ private eventsByLevelMap: Map = new Map(); + /** + * A map of blog entries + */ + private blogEntries: Map = new Map(); + /** * When `true`, `getEvents()` should regenerate the event tree to propagate updates. */ @@ -180,6 +185,14 @@ export default class Pharos { this.parse(content); } + getBlogEntries() { + return this.blogEntries; + } + + getIndexMetadata(): IndexMetadata { + return this.rootIndexMetadata; + } + /** * Generates and stores Nostr events from the parsed AsciiDoc document. The events can be * modified via the parser's API and retrieved via the `getEvents()` method. @@ -635,6 +648,23 @@ export default class Pharos { tags.map(tag => this.ndk.fetchEventFromTag(tag, event)) ); + // if a blog, save complete events for later + if (event.getMatchingTags("type").length > 0 && event.getMatchingTags("type")[0][1] === 'blog') { + childEvents.forEach(child => { + if (child) { + this.blogEntries.set(child?.getMatchingTags("d")?.[0]?.[1], child); + } + }) + } + + // populate metadata + if (event.created_at) { + this.rootIndexMetadata.publicationDate = new Date(event.created_at * 1000).toDateString(); + } + if (event.getMatchingTags('image').length > 0) { + this.rootIndexMetadata.coverImage = event.getMatchingTags('image')[0][1]; + } + // Michael J - 15 December 2024 - This could be further parallelized by recursively fetching // children of index events before processing them for content. We won't make that change now, // as it would increase complexity, but if performance suffers, we can revisit this option. @@ -1084,3 +1114,8 @@ export default class Pharos { } export const pharosInstance: Writable = writable(); + +export const tocUpdate = writable(0); + +// Whenever you update the publication tree, call: +tocUpdate.update(n => n + 1); diff --git a/src/lib/stores.ts b/src/lib/stores.ts index 71f9fdc..e38f0d4 100644 --- a/src/lib/stores.ts +++ b/src/lib/stores.ts @@ -6,3 +6,26 @@ export let idList = writable([]); export let alexandriaKinds = readable([30040, 30041, 30818]); export let feedType = writable(FeedType.StandardRelays); + + +const defaultVisibility = { + toc: false, + blog: true, + main: true, + inner: false, + discussion: false, + editing: false +}; + +function createVisibilityStore() { + const { subscribe, set, update } = writable({ ...defaultVisibility }); + + return { + subscribe, + set, + update, + reset: () => set({ ...defaultVisibility }) + }; +} + +export const publicationColumnVisibility = createVisibilityStore(); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index fd29d79..89660e5 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,9 +6,6 @@ import { Alert } from "flowbite-svelte"; import { HammerSolid } from "flowbite-svelte-icons"; - // Compute viewport height. - $: displayHeight = window.innerHeight; - // Get standard metadata for OpenGraph tags let title = 'Library of Alexandria'; let currentUrl = $page.url.href; @@ -18,7 +15,8 @@ let summary = 'Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.'; onMount(() => { - document.body.style.height = `${displayHeight}px`; + const rect = document.body.getBoundingClientRect(); + // document.body.style.height = `${rect.height}px`; }); @@ -42,14 +40,7 @@ -
- - - - -

Pardon our dust! The publication view is currently using an experimental loader, and may be unstable.

-

New to Alexandria? Check out our Getting Started guide to learn more about using the library.

- - +
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1b204f1..9c7ca31 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,7 +1,7 @@ + + + + Pardon our dust! The publication view is currently using an experimental loader, and may be unstable. + + +
{#if !$ndkSignedIn} diff --git a/src/routes/new/compose/+page.svelte b/src/routes/new/compose/+page.svelte index 380b0bc..d1db98e 100644 --- a/src/routes/new/compose/+page.svelte +++ b/src/routes/new/compose/+page.svelte @@ -5,6 +5,7 @@ let treeNeedsUpdate: boolean = false; let treeUpdateCount: number = 0; + let someIndexValue = 0; $: { if (treeNeedsUpdate) { @@ -17,7 +18,7 @@
Compose {#key treeUpdateCount} - + {/key}
diff --git a/src/routes/new/edit/+page.svelte b/src/routes/new/edit/+page.svelte index 7a04b36..dfd399a 100644 --- a/src/routes/new/edit/+page.svelte +++ b/src/routes/new/edit/+page.svelte @@ -5,6 +5,7 @@ import Pharos, { pharosInstance } from "$lib/parser"; import { ndkInstance } from "$lib/ndk"; import { goto } from "$app/navigation"; + let someIndexValue = 0; // TODO: Prompt user to sign in before editing. @@ -80,7 +81,7 @@ {#if rootIndexId} - + {/if} {/if} diff --git a/src/routes/publication/+page.svelte b/src/routes/publication/+page.svelte index 9e9bf1d..48b156c 100644 --- a/src/routes/publication/+page.svelte +++ b/src/routes/publication/+page.svelte @@ -5,22 +5,33 @@ import { onDestroy, setContext } from "svelte"; import { PublicationTree } from "$lib/data_structures/publication_tree"; import Processor from "asciidoctor"; + import ArticleNav from "$components/util/ArticleNav.svelte"; let { data }: PageProps = $props(); const publicationTree = new PublicationTree(data.indexEvent, data.ndk); - setContext('publicationTree', publicationTree); - setContext('asciidoctor', Processor()); + setContext("publicationTree", publicationTree); + setContext("asciidoctor", Processor()); // Get publication metadata for OpenGraph tags - let title = $derived(data.indexEvent?.getMatchingTags('title')[0]?.[1] || data.parser?.getIndexTitle(data.parser?.getRootIndexId()) || 'Alexandria Publication'); - let currentUrl = data.url?.href ?? ''; - + let title = $derived( + data.indexEvent?.getMatchingTags("title")[0]?.[1] || + data.parser?.getIndexTitle(data.parser?.getRootIndexId()) || + "Alexandria Publication", + ); + let currentUrl = data.url?.href ?? ""; + // Get image and summary from the event tags if available // If image unavailable, use the Alexandria default pic. - let image = $derived(data.indexEvent?.getMatchingTags('image')[0]?.[1] || '/screenshots/old_books.jpg'); - let summary = $derived(data.indexEvent?.getMatchingTags('summary')[0]?.[1] || 'Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.'); + let image = $derived( + data.indexEvent?.getMatchingTags("image")[0]?.[1] || + "/screenshots/old_books.jpg", + ); + let summary = $derived( + data.indexEvent?.getMatchingTags("summary")[0]?.[1] || + "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.", + ); onDestroy(() => data.parser.reset()); @@ -28,30 +39,38 @@ {title} - - + + - - - + + + - - + + - - - + + + -
+{#key data} + +{/key} + +
{#await data.waitable} - + {:then} - {/await} diff --git a/src/routes/start/+page.svelte b/src/routes/start/+page.svelte index d2edd47..05d0776 100644 --- a/src/routes/start/+page.svelte +++ b/src/routes/start/+page.svelte @@ -53,20 +53,23 @@

Each content section (30041 or 30818) is also a level in the table of contents, which can be accessed from the floating icon top-left in the - reading view. This allows for navigation within the publication. (This - functionality has been temporarily disabled.) + reading view. This allows for navigation within the publication. + Publications of type "blog" have a ToC which emphasizes that each entry + is a blog post. + + (This functionality has been temporarily disabled, but the TOC is visible.)

ToC icon Table of contents example diff --git a/src/styles/base.css b/src/styles/base.css index bd6213e..e655206 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1,3 +1,9 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +@layer components { + body { + @apply bg-primary-0 dark:bg-primary-1000; + } +} \ No newline at end of file diff --git a/src/styles/publications.css b/src/styles/publications.css index 17adefc..f5c643c 100644 --- a/src/styles/publications.css +++ b/src/styles/publications.css @@ -1,55 +1,55 @@ @layer components { /* AsciiDoc content */ - .note-leather p a { + .publication-leather p a { @apply underline hover:text-primary-500 dark:hover:text-primary-400; } - .note-leather section p { + .publication-leather section p { @apply w-full; } - .note-leather section p table { + .publication-leather section p table { @apply w-full table-fixed space-x-2 space-y-2; } - .note-leather section p table td { + .publication-leather section p table td { @apply p-2; } - .note-leather section p table td .content:has(> .imageblock) { + .publication-leather section p table td .content:has(> .imageblock) { @apply flex flex-col items-center; } - .note-leather .imageblock { + .publication-leather .imageblock { @apply flex flex-col space-y-2; } - .note-leather .imageblock .content { + .publication-leather .imageblock .content { @apply flex justify-center; } - .note-leather .imageblock .title { + .publication-leather .imageblock .title { @apply text-center; } - .note-leather .imageblock.left .content { + .publication-leather .imageblock.left .content { @apply justify-start; } - .note-leather .imageblock.left .title { + .publication-leather .imageblock.left .title { @apply text-left; } - .note-leather .imageblock.right .content { + .publication-leather .imageblock.right .content { @apply justify-end; } - .note-leather .imageblock.right .title { + .publication-leather .imageblock.right .title { @apply text-right; } - .note-leather section p table td .literalblock { + .publication-leather section p table td .literalblock { @apply my-2 p-2 border rounded border-gray-400 dark:border-gray-600; } - .note-leather .literalblock pre { + .publication-leather .literalblock pre { @apply p-3 text-wrap break-words; } @@ -58,7 +58,7 @@ } /* lists */ - .note-leather .ulist ul { + .publication-leather .ulist ul { @apply space-y-1 list-disc list-inside; } @@ -104,7 +104,7 @@ } .publication-leather .verseblock pre.content { - @apply text-base font-sans; + @apply text-base font-sans overflow-x-scroll py-1; } .publication-leather .attribution { @@ -234,6 +234,34 @@ @apply w-full; } + .coverImage { + @apply max-h-[230px] overflow-hidden; + } + + .coverImage.depth-0 { + @apply max-h-[460px] overflow-hidden; + } + + .coverImage img { + @apply object-contain w-full; + } + + .coverImage.depth-0 img { + @apply m-auto w-auto; + } + + /** blog */ + @screen lg { + @media (hover: hover) { + .blog .discreet .card-leather:not(:hover) { + @apply bg-primary-50 dark:bg-primary-1000 opacity-75 transition duration-500 ease-in-out ; + } + .blog .discreet .group { + @apply bg-transparent; + } + } + } + /* Discrete headers */ h3.discrete, h4.discrete, diff --git a/src/styles/scrollbar.css b/src/styles/scrollbar.css new file mode 100644 index 0000000..8d2735d --- /dev/null +++ b/src/styles/scrollbar.css @@ -0,0 +1,20 @@ +@layer components { + /* Global scrollbar styles */ + * { + scrollbar-color: rgba(87, 66, 41, 0.8) transparent; /* Transparent track, default scrollbar thumb */ + } + + /* Webkit Browsers (Chrome, Safari, Edge) */ + *::-webkit-scrollbar { + width: 12px; /* Thin scrollbar */ + } + + *::-webkit-scrollbar-track { + background: transparent; /* Fully transparent track */ + } + + *::-webkit-scrollbar-thumb { + @apply bg-primary-500 dark:bg-primary-600 hover:bg-primary-600 dark:hover:bg-primary-800;; + border-radius: 6px; /* Rounded scrollbar */ + } +} \ No newline at end of file diff --git a/static/screenshots/ToC_blog.png b/static/screenshots/ToC_blog.png new file mode 100644 index 0000000..8e1c6e9 Binary files /dev/null and b/static/screenshots/ToC_blog.png differ diff --git a/static/screenshots/ToC_normal.png b/static/screenshots/ToC_normal.png new file mode 100644 index 0000000..297a503 Binary files /dev/null and b/static/screenshots/ToC_normal.png differ diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 5ca08f6..e28c2eb 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -22,11 +22,11 @@ const config = { 400: '#ad8351', 500: '#c6a885', 600: '#795c39', - 700: '#574229', - 800: '#342718', - 900: '#231a10', - 950: '#17110A', - 1000: '#110d08', + 700: '#564a3e', + 800: '#3c352c', + 900: '#2a241c', + 950: '#1d1812', + 1000: '#15110d', }, success: { 50: '#e3f2e7', @@ -80,6 +80,14 @@ const config = { listStyleType: { 'upper-alpha': 'upper-alpha', // Uppercase letters 'lower-alpha': 'lower-alpha', // Lowercase letters + }, + flexGrow: { + '1': '1', + '2': '2', + '3': '3', + }, + hueRotate: { + 20: '20deg', } }, },