Lay the home dashboard out as full-width sections

This commit is contained in:
Coracle-Bot 2026-09-08 23:30:23 +00:00 committed by hodlbod
parent d3ceaac1b9
commit c4934db5ff
10 changed files with 178 additions and 130 deletions

View file

@ -2,7 +2,6 @@
import Stars from "@assets/icons/stars.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Card from "@lib/components/Card.svelte"
import type {HealthCheck} from "@app/healthChecks"
import {healthChecks} from "@app/healthChecks"
@ -15,13 +14,13 @@
const apply = () => $healthChecks.apply(healthCheck)
</script>
<Card sm class="bg-surface flex justify-between">
<div class="flex flex-col gap-1">
<strong>{healthCheck.title}</strong>
<p class="text-sm">{healthCheck.description}</p>
<div class="flex items-start justify-between gap-3 px-4 py-3">
<div class="flex min-w-0 flex-col gap-1">
<strong class="text-sm">{healthCheck.title}</strong>
<p class="text-sm opacity-75">{healthCheck.description}</p>
</div>
<Button class="button button-neutral button-sm" onclick={apply}>
<Icon icon={Stars} />
<Button class="button button-neutral button-sm shrink-0" onclick={apply}>
<Icon icon={Stars} size={4} />
{healthCheck.action}
</Button>
</Card>
</div>

View file

@ -5,6 +5,7 @@
import Icon from "@lib/components/Icon.svelte"
import Badge from "@lib/components/Badge.svelte"
import Button from "@lib/components/Button.svelte"
import HomeSection from "@app/components/HomeSection.svelte"
import HealthCheckItem from "@app/components/HealthCheckItem.svelte"
import {healthChecks} from "@app/healthChecks"
@ -17,30 +18,30 @@
}
</script>
<div class="card flex flex-col gap-3">
<div class="flex items-center justify-between gap-3">
<strong class="flex items-center gap-2 text-lg">
<Icon icon={Stethoscope} />
Health checks
</strong>
<HomeSection title="Health checks" icon={Stethoscope}>
{#snippet action()}
{#if $pending.length > 0}
<Badge variant="warning">{$pending.length} open</Badge>
{/if}
</div>
{/snippet}
{#if $pending.length === 0}
<p class="flex items-center gap-2 text-sm opacity-75">
<p class="flex items-center gap-2 px-4 pb-4 text-sm opacity-75">
<Icon icon={CheckCircle} size={4} />
Your connection to the network looks healthy.
</p>
{:else}
{#each $pending as healthCheck (healthCheck.title)}
<HealthCheckItem {healthCheck} />
{/each}
{#if $pending.length > 1}
<Button class="button button-primary button-sm" onclick={applyAll}>
<Icon icon={Stars} size={4} />
Apply all recommendations
</Button>
{/if}
<div class="flex flex-col divide-y divide-line border-t border-line">
{#each $pending as healthCheck (healthCheck.title)}
<HealthCheckItem {healthCheck} />
{/each}
{#if $pending.length > 1}
<div class="flex justify-center p-3">
<Button class="button button-primary button-sm" onclick={applyAll}>
<Icon icon={Stars} size={4} />
Apply all recommendations
</Button>
</div>
{/if}
</div>
{/if}
</div>
</HomeSection>

View file

@ -5,6 +5,7 @@
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import HomeSection from "@app/components/HomeSection.svelte"
import RelayListItem from "@app/components/hosting/RelayListItem.svelte"
import RelayCreate from "@app/components/hosting/RelayCreate.svelte"
import {user} from "@app/core"
@ -31,30 +32,32 @@
</script>
{#if loaded}
<div class="card flex flex-col gap-3">
<div class="flex items-center justify-between gap-3">
<strong class="flex items-center gap-2 text-lg">
<Icon icon={Server} />
Hosting
</strong>
<HomeSection title="Hosting" icon={Server}>
{#snippet action()}
{#if relays.length > 0}
<Link href="/settings/hosting" class="button button-neutral button-xs">Manage</Link>
{/if}
{/snippet}
{#if relays.length > 0}
<div class="flex flex-col divide-y divide-line border-y border-line">
{#each relays as relay (relay.id)}
<RelayListItem {relay} class="px-4 py-3" />
{/each}
</div>
{/if}
<div class="flex flex-col items-start gap-3 px-4 pt-3 pb-4">
<p class="text-sm opacity-75">
{#if relays.length > 0}
Spin up another hosted space — rooms, calendar and moderation included.
{:else}
Don't wait for an invite. Flotilla Hosting sets up a space with rooms, calendar and
moderation in about a minute.
{/if}
</p>
<Button class="button button-primary button-sm" onclick={openCreate}>
<Icon icon={Add} size={4} />
{relays.length > 0 ? "Add another space" : "Start a space"}
</Button>
</div>
{#each relays as relay (relay.id)}
<RelayListItem {relay} />
{/each}
<p class="text-sm opacity-75">
{#if relays.length > 0}
Spin up another hosted space — rooms, calendar and moderation included.
{:else}
Don't wait for an invite. Flotilla Hosting sets up a space with rooms, calendar and
moderation in about a minute.
{/if}
</p>
<Button class="button button-primary button-sm" onclick={openCreate}>
<Icon icon={Add} size={4} />
{relays.length > 0 ? "Add another space" : "Start a space"}
</Button>
</div>
</HomeSection>
{/if}

View file

@ -5,7 +5,9 @@
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import HomeSection from "@app/components/HomeSection.svelte"
import HomeInboxItem from "@app/components/HomeInboxItem.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte"
import {displayContentCount} from "@app/content"
import {inboxConversations, inboxSpaceContent} from "@app/inbox"
@ -27,18 +29,14 @@
const startChat = () => navigate("/chat")
</script>
<div class="card flex flex-col gap-3">
<div class="flex items-center justify-between gap-3">
<strong class="flex items-center gap-2 text-lg">
<Icon icon={Inbox} />
Inbox
</strong>
<HomeSection title="Inbox" icon={Inbox}>
{#snippet action()}
{#if hasUnread}
<Button class="button button-neutral button-xs" onclick={markAllRead}>Mark all read</Button>
{/if}
</div>
{#if conversations.length === 0}
<div class="flex flex-col items-center gap-3 py-8 text-center">
{/snippet}
{#if conversations.length === 0 && $inboxSpaceContent.length === 0}
<div class="flex flex-col items-center gap-3 px-4 pb-8 text-center">
<p class="font-medium">Nothing in your inbox yet</p>
<p class="max-w-md text-sm opacity-75">
Rooms and spaces you belong to report their activity here — messages, threads, classifieds,
@ -56,16 +54,19 @@
</div>
</div>
{:else}
<div class="flex flex-col gap-2">
<div class="flex flex-col divide-y divide-line border-t border-line">
{#each conversations as conversation (conversation.path)}
<HomeInboxItem {conversation} />
{/each}
{#each $inboxSpaceContent as { url, countsByKind } (url)}
<Link
href={makeSpacePath(url)}
class="flex items-center gap-3 px-4 py-3 text-sm hover:bg-surface-more">
<RelayIcon {url} size={6} class="shrink-0" />
<strong class="truncate"><RelayName {url} /></strong>
<span class="truncate opacity-75">{displayContent(countsByKind)}</span>
</Link>
{/each}
</div>
{/if}
{#each $inboxSpaceContent as { url, countsByKind } (url)}
<Link href={makeSpacePath(url)} class="flex flex-wrap items-baseline gap-x-2 text-sm">
<strong><RelayName {url} /></strong>
<span class="opacity-75">{displayContent(countsByKind)}</span>
</Link>
{/each}
</div>
</HomeSection>

View file

@ -1,10 +1,9 @@
<script lang="ts">
import {formatTimestamp, remove, uniq} from "@welshman/lib"
import ChatRoundDots from "@assets/icons/chat-round-dots.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte"
import RoomName from "@app/components/RoomName.svelte"
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
@ -25,23 +24,22 @@
const others = $derived(uniq(remove($user.pubkey, conversation.pubkeys ?? [])))
</script>
<Link href={path} class="card card-sm card-interactive flex flex-col gap-2">
<div class="flex min-w-0 items-center gap-2 text-sm">
{#if url}
<Link href={path} class="flex items-center gap-3 px-4 py-3 hover:bg-surface-more">
{#if url}
<RelayIcon {url} size={9} class="shrink-0" />
{:else}
<ProfileCircle pubkey={others[0] || $user.pubkey} size={9} class="shrink-0" />
{/if}
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<div class="flex min-w-0 items-baseline gap-2 text-sm">
<strong class="truncate">
{#if h}
<RoomName {url} {h} />
{:else}
Chat
{/if}
</strong>
<span class="truncate opacity-50">
<RelayName {url} />
</span>
{:else}
<Icon icon={ChatRoundDots} size={4} class="shrink-0 opacity-50" />
<strong class="truncate">
{#if others.length === 0}
{#if url}
{#if h}
<RoomName {url} {h} />
{:else}
Chat
{/if}
{:else if others.length === 0}
Note to self
{:else}
<ProfileName pubkey={others[0]} />
@ -50,24 +48,29 @@
{/if}
{/if}
</strong>
<span class="truncate opacity-50">direct message</span>
{/if}
<span class="ml-auto flex shrink-0 items-center gap-2 text-xs opacity-50">
{formatTimestamp(event.created_at)}
<UnreadDot {path} />
</span>
</div>
<div class="flex min-w-0 items-center gap-2">
<ProfileCircle pubkey={event.pubkey} size={5} />
<span class="shrink-0 text-xs opacity-50">
{#if event.pubkey === $user.pubkey}
You:
{:else}
<ProfileName pubkey={event.pubkey} {url} />:
{/if}
</span>
<div class="min-w-0 flex-1">
<NoteContentMinimal {event} {url} singleLine />
<span class="truncate opacity-50">
{#if url}
<RelayName {url} />
{:else}
direct message
{/if}
</span>
</div>
<div class="flex min-w-0 items-center gap-1 text-sm opacity-75">
<span class="shrink-0">
{#if event.pubkey === $user.pubkey}
You:
{:else}
<ProfileName pubkey={event.pubkey} {url} />:
{/if}
</span>
<div class="min-w-0 flex-1">
<NoteContentMinimal {event} {url} singleLine />
</div>
</div>
</div>
<div class="flex shrink-0 items-center gap-2 text-xs opacity-50">
{formatTimestamp(event.created_at)}
<UnreadDot {path} />
</div>
</Link>

View file

@ -7,10 +7,10 @@
import {Feeds} from "@welshman/app"
import {Scope, feedFromFilter, makeIntersectionFeed, makeScopeFeed} from "@welshman/feeds"
import Planet from "@assets/icons/planet.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import HomeSection from "@app/components/HomeSection.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
import {app, relayLists, user} from "@app/core"
import {makeFeedContext} from "@app/feeds"
@ -60,18 +60,14 @@
onMount(load)
</script>
<div class="card flex flex-col gap-3">
<strong class="flex items-center gap-2 text-lg">
<Icon icon={Planet} />
Network
</strong>
<HomeSection title="Network" icon={Planet}>
{#if events.length === 0}
{#if loading}
<div class="flex justify-center py-8">
<div class="flex justify-center px-4 pb-8">
<Spinner {loading}>Looking for notes from people you follow…</Spinner>
</div>
{:else}
<div class="flex flex-col items-center gap-3 py-8 text-center">
<div class="flex flex-col items-center gap-3 px-4 pb-8 text-center">
<p class="font-medium">Follow a few people to fill this out</p>
<p class="max-w-md text-sm opacity-75">
Notes from the people you follow collect here. Spaces are a good place to find some.
@ -80,11 +76,13 @@
</div>
{/if}
{:else}
<div class="flex flex-col gap-2">
<div class="flex flex-col divide-y divide-line border-t border-line">
{#each events.slice(0, limit) as event (event.id)}
<NoteItem {event} {context} />
<NoteItem {event} {context} class="cv px-4 py-3" />
{/each}
</div>
<Button class="button button-neutral button-sm" onclick={showMore}>Show more</Button>
<div class="flex justify-center border-t border-line p-3">
<Button class="button button-neutral button-sm" onclick={showMore}>Show more</Button>
</div>
{/if}
</div>
</HomeSection>

View file

@ -0,0 +1,24 @@
<script lang="ts">
import type {Snippet} from "svelte"
import Icon from "@lib/components/Icon.svelte"
type Props = {
title: string
icon: string
action?: Snippet
children: Snippet
}
const {title, icon, action, children}: Props = $props()
</script>
<section class="flex min-w-0 flex-col border-b border-line last:border-b-0">
<div class="flex items-center justify-between gap-3 px-4 pt-4 pb-3">
<h2 class="flex items-center gap-2 text-xs font-bold tracking-widest uppercase opacity-75">
<Icon {icon} size={4} />
{title}
</h2>
{@render action?.()}
</div>
{@render children()}
</section>

View file

@ -19,9 +19,16 @@
children?: Snippet
context: FeedContext
url?: string
class?: string
}
const {url, event, children, context}: Props = $props()
const {
url,
event,
children,
context,
class: className = "card card-interactive",
}: Props = $props()
const getRelays = () => (url ? [url] : $router.resolver.relays([seen(event)]))
@ -34,7 +41,7 @@
const onEmoji = (emoji: NativeEmoji) => createReaction({content: emoji.unicode, tags: []})
</script>
<Cv tag={NoteCard} {event} {url} class="card card-interactive">
<Cv tag={NoteCard} {event} {url} class={className}>
<NoteContent {event} expandMode="inline" />
<div class="flex w-full justify-between gap-2">
<ReactionSummary

View file

@ -9,16 +9,17 @@
type Props = {
relay: HostedRelay
class?: string
}
const {relay}: Props = $props()
const {relay, class: className = "card p-3 sm:p-4"}: Props = $props()
const name = $derived(relay.info_name || relay.subdomain)
const host = $derived(canonicalRelayHost(relay))
const href = $derived(makeSpacePath(getHostedRelayUrl(relay), "admin"))
</script>
<div class="card flex flex-row items-center justify-between gap-3 p-3 sm:p-4">
<div class="flex flex-row items-center justify-between gap-3 {className}">
<ImageIcon size={8} alt="" class="rounded-xl" src={relay.info_icon || Server} />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">{name}</p>

View file

@ -13,6 +13,12 @@
import {HOSTING_ENABLED} from "@app/hosting"
import {PLATFORM_RELAYS} from "@app/env"
const today = new Date().toLocaleDateString(undefined, {
weekday: "long",
month: "short",
day: "numeric",
})
// A single-space build has no dashboard to show - everything on it is scoped to the one space.
onMount(() => {
if (PLATFORM_RELAYS.length > 0) {
@ -23,21 +29,26 @@
<Page>
<PageBar>
<div class="flex items-center gap-2">
<Icon icon={Home} size={6} />
<strong>Home</strong>
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<Icon icon={Home} size={6} />
<strong>Home</strong>
</div>
<span class="hidden text-sm opacity-75 sm:inline">{today}</span>
</div>
</PageBar>
<PageContent class="flex flex-col gap-4 p-2 sm:p-4 lg:flex-row lg:items-start">
<div class="flex min-w-0 flex-col gap-4 lg:flex-[2]">
<HomeInbox />
<HomeNetwork />
</div>
<div class="flex min-w-0 flex-col gap-4 lg:flex-1">
<HomeHealthChecks />
{#if HOSTING_ENABLED}
<HomeHosting />
{/if}
<PageContent noPad class="flex flex-col">
<div class="flex min-w-0 flex-col lg:flex-row lg:items-stretch">
<div class="flex min-w-0 flex-col bg-surface lg:flex-[2] lg:border-r lg:border-line">
<HomeInbox />
<HomeNetwork />
</div>
<div class="flex min-w-0 flex-col border-t border-line lg:w-80 lg:shrink-0 lg:border-t-0">
<HomeHealthChecks />
{#if HOSTING_ENABLED}
<HomeHosting />
{/if}
</div>
</div>
</PageContent>
</Page>