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:
parent
c8c6bd4465
commit
66bb75797b
71 changed files with 385 additions and 602 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
label?: Snippet
|
||||
secondary?: Snippet
|
||||
input?: Snippet
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
type Props = {
|
||||
label?: import("svelte").Snippet
|
||||
input?: import("svelte").Snippet
|
||||
info?: import("svelte").Snippet
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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})
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import cx from "classnames"
|
||||
import type {Snippet} from "svelte"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
class?: string
|
||||
children?: Snippet
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
children: Snippet
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
children: Snippet
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
type Props = {
|
||||
children?: import("svelte").Snippet
|
||||
[key: string]: any
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
children?: Snippet
|
||||
class?: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
type Props = {
|
||||
title?: import("svelte").Snippet
|
||||
info?: import("svelte").Snippet
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import cx from "classnames"
|
||||
import type {Snippet} from "svelte"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
class?: string
|
||||
visible?: boolean
|
||||
children?: Snippet
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
type Props = {
|
||||
children?: import("svelte").Snippet
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
type Props = {
|
||||
children?: import("svelte").Snippet
|
||||
[key: string]: any
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let value
|
||||
</script>
|
||||
|
||||
{value}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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+: /, "")
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue