Browse Source

Merge branch 'issue#199#202' of ssh://onedev.gitcitadel.eu:6611/Alexandria/gc-alexandria into issue#199#202

master
Silberengel 10 months ago
parent
commit
8c6a44332a
  1. 42
      src/app.css
  2. 2
      src/lib/components/Login.svelte
  3. 21
      src/lib/components/Preview.svelte
  4. 338
      src/lib/components/Publication.svelte
  5. 70
      src/lib/components/blog/BlogHeader.svelte
  6. 154
      src/lib/components/util/ArticleNav.svelte
  7. 7
      src/lib/components/util/CardActions.svelte
  8. 113
      src/lib/components/util/Details.svelte
  9. 2
      src/lib/components/util/InlineProfile.svelte
  10. 93
      src/lib/components/util/Interactions.svelte
  11. 51
      src/lib/components/util/TocToggle.svelte
  12. 19
      src/lib/components/util/ZapOutline.svelte
  13. 4
      src/lib/parser.ts
  14. 23
      src/lib/stores.ts
  15. 17
      src/routes/+layout.svelte
  16. 11
      src/routes/+page.svelte
  17. 4
      src/routes/publication/+page.svelte
  18. 8
      src/styles/base.css
  19. 41
      src/styles/publications.css
  20. 3
      src/styles/scrollbar.css
  21. 10
      tailwind.config.cjs

42
src/app.css

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
@import './styles/base.css';
@import 'styles/scrollbar.css';
@import './styles/scrollbar.css';
@import './styles/publications.css';
@import './styles/visualize.css';
@ -55,10 +55,15 @@ @@ -55,10 +55,15 @@
@apply max-w-full flex;
}
main.blog {
max-height: calc(100vh - 130px);
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;
@ -148,12 +153,18 @@ @@ -148,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 {
@ -239,6 +250,21 @@ @@ -239,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 {
@ -391,6 +417,10 @@ @@ -391,6 +417,10 @@
padding-left: 1rem;
}
.line-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
}
.footnotes li {
margin-bottom: 0.5rem;
}

2
src/lib/components/Login.svelte

@ -50,7 +50,7 @@ @@ -50,7 +50,7 @@
{#if $ndkSignedIn}
<Profile pubkey={$activePubkey} isNav={true} />
{:else}
<Avatar rounded class='h-6 w-6 cursor-pointer' id='avatar' />
<Avatar rounded class='h-6 w-6 cursor-pointer bg-transparent' id='avatar' />
<Popover
class='popover-leather w-fit'
placement='bottom'

21
src/lib/components/Preview.svelte

@ -4,6 +4,7 @@ @@ -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.
@ -93,6 +94,10 @@ @@ -93,6 +94,10 @@
}
});
function getBlogEvent(index: number) {
return blogEntries[index][1];
}
function byline(rootId: string, index: number) {
console.log(rootId, index, blogEntries);
const event = blogEntries[index][1];
@ -198,7 +203,7 @@ @@ -198,7 +203,7 @@
{#snippet sectionHeading(title: string, depth: number)}
{@const headingLevel = Math.min(depth + 1, 6)}
{@const className = $pharosInstance.isFloatingTitle(rootId) ? 'discrete' : 'h-leather'}
<svelte:element this={`h${headingLevel}`} class={className}>
{title}
</svelte:element>
@ -221,12 +226,6 @@ @@ -221,12 +226,6 @@
</p>
{/snippet}
{#snippet readMoreLink(rootId: string, index: number)}
<p class='h-leather'>
<button class="underline" onclick={() => readBlog(rootId)}>Read all about it...</button>
</p>
{/snippet}
{#snippet contentParagraph(content: string, publicationType: string)}
{#if publicationType === 'novel'}
<P class='whitespace-normal' firstupper={isSectionStart}>
@ -294,17 +293,13 @@ @@ -294,17 +293,13 @@
</Button>
</ButtonGroup>
{:else}
{#if publicationType === 'blog' && depth === 1}
{@render coverImage(rootId, index, depth)}
{@render sectionHeading(title!, depth)}
{@render blogMetadata(rootId, index)}
{:else}
{#if !(publicationType === 'blog' && depth === 1)}
{@render sectionHeading(title!, depth)}
{/if}
{/if}
<!-- Recurse on child indices and zettels -->
{#if publicationType === 'blog' && depth === 1}
{@render readMoreLink(rootId, index)}
<BlogHeader event={getBlogEvent(index)} rootId={rootId} onBlogUpdate={readBlog} />
{:else }
{#key subtreeUpdateCount}
{#each orderedChildren as id, index}

338
src/lib/components/Publication.svelte

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
import {
Alert,
Button,
Card,
Sidebar,
SidebarGroup,
SidebarItem,
@ -9,21 +10,31 @@ @@ -9,21 +10,31 @@
Skeleton,
TextPlaceholder,
Tooltip,
Heading,
} from "flowbite-svelte";
import { getContext, onMount } from "svelte";
import { BookOutline, ExclamationCircleOutline } from "flowbite-svelte-icons";
import { getContext, onDestroy, onMount } from "svelte";
import {
CloseOutline,
BookOutline,
ExclamationCircleOutline,
} from "flowbite-svelte-icons";
import { page } from "$app/state";
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";
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,157 +87,214 @@ @@ -76,157 +87,214 @@
// #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 currentBlog: null|string = $state(null);
function isInnerActive() {
return currentBlog !== null && $publicationColumnVisibility.inner;
}
function isDefaultVisible() {
if (publicationType !== 'blog') {
return true;
} else {
return $publicationColumnVisibility.blog;
}
function closeDiscussion() {
publicationColumnVisibility.update((v) => ({ ...v, discussion: false }));
}
function loadBlog(rootId: string) {
// depending on the size of the screen, also toggle blog list visibility
if (window.innerWidth < 1024) {
$publicationColumnVisibility.blog = false;
}
$publicationColumnVisibility.inner = true;
// 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
currentBlogEvent =
leaves.find((i) => i.tagAddress() === currentBlog) ?? null;
}
</script>
{#if $publicationColumnVisibility.details}
<div class="flex flex-col space-y-4 max-w-xl flex-grow-1 p-2 bg-highlight">
<Details event={indexEvent} />
</div>
{/if}
{#if isDefaultVisible()}
<div class="flex flex-col space-y-4 overflow-auto
{publicationType === 'blog' ? 'max-w-xl flex-grow-1' : 'max-w-2xl flex-grow-2' }
{currentBlog !== null ? 'discreet' : ''}
">
<Preview {rootId} {publicationType} index={0} onBlogUpdate={loadBlog} />
</div>
{/if}
{#if currentBlog !== null && $publicationColumnVisibility.inner }
{#key currentBlog }
<div class="flex flex-col space-y-4 max-w-3xl overflow-auto flex-grow-2">
<Preview rootId={currentBlog} {publicationType} index={0} />
</div>
{/key}
{/if}
{#if $publicationColumnVisibility.social }
<div class="flex flex-col space-y-4 max-w-xl overflow-auto flex-grow-1 bg-highlight">
<p>Social column</p>
</div>
{/if}
function showBlogHeader() {
return currentBlog && currentBlogEvent && window.innerWidth < 1140;
}
// #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();
};
});
</script>
<!-- TODO: Keep track of already-loaded leaves. -->
<!-- TODO: Handle entering mid-document and scrolling up. -->
{#if showTocButton && !showToc}
<!-- <Button
class="btn-leather fixed top-20 left-4 h-6 w-6"
outline={true}
on:click={(ev) => {
showToc = true;
ev.stopPropagation();
}}
<!-- Table of contents -->
{#if publicationType !== "blog" || !isLeaf}
<TocToggle rootId={rootAddress} />
{/if}
<!-- Default publications -->
{#if $publicationColumnVisibility.main}
<div class="flex flex-col p-4 space-y-4 overflow-auto max-w-2xl flex-grow-2">
<div
class="card-leather bg-highlight dark:bg-primary-800 p-4 mb-4 rounded-lg border"
>
<Details event={indexEvent} />
</div>
<!-- Publication -->
{#each leaves as leaf, i}
{#if leaf == null}
<Alert class="flex space-x-2">
<ExclamationCircleOutline class="w-5 h-5" />
Error loading content. One or more events could not be loaded.
</Alert>
{:else}
<PublicationSection
{rootAddress}
{leaves}
address={leaf.tagAddress()}
ref={(el) => setLastElementRef(el, i)}
/>
{/if}
{/each}
<div class="flex justify-center my-4">
{#if isLoading}
<Button disabled color="primary">Loading...</Button>
{:else if !isDone}
<Button color="primary" on:click={() => loadMore(1)}>Show More</Button>
{:else}
<p class="text-gray-500 dark:text-gray-400">
You've reached the end of the publication.
</p>
{/if}
</div>
</div>
{/if}
<!-- Blog list -->
{#if $publicationColumnVisibility.blog}
<div
class="flex flex-col p-4 space-y-4 overflow-auto max-w-xl flex-grow-1
{isInnerActive() ? 'discreet' : ''}
"
>
<BookOutline />
</Button>
<Tooltip>Show Table of Contents</Tooltip> -->
<div
class="card-leather bg-highlight dark:bg-primary-800 p-4 mb-4 rounded-lg border"
>
<Details event={indexEvent} />
</div>
<!-- List blog excerpts -->
{#each leaves as leaf, i}
<BlogHeader
rootId={leaf.tagAddress()}
event={leaf}
onBlogUpdate={loadBlog}
active={!isInnerActive()}
/>
{/each}
</div>
{/if}
<!-- TODO: Use loader to build ToC. -->
<!-- {#if showToc}
<Sidebar class='sidebar-leather fixed top-20 left-0 px-4 w-60' {activeHash}>
<SidebarWrapper>
<SidebarGroup class='sidebar-group-leather overflow-y-scroll'>
{#each events as event}
<SidebarItem
class='sidebar-item-leather'
label={event.getMatchingTags('title')[0][1]}
href={`${$page.url.pathname}#${normalizeHashPath(event.getMatchingTags('title')[0][1])}`}
{#if isInnerActive()}
{#key currentBlog}
<div
class="flex flex-col p-4 max-w-3xl overflow-auto flex-grow-2 max-h-[calc(100vh-146px)] sticky top-[146px]"
>
{#each leaves as leaf, i}
{#if leaf.tagAddress() === currentBlog}
<div
class="card-leather bg-highlight dark:bg-primary-800 p-4 mb-4 rounded-lg border"
>
<Details event={leaf} />
</div>
<PublicationSection
{rootAddress}
{leaves}
address={leaf.tagAddress()}
ref={(el) => setLastElementRef(el, i)}
/>
{/each}
<Card class="ArticleBox !hidden card-leather min-w-full grid mt-4">
<Interactions rootId={currentBlog} />
</Card>
{/if}
{/each}
</div>
{/key}
{/if}
{#if $publicationColumnVisibility.discussion}
<Sidebar class="sidebar-leather right-0 md:!pl-8">
<SidebarWrapper>
<SidebarGroup class="sidebar-group-leather">
<div class="flex justify-between items-baseline">
<Heading tag="h1" class="h-leather !text-lg">Discussion</Heading>
<Button
class="btn-leather hidden sm:flex z-30 !p-1 bg-primary-50 dark:bg-gray-800"
outline
onclick={closeDiscussion}
>
<CloseOutline />
</Button>
</div>
<div class="flex flex-col space-y-4">
<!-- TODO
alternative for other publications and
when blog is not opened, but discussion is opened from the list
-->
{#if showBlogHeader()}
<BlogHeader
rootId={currentBlog}
event={currentBlogEvent}
onBlogUpdate={loadBlog}
active={true}
/>
{/if}
<div class="flex flex-col w-full space-y-4">
<Card class="ArticleBox card-leather w-full grid max-w-xl">
<div class="flex flex-col my-2">
<span>Unknown</span>
<span class="text-gray-500">1.1.1970</span>
</div>
<div class="flex flex-col flex-grow space-y-4">
This is a very intelligent comment placeholder that applies to
all the content equally well.
</div>
</Card>
</div>
</div>
</SidebarGroup>
</SidebarWrapper>
</Sidebar>
{/if} -->
<div class="flex flex-col space-y-4 max-w-2xl pb-10 px-4 sm:px-6 md:px-8">
{#each leaves as leaf, i}
{#if leaf == null}
<Alert class='flex space-x-2'>
<ExclamationCircleOutline class='w-5 h-5' />
Error loading content. One or more events could not be loaded.
</Alert>
{:else}
<PublicationSection
rootAddress={rootAddress}
leaves={leaves}
address={leaf.tagAddress()}
ref={(el) => setLastElementRef(el, i)}
/>
{/if}
{/each}
<div class="flex justify-center my-4">
{#if isLoading}
<Button disabled color="primary">
Loading...
</Button>
{:else if !isDone}
<Button color="primary" on:click={() => loadMore(1)}>
Show More
</Button>
{:else}
<p class="text-gray-500 dark:text-gray-400">You've reached the end of the publication.</p>
{/if}
</div>
</div>
<style>
:global(.sidebar-group-leather) {
max-height: calc(100vh - 8rem);
}
</style>
{/if}

70
src/lib/components/blog/BlogHeader.svelte

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
<script lang="ts">
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import { scale } from 'svelte/transition';
import { Card, Img } from "flowbite-svelte";
import InlineProfile from "$components/util/InlineProfile.svelte";
import Interactions from "$components/util/Interactions.svelte";
import { quintOut } from "svelte/easing";
import CardActions from "$components/util/CardActions.svelte";
const { rootId, event, onBlogUpdate, active = true } = $props<{ rootId: string, event: NDKEvent, onBlogUpdate?: any, active: boolean }>();
let title: string = $derived(event.getMatchingTags('title')[0]?.[1]);
let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown');
let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null);
let authorPubkey: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null);
let hashtags: string = $derived(event.getMatchingTags('t') ?? null);
function publishedAt() {
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 showBlog() {
onBlogUpdate?.(rootId);
}
</script>
{#if title != null}
<Card class="ArticleBox card-leather w-full grid max-w-xl {active ? 'active' : ''}">
<div class='space-y-4'>
<div class="flex flex-row justify-between my-2">
<div class="flex flex-col">
<InlineProfile pubkey={authorPubkey} title={author} />
<span class='text-gray-500'>{publishedAt()}</span>
</div>
<CardActions event={event} />
</div>
{#if image && active}
<div class="ArticleBoxImage flex col justify-center"
in:scale={{ start: 0.8, duration: 500, delay: 100, easing: quintOut }}
>
<Img src={image} class="rounded w-full max-h-72 object-cover"/>
</div>
{/if}
<div class='flex flex-col flex-grow space-y-4'>
<button onclick={() => showBlog()} class='text-left'>
<h2 class='text-lg font-bold line-clamp-2' title="{title}">{title}</h2>
</button>
{#if hashtags}
<div class="tags">
{#each hashtags as tag}
<span>{tag}</span>
{/each}
</div>
{/if}
</div>
{#if active}
<Interactions rootId={rootId} event={event} />
{/if}
</div>
</Card>
{/if}

154
src/lib/components/util/ArticleNav.svelte

@ -1,55 +1,149 @@ @@ -1,55 +1,149 @@
<script lang="ts">
import { pharosInstance } from "$lib/parser";
import TocToggle from "$components/util/TocToggle.svelte";
import { EyeOutline, BookOutline, AlignCenterOutline, BookOpenOutline, GlobeOutline, ShareAllOutline, PenOutline } from "flowbite-svelte-icons";
import { BookOutline, CaretLeftOutline, CloseOutline, GlobeOutline } from "flowbite-svelte-icons";
import { Button } from "flowbite-svelte";
import { onMount } from "svelte";
import { publicationColumnVisibility } from "$lib/stores";
import InlineProfile from "$components/util/InlineProfile.svelte";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { onDestroy, onMount } from "svelte";
let {
rootId,
publicationType
publicationType,
indexEvent
} = $props<{
rootId: any,
publicationType: string
publicationType: string,
indexEvent: NDKEvent
}>();
onMount(async () => {
console.log($pharosInstance.getIndexMetadata());
});
let title: string = $derived(indexEvent.getMatchingTags('title')[0]?.[1]);
let author: string = $derived(indexEvent.getMatchingTags('author')[0]?.[1] ?? 'unknown');
let pubkey: string = $derived(indexEvent.getMatchingTags('p')[0]?.[1] ?? null);
let isLeaf: boolean = $derived(indexEvent.kind === 30041);
let lastScrollY = $state(0);
let isVisible = $state(true);
// Function to toggle column visibility
function toggleColumn(column: 'details'|'blog'|'inner'|'social') {
publicationColumnVisibility.update(store => {
store[column] = !store[column]; // Toggle true/false
if (window.innerWidth < 1140) {
$publicationColumnVisibility.inner = false;
function toggleColumn(column: 'toc' | 'blog' | 'inner' | 'discussion') {
publicationColumnVisibility.update(current => {
const newValue = !current[column];
const updated = { ...current, [column]: newValue };
if (window.innerWidth < 1400 && column === 'blog' && newValue) {
updated.discussion = false;
}
return updated;
});
}
function shouldShowBack() {
const vis = $publicationColumnVisibility;
return ['discussion', 'toc', 'inner'].some(key => vis[key]);
}
function backToMain() {
publicationColumnVisibility.update(current => {
const updated = { ...current };
// if current is 'inner', just go back to blog
if (current.inner && !(current.discussion || current.toc)) {
updated.inner = false;
updated.blog = true;
return updated;
}
updated.discussion = false;
updated.toc = false;
if (publicationType === 'blog') {
updated.inner = true;
updated.blog = false;
} else {
updated.main = true;
}
return { ...store }; // Ensure reactivity
return updated;
});
}
function backToBlog() {
publicationColumnVisibility.update(current => {
const updated = { ...current };
updated.inner = false;
updated.discussion = false;
updated.blog = true;
return updated;
})
}
function handleScroll() {
if (window.innerWidth < 768) {
const currentScrollY = window.scrollY;
// Hide on scroll down
if (currentScrollY > lastScrollY && currentScrollY > 50) {
isVisible = false;
}
// Show on scroll up
else if (currentScrollY < lastScrollY) {
isVisible = true;
}
lastScrollY = currentScrollY;
}
}
let unsubscribe: () => void;
onMount(() => {
window.addEventListener('scroll', handleScroll);
unsubscribe = publicationColumnVisibility.subscribe(() => {
isVisible = true; // show navbar when store changes
});
});
onDestroy(() => {
window.removeEventListener('scroll', handleScroll);
unsubscribe();
});
</script>
<nav class="Navbar navbar-leather sticky top-[76px] w-full px-2 sm:px-4 py-2.5 z-10">
<div class="mx-auto flex flex-wrap justify-center space-x-2 container">
<Button class='btn-leather !w-auto' outline={true} onclick={() => toggleColumn('details')} >
<EyeOutline class="!fill-none inline mr-1" /><span class="hidden sm:inline">Details</span>
</Button>
{#if publicationType === 'blog'}
<Button class='btn-leather !w-auto' outline={true} onclick={() => toggleColumn('blog')} >
<BookOutline class="!fill-none inline mr-1" /><span class="hidden sm:inline">Table of Contents</span>
<nav class="Navbar navbar-leather flex fixed top-[60px] sm:top-[76px] w-full min-h-[70px] px-2 sm:px-4 py-2.5 z-10 transition-transform duration-300 {isVisible ? 'translate-y-0' : '-translate-y-full'}">
<div class="mx-auto flex space-x-2 container">
<div class="flex items-center space-x-2 md:min-w-52 min-w-8">
{#if shouldShowBack()}
<Button class='btn-leather !w-auto sm:hidden' outline={true} onclick={backToMain}>
<CaretLeftOutline class="!fill-none inline mr-1" /><span class="hidden sm:inline">Back</span>
</Button>
<Button class='btn-leather !w-auto' outline={true} onclick={() => toggleColumn('inner')} >
<BookOpenOutline class="!fill-none inline mr-1" /><span class="hidden sm:inline">Content</span>
{/if}
{#if !isLeaf}
{#if publicationType === 'blog'}
<Button class="btn-leather hidden sm:flex !w-auto {$publicationColumnVisibility.blog ? 'active' : ''}"
outline={true} onclick={() => toggleColumn('blog')} >
<BookOutline class="!fill-none inline mr-1" /><span class="hidden sm:inline">Table of Contents</span>
</Button>
{:else if !$publicationColumnVisibility.discussion && !$publicationColumnVisibility.toc}
<Button class='btn-leather !w-auto' outline={true} onclick={() => toggleColumn('toc')}>
<BookOutline class="!fill-none inline mr-1" /><span class="hidden sm:inline">Table of Contents</span>
</Button>
{/if}
{/if}
</div>
<div class="flex flex-grow text justify-center items-center">
<p class="max-w-[60vw] line-ellipsis"><b class="text-nowrap">{title}</b> <span class="whitespace-nowrap">by <InlineProfile pubkey={pubkey} title={author} /></span></p>
</div>
<div class="flex justify-end items-center space-x-2 md:min-w-52 min-w-8">
{#if $publicationColumnVisibility.inner}
<Button class='btn-leather !w-auto hidden sm:flex' outline={true} onclick={backToBlog}>
<CloseOutline class="!fill-none inline mr-1" /><span class="hidden sm:inline">Close</span>
</Button>
{/if}
{#if publicationType !== 'blog' && !$publicationColumnVisibility.discussion}
<Button class="btn-leather !hidden hidden sm:flex !w-auto" outline={true} onclick={() => toggleColumn('discussion')} >
<GlobeOutline class="!fill-none inline mr-1" /><span class="hidden sm:inline">Discussion</span>
</Button>
{:else}
<TocToggle rootId={rootId} />
{/if}
<Button class='btn-leather !w-auto' outline={true} onclick={() => toggleColumn('social')} >
<GlobeOutline class="!fill-none inline mr-1" /><span class="hidden sm:inline">Social</span>
</Button>
</div>
</div>
</nav>

7
src/lib/components/util/CardActions.svelte

@ -10,8 +10,7 @@ @@ -10,8 +10,7 @@
import { Button, Modal, Popover } from "flowbite-svelte";
import { standardRelays } from "$lib/consts";
import { neventEncode, naddrEncode } from "$lib/utils";
import InlineProfile from "$components/util/InlineProfile.svelte";
import Details from "./Details.svelte";
import Details from "./Details.svelte";
let { event } = $props();
@ -74,7 +73,7 @@ @@ -74,7 +73,7 @@
</script>
<div class="group" role="group" onmouseenter={openPopover}>
<div class="group bg-highlight dark:bg-primary-1000 rounded" role="group" onmouseenter={openPopover}>
<!-- Main button -->
<Button type="button"
id="dots-{event.id}"
@ -135,6 +134,6 @@ @@ -135,6 +134,6 @@
</Modal>
<!-- Event details -->
<Modal class='modal-leather' title='Publication details' bind:open={detailsModalOpen} autoclose outsideclose size='sm'>
<Details event={event} />
<Details event={event} isModal={true} />
</Modal>
</div>

113
src/lib/components/util/Details.svelte

@ -1,7 +1,13 @@ @@ -1,7 +1,13 @@
<script lang="ts">
import InlineProfile from "$components/util/InlineProfile.svelte";
import CardActions from "$components/util/CardActions.svelte";
import Interactions from "$components/util/Interactions.svelte";
import { P } from "flowbite-svelte";
let { event } = $props();
// isModal
// - don't show interactions in modal view
// - don't show all the details when _not_ in modal view
let { event, isModal = false } = $props();
let title: string = $derived(event.getMatchingTags('title')[0]?.[1]);
let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown');
@ -14,53 +20,90 @@ @@ -14,53 +20,90 @@
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 hashtags: [] = $derived(event.getMatchingTags('t') ?? []);
let rootId: string = $derived(event.getMatchingTags('d')[0]?.[1] ?? null);
let kind = $derived(event.kind);
</script>
<div class="flex flex-row md:space-x-4 max-sm:flex-wrap">
{#if image}
<div class="flex col">
<img class="md:max-w-48 max-sm:w-full object-cover" alt={title} src={image} />
<div class="flex flex-col relative mb-2">
{#if !isModal}
<div class="flex flex-row justify-between items-center">
<P class='text-base font-normal'><InlineProfile pubkey={event.pubkey} /></P>
<CardActions event={event}></CardActions>
</div>
{/if}
<div class="flex flex-col col space-y-5 justify-center align-middle">
<h1 class="text-3xl font-bold mt-5">{title}</h1>
<h2 class="text-base font-bold">by
{#if originalAuthor !== null}
<InlineProfile pubkey={originalAuthor} title={author} />
{:else}
{author}
<div class="flex-grow grid grid-cols-1 md:grid-cols-[auto_1fr] gap-4 items-center">
{#if image}
<div class="my-2">
<img class="w-full md:max-w-48 object-contain rounded" alt={title} src={image} />
</div>
{/if}
<div class="space-y-4 my-4">
<h1 class="text-3xl font-bold">{title}</h1>
<h2 class="text-base font-bold">
by
{#if originalAuthor !== null}
<InlineProfile pubkey={originalAuthor} title={author} />
{:else}
{author}
{/if}
</h2>
{#if version !== '1' }
<h4 class="text-base font-thin">Version: {version}</h4>
{/if}
</h2>
<h4 class='text-base font-thin mt-2'>Version: {version}</h4>
</div>
</div>
</div>
{#if summary}
<div class="flex flex-row ">
<div class="flex flex-row my-2">
<p class='text-base text-primary-900 dark:text-highlight'>{summary}</p>
</div>
{/if}
<div class="flex flex-row ">
<h4 class='text-base font-normal mt-2'>Index author: <InlineProfile pubkey={event.pubkey} /></h4>
</div>
{#if hashtags.length}
<div class="tags my-2">
{#each hashtags as tag}
<span class="text-sm">#{tag[1]}</span>
{/each}
</div>
{/if}
<div class="flex flex-col pb-4 space-y-1">
{#if source !== null}
<h5 class="text-sm">Source: <a class="underline" href={source} target="_blank">{source}</a></h5>
{/if}
{#if type !== null}
<h5 class="text-sm">Publication type: {type}</h5>
{/if}
{#if language !== null}
<h5 class="text-sm">Language: {language}</h5>
{/if}
{#if publisher !== null}
<h5 class="text-sm">Published by: {publisher}</h5>
{/if}
{#if identifier !== null}
<h5 class="text-sm">{identifier}</h5>
{/if}
{#if isModal}
<div class="flex flex-row my-4">
<h4 class='text-base font-normal mt-2'>
{#if kind === 30040}
<span>Index author:</span>
{:else}
<span>Author:</span>
{/if}
<InlineProfile pubkey={event.pubkey} />
</h4>
</div>
<div class="flex flex-col pb-4 space-y-1">
{#if source !== null}
<h5 class="text-sm">Source: <a class="underline break-all" href={source} target="_blank">{source}</a></h5>
{/if}
{#if type !== null}
<h5 class="text-sm">Publication type: {type}</h5>
{/if}
{#if language !== null}
<h5 class="text-sm">Language: {language}</h5>
{/if}
{#if publisher !== null}
<h5 class="text-sm">Published by: {publisher}</h5>
{/if}
{#if identifier !== null}
<h5 class="text-sm">{identifier}</h5>
{/if}
</div>
{/if}
</div>
{#if !isModal}
<Interactions event={event} rootId={rootId} direction="row"/>
{/if}

2
src/lib/components/util/InlineProfile.svelte

@ -49,7 +49,7 @@ @@ -49,7 +49,7 @@
{:else if npub }
<a href='{externalProfileDestination}{npub}' title={title ?? username} target='_blank'>
<Avatar rounded
class='h-6 w-6 mx-1 cursor-pointer inline'
class='h-6 w-6 mx-1 cursor-pointer inline bg-transparent'
src={pfp}
alt={username} />
<span class='underline'>{username ?? shortenNpub(npub)}</span>

93
src/lib/components/util/Interactions.svelte

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
<script lang="ts">
import {
Button, Modal, P
} from "flowbite-svelte";
import { HeartOutline, FilePenOutline, AnnotationOutline } from 'flowbite-svelte-icons';
import ZapOutline from "$components/util/ZapOutline.svelte";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { onMount } from "svelte";
import { ndkInstance } from '$lib/ndk';
import { publicationColumnVisibility } from "$lib/stores";
const { rootId, event, direction = 'row' } = $props<{ rootId: string, event?: NDKEvent, direction?: string }>();
// Reactive arrays to hold incoming events
let likes: NDKEvent[] = [];
let zaps: NDKEvent[] = [];
let highlights: NDKEvent[] = [];
let comments: NDKEvent[] = [];
let interactionOpen: boolean = $state(false);
// Reactive counts derived from array lengths
// Derived counts from store values
const likeCount = $derived(likes.length);
const zapCount = $derived(zaps.length);
const highlightCount = $derived(highlights.length);
const commentCount = $derived(comments.length);
/**
* Subscribe to Nostr events of a given kind that reference our root event via e-tag.
* Push new events into the provided array if not already present.
* Returns the subscription for later cleanup.
*/
function subscribeCount(kind: number, targetArray: NDKEvent[]) {
const sub = $ndkInstance.subscribe({
kinds: [kind],
'#a': [rootId] // Will this work?
});
sub.on('event', (evt: NDKEvent) => {
// Only add if we haven't seen this event ID yet
if (!targetArray.find(e => e.id === evt.id)) {
targetArray.push(evt);
}
});
return sub;
}
let subs: any[] = [];
onMount(() => {
// Subscribe to each kind; store subs for cleanup
subs.push(subscribeCount(7, likes)); // likes (Reaction)
subs.push(subscribeCount(9735, zaps)); // zaps (Zap Receipts)
subs.push(subscribeCount(30023, highlights)); // highlights (custom kind)
subs.push(subscribeCount(1, comments)); // comments (Text Notes)
});
function showDiscussion() {
publicationColumnVisibility.update(v => {
const updated = { ...v, discussion: true};
// hide blog, unless the only column
if (v.inner) {
updated.blog = (v.blog && window.innerWidth >= 1400 );
}
return updated;
});
}
function doLike() {
interactionOpen = true;
}
function doHighlight() {
interactionOpen = true;
}
function doZap() {
interactionOpen = true;
}
</script>
<div class='InteractiveMenu !hidden flex flex-{direction} justify-around align-middle text-primary-700 dark:text-gray-500'>
<Button color="none" class='flex flex-{direction} shrink-0 md:min-w-11 min-h-11 items-center p-0' onclick={doLike}><HeartOutline class="mx-2" size="lg" /><span>{likeCount}</span></Button>
<Button color="none" class='flex flex-{direction} shrink-0 md:min-w-11 min-h-11 items-center p-0' onclick={doZap}><ZapOutline className="mx-2" /><span>{zapCount}</span></Button>
<Button color="none" class='flex flex-{direction} shrink-0 md:min-w-11 min-h-11 items-center p-0' onclick={doHighlight}><FilePenOutline class="mx-2" size="lg"/><span>{highlightCount}</span></Button>
<Button color="none" class='flex flex-{direction} shrink-0 md:min-w-11 min-h-11 items-center p-0' onclick={showDiscussion}><AnnotationOutline class="mx-2" size="lg"/><span>{commentCount}</span></Button>
</div>
<Modal class='modal-leather' title='Interaction' bind:open={interactionOpen} autoclose outsideclose size='sm'>
<P>Can't like, zap or highlight yet.</P>
<P>You should totally check out the discussion though.</P>
</Modal>

51
src/lib/components/util/TocToggle.svelte

@ -1,17 +1,17 @@ @@ -1,17 +1,17 @@
<script lang="ts">
import {
Button,
Button, Heading,
Sidebar,
SidebarGroup,
SidebarItem,
SidebarWrapper,
Skeleton,
TextPlaceholder,
Tooltip,
Tooltip
} from "flowbite-svelte";
import { onMount } from "svelte";
import { BookOutline } from "flowbite-svelte-icons";
import { pharosInstance } from "$lib/parser";
import { publicationColumnVisibility } from "$lib/stores";
import { page } from "$app/state";
let { rootId } = $props<{ rootId: string }>();
@ -23,8 +23,6 @@ @@ -23,8 +23,6 @@
const tocBreakpoint = 1140;
let activeHash = $state(page.url.hash);
let showToc: boolean = $state(true);
let showTocButton: boolean = $state(false);
function normalizeHashPath(str: string): string {
return str
@ -55,8 +53,7 @@ @@ -55,8 +53,7 @@
* prevents the sidebar from occluding the article content.
*/
function setTocVisibilityOnResize() {
showToc = window.innerWidth >= tocBreakpoint;
showTocButton = window.innerWidth < tocBreakpoint;
publicationColumnVisibility.update(v => ({ ...v, toc: window.innerWidth >= tocBreakpoint}));
}
/**
@ -69,8 +66,8 @@ @@ -69,8 +66,8 @@
return;
}
if (showToc) {
showToc = false;
if ($publicationColumnVisibility.toc) {
publicationColumnVisibility.update(v => ({ ...v, toc: false}));
}
}
@ -93,32 +90,20 @@ @@ -93,32 +90,20 @@
});
</script>
{#if showTocButton && !showToc}
<Button
class="btn-leather h-6 !w-auto"
outline={true}
on:click={(ev) => {
showToc = true;
ev.stopPropagation();
}}
>
<BookOutline class="!fill-none mr-1"/>
Table of Contents
</Button>
{/if}
<!-- TODO: Get TOC from parser. -->
<!-- {#if showToc}
<Sidebar class='sidebar-leather fixed top-20 left-0 px-4 w-60' {activeHash}>
{#if $publicationColumnVisibility.toc}
<Sidebar class='sidebar-leather left-0' {activeHash}>
<SidebarWrapper>
<SidebarGroup class='sidebar-group-leather overflow-y-scroll'>
{#each events as event}
<SidebarItem
class='sidebar-item-leather'
label={event.getMatchingTags('title')[0][1]}
href={`${$page.url.pathname}#${normalizeHashPath(event.getMatchingTags('title')[0][1])}`}
/>
{/each}
<SidebarGroup class='sidebar-group-leather'>
<Heading tag="h1" class="h-leather !text-lg">Table of contents</Heading>
<!--{#each events as event}-->
<!-- <SidebarItem-->
<!-- class='sidebar-item-leather'-->
<!-- label={event.getMatchingTags('title')[0][1]}-->
<!-- href={`${$page.url.pathname}#${normalizeHashPath(event.getMatchingTags('title')[0][1])}`}-->
<!-- />-->
<!--{/each}-->
</SidebarGroup>
</SidebarWrapper>
</Sidebar>
{/if} -->
{/if}

19
src/lib/components/util/ZapOutline.svelte

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
<script>
export let size = 24; // default size
export let className = '';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class={className}
viewBox="0 0 24 24"
>
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
</svg>

4
src/lib/parser.ts

@ -649,7 +649,7 @@ export default class Pharos { @@ -649,7 +649,7 @@ export default class Pharos {
);
// if a blog, save complete events for later
if (event.getMatchingTags("type")[0][1] === 'blog') {
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);
@ -661,7 +661,7 @@ export default class Pharos { @@ -661,7 +661,7 @@ export default class Pharos {
if (event.created_at) {
this.rootIndexMetadata.publicationDate = new Date(event.created_at * 1000).toDateString();
}
if (event.getMatchingTags('image')) {
if (event.getMatchingTags('image').length > 0) {
this.rootIndexMetadata.coverImage = event.getMatchingTags('image')[0][1];
}

23
src/lib/stores.ts

@ -7,12 +7,25 @@ export let alexandriaKinds = readable<number[]>([30040, 30041, 30818]); @@ -7,12 +7,25 @@ export let alexandriaKinds = readable<number[]>([30040, 30041, 30818]);
export let feedType = writable<FeedType>(FeedType.StandardRelays);
export const publicationColumnVisibility = writable({
details: false,
const defaultVisibility = {
toc: false,
blog: true,
main: true,
inner: true,
social: false,
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();

17
src/routes/+layout.svelte

@ -6,9 +6,6 @@ @@ -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 @@ @@ -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`;
});
</script>
@ -42,14 +40,7 @@ @@ -42,14 +40,7 @@
<meta name="twitter:image" content="{image}" />
</svelte:head>
<div class={'leather min-h-full w-full flex flex-col items-center'}>
<Navigation class='sticky top-0' />
<Alert rounded={false} class='border-t-4 border-primary-500 text-gray-900 dark:text-gray-100 dark:border-primary-500 flex justify-left mb-4'>
<HammerSolid class='mr-2 h-5 w-5 text-primary-500 dark:text-primary-500' />
<span class='font-medium'>
<p>Pardon our dust! The publication view is currently using an experimental loader, and may be unstable.</p>
<p>New to Alexandria? Check out our <a href="/start" class='text-primary-600 dark:text-primary-400 hover:underline'>Getting Started guide</a> to learn more about using the library.</p>
</span>
</Alert>
<div class={'leather mt-[76px] h-full w-full flex flex-col items-center'}>
<Navigation class='fixed top-0' />
<slot />
</div>

11
src/routes/+page.svelte

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
<script lang='ts'>
import { FeedType, feedTypeStorageKey, standardRelays } from '$lib/consts';
import { Button, Dropdown, Radio } from 'flowbite-svelte';
import { ChevronDownOutline } from 'flowbite-svelte-icons';
import { Alert, Button, Dropdown, Radio } from "flowbite-svelte";
import { ChevronDownOutline, HammerSolid } from "flowbite-svelte-icons";
import { inboxRelays, ndkSignedIn } from '$lib/ndk';
import PublicationFeed from '$lib/components/PublicationFeed.svelte';
import { feedType } from '$lib/stores';
@ -22,6 +22,13 @@ @@ -22,6 +22,13 @@
};
</script>
<Alert rounded={false} id="alert-experimental" class='border-t-4 border-primary-500 text-gray-900 dark:text-gray-100 dark:border-primary-500 flex justify-left mb-2'>
<HammerSolid class='mr-2 h-5 w-5 text-primary-500 dark:text-primary-500' />
<span class='font-medium'>
Pardon our dust! The publication view is currently using an experimental loader, and may be unstable.
</span>
</Alert>
<main class='leather flex flex-col flex-grow-0 space-y-4 p-4'>
{#if !$ndkSignedIn}
<PublicationFeed relays={standardRelays} />

4
src/routes/publication/+page.svelte

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
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();
@ -59,10 +60,11 @@ @@ -59,10 +60,11 @@
<ArticleNav
publicationType={data.publicationType}
rootId={data.parser.getRootIndexId()}
indexEvent={data.indexEvent}
/>
{/key}
<main class={data.publicationType}>
<main class="publication {data.publicationType}">
{#await data.waitable}
<TextPlaceholder divClass="skeleton-leather w-full" size="xxl" />
{:then}

8
src/styles/base.css

@ -1,3 +1,9 @@ @@ -1,3 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind utilities;
@layer components {
body {
@apply bg-primary-0 dark:bg-primary-1000;
}
}

41
src/styles/publications.css

@ -1,55 +1,55 @@ @@ -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 @@ @@ -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 @@ @@ -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 {
@ -253,12 +253,11 @@ @@ -253,12 +253,11 @@
/** blog */
@screen lg {
@media (hover: hover) {
.blog .discreet:not(:hover) .coverImage img {
@apply filter grayscale sepia brightness-75 opacity-50 transition duration-500 ease-in-out saturate-200 hue-rotate-20;
.blog .discreet .card-leather:not(:hover) {
@apply bg-primary-50 dark:bg-primary-1000 opacity-75 transition duration-500 ease-in-out ;
}
.blog .discreet:not(:hover) .h-leather {
@apply filter grayscale sepia brightness-75 opacity-50 transition duration-500 ease-in-out saturate-200 hue-rotate-20;
.blog .discreet .group {
@apply bg-transparent;
}
}
}

3
src/styles/scrollbar.css

@ -1,13 +1,12 @@ @@ -1,13 +1,12 @@
@layer components {
/* Global scrollbar styles */
* {
scrollbar-width: thin; /* Firefox */
scrollbar-color: rgba(87, 66, 41, 0.8) transparent; /* Transparent track, default scrollbar thumb */
}
/* Webkit Browsers (Chrome, Safari, Edge) */
*::-webkit-scrollbar {
width: 8px; /* Thin scrollbar */
width: 12px; /* Thin scrollbar */
}
*::-webkit-scrollbar-track {

10
tailwind.config.cjs

@ -22,11 +22,11 @@ const config = { @@ -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',

Loading…
Cancel
Save