Delete dead lib code and the Card, Badge and Input class wrappers, move the icon workaround into ImageIcon, and type the remaining lib props

This commit is contained in:
Jon Staab 2026-09-26 00:50:26 -07:00
parent c8c6bd4465
commit 66bb75797b
71 changed files with 385 additions and 602 deletions

View file

@ -229,11 +229,14 @@ lives there:
- page chrome: `Page`, `PageBar`, `PageContent`, `SecondaryNav*`, `PrimaryNavItem`, `FAB`
- modal chrome: `Dialog`, `Drawer`, `Modal`, `ModalBody`, `ModalHeader`, `ModalTitle`,
`ModalSubtitle`, `ModalFooter`, `Confirm`
- controls: `Button`, `Link`, `Field`, `FieldInline`, `Input`, `InputList`, `ToggleInput`,
- controls: `Button`, `Link`, `Field`, `FieldInline`, `InputList`, `ToggleInput`,
`DateTimeInput`, `ImagesInput`, `EmojiPicker`, `MenuButton`, `SegmentedButtons`
- display and lists: `Icon`, `Badge`, `Card`, `Divider`, `Spinner`, `LoadingFallback`, `Tooltip`,
- display and lists: `Icon`, `ImageIcon`, `Divider`, `Spinner`, `LoadingFallback`, `Tooltip`,
`Tippy`, `Cv`, `VirtualList`, `Masonry`, `DragList`, `ScrollToTop`
- the CSS component families (`button.css`, `card.css`, …) and the theme tokens
- the CSS component families (`button.css`, `card.css`, `badge.css`, `input.css`, …) and the
theme tokens. Cards, badges and inputs are plain markup with these classes
(`<div class="card card-sm">`, `<span class="badge badge-primary">`,
`<div class="input input-group"><input /></div>`) rather than components.
Anything that takes an identifier and reads a store, publishes, or knows a kind is an app
component. A lib component that needs app behavior takes it as a prop, such as `MenuButton`'s
@ -467,7 +470,7 @@ Rich text uses `makeEditor` from `src/app/editor` with `EditorContent`.
- Colors are the semantic tokens registered in `base.css`: `bg-surface`, `bg-surface-more`,
`text-content`, `text-content-muted`, `border-line`, `text-primary`, `text-error`. The clay,
flat, navy and nature themes supply the values through `data-fl-theme`.
- Seven `class:` directives remain as leftovers; everything else builds classes with `cx`.
- A few `class:` directives remain in `src/app` as leftovers; everything else builds classes with `cx`.
- Container queries go on the `PageContent`, as in `@2xl:grid-cols-2` for the classifieds grid.
- Icons are `import X from "@assets/icons/<name>.svg?dataurl"` with `<Icon icon={X} size={4} />`,
where `size` counts 4px steps. List entries animate with `in:fly` from `@lib/transition`.

View file

@ -1,8 +1,8 @@
<script lang="ts">
import cx from "classnames"
import type {TrustedEvent} from "@welshman/util"
import {Classified} from "@welshman/domain"
import {ucFirst} from "@lib/util"
import Badge from "@lib/components/Badge.svelte"
import {reader} from "@app/core"
type Props = {
@ -15,7 +15,11 @@
</script>
{#if status}
<Badge variant={status !== "active" ? "primary" : "neutral"} class="badge-sm rounded-full">
<span
class={cx("badge badge-sm rounded-full", {
"badge-primary": status !== "active",
"badge-neutral": status === "active",
})}>
{ucFirst(status)}
</Badge>
</span>
{/if}

View file

@ -26,7 +26,6 @@
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Card from "@lib/components/Card.svelte"
import ContentToken from "@app/components/ContentToken.svelte"
import ContentEmoji from "@app/components/ContentEmoji.svelte"
import ContentEmail from "@app/components/ContentEmail.svelte"
@ -167,13 +166,13 @@
<div class="relative">
{#if warning}
<Card sm class="shadow-none flex gap-2">
<div class="card card-sm shadow-none flex gap-2">
<Icon icon={Danger} />
<p>
This note has been flagged by the author as "{warning}".<br />
<Button onclick={ignoreWarning} class="button button-link">Show anyway</Button>
</p>
</Card>
</div>
{:else}
<div
class="overflow-hidden text-ellipsis wrap-anywhere"

View file

@ -10,7 +10,6 @@
import Icon from "@lib/components/Icon.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
import Button from "@lib/components/Button.svelte"
import Badge from "@lib/components/Badge.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
@ -99,9 +98,9 @@
{#snippet input()}
<div class="flex flex-wrap gap-2">
{#each seenOn as url, i (url)}
<Badge class="bg-surface flex gap-1">
<span class="badge badge-neutral bg-surface flex gap-1">
<span>{displayRelayUrl(url)}</span>
</Badge>
</span>
{/each}
</div>
{/snippet}

View file

@ -3,7 +3,6 @@
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
import Stethoscope from "@assets/icons/stethoscope.svg?dataurl"
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"
@ -48,7 +47,7 @@
<HomeSection title="Health checks" icon={Stethoscope}>
{#snippet action()}
{#if count > 0}
<Badge variant="warning">{count} open</Badge>
<span class="badge badge-warning">{count} open</span>
{/if}
{/snippet}
{#if count === 0}

View file

@ -1,7 +1,6 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {Pin} from "@welshman/domain"
import Badge from "@lib/components/Badge.svelte"
import Content from "@app/components/Content.svelte"
import PinContentEvent from "@app/components/PinContentEvent.svelte"
import {reader} from "@app/core"
@ -34,7 +33,7 @@
{#if pin.topics().length > 0}
<div class="mt-auto flex flex-wrap gap-1">
{#each pin.topics() as topic (topic)}
<Badge>#{topic}</Badge>
<span class="badge badge-neutral">#{topic}</span>
{/each}
</div>
{/if}

View file

@ -3,7 +3,6 @@
import {removeUndefined} from "@welshman/lib"
import {Address} from "@welshman/util"
import type {PinReader} from "@welshman/domain"
import Badge from "@lib/components/Badge.svelte"
import MenuButton from "@lib/components/MenuButton.svelte"
import Content from "@app/components/Content.svelte"
import PinContentEvent from "@app/components/PinContentEvent.svelte"
@ -65,7 +64,7 @@
{#if !minimal && pin.topics().length > 0}
<div class="mt-auto flex flex-wrap gap-1">
{#each pin.topics() as topic (topic)}
<Badge>#{topic}</Badge>
<span class="badge badge-neutral">#{topic}</span>
{/each}
</div>
{/if}

View file

@ -1,7 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import MenuButton from "@lib/components/MenuButton.svelte"
import Badge from "@lib/components/Badge.svelte"
import PomadeSessionMenu from "@app/components/PomadeSessionMenu.svelte"
import {pushToast} from "@app/toast"
import {deletePomadeSession, loadOtherPomadeSessions} from "@app/pomade"
@ -60,12 +59,12 @@
componentProps={{onDelete: () => deleteSession(sessionItem)}} />
</div>
<div class="flex gap-1">
<Badge variant="neutral">
<span class="badge badge-neutral">
Created {formatDate(sessionItem.created_at)}
</Badge>
<Badge variant="neutral">
</span>
<span class="badge badge-neutral">
Active {formatDate(sessionItem.last_activity)}
</Badge>
</span>
</div>
</div>
{/each}

View file

@ -10,7 +10,6 @@
import Icon from "@lib/components/Icon.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
import Button from "@lib/components/Button.svelte"
import Badge from "@lib/components/Badge.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
@ -105,9 +104,9 @@
{#snippet input()}
<div class="flex flex-wrap gap-2">
{#each seenOn as seenUrl (seenUrl)}
<Badge class="bg-surface flex gap-1">
<span class="badge badge-neutral bg-surface flex gap-1">
<span>{displayRelayUrl(seenUrl)}</span>
</Badge>
</span>
{/each}
</div>
{/snippet}

View file

@ -12,7 +12,6 @@
import Tippy from "@lib/components/Tippy.svelte"
import type {TippyController} from "@lib/components/Tippy.svelte"
import Button from "@lib/components/Button.svelte"
import Badge from "@lib/components/Badge.svelte"
import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
@ -74,14 +73,14 @@
<div>
{#each value as pubkey (pubkey)}
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
<Badge variant="neutral" class="mr-1 mb-1 gap-1">
<span class="badge badge-neutral mr-1 mb-1 gap-1">
<Button class="flex items-center" onclick={() => removePubkey(pubkey)}>
<Icon icon={CloseCircle} size={4} />
</Button>
<Button onclick={onClick}>
<ProfileName {pubkey} />
</Button>
</Badge>
</span>
{/each}
</div>
<label class="input flex w-full items-center gap-2" bind:this={label}>

View file

@ -2,7 +2,6 @@
import cx from "classnames"
import UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Badge from "@lib/components/Badge.svelte"
import Link from "@lib/components/Link.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte"
@ -28,7 +27,7 @@
<Icon icon={UsersGroup} size={5} />
<strong>Spaces</strong>
</div>
<Badge variant="neutral">{spaceUrls.length}</Badge>
<span class="badge badge-neutral">{spaceUrls.length}</span>
</div>
{#if spaceUrls.length > 0}
<div class="flex flex-col gap-2 border-t border-line pt-4">
@ -43,7 +42,7 @@
<div class="flex items-center gap-2">
<RelayName {url} class="truncate text-sm font-medium" />
{#if isMember}
<Badge variant="primary" class="text-xs">Member</Badge>
<span class="badge badge-primary text-xs">Member</span>
{/if}
</div>
<p class="text-xs opacity-75">

View file

@ -4,7 +4,6 @@
import {RelayStats} from "@welshman/app"
import Server from "@assets/icons/server.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Card from "@lib/components/Card.svelte"
import Link from "@lib/components/Link.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
import {app, relays} from "@app/core"
@ -16,7 +15,7 @@
const connections = $derived($relayStats?.open_count || 0)
</script>
<Card sm class="flex flex-col gap-2">
<div class="card card-sm flex flex-col gap-2">
<div class="flex items-center justify-between gap-4">
<div class="truncate min-w-0 flex items-center gap-2">
<Icon icon={Server} />
@ -44,4 +43,4 @@
Connected {connections}
{connections === 1 ? "time" : "times"}
</span>
</Card>
</div>

View file

@ -3,7 +3,6 @@
import {getIdFilters, matchTag, tagSpec} from "@welshman/util"
import {LOCAL_RELAY_URL} from "@welshman/net"
import type {TrustedEvent} from "@welshman/util"
import Card from "@lib/components/Card.svelte"
import Cv from "@lib/components/Cv.svelte"
import Button from "@lib/components/Button.svelte"
import MenuButton from "@lib/components/MenuButton.svelte"
@ -38,7 +37,7 @@
}
</script>
<Cv tag={Card} sm class="flex flex-col gap-4">
<Cv class="card card-sm flex flex-col gap-4">
<div class="flex justify-between">
<div>
<ProfileName pubkey={event.pubkey} {url} />

View file

@ -1,7 +1,6 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {RoomJoin} from "@welshman/domain"
import Card from "@lib/components/Card.svelte"
import Button from "@lib/components/Button.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
@ -72,7 +71,7 @@
let loading = $state(false)
</script>
<Card sm class="flex flex-col gap-4">
<div class="card card-sm flex flex-col gap-4">
<div class="flex justify-between gap-2">
<div>
<Button class="link text-primary" onclick={showProfile}>
@ -91,4 +90,4 @@
>Accept</Button>
</div>
</div>
</Card>
</div>

View file

@ -4,7 +4,6 @@
import {publish} from "@welshman/app"
import Server from "@assets/icons/server.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Badge from "@lib/components/Badge.svelte"
import Button from "@lib/components/Button.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
@ -58,9 +57,9 @@
{#snippet badges(event)}
{@const url = getSpaceUrl(event)}
{#if url}
<Badge variant="neutral">
<span class="badge badge-neutral">
<RelayName {url} />
</Badge>
</span>
{/if}
{/snippet}
</SearchBody>

View file

@ -9,7 +9,6 @@
import Icon from "@lib/components/Icon.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import Badge from "@lib/components/Badge.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import PeopleItem from "@app/components/PeopleItem.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
@ -164,9 +163,9 @@
<NoteContentMinimal {event} />
</NoteCard>
<div class="flex gap-2">
<Badge variant="neutral">
<span class="badge badge-neutral">
{formatTimestampRelative(event.created_at)}
</Badge>
</span>
{@render badges?.(event)}
</div>
</Button>

View file

@ -3,7 +3,6 @@
import {Relays} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl"
import Badge from "@lib/components/Badge.svelte"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import Spinner from "@lib/components/Spinner.svelte"
@ -61,7 +60,7 @@
<div class="min-w-0 flex-1">
<Profile pubkey={$relay.pubkey} {url} />
</div>
<Badge>Owner</Badge>
<span class="badge badge-neutral">Owner</span>
</div>
{/if}
{#if loading}
@ -88,7 +87,8 @@
</div>
<div class="flex flex-wrap gap-1">
{#each methods as method (method)}
<Badge class="badge-sm font-normal">{displayManagementMethod(method)}</Badge>
<span class="badge badge-neutral badge-sm font-normal"
>{displayManagementMethod(method)}</span>
{/each}
</div>
</div>

View file

@ -3,7 +3,6 @@
import Pen from "@assets/icons/pen.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Card from "@lib/components/Card.svelte"
import Content from "@app/components/Content.svelte"
import EditFeaturedContent from "@app/components/EditFeaturedContent.svelte"
import {deriveFeaturedContent} from "@app/featured"
@ -23,7 +22,7 @@
</script>
{#if $content.length > 0 || $canEdit}
<Card class="bg-surface flex flex-col gap-3 @container">
<div class="card bg-surface flex flex-col gap-3 @container">
<div class="flex items-center justify-between gap-2">
<h3 class="flex items-center gap-2 text-lg font-bold">
<Icon icon={Bookmark} />
@ -44,5 +43,5 @@
{/each}
</div>
{/if}
</Card>
</div>
{/if}

View file

@ -1,6 +1,5 @@
<script lang="ts">
import {MESSAGE, displayRelayUrl, tagSpec, tagValue} from "@welshman/util"
import Badge from "@lib/components/Badge.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
@ -37,9 +36,9 @@
{#snippet badges(event)}
{@const h = tagValue(tagSpec("h"), event.tags)}
{#if h}
<Badge variant="neutral">
<span class="badge badge-neutral">
<RoomName {url} {h} />
</Badge>
</span>
{/if}
{/snippet}
</SearchBody>

View file

@ -7,7 +7,6 @@
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Badge from "@lib/components/Badge.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
@ -62,7 +61,7 @@
<span class="truncate min-w-0 text-xs opacity-75">{displayHandle($handle)}</span>
{/if}
{#if isOp}
<Badge variant="primary" class="w-fit self-start @2xl:self-center">OP</Badge>
<span class="badge badge-primary w-fit self-start @2xl:self-center">OP</span>
{/if}
</div>
</aside>

View file

@ -7,7 +7,6 @@
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import {addPeriod} from "@lib/util"
interface Props {
thunk: BaseThunk
@ -27,14 +26,14 @@
const relayMessage = (status: PublishStatus | undefined, detail: string | undefined) => {
if (status === PublishStatus.Timeout) {
return "request timed out"
return "request timed out."
}
if (detail) {
return detail
return `${detail}.`.replace(/\.+$/, ".")
}
return "no details received"
return "no details received."
}
</script>
@ -57,7 +56,7 @@
<Icon icon={Danger} class="mt-0.5 text-error" size={4} />
<div class="min-w-0">
<p class="break-all">{displayRelayUrl(url)}</p>
<p class="text-xs opacity-60">{addPeriod(relayMessage(status, detail))}</p>
<p class="text-xs opacity-60">{relayMessage(status, detail)}</p>
</div>
<Button
class="button button-link shrink-0 px-1"

View file

@ -5,9 +5,7 @@
import VerifiedCheck from "@assets/icons/verified-check.svg?dataurl"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Badge from "@lib/components/Badge.svelte"
import Button from "@lib/components/Button.svelte"
import Input from "@lib/components/Input.svelte"
import Field from "@lib/components/Field.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Modal from "@lib/components/Modal.svelte"
@ -81,31 +79,30 @@
{/snippet}
{#snippet secondary()}
{#if relay.custom_domain && flagToBool(relay.custom_domain_verified, false)}
<Badge variant="primary">
<span class="badge badge-primary">
<Icon icon={VerifiedCheck} size={3} />
Verified
</Badge>
</span>
{/if}
{/snippet}
{#snippet input()}
<Input
bind:value={domain}
type="text"
placeholder="relay.example.com"
autocomplete="off"
autocapitalize="none"
spellcheck="false">
{#snippet after()}
{#if domain}
<Button
onclick={clearDomain}
aria-label="Clear domain"
class="shrink-0 text-content-subtle hover:text-content">
<Icon icon={CloseCircle} size={4} />
</Button>
{/if}
{/snippet}
</Input>
<div class="input input-group">
<input
bind:value={domain}
type="text"
placeholder="relay.example.com"
autocomplete="off"
autocapitalize="none"
spellcheck="false" />
{#if domain}
<Button
onclick={clearDomain}
aria-label="Clear domain"
class="shrink-0 text-content-subtle hover:text-content">
<Icon icon={CloseCircle} size={4} />
</Button>
{/if}
</div>
{/snippet}
{#snippet info()}
<p>Must be a domain you control, e.g. relay.example.com.</p>

View file

@ -1,6 +1,5 @@
<script lang="ts">
import cx from "classnames"
import Badge from "@lib/components/Badge.svelte"
import Button from "@lib/components/Button.svelte"
import {formatUsd, formatPeriod, type Invoice} from "@app/hosting"
@ -36,7 +35,7 @@
<div class="min-w-0">
<div class="flex items-center gap-2">
<span class="font-medium">{formatUsd(invoice.amount)}</span>
<Badge {variant} class="badge-sm capitalize">{status}</Badge>
<span class="badge badge-{variant} badge-sm capitalize">{status}</span>
{#if invoice.method}
<span class="text-xs text-content-muted"
>· paid via {invoice.method === "stripe" ? "Card" : "Lightning"}</span>

View file

@ -9,7 +9,6 @@
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Field from "@lib/components/Field.svelte"
import Input from "@lib/components/Input.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
@ -161,10 +160,12 @@
Nostr Wallet Connect URL
{/snippet}
{#snippet input()}
<Input
bind:value={nwcUrl}
autocomplete="off"
placeholder="nostr+walletconnect://..." />
<div class="input input-group">
<input
bind:value={nwcUrl}
autocomplete="off"
placeholder="nostr+walletconnect://..." />
</div>
{/snippet}
</Field>
{#if nwcConnected}

View file

@ -8,7 +8,6 @@
import Refresh from "@assets/icons/refresh.svg?dataurl"
import {ucFirst} from "@lib/util"
import Icon from "@lib/components/Icon.svelte"
import Badge from "@lib/components/Badge.svelte"
import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.svelte"
import MenuButton from "@lib/components/MenuButton.svelte"
@ -16,6 +15,7 @@
import Spinner from "@lib/components/Spinner.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
import ToggleInput from "@lib/components/ToggleInput.svelte"
import Tooltip from "@lib/components/Tooltip.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayDetailMenu from "@app/components/hosting/RelayDetailMenu.svelte"
import CustomDomainModal from "@app/components/hosting/CustomDomainModal.svelte"
@ -102,16 +102,25 @@
</script>
{#snippet flagToggle(title: string, flag: RelayFlag, gated = false)}
{#snippet toggle()}
<ToggleInput
checked={flagToBool(relay[flag], Boolean(RELAY_FLAG_DEFAULTS[flag]))}
disabled={gated}
onchange={setFlag(flag)} />
{/snippet}
<FieldInline class={cx({"opacity-50": gated})}>
{#snippet label()}
<span>{title}</span>
{/snippet}
{#snippet input()}
<ToggleInput
checked={flagToBool(relay[flag], Boolean(RELAY_FLAG_DEFAULTS[flag]))}
disabled={gated}
tooltip={gated ? "Not available on your current plan" : undefined}
onchange={setFlag(flag)} />
{#if gated}
<!-- On a wrapper rather than the input, so it still shows while the toggle is disabled -->
<Tooltip content="Not available on your current plan">
{@render toggle()}
</Tooltip>
{:else}
{@render toggle()}
{/if}
{/snippet}
</FieldInline>
{/snippet}
@ -123,12 +132,20 @@
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<h2 class="text-xl font-bold">{relay.info_name || relay.subdomain}</h2>
<Badge variant={relay.status === "active" ? "primary" : "neutral"}>
<span
class={cx("badge", {
"badge-primary": relay.status === "active",
"badge-neutral": relay.status !== "active",
})}>
{ucFirst(relay.status.replace(/_/g, " "))}
</Badge>
<Badge variant={relay.plan_id === "free" ? "neutral" : "primary"}>
</span>
<span
class={cx("badge", {
"badge-neutral": relay.plan_id === "free",
"badge-primary": relay.plan_id !== "free",
})}>
{ucFirst(relay.plan_id)}
</Badge>
</span>
</div>
<Link external href={`https://${host}`} class="break-all text-sm text-primary">
wss://{host}
@ -165,15 +182,15 @@
<div class="flex min-w-0 items-center gap-2">
<span class="truncate font-medium">{relay.custom_domain}</span>
{#if domainVerified}
<Badge variant="primary">
<span class="badge badge-primary">
<Icon icon={VerifiedCheck} size={3} />
Verified
</Badge>
</span>
{:else}
<Badge variant="warning">
<span class="badge badge-warning">
<Icon icon={ShieldWarning} size={3} />
Pending
</Badge>
</span>
{/if}
</div>
<Button class="button button-neutral button-sm shrink-0" onclick={openCustomDomain}>

View file

@ -14,7 +14,6 @@
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Field from "@lib/components/Field.svelte"
import Icon from "@lib/components/Icon.svelte"
import Input from "@lib/components/Input.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Modal from "@lib/components/Modal.svelte"
@ -168,7 +167,9 @@
<p>Name</p>
{/snippet}
{#snippet input()}
<Input bind:value={name} type="text" placeholder="My Space" required />
<div class="input input-group">
<input bind:value={name} type="text" placeholder="My Space" required />
</div>
{/snippet}
</Field>
<Field>
@ -176,11 +177,10 @@
<p>Subdomain</p>
{/snippet}
{#snippet input()}
<Input bind:value={subdomain} type="text" placeholder="my-space" required>
{#snippet after()}
<span class="whitespace-nowrap text-sm opacity-50">.{zooidDomain}</span>
{/snippet}
</Input>
<div class="input input-group">
<input bind:value={subdomain} type="text" placeholder="my-space" required />
<span class="whitespace-nowrap text-sm opacity-50">.{zooidDomain}</span>
</div>
{#if subdomain && subdomainError}
<p class="text-sm text-error">{subdomainError}</p>
{/if}

View file

@ -2,7 +2,6 @@
import cx from "classnames"
import Server from "@assets/icons/server.svg?dataurl"
import {ucFirst} from "@lib/util"
import Badge from "@lib/components/Badge.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import Link from "@lib/components/Link.svelte"
import {canonicalRelayHost, getHostedRelayUrl, type HostedRelay} from "@app/hosting"
@ -31,16 +30,24 @@
</div>
<div class="flex flex-wrap items-center gap-2">
{#if relay.sync_error}
<Badge variant="warning" title={relay.sync_error}>Failed to sync</Badge>
<span class="badge badge-warning" title={relay.sync_error}>Failed to sync</span>
{:else}
<Badge variant={relay.status === "active" ? "primary" : "neutral"}>
<span
class={cx("badge", {
"badge-primary": relay.status === "active",
"badge-neutral": relay.status !== "active",
})}>
{ucFirst(relay.status.replace(/_/g, " "))}
</Badge>
</span>
{/if}
{#if !compact}
<Badge variant={relay.plan_id === "free" ? "neutral" : "primary"}>
<span
class={cx("badge", {
"badge-neutral": relay.plan_id === "free",
"badge-primary": relay.plan_id !== "free",
})}>
{ucFirst(relay.plan_id)}
</Badge>
</span>
<Link class="button button-neutral button-sm ml-auto" {href}>Manage</Link>
{/if}
</div>

View file

@ -7,9 +7,11 @@ import {
ms,
now,
on,
range,
removeUndefined,
sleep,
uniqBy,
DAY,
MONTH,
YEAR,
} from "@welshman/lib"
@ -37,7 +39,6 @@ import {mergeRepositoryUpdates} from "@welshman/net"
import type {RepositoryUpdate} from "@welshman/net"
import {createScroller} from "@lib/html"
import type {ScrollerOpts} from "@lib/html"
import {daysBetween} from "@lib/util"
import {EVENT_CONTEXT_KINDS, REACTION_KINDS} from "@app/content"
import {app, network, reader} from "@app/core"
@ -768,7 +769,12 @@ export const makeCalendarFeed = ({
priority: 1,
signal: spanSignal(controller.signal),
threshold: SPAN_THRESHOLD,
filters: [{kinds: [EVENT_TIME], "#D": daysBetween(since, until).map(String)}],
filters: [
{
kinds: [EVENT_TIME],
"#D": [...range(since, until, DAY)].map(t => String(Math.floor(t / DAY))),
},
],
onEose: () => {
complete = true
},

View file

@ -1,18 +0,0 @@
<script lang="ts">
import type {Snippet} from "svelte"
import type {HTMLAttributes} from "svelte/elements"
import cx from "classnames"
type Props = HTMLAttributes<HTMLSpanElement> & {
children?: Snippet
variant?: "neutral" | "primary" | "warning"
}
const {children, variant = "neutral", ...restProps}: Props = $props()
const className = $derived(cx("badge", `badge-${variant}`, restProps.class))
</script>
<span {...restProps} class={className}>
{@render children?.()}
</span>

View file

@ -2,14 +2,9 @@
import type {Snippet} from "svelte"
// Behavior only, so the same `button*` classes apply to <a> and <span> without a prop API.
const {
children,
onclick,
type = "button",
...restProps
}: {
type Props = {
children: Snippet
onclick?: (event: Event) => any
onclick?: (event: Event) => unknown
type?: "button" | "submit"
class?: string
style?: string
@ -17,7 +12,9 @@
"data-tip"?: string
"aria-label"?: string
"aria-pressed"?: boolean
} = $props()
}
const {children, onclick, type = "button", ...restProps}: Props = $props()
const onClick = (e: Event) => {
e.preventDefault()

View file

@ -1,38 +0,0 @@
<script lang="ts">
import type {Snippet} from "svelte"
import type {HTMLAttributes} from "svelte/elements"
import cx from "classnames"
type Props = HTMLAttributes<HTMLElement> & {
children?: Snippet
tag?: string
sm?: boolean
flat?: boolean
interactive?: boolean
}
const {
children,
tag = "div",
sm = false,
flat = false,
interactive = false,
...restProps
}: Props = $props()
const className = $derived(
cx(
"card",
{
"card-sm": sm,
"card-flat": flat,
"card-interactive": interactive,
},
restProps.class,
),
)
</script>
<svelte:element this={tag} {...restProps} class={className}>
{@render children?.()}
</svelte:element>

View file

@ -12,7 +12,7 @@
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
interface Props {
type Props = {
title?: string
subtitle?: string
message: any

View file

@ -3,15 +3,13 @@
import cx from "classnames"
import Button from "@lib/components/Button.svelte"
const {
onclick = () => {},
className = "",
children,
}: {
type Props = {
onclick?: () => void
className?: string
children?: Snippet
} = $props()
}
const {onclick = () => {}, className = "", children}: Props = $props()
</script>
<div class={cx("fixed bottom-20 mb-sai right-4 z-nav hide-on-keyboard md:hidden", className)}>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type {Snippet} from "svelte"
interface Props {
type Props = {
label?: Snippet
secondary?: Snippet
input?: Snippet

View file

@ -1,5 +1,5 @@
<script lang="ts">
interface Props {
type Props = {
label?: import("svelte").Snippet
input?: import("svelte").Snippet
info?: import("svelte").Snippet

View file

@ -6,86 +6,20 @@
</style>
<script lang="ts">
import {maybe} from "@welshman/lib"
const {
icon,
size = 5,
...restProps
}: {
type Props = {
icon: string
size?: number
class?: string
style?: string
} = $props()
const px = size * 4
// The ios wkwebview has no "Safari" token in its user agent, so detect the engine instead.
const isWebkit =
typeof navigator !== "undefined" &&
/applewebkit/i.test(navigator.userAgent) &&
!/chrome|chromium|android|firefox/i.test(navigator.userAgent)
// Strip any query string/fragment - hosted icons are often served with one.
const isRemoteSvg = (value: string) =>
!value.startsWith("data:") && /\.svg$/i.test(value.split(/[?#]/)[0]!)
let objectUrl = $state(maybe<string>())
const src = $derived(objectUrl || icon)
// Primal issues 302 redirects from blossom, which breaks mask-image on webkit.
const fetchSvg = async (url: string) => {
try {
const response = await fetch(url, {
mode: "cors",
credentials: "omit",
})
if (response.ok) {
return URL.createObjectURL(await response.blob())
}
} catch {
// pass
}
}
// Re-fetch when icon changes, revoking the previous blob url.
$effect(() => {
const url = icon
const {icon, size = 5, ...restProps}: Props = $props()
let canceled = false
let created: string | undefined
if (isWebkit && isRemoteSvg(url)) {
fetchSvg(url).then(blobUrl => {
if (!blobUrl) {
return
}
if (canceled) {
URL.revokeObjectURL(blobUrl)
} else {
created = blobUrl
objectUrl = blobUrl
}
})
}
return () => {
canceled = true
if (created) {
URL.revokeObjectURL(created)
objectUrl = undefined
}
}
})
const px = size * 4
</script>
<div
class="inline-block {restProps.class}"
style="mask-image: url({src}); width: {px}px; height: {px}px; min-width: {px}px; min-height: {px}px; background-color: currentcolor; {restProps.style ??
style="mask-image: url({icon}); width: {px}px; height: {px}px; min-width: {px}px; min-height: {px}px; background-color: currentcolor; {restProps.style ??
''}">
</div>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import {maybe} from "@welshman/lib"
import Icon from "@lib/components/Icon.svelte"
type Props = {
@ -16,10 +17,69 @@
const imgStyle = $derived(
`width: ${rem}rem; height: ${rem}rem; min-width: ${rem}rem; min-height: ${rem}rem${style ? `; ${style}` : ""}`,
)
// The ios wkwebview has no "Safari" token in its user agent, so detect the engine instead.
const isWebkit =
typeof navigator !== "undefined" &&
/applewebkit/i.test(navigator.userAgent) &&
!/chrome|chromium|android|firefox/i.test(navigator.userAgent)
// Strip any query string/fragment - hosted icons are often served with one.
const isRemoteSvg = (value: string) =>
!value.startsWith("data:") && /\.svg$/i.test(value.split(/[?#]/)[0]!)
// Primal issues 302 redirects from blossom, which breaks mask-image on webkit.
const fetchSvg = async (url: string) => {
try {
const response = await fetch(url, {
mode: "cors",
credentials: "omit",
})
if (response.ok) {
return URL.createObjectURL(await response.blob())
}
} catch {
// pass
}
}
let objectUrl = $state(maybe<string>())
$effect(() => {
const url = src
let canceled = false
let created: string | undefined
if (isWebkit && isRemoteSvg(url)) {
fetchSvg(url).then(blobUrl => {
if (!blobUrl) {
return
}
if (canceled) {
URL.revokeObjectURL(blobUrl)
} else {
created = blobUrl
objectUrl = blobUrl
}
})
}
return () => {
canceled = true
if (created) {
URL.revokeObjectURL(created)
objectUrl = undefined
}
}
})
</script>
{#if src.includes("image/svg") || src.endsWith(".svg")}
<Icon icon={src} {size} {style} class={className} />
<Icon icon={objectUrl || src} {size} {style} class={className} />
{:else}
<img {src} {alt} style={imgStyle} class="aspect-square object-cover {className}" />
{/if}

View file

@ -1,29 +0,0 @@
<script lang="ts">
import type {Snippet} from "svelte"
import type {HTMLInputAttributes} from "svelte/elements"
import cx from "classnames"
type Props = Omit<HTMLInputAttributes, "value"> & {
value?: string
before?: Snippet
after?: Snippet
sm?: boolean
}
let {
value = $bindable(),
before,
after,
sm = false,
class: className = "",
...restProps
}: Props = $props()
const wrapperClass = $derived(cx("input", "input-group", {"input-sm": sm}, className))
</script>
<div class={wrapperClass}>
{@render before?.()}
<input bind:value {...restProps} />
{@render after?.()}
</div>

View file

@ -12,6 +12,7 @@
value: string[]
addLabel?: Snippet
placeholder?: string
allowAdd?: boolean
}
type Entry = {id: string; value: string}
@ -21,7 +22,7 @@
addLabel,
placeholder = "Enter text...",
allowAdd = true,
}: Props & {allowAdd?: boolean} = $props()
}: Props = $props()
const toEntry = (value: string) => ({id: randomId(), value})

View file

@ -4,13 +4,7 @@
import {stopPropagation} from "@lib/html"
import {navigate} from "@app/modal"
const {
children,
href,
external = false,
replaceState = false,
...restProps
}: {
type Props = {
children?: Snippet
href: string
external?: boolean
@ -20,7 +14,9 @@
style?: string
"data-tip"?: string
"aria-label"?: string
} = $props()
}
const {children, href, external = false, replaceState = false, ...restProps}: Props = $props()
const go = (e: Event) => {
if (!external) {

View file

@ -2,7 +2,7 @@
import cx from "classnames"
import type {Snippet} from "svelte"
interface Props {
type Props = {
class?: string
children?: Snippet
}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type {Snippet} from "svelte"
interface Props {
type Props = {
children?: Snippet
}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type {Snippet} from "svelte"
interface Props {
type Props = {
children: Snippet
}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type {Snippet} from "svelte"
interface Props {
type Props = {
children: Snippet
}

View file

@ -1,5 +1,5 @@
<script lang="ts">
interface Props {
type Props = {
children?: import("svelte").Snippet
[key: string]: any
}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type {Snippet} from "svelte"
interface Props {
type Props = {
children?: Snippet
class?: string
}

View file

@ -1,5 +1,5 @@
<script lang="ts">
interface Props {
type Props = {
title?: import("svelte").Snippet
info?: import("svelte").Snippet
}

View file

@ -1,18 +1,30 @@
<script lang="ts">
import type {Snippet} from "svelte"
import cx from "classnames"
import {page} from "$app/stores"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
type Props = {
children?: Snippet
onclick?: (event: Event) => unknown
title?: string
href?: string
prefix?: string
notification?: boolean
class?: string
"aria-label"?: string
}
const {
children,
onclick = undefined,
onclick,
title = "",
href = "",
prefix = "",
notification = false,
...restProps
} = $props()
}: Props = $props()
const active = $derived($page.url?.pathname?.startsWith(prefix || href || "bogus"))

View file

@ -1,9 +1,14 @@
<script lang="ts">
import {onMount} from "svelte"
import QrScanner from "qr-scanner"
import cx from "classnames"
import Spinner from "@lib/components/Spinner.svelte"
const {onscan} = $props()
type Props = {
onscan: (data: string) => void
}
const {onscan}: Props = $props()
const changeCamera = async () => {
if (camera && scanner) {
@ -56,7 +61,7 @@
<Spinner loading>Loading your camera...</Spinner>
</p>
{/if}
<video class="m-auto rounded" class:h-0={loading} bind:this={video}></video>
<video class={cx("m-auto rounded", {"h-0": loading})} bind:this={video}></video>
{#if cameras.length > 1}
<select
class="select input input-sm absolute bottom-1 right-1 w-auto"

View file

@ -4,7 +4,11 @@
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
const {element}: {element?: Element} = $props()
type Props = {
element?: Element
}
const {element}: Props = $props()
let scrolled = $state(false)

View file

@ -2,7 +2,7 @@
import cx from "classnames"
import type {Snippet} from "svelte"
interface Props {
type Props = {
class?: string
visible?: boolean
children?: Snippet

View file

@ -1,5 +1,5 @@
<script lang="ts">
interface Props {
type Props = {
children?: import("svelte").Snippet
}

View file

@ -1,31 +1,18 @@
<style>
a,
button {
padding: 12px 16px;
display: flex;
border-radius: var(--radius-2xl);
cursor: pointer;
animation: nav-button-pop 200ms ease-out;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
a:active:hover,
a:active:focus,
button:active:hover,
button:active:focus {
animation: button-pop 0s ease-out;
transform: scale(0.97);
}
</style>
<script lang="ts">
import type {Snippet} from "svelte"
import type {HTMLAttributes} from "svelte/elements"
import cx from "classnames"
import {fade} from "@lib/transition"
import {page} from "$app/stores"
const {children, href = "", title = "", notification = false, ...restProps} = $props()
type Props = HTMLAttributes<HTMLElement> & {
children?: Snippet
href?: string
title?: string
notification?: boolean
}
const {children, href = "", title = "", notification = false, ...restProps}: Props = $props()
const active = $derived($page.url.pathname === href)
const wrapperClass = $derived(
@ -44,19 +31,13 @@
<a {href} {...restProps} data-tip={title} class={wrapperClass}>
{@render children?.()}
{#if notification}
<div
class="absolute right-[1.15rem] top-5 h-2 w-2 rounded-full bg-primary text-primary-content"
transition:fade>
</div>
<div class="secondary-nav__dot" transition:fade></div>
{/if}
</a>
{:else}
<button {...restProps} data-tip={title} class={wrapperClass}>
{#if notification}
<div
class="absolute right-[1.15rem] top-5 h-2 w-2 rounded-full bg-primary text-primary-content"
transition:fade>
</div>
<div class="secondary-nav__dot" transition:fade></div>
{/if}
{@render children?.()}
</button>

View file

@ -1,5 +1,5 @@
<script lang="ts">
interface Props {
type Props = {
children?: import("svelte").Snippet
[key: string]: any
}

View file

@ -2,20 +2,22 @@
import type {Snippet} from "svelte"
import cx from "classnames"
type Props = {
children?: Snippet
size?: "md" | "sm" | "xs"
loading?: boolean
// Reserves the spinner's slot while idle, so toggling `loading` doesn't reflow the button.
reserveSpace?: boolean
class?: string
}
const {
children,
size = "md",
loading = true,
reserveSpace = false,
...restProps
}: {
children?: Snippet
size?: "md" | "sm" | "xs"
loading?: boolean
// Reserves the spinner's slot while idle, so toggling `loading` doesn't reflow the button.
reserveSpace?: boolean
class?: string
} = $props()
}: Props = $props()
const spinnerClass = $derived(
cx("spinner", {

View file

@ -5,9 +5,8 @@
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Badge from "@lib/components/Badge.svelte"
interface Props {
type Props = {
value: string[]
term?: Writable<string>
placeholder?: string
@ -57,12 +56,12 @@
<div class="flex flex-col gap-2">
<div>
{#each value as item (item)}
<Badge variant="neutral" class="mr-1">
<span class="badge badge-neutral mr-1">
<Button class="p-1 flex items-center" onclick={() => removeItem(item)}>
<Icon icon={CloseCircle} size={4} class="-ml-1 mt-px" />
</Button>
<span>{item}</span>
</Badge>
</span>
{/each}
</div>
<label class="input input-group flex w-full items-center gap-2">

View file

@ -1,5 +0,0 @@
<script lang="ts">
export let value
</script>
{value}

View file

@ -1,22 +1,41 @@
<script lang="ts">
import type {Component as SvelteComponent} from "svelte"
import type {Readable} from "svelte/store"
import {fly} from "svelte/transition"
import cx from "classnames"
import {throttle, clamp} from "@welshman/lib"
import {preventDefault, stopPropagation} from "@lib/html"
const {term, search, select, component: Component, style = "", allowCreate = false} = $props()
type Props = {
term: Readable<string>
search: (term: string) => string[]
select: (value: string) => void
component: SvelteComponent<{value: string}>
style?: string
allowCreate?: boolean
}
const {
term,
search,
select,
component: Component,
style = "",
allowCreate = false,
}: Props = $props()
let index = $state(0)
let items: string[] = $state([])
const populateItems = throttle(300, term => {
const populateItems = throttle(300, (term: string) => {
items = term ? search(term).slice(0, 5) : []
})
const setIndex = (newIndex: number, block: any) => {
const setIndex = (newIndex: number) => {
index = clamp([0, items.length - 1], newIndex)
}
export const onKeyDown = (e: any) => {
export const onKeyDown = (e: KeyboardEvent) => {
if (["Enter", "Tab"].includes(e.code)) {
const value = items[index]
@ -35,13 +54,13 @@
}
if (e.code === "ArrowUp") {
setIndex(index - 1, "start")
setIndex(index - 1)
return true
}
if (e.code === "ArrowDown") {
setIndex(index + 1, "start")
setIndex(index + 1)
return true
}
@ -70,8 +89,7 @@
{#each items as value, i (value)}
<button
aria-label={value}
class="tiptap-suggestions__item"
class:tiptap-suggestions__selected={index === i}
class={cx("tiptap-suggestions__item", {"tiptap-suggestions__selected": index === i})}
{onmousedown}
onclick={stopPropagation(preventDefault(() => select(value)))}>
<Component {value}></Component>

View file

@ -1,7 +1,14 @@
<script lang="ts">
import type {Snippet} from "svelte"
import type {HTMLAttributes} from "svelte/elements"
import {isMobile} from "@lib/html"
const {children, onTap, ...restProps} = $props()
type Props = HTMLAttributes<HTMLDivElement> & {
children: Snippet
onTap: (event: MouseEvent) => void
}
const {children, onTap, ...restProps}: Props = $props()
const onclick = (event: MouseEvent) => {
if (isMobile) {

View file

@ -1,29 +1,15 @@
<script lang="ts">
import Tooltip from "@lib/components/Tooltip.svelte"
import type {HTMLInputAttributes} from "svelte/elements"
type Props = {
type Props = Omit<HTMLInputAttributes, "checked" | "onchange" | "type"> & {
checked?: boolean
tooltip?: string
onchange?: (checked: boolean) => void
[key: string]: any
}
let {checked = $bindable(), tooltip, onchange, ...restProps}: Props = $props()
let {checked = $bindable(), onchange, ...restProps}: Props = $props()
const onInput = (event: Event & {currentTarget: HTMLInputElement}) =>
onchange?.(event.currentTarget.checked)
</script>
{#snippet toggle()}
<input {...restProps} type="checkbox" class="toggle" bind:checked onchange={onInput} />
{/snippet}
<!-- The tooltip hangs off a wrapper rather than the input, so it still shows
while the toggle is disabled. -->
{#if tooltip}
<Tooltip content={tooltip}>
{@render toggle()}
</Tooltip>
{:else}
{@render toggle()}
{/if}
<input {...restProps} type="checkbox" class="toggle" bind:checked onchange={onInput} />

View file

@ -1,4 +1,18 @@
@layer components {
.secondary-nav__nav-item {
@apply cursor-pointer rounded-2xl;
padding: 12px 16px;
&:active:hover,
&:active:focus {
transform: scale(0.97);
}
}
.secondary-nav__dot {
@apply bg-primary absolute top-5 right-[1.15rem] h-2 w-2 rounded-full;
}
.secondary-nav {
@apply border-line mt-sai mb-sai bg-surface-more z-nav max-h-screen min-h-0 w-60 shrink-0 flex-col gap-1 border-r border-solid;

View file

@ -1,78 +0,0 @@
import {fromPairs, parseJson, randomId} from "@welshman/lib"
import {FEED, Address} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {
makeIntersectionFeed,
hasSubFeeds,
isTagFeed,
isAuthorFeed,
isScopeFeed,
} from "@welshman/feeds"
import type {Feed as IFeed} from "@welshman/feeds"
export type Feed = {
title: string
identifier: string
description: string
definition: IFeed
event?: TrustedEvent
}
export type PublishedFeed = Omit<Feed, "event"> & {
event: TrustedEvent
}
export const normalizeFeedDefinition = (feed: IFeed) =>
hasSubFeeds(feed) ? feed : makeIntersectionFeed(feed)
export const makeFeed = (feed: Partial<Feed> = {}): Feed => ({
title: "",
description: "",
identifier: randomId(),
definition: makeIntersectionFeed(),
...feed,
})
export const readFeed = (event: TrustedEvent) => {
const {d: identifier, title = "", description = "", feed = ""} = fromPairs(event.tags)
const definition = parseJson(feed) || makeIntersectionFeed()
return {title, identifier, description, definition, event} as PublishedFeed
}
export const createFeed = ({identifier, definition, title, description}: Feed) => ({
kind: FEED,
content: "",
tags: [
["d", identifier],
["alt", title],
["title", title],
["description", description],
["feed", JSON.stringify(definition)],
],
})
export const editFeed = (feed: PublishedFeed) => ({
kind: FEED,
content: feed.event.content,
tags: Object.entries({
...fromPairs(feed.event.tags),
title: feed.title,
alt: feed.title,
description: feed.description,
feed: JSON.stringify(feed.definition),
}),
})
export const displayFeed = (feed?: Feed) => feed?.title || "[no name]"
export const isTopicFeed = (f: IFeed) => isTagFeed(f) && f[1] === "#t"
export const isMentionFeed = (f: IFeed) => isTagFeed(f) && f[1] === "#p"
export const isAddressFeed = (f: IFeed) => isTagFeed(f) && f[1] === "#a"
export const isContextFeed = (f: IFeed) =>
isTagFeed(f) && f[1] === "#a" && f.slice(2).every(Address.isAddress)
export const isPeopleFeed = (f: IFeed) => isAuthorFeed(f) || isScopeFeed(f)

View file

@ -150,16 +150,6 @@ export const downloadText = async (filename: string, text: string) => {
URL.revokeObjectURL(url)
}
export const isIntersecting = async (element: Element) =>
new Promise(resolve => {
const observer = new IntersectionObserver(xs => {
resolve(xs.some(x => x.isIntersecting))
observer.unobserve(element)
})
observer.observe(element)
})
export const escapeHtml = (html: string) => {
const element = document.createElement("div")

View file

@ -106,15 +106,13 @@ export class IDB {
const tx = connection.transaction(table, "readwrite")
const store = tx.objectStore(table)
await Promise.all(
Array.from(data).map(item => {
try {
store.put(item)
} catch (e) {
console.error(e, item)
}
}),
)
for (const item of data) {
try {
store.put(item)
} catch (e) {
console.error(e, item)
}
}
await tx.done
}

View file

@ -1,6 +1,5 @@
// @ts-nocheck
import {cubicOut} from "svelte/easing"
import type {FlyParams} from "svelte/transition"
import type {FlyParams, SlideParams} from "svelte/transition"
import {fly as baseFly} from "svelte/transition"
export {fade, slide} from "svelte/transition"
@ -44,62 +43,28 @@ export const translate = (
}
}
// Copy-pasted and tweaked from slide source code
export function slideAndFade(
node: any,
{delay = 0, duration = 400, easing = cubicOut, axis = "y"} = {},
) {
export const slideAndFade = (
node: Element,
{delay = 0, duration = 400, easing = cubicOut, axis = "y"}: SlideParams = {},
) => {
const style = getComputedStyle(node)
const primary_property = axis === "y" ? "height" : "width"
const primary_property_value = parseFloat(style[primary_property])
const secondary_properties = axis === "y" ? ["top", "bottom"] : ["left", "right"]
const capitalized_secondary_properties = secondary_properties.map(
(e: string) => `${e[0].toUpperCase()}${e.slice(1)}`,
)
const padding_start_value = parseFloat(style[`padding${capitalized_secondary_properties[0]}`])
const padding_end_value = parseFloat(style[`padding${capitalized_secondary_properties[1]}`])
const margin_start_value = parseFloat(style[`margin${capitalized_secondary_properties[0]}`])
const margin_end_value = parseFloat(style[`margin${capitalized_secondary_properties[1]}`])
const border_width_start_value = parseFloat(
style[`border${capitalized_secondary_properties[0]}Width`],
)
const border_width_end_value = parseFloat(
style[`border${capitalized_secondary_properties[1]}Width`],
)
const [start, end] = axis === "y" ? ["top", "bottom"] : ["left", "right"]
const sizes = [
axis === "y" ? "height" : "width",
`padding-${start}`,
`padding-${end}`,
`margin-${start}`,
`margin-${end}`,
`border-${start}-width`,
`border-${end}-width`,
].map((property): [string, number] => [property, parseFloat(style.getPropertyValue(property))])
return {
delay,
duration,
easing,
css: (t: number) =>
"overflow: hidden;" +
`opacity: ${t};` +
`${primary_property}: ${t * primary_property_value}px;` +
`padding-${secondary_properties[0]}: ${t * padding_start_value}px;` +
`padding-${secondary_properties[1]}: ${t * padding_end_value}px;` +
`margin-${secondary_properties[0]}: ${t * margin_start_value}px;` +
`margin-${secondary_properties[1]}: ${t * margin_end_value}px;` +
`border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` +
`border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;`,
`overflow: hidden;opacity: ${t};` +
sizes.map(([property, size]) => `${property}: ${t * size}px;`).join(""),
}
}
export type ScaleParams = {
delay?: number
duration?: number
easing?: (t: number) => number
start?: number
}
export const scale = (
node: Element,
{delay = 0, duration = 200, easing = cubicOut, start = 0.96}: ScaleParams = {},
) => ({
delay,
duration,
easing,
css: (t: number) => {
const s = start + (1 - start) * t
return `transform: scale(${s}); opacity: ${t}`
},
})

View file

@ -1,28 +1,9 @@
import {identity, range, DAY, hexToBytes, bytesToHex} from "@welshman/lib"
import {fromNostrURI} from "@welshman/util"
import {tryCatch, hexToBytes, bytesToHex} from "@welshman/lib"
import {fromNostrURI, Pubkey} from "@welshman/util"
import * as nip19 from "nostr-tools/nip19"
export const decodePubkey = (entity: string): string | undefined => {
if (/^[a-f0-9]{64}$/i.test(entity)) {
return entity.toLowerCase()
}
try {
const decoded = nip19.decode(fromNostrURI(entity))
if (decoded.type === "npub") {
return decoded.data
}
if (decoded.type === "nprofile") {
return decoded.data.pubkey
}
return undefined
} catch {
return undefined
}
}
export const decodePubkey = (entity: string) =>
tryCatch(() => Pubkey.from(fromNostrURI(entity).toLowerCase()).pubkey)
export const nsecEncode = (secret: string) => nip19.nsecEncode(hexToBytes(secret))
@ -36,10 +17,6 @@ export const nsecDecode = (nsec: string) => {
return bytesToHex(data)
}
export const day = (seconds: number) => Math.floor(seconds / DAY)
export const daysBetween = (start: number, end: number) => [...range(start, end, DAY)].map(day)
export const ucFirst = (s: string) => s.slice(0, 1).toUpperCase() + s.slice(1)
export const errorMessage = (err: unknown) => String(err).replace(/^.*Error: /, "")
@ -52,10 +29,6 @@ export const buildUrl = (base: string | URL, ...pathname: string[]) => {
return url.toString()
}
export const addPeriod = (s: string) => (s + ".").replace(/\.+$/, ".")
export const normalizeTopic = (topic: string) => topic.trim().replace(/^#+/, "").toLowerCase()
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
export const stripPrefix = (m: string) => m.replace(/^\w+: /, "")

View file

@ -2,7 +2,6 @@
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import Card from "@lib/components/Card.svelte"
import Code from "@assets/icons/code-2.svg?dataurl"
import Planet from "@assets/icons/planet.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl"
@ -27,19 +26,19 @@
<h1 class="mb-4 text-center text-5xl font-bold uppercase">{PLATFORM_NAME}</h1>
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
{#if ENABLE_ZAPS}
<Card class="flex flex-col gap-2 text-center">
<div class="card flex flex-col gap-2 text-center">
<h3 class="text-2xl sm:h-12">Donate</h3>
<p class="sm:h-16">Funds will be used to support development.</p>
<Button onclick={zap} class="button button-primary">Zap the Developer</Button>
</Card>
</div>
{/if}
<Card class="flex flex-col gap-2 text-center">
<div class="card flex flex-col gap-2 text-center">
<h3 class="text-2xl sm:h-12">Get in touch</h3>
<p class="sm:h-16">Having problems? Let us know.</p>
<Link class="button button-primary" href={makeSpacePath("support.flotilla.social")}>
Get Support
</Link>
</Card>
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2 items-center text-center">

View file

@ -17,7 +17,6 @@
import InputList from "@lib/components/InputList.svelte"
import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte"
import Card from "@lib/components/Card.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import PageContent from "@lib/components/PageContent.svelte"
@ -107,7 +106,7 @@
<form {onsubmit}>
<PageContent>
<Card class="flex flex-col gap-4">
<div class="card flex flex-col gap-4">
<strong class="flex items-center gap-3 text-lg">
<Icon icon={NotesMinimalistic} />
Content Settings
@ -146,8 +145,8 @@
</div>
{/snippet}
</Field>
</Card>
<Card class="flex flex-col gap-4">
</div>
<div class="card flex flex-col gap-4">
<strong class="flex items-center gap-3 text-lg">
<Icon icon={Palette} />
Theme Settings
@ -182,8 +181,8 @@
<p>Changes the shape and texture of everything, not just its colors.</p>
{/snippet}
</FieldInline>
</Card>
<Card class="flex flex-col gap-4">
</div>
<div class="card flex flex-col gap-4">
<strong class="flex items-center gap-3 text-lg">
<Icon icon={Accessibility} />
Accessibility
@ -206,8 +205,8 @@
bind:value={$settings.font_size} />
{/snippet}
</Field>
</Card>
<Card class="flex flex-col gap-4 shadow-md">
</div>
<div class="card flex flex-col gap-4 shadow-md">
<strong class="text-lg">Editor Settings</strong>
<Field>
{#snippet label()}
@ -271,13 +270,13 @@
<p>Choose a media server type and url for files you upload to {PLATFORM_NAME}.</p>
{/snippet}
</Field>
</Card>
<Card class="sticky -bottom-3 shadow-md flex flex-row items-center justify-between gap-4">
</div>
<div class="card sticky -bottom-3 shadow-md flex flex-row items-center justify-between gap-4">
<Button class="button button-neutral" onclick={reset} disabled={loading}
>Discard Changes</Button>
<Button class="button button-primary" type="submit" disabled={loading}>
<Spinner {loading}>Save Changes</Spinner>
</Button>
</Card>
</div>
</PageContent>
</form>

View file

@ -10,7 +10,6 @@
import Bill from "@assets/icons/bill.svg?dataurl"
import DangerCircle from "@assets/icons/danger-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Badge from "@lib/components/Badge.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import PageContent from "@lib/components/PageContent.svelte"
@ -235,9 +234,9 @@
Payment Methods
</strong>
{#if tenant}
<Badge variant={statusVariant} class="badge-sm capitalize">
<span class="badge badge-{statusVariant} badge-sm capitalize">
{status}
</Badge>
</span>
{/if}
</div>
<ul class="flex flex-col gap-3">

View file

@ -8,7 +8,6 @@
import FieldInline from "@lib/components/FieldInline.svelte"
import ToggleInput from "@lib/components/ToggleInput.svelte"
import Icon from "@lib/components/Icon.svelte"
import Card from "@lib/components/Card.svelte"
import PageContent from "@lib/components/PageContent.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
@ -87,7 +86,7 @@
<form {onsubmit}>
<PageContent>
<Card class="flex flex-col gap-4">
<div class="card flex flex-col gap-4">
<strong class="flex items-center gap-3 text-lg">
<Icon icon={ShieldMinimalistic} />
Privacy Settings
@ -147,8 +146,8 @@
</p>
{/snippet}
</FieldInline>
</Card>
<Card class="flex flex-col gap-4">
</div>
<div class="card flex flex-col gap-4">
<strong class="flex items-center gap-3 text-lg">
<Icon icon={LetterUnread} />
Message Requests
@ -201,13 +200,13 @@
<p>A sender counts as known when this many of the people you follow also follow them.</p>
{/snippet}
</Field>
</Card>
<Card class="sticky -bottom-3 shadow-md flex flex-row items-center justify-between gap-4">
</div>
<div class="card sticky -bottom-3 shadow-md flex flex-row items-center justify-between gap-4">
<Button class="button button-neutral" onclick={reset} disabled={loading}
>Discard Changes</Button>
<Button class="button button-primary" type="submit" disabled={loading}>
<Spinner {loading}>Save Changes</Spinner>
</Button>
</Card>
</div>
</PageContent>
</form>

View file

@ -4,7 +4,6 @@
import ArrowRight from "@assets/icons/arrow-right.svg?dataurl"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import Badge from "@lib/components/Badge.svelte"
import Icon from "@lib/components/Icon.svelte"
import Page from "@lib/components/Page.svelte"
import PageHeader from "@lib/components/PageHeader.svelte"
@ -35,7 +34,7 @@
<div class="flex flex-col gap-3">
<div class="flex items-start justify-between">
<img alt="{PLATFORM_NAME} Logo" src={PLATFORM_LOGO} class="h-10 w-10" />
<Badge variant="primary">Recommended</Badge>
<span class="badge badge-primary">Recommended</span>
</div>
<div class="flex flex-col gap-1">
<h3 class="text-lg font-bold">