Add the ability to save events to the library

This commit is contained in:
Jon Staab 2026-07-02 11:36:05 -07:00
parent 61a8c44750
commit ff5d7aeb87
25 changed files with 207 additions and 126 deletions

View file

@ -52,14 +52,13 @@
<EmojiButton {onEmoji} class="button button-neutral button-xs join-item"> <EmojiButton {onEmoji} class="button button-neutral button-xs join-item">
<Icon icon={SmileCircle} size={4} /> <Icon icon={SmileCircle} size={4} />
</EmojiButton> </EmojiButton>
<Tippy <Button onclick={showPopover} class="flex join-item button button-neutral button-xs">
bind:popover <Tippy
class="flex join-item button button-neutral button-xs" bind:popover
component={EventMenu} component={EventMenu}
props={{url, noun, event, customActions, onClick: hidePopover}} props={{url, noun, event, customActions, onClick: hidePopover}}
params={{trigger: "manual", interactive: true}}> params={{trigger: "manual", interactive: true}}>
<Button onclick={showPopover}>
<Icon icon={MenuDots} size={4} /> <Icon icon={MenuDots} size={4} />
</Button> </Tippy>
</Tippy> </Button>
</div> </div>

View file

@ -3,7 +3,7 @@
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
import {LOCALE, secondsToDate} from "@welshman/lib" import {LOCALE, secondsToDate} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl, toNostrURI} from "@welshman/util"
import {tracker} from "@welshman/app" import {tracker} from "@welshman/app"
import FileText from "@assets/icons/file-text.svg?dataurl" import FileText from "@assets/icons/file-text.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl" import Copy from "@assets/icons/copy.svg?dataurl"
@ -28,7 +28,7 @@
const {url, event}: Props = $props() const {url, event}: Props = $props()
const relays = url ? [url] : Router.get().Event(event).getUrls() const relays = url ? [url] : Router.get().Event(event).getUrls()
const nevent1 = nip19.neventEncode({...event, relays}) const nevent1 = toNostrURI(nip19.neventEncode({...event, relays}))
const seenOn = tracker.getRelays(event.id) const seenOn = tracker.getRelays(event.id)
const npub1 = nip19.npubEncode(event.pubkey) const npub1 = nip19.npubEncode(event.pubkey)
const json = JSON.stringify(event, null, 2) const json = JSON.stringify(event, null, 2)

View file

@ -3,8 +3,9 @@
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT, ManagementMethod} from "@welshman/util" import {COMMENT, getAddress, ManagementMethod} from "@welshman/util"
import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app" import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
import GalleryWide from "@assets/icons/gallery-wide.svg?dataurl"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl" import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
@ -17,6 +18,7 @@
import Report from "@app/components/Report.svelte" import Report from "@app/components/Report.svelte"
import EventShare from "@app/components/EventShare.svelte" import EventShare from "@app/components/EventShare.svelte"
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte" import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
import PinboardSelect from "@app/components/PinboardSelect.svelte"
import {hasNip29} from "@app/relays" import {hasNip29} from "@app/relays"
import {deriveUserIsSpaceAdmin} from "@app/members" import {deriveUserIsSpaceAdmin} from "@app/members"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
@ -40,6 +42,8 @@
const showInfo = () => pushModal(EventInfo, {url, event}) const showInfo = () => pushModal(EventInfo, {url, event})
const addToLibrary = () => pushModal(PinboardSelect, {url, address: getAddress(event)})
const share = async () => { const share = async () => {
if (hasNip29($relaysByUrl.get(url))) { if (hasNip29($relaysByUrl.get(url))) {
pushModal(EventShare, {url, event}) pushModal(EventShare, {url, event})
@ -79,6 +83,12 @@
</script> </script>
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}> <ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
<li>
<Button onclick={showInfo}>
<Icon size={4} icon={Code2} />
{noun} Details
</Button>
</li>
{#if isRoot} {#if isRoot}
<li> <li>
<Button onclick={share}> <Button onclick={share}>
@ -88,9 +98,9 @@
</li> </li>
{/if} {/if}
<li> <li>
<Button onclick={showInfo}> <Button onclick={addToLibrary}>
<Icon size={4} icon={Code2} /> <Icon size={4} icon={GalleryWide} />
{noun} Details Add to Library
</Button> </Button>
</li> </li>
{@render customActions?.()} {@render customActions?.()}

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames"
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
@ -14,7 +13,7 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import Modal from "@lib/components/Modal.svelte" import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte" import ModalBody from "@lib/components/ModalBody.svelte"
import RoomName from "@app/components/RoomName.svelte" import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
import {roomsByUrl} from "@app/groups" import {roomsByUrl} from "@app/groups"
import {makeRoomPath} from "@app/routes" import {makeRoomPath} from "@app/routes"
@ -40,13 +39,15 @@
<ModalTitle>Share {noun}</ModalTitle> <ModalTitle>Share {noun}</ModalTitle>
<ModalSubtitle>Which room would you like to share this event to?</ModalSubtitle> <ModalSubtitle>Which room would you like to share this event to?</ModalSubtitle>
</ModalHeader> </ModalHeader>
<div class="grid grid-cols-3 gap-2"> <div class="grid grid-cols-2 gap-2">
{#each $roomsByUrl.get(url) || [] as room (room.h)} {#each $roomsByUrl.get(url) || [] as room (room.h)}
<Button <Button
type="button" type="button"
class={cx(`button button-${selection === room.h ? "primary" : "neutral"}`)} class="flex justify-center button {selection === room.h
? 'button-primary'
: 'button-neutral'}"
onclick={() => toggleRoom(room.h)}> onclick={() => toggleRoom(room.h)}>
#<RoomName {...room} /> <RoomNameWithImage {...room} />
</Button> </Button>
{/each} {/each}
</div> </div>

View file

@ -5,9 +5,10 @@
type Props = { type Props = {
url: string url: string
address: string address: string
reference?: string
} }
const {url, address}: Props = $props() const {url, address, reference}: Props = $props()
const submit = async ({title, topics, value}: PinFormValues) => { const submit = async ({title, topics, value}: PinFormValues) => {
const params = referenceToPin(value) const params = referenceToPin(value)
@ -18,4 +19,10 @@
} }
</script> </script>
<PinForm {url} heading="Add Link" action="Add link" successMessage="Link added!" {submit} /> <PinForm
{url}
heading="Add Link"
action="Add link"
successMessage="Link added!"
values={{value: reference}}
{submit} />

View file

@ -21,6 +21,7 @@
import StringMultiInput from "@lib/components/StringMultiInput.svelte" import StringMultiInput from "@lib/components/StringMultiInput.svelte"
import RelayName from "@app/components/RelayName.svelte" import RelayName from "@app/components/RelayName.svelte"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {clearModals} from "@app/modal"
type Props = { type Props = {
url: string url: string
@ -50,7 +51,7 @@
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
} else { } else {
pushToast({message: successMessage}) pushToast({message: successMessage})
back() clearModals()
} }
} finally { } finally {
loading = false loading = false

View file

@ -0,0 +1,59 @@
<script lang="ts">
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import RelayName from "@app/components/RelayName.svelte"
import PinAdd from "@app/components/PinAdd.svelte"
import {deriveBoards, addressToReference, type Board} from "@app/pinboards"
import {makeSpacePath} from "@app/routes"
import {pushModal} from "@app/modal"
type Props = {
url: string
address: string
}
const {url, address}: Props = $props()
const boards = deriveBoards(url)
const reference = addressToReference(address)
const selectBoard = (board: Board) => pushModal(PinAdd, {url, address: board.address, reference})
</script>
<Modal class="flex flex-col gap-2">
<ModalHeader>
<ModalTitle>Add to Library</ModalTitle>
<ModalSubtitle>on <RelayName {url} class="text-primary" /></ModalSubtitle>
</ModalHeader>
<ModalBody class="flex flex-col gap-2">
{#if $boards.length === 0}
<div class="flex flex-col items-center gap-4 py-8 text-center">
<p class="opacity-70">This space doesn't have any shelves yet.</p>
<Link href={makeSpacePath(url, "library")} class="button button-primary">
Go to Library
</Link>
</div>
{:else}
{#each $boards as board (board.address)}
<Button
class="card card-sm card-interactive flex flex-row items-center justify-between gap-3"
onclick={() => selectBoard(board)}>
<span class="flex min-w-0 flex-col">
<strong class="ellipsize">{board.title || "Untitled shelf"}</strong>
{#if board.description}
<span class="ellipsize text-sm opacity-70">{board.description}</span>
{/if}
</span>
<Icon size={4} icon={AltArrowRight} class="shrink-0" />
</Button>
{/each}
{/if}
</ModalBody>
</Modal>

View file

@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {ManagementMethod} from "@welshman/util" import {ManagementMethod, getAddress} from "@welshman/util"
import {pubkey, manageRelay, repository} from "@welshman/app" import {pubkey, manageRelay, repository} from "@welshman/app"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import GalleryWide from "@assets/icons/gallery-wide.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl" import Danger from "@assets/icons/danger.svg?dataurl"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@ -10,6 +11,7 @@
import Confirm from "@lib/components/Confirm.svelte" import Confirm from "@lib/components/Confirm.svelte"
import EventInfo from "@app/components/EventInfo.svelte" import EventInfo from "@app/components/EventInfo.svelte"
import Report from "@app/components/Report.svelte" import Report from "@app/components/Report.svelte"
import PinboardSelect from "@app/components/PinboardSelect.svelte"
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte" import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
@ -25,6 +27,11 @@
const userIsAdmin = deriveUserIsSpaceAdmin(url) const userIsAdmin = deriveUserIsSpaceAdmin(url)
const addToLibrary = () => {
onClick()
pushModal(PinboardSelect, {url, address: getAddress(event)})
}
const report = () => { const report = () => {
onClick() onClick()
pushModal(Report, {url, event}) pushModal(Report, {url, event})
@ -65,7 +72,13 @@
<li> <li>
<Button onclick={showInfo}> <Button onclick={showInfo}>
<Icon size={4} icon={Code2} /> <Icon size={4} icon={Code2} />
Show JSON Message Details
</Button>
</li>
<li>
<Button onclick={addToLibrary}>
<Icon size={4} icon={GalleryWide} />
Add to Library
</Button> </Button>
</li> </li>
{#if event.pubkey === $pubkey} {#if event.pubkey === $pubkey}

View file

@ -28,13 +28,12 @@
<svelte:document onmousemove={onMouseMove} /> <svelte:document onmousemove={onMouseMove} />
<Tippy <Button onclick={open} class="button button-xs button-neutral join-item">
bind:popover <Tippy
component={RoomItemMenu} bind:popover
class="button button-xs button-neutral join-item" component={RoomItemMenu}
props={{url, event, onClick}} props={{url, event, onClick}}
params={{trigger: "manual", interactive: true}}> params={{trigger: "manual", interactive: true}}>
<Button onclick={open}>
<Icon icon={MenuDots} size={4} /> <Icon icon={MenuDots} size={4} />
</Button> </Tippy>
</Tippy> </Button>

View file

@ -80,7 +80,7 @@
</div> </div>
{:else if $members.length === 0} {:else if $members.length === 0}
<div class="card bg-surface p-4"> <div class="card bg-surface p-4">
<span class="text-muted">No members yet</span> <span class="text-content-muted">No members yet</span>
</div> </div>
{:else} {:else}
{#each $members as pubkey (pubkey)} {#each $members as pubkey (pubkey)}

View file

@ -11,11 +11,9 @@
const {url, h, ...props}: Props = $props() const {url, h, ...props}: Props = $props()
</script> </script>
<div class="flex grow items-center justify-between gap-4 {props.class}"> <div class="flex min-w-0 items-center gap-2 {props.class}">
<div class="flex items-center gap-2"> <RoomImage {url} {h} />
<RoomImage {url} {h} /> <div class="min-w-0 overflow-hidden text-ellipsis">
<div class="min-w-0 overflow-hidden text-ellipsis"> <RoomName {url} {h} />
<RoomName {url} {h} />
</div>
</div> </div>
</div> </div>

View file

@ -34,7 +34,7 @@
<SecondaryNavItem href={path} title={roomName} {replaceState} {notification}> <SecondaryNavItem href={path} title={roomName} {replaceState} {notification}>
<RoomNameWithImage {url} {h} /> <RoomNameWithImage {url} {h} />
{#if showDifferenceIcon} {#if showDifferenceIcon}
<Icon icon={$shouldNotifyForRoom ? Bell : BellOff} size={4} class="opacity-50" /> <Icon icon={$shouldNotifyForRoom ? Bell : BellOff} size={4} class="ml-auto opacity-50" />
{/if} {/if}
</SecondaryNavItem> </SecondaryNavItem>
{/if} {/if}

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import RoomName from "@app/components/RoomName.svelte" import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
import ThreadBoardItem from "@app/components/ThreadBoardItem.svelte" import ThreadBoardItem from "@app/components/ThreadBoardItem.svelte"
type Props = { type Props = {
@ -13,27 +13,28 @@
</script> </script>
<section class="card card-flat overflow-hidden pb-4 p-0"> <section class="card card-flat overflow-hidden pb-4 p-0">
<header class="flex gap-2 justify-between bg-surface px-4 py-2.5"> <header
<h2 class="text-sm font-bold"> class="flex gap-2 items-center justify-between px-4 py-3 border-b border-line border-solid">
<h2 class="text-lg">
{#if h} {#if h}
#<RoomName {url} {h} /> <RoomNameWithImage {url} {h} />
{:else} {:else}
General General
{/if} {/if}
</h2> </h2>
<span class="text-xs text-muted"> <span class="text-content-muted text-sm">
{threads.length} {threads.length}
{threads.length === 1 ? "topic" : "topics"} {threads.length === 1 ? "Topic" : "Topics"}
</span> </span>
</header> </header>
<table class="w-full border-collapse"> <table class="w-full border-collapse">
<thead <thead
class="hidden text-xs font-bold uppercase tracking-wide text-muted sm:table-header-group"> class="hidden text-xs font-bold uppercase tracking-wide text-content-muted sm:table-header-group border-b border-line border-solid bg-surface-less">
<tr> <tr>
<th class="px-4 py-2 text-left">Topic</th> <th class="px-4 py-3 text-left">Topic</th>
<th class="w-32 px-4 py-2 text-left">Author</th> <th class="w-32 px-4 py-3 text-left">Author</th>
<th class="w-20 px-4 py-2 text-center">Replies</th> <th class="w-20 px-4 py-3 text-center">Replies</th>
<th class="w-32 px-4 py-2 text-right">Last post</th> <th class="w-32 px-4 py-3 text-right">Last post</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View file

@ -3,7 +3,6 @@
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT, getTagValue} from "@welshman/util" import {COMMENT, getTagValue} from "@welshman/util"
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import Link from "@lib/components/Link.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileName from "@app/components/ProfileName.svelte" import ProfileName from "@app/components/ProfileName.svelte"
import {deriveEventsForUrl} from "@app/repository" import {deriveEventsForUrl} from "@app/repository"
@ -25,21 +24,19 @@
const goToThread = () => goto(path) const goToThread = () => goto(path)
</script> </script>
<tr class="cursor-pointer transition-colors hover:bg-surface text-sm" onclick={goToThread}> <tr
<td class="px-4 py-2 align-top"> class="cursor-pointer transition-colors hover:bg-surface-less text-sm border-b border-solid border-line"
<Link href={path} class="ellipsize font-semibold"> onclick={goToThread}>
{title || "Untitled thread"} <td class="px-4 py-3 align-top ellipsize">
</Link> {title || "Untitled thread"}
</td> </td>
<td class="px-4 py-2 align-middle"> <td class="px-4 py-3 align-middle flex items-center gap-2">
<div class="flex items-center gap-2"> <ProfileCircle pubkey={event.pubkey} {url} size={5} />
<ProfileCircle pubkey={event.pubkey} {url} size={5} /> <span class="ellipsize">
<span class="ellipsize"> <ProfileName pubkey={event.pubkey} {url} />
<ProfileName pubkey={event.pubkey} {url} /> </span>
</span>
</div>
</td> </td>
<td class="px-4 py-2 align-middle text-right"> <td class="px-4 py-3 align-middle text-right">
{replyCount} {replyCount}
</td> </td>
<td class="whitespace-nowrap px-4 py-2 align-middle text-right"> <td class="whitespace-nowrap px-4 py-2 align-middle text-right">

View file

@ -138,9 +138,9 @@
{:else if $callState === CallState.Connected} {:else if $callState === CallState.Connected}
<span class="text-sm font-semibold text-success">Voice Connected</span> <span class="text-sm font-semibold text-success">Voice Connected</span>
{:else} {:else}
<span class="text-sm font-semibold text-muted">Disconnected</span> <span class="text-sm font-semibold text-content-muted">Disconnected</span>
{/if} {/if}
<span class="ellipsize text-xs text-muted"> <span class="ellipsize text-xs text-content-muted">
{roomName} / {spaceName} {roomName} / {spaceName}
</span> </span>
</div> </div>

View file

@ -33,7 +33,6 @@ import {
getTagValue, getTagValue,
matchFilters, matchFilters,
readList, readList,
sortEventsDesc,
} from "@welshman/util" } from "@welshman/util"
import type {Filter, PublishedList, TrustedEvent} from "@welshman/util" import type {Filter, PublishedList, TrustedEvent} from "@welshman/util"
import {load, mergeRepositoryUpdates, request} from "@welshman/net" import {load, mergeRepositoryUpdates, request} from "@welshman/net"
@ -124,13 +123,8 @@ export const makeFeed = ({
const events = writable<TrustedEvent[]>([]) const events = writable<TrustedEvent[]>([])
const seen = new Set<string>() const seen = new Set<string>()
let interval = int(MONTH) let interval = int(6, MONTH)
let buffer = sortEventsDesc( let buffer: TrustedEvent[] = []
uniqBy(
e => e.id,
relays.flatMap(url => Array.from(getEventsForUrl(url, filters))),
),
)
let backwardWindow = [at - interval, at] let backwardWindow = [at - interval, at]
let forwardWindow = [at, at + interval] let forwardWindow = [at, at + interval]
@ -145,7 +139,7 @@ export const makeFeed = ({
} }
// Batch-insert events into the visible store with a single update // Batch-insert events into the visible store with a single update
const insertEvents = (newEvents: TrustedEvent[]) => { const insertEvents = (newEvents: Iterable<TrustedEvent>) => {
const visible: TrustedEvent[] = [] const visible: TrustedEvent[] = []
for (const event of newEvents) { for (const event of newEvents) {
@ -153,10 +147,9 @@ export const makeFeed = ({
continue continue
} }
seen.add(event.id)
if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) { if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) {
visible.push(event) visible.push(event)
seen.add(event.id)
} else { } else {
insertIntoBuffer(event) insertIntoBuffer(event)
} }
@ -274,6 +267,10 @@ export const makeFeed = ({
}, },
}) })
for (const url of relays) {
insertEvents(getEventsForUrl(url, filters))
}
return { return {
events, events,
cleanup: () => { cleanup: () => {

View file

@ -166,8 +166,8 @@ export const deleteBoard = (url: string, address: string) =>
]), ]),
) )
export const pinToReference = (pin: Pin): string => { const valueToReference = (value: string[]): string => {
const [type, data = "", relay] = pin.value const [type, data = "", relay] = value
const relays = relay ? [relay] : [] const relays = relay ? [relay] : []
if (type === "e") return nip19.neventEncode({id: data, relays}) if (type === "e") return nip19.neventEncode({id: data, relays})
@ -181,6 +181,10 @@ export const pinToReference = (pin: Pin): string => {
return data return data
} }
export const pinToReference = (pin: Pin): string => valueToReference(pin.value)
export const addressToReference = (address: string): string => valueToReference(["a", address])
export const referenceToPin = (reference: string): Partial<Pin> | undefined => { export const referenceToPin = (reference: string): Partial<Pin> | undefined => {
const trimmed = reference.trim() const trimmed = reference.trim()

View file

@ -8,7 +8,7 @@
const {children, ...props}: Props = $props() const {children, ...props}: Props = $props()
</script> </script>
<div class="flex items-center gap-2 p-2 text-xs uppercase text-subtle"> <div class="flex items-center gap-2 p-2 text-xs uppercase text-content-subtle">
<div class="h-px grow" style="background-color: var(--line)"></div> <div class="h-px grow" style="background-color: var(--line)"></div>
{#if children} {#if children}
<p>{@render children?.()}</p> <p>{@render children?.()}</p>

View file

@ -23,7 +23,7 @@
</div> </div>
</div> </div>
{#if props.info} {#if props.info}
<p class="text-sm text-muted"> <p class="text-sm text-content-muted">
{@render props.info()} {@render props.info()}
</p> </p>
{/if} {/if}

View file

@ -119,7 +119,7 @@
ondrop={stopPropagation(preventDefault(onDrop))}> ondrop={stopPropagation(preventDefault(onDrop))}>
<div class="flex flex-col items-center gap-2 text-center"> <div class="flex flex-col items-center gap-2 text-center">
<Icon icon={GallerySend} size={8} /> <Icon icon={GallerySend} size={8} />
<p class="text-muted text-sm">Drag and drop images or click to select</p> <p class="text-content-muted text-sm">Drag and drop images or click to select</p>
</div> </div>
</label> </label>
</div> </div>

View file

@ -8,4 +8,4 @@
const {children}: Props = $props() const {children}: Props = $props()
</script> </script>
<p class="text-muted text-center text-sm">{@render children()}</p> <p class="text-content-muted text-center text-sm">{@render children()}</p>

View file

@ -20,8 +20,7 @@
.input:focus-within, .input:focus-within,
.tiptap:focus, .tiptap:focus,
.tiptap:focus-within { .tiptap:focus-within {
@apply outline-none; @apply border-primary outline-none;
border: 1px solid var(--primary) !important;
box-shadow: box-shadow:
0 0 0 4px color-mix(in srgb, var(--primary) 18%, transparent), 0 0 0 4px color-mix(in srgb, var(--primary) 18%, transparent),
inset 0 2px 4px rgba(0, 0, 0, 0.04); inset 0 2px 4px rgba(0, 0, 0, 0.04);

View file

@ -265,7 +265,7 @@
onclick={() => openSpace(url)}> onclick={() => openSpace(url)}>
<div class="flex w-full items-start gap-2"> <div class="flex w-full items-start gap-2">
<div <div
class="mt-4 flex cursor-grab p-1 text-subtle transition-colors group-hover:text-muted"> class="mt-4 flex cursor-grab p-1 text-content-subtle transition-colors group-hover:text-content-muted">
<Icon icon={DragHandle} /> <Icon icon={DragHandle} />
</div> </div>
<RelaySummary hideFavorites {url} /> <RelaySummary hideFavorites {url} />

View file

@ -128,23 +128,21 @@
</SpaceBar> </SpaceBar>
<PageContent class="flex flex-col gap-4 p-4"> <PageContent class="flex flex-col gap-4 p-4">
<div class="card flex flex-col gap-2"> <label class="input input-group flex w-full items-center gap-2 card">
<label class="input input-sm input-group flex w-full items-center gap-2"> <Icon size={4} icon={Magnifier} />
<Icon size={4} icon={Magnifier} /> <input
<input bind:value={term}
bind:value={term} class="min-w-0 grow"
class="min-w-0 grow" type="text"
type="text" placeholder="Search people or roles..." />
placeholder="Search people or roles..." /> </label>
</label> {#if visibleMembers.length === 0}
{#if visibleMembers.length === 0} <p class="flex flex-col items-center py-20 text-center">No members found.</p>
<p class="flex flex-col items-center py-20 text-center">No members found.</p> {:else}
{:else} <div class="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-2 xl:grid-cols-3"> {#each visibleMembers as { pubkey, roleList } (pubkey)}
{#each visibleMembers as { pubkey, roleList } (pubkey)} <SpaceMember {url} {pubkey} roles={roleList} />
<SpaceMember {url} {pubkey} roles={roleList} /> {/each}
{/each} </div>
</div> {/if}
{/if}
</div>
</PageContent> </PageContent>

View file

@ -86,22 +86,20 @@
</SpaceBar> </SpaceBar>
<PageContent class="flex flex-col gap-4 p-4"> <PageContent class="flex flex-col gap-4 p-4">
<div class="card flex flex-col gap-2"> <label class="card input input-group flex w-full items-center gap-2">
<label class="input input-sm input-group flex w-full items-center gap-2"> <Icon size={4} icon={Magnifier} />
<Icon size={4} icon={Magnifier} /> <input bind:value={term} class="min-w-0 grow" type="text" placeholder="Search library..." />
<input bind:value={term} class="min-w-0 grow" type="text" placeholder="Search library..." /> </label>
</label> <Masonry items={filtered} getKey={board => board.address} columnWidth={80} gap={4}>
<Masonry items={filtered} getKey={board => board.address} columnWidth={80} gap={2}> {#snippet child(board)}
{#snippet child(board)} <PinboardItem {url} {board} />
<PinboardItem {url} {board} /> {/snippet}
{/snippet} </Masonry>
</Masonry> {#if loading}
{#if loading} <p class="flex items-center justify-center py-20">
<p class="flex items-center justify-center py-20"> <Spinner loading>Loading library...</Spinner>
<Spinner loading>Loading library...</Spinner> </p>
</p> {:else if filtered.length === 0}
{:else if filtered.length === 0} <p class="flex flex-col items-center py-20 text-center">No shelves found.</p>
<p class="flex flex-col items-center py-20 text-center">No shelves found.</p> {/if}
{/if}
</div>
</PageContent> </PageContent>