Clean up convention violations, dead code, and daisyUI leftovers

This commit is contained in:
Jon Staab 2026-07-27 15:38:23 -07:00
parent c0c8354a76
commit a2c5a8e2ff
38 changed files with 120 additions and 224 deletions

View file

@ -6,7 +6,7 @@ Flotilla is a Nostr "relays as groups" community chat client. It implements NIP-
- SvelteKit 5.48+ with TypeScript 5.9+ - SvelteKit 5.48+ with TypeScript 5.9+
- Capacitor for cross-platform (Web/PWA, Android, iOS) - Capacitor for cross-platform (Web/PWA, Android, iOS)
- TailwindCSS + DaisyUI for styling - TailwindCSS for styling
- Welshman library suite for Nostr protocol - Welshman library suite for Nostr protocol
- IndexedDB for local storage - IndexedDB for local storage
- Vite for building - Vite for building

View file

@ -26,6 +26,10 @@ export default [
parser: ts.parser, parser: ts.parser,
}, },
}, },
rules: {
// eslint doesn't see type parameters declared by <script generics="...">
"no-undef": "off",
},
}, },
{ {
ignores: ["build/", ".svelte-kit/", "dist/"], ignores: ["build/", ".svelte-kit/", "dist/"],

View file

@ -35,7 +35,7 @@
"postcss": "^8.5.15", "postcss": "^8.5.15",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1", "prettier-plugin-svelte": "^3.4.1",
"sharp": "0.35.0-rc.0", "sharp": "^0.35.3",
"svelte": "^5.55.9", "svelte": "^5.55.9",
"svelte-check": "^4.3.5", "svelte-check": "^4.3.5",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",

Binary file not shown.

View file

@ -16,4 +16,4 @@ minimumReleaseAgeExclude:
- '@welshman/store' - '@welshman/store'
- '@welshman/util' - '@welshman/util'
overrides: overrides:
sharp: 0.35.0-rc.0 sharp: ^0.35.3

View file

@ -1,16 +1,6 @@
import {derived, get, writable, type Readable} from "svelte/store" import {derived, get, writable, type Readable} from "svelte/store"
import { import {dissoc, fromPairs, last, poll, randomId, sleep, tryCatch} from "@welshman/lib"
dissoc, import {AuthStatus, Pool, request, SocketStatus} from "@welshman/net"
filterVals,
fromPairs,
isDefined,
last,
poll,
randomId,
sleep,
tryCatch,
} from "@welshman/lib"
import {AuthStatus, load, Pool, request, SocketStatus} from "@welshman/net"
import { import {
displayRelayUrl, displayRelayUrl,
getTagValue, getTagValue,
@ -42,8 +32,6 @@ export type InviteData = {
code?: string code?: string
} }
export type InviteCreateStatus = "loading" | "network" | "auth" | "failed" | "noclaim" | "ready"
export type JoinRequestParams = { export type JoinRequestParams = {
url: string url: string
claim: string claim: string
@ -116,21 +104,6 @@ export const deriveRelayAuthError = (url: string) =>
} }
}) })
export const requestRelayClaim = async (url: string) => {
const filters = [{kinds: [RELAY_INVITE], limit: 1}]
const events = await load({filters, relays: [url]})
if (events.length > 0) {
return getTagValue("claim", events[0].tags)
}
}
export const requestRelayClaims = async (urls: string[]) =>
filterVals(
isDefined,
fromPairs(await Promise.all(urls.map(async url => [url, await requestRelayClaim(url)]))),
)
export const makeJoinRequest = (params: JoinRequestParams) => export const makeJoinRequest = (params: JoinRequestParams) =>
makeEvent(RELAY_JOIN, {tags: [["claim", params.claim]]}) makeEvent(RELAY_JOIN, {tags: [["claim", params.claim]]})

View file

@ -29,8 +29,6 @@ import {pushToast} from "@app/toast"
export const LIVEKIT_PARTICIPANTS = 39004 export const LIVEKIT_PARTICIPANTS = 39004
export {checkRelayHasLivekit} from "$lib/livekit"
export {supportsAudioOutputSelection} export {supportsAudioOutputSelection}
const LIVEKIT_DEFAULT_DEVICE_ID = "default" const LIVEKIT_DEFAULT_DEVICE_ID = "default"
@ -105,15 +103,6 @@ export const isParticipantSpeaking = derived(
$participants.some(sp => participantKey(sp) === participantKey(p)), $participants.some(sp => participantKey(sp) === participantKey(p)),
) )
export const isLocalSpeaking = derived(
[currentCallSession, speakingParticipants],
([$session, $speaking]) => {
if (!$session?.livekit) return false
const local = participantFromLiveKitIdentity($session.livekit.localParticipant.identity)
return $speaking.some(sp => participantKey(sp) === participantKey(local))
},
)
export const videoTrackRevision = writable(0) export const videoTrackRevision = writable(0)
export const triggerVideoTrackRevision = () => { export const triggerVideoTrackRevision = () => {

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import cx from "classnames"
import {page} from "$app/stores" import {page} from "$app/stores"
import {remove, uniq, formatTimestamp} from "@welshman/lib" import {remove, uniq, formatTimestamp} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
@ -35,8 +36,11 @@
<Button class="flex flex-col justify-start gap-1 w-full" onclick={openChat}> <Button class="flex flex-col justify-start gap-1 w-full" onclick={openChat}>
<div <div
class="cursor-pointer border-t border-solid px-3 py-2 transition-colors hover:bg-surface {props.class}" class={cx(
class:bg-surface={active} "cursor-pointer border-t border-solid px-3 py-2 transition-colors hover:bg-surface",
props.class,
{"bg-surface": active},
)}
style="border-color: var(--line)"> style="border-color: var(--line)">
<div class="flex flex-col justify-start gap-1"> <div class="flex flex-col justify-start gap-1">
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">

View file

@ -12,6 +12,8 @@
} }
const {pubkeys}: Props = $props() const {pubkeys}: Props = $props()
const back = () => history.back()
</script> </script>
<Modal> <Modal>
@ -28,6 +30,6 @@
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="button button-primary" onclick={() => history.back()}>Got it</Button> <Button class="button button-primary" onclick={back}>Got it</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View file

@ -72,8 +72,7 @@
{/if} {/if}
<div <div
data-event={event.id} data-event={event.id}
class="group flex items-center justify-end gap-1 px-2" class={cx("group flex items-center justify-end gap-1 px-2", {"flex-row-reverse": !isOwn})}>
class:flex-row-reverse={!isOwn}>
{#if !isMobile} {#if !isMobile}
<Tippy <Tippy
bind:popover bind:popover

View file

@ -35,6 +35,7 @@
const copyLink = () => clip(nevent1) const copyLink = () => clip(nevent1)
const copyPubkey = () => clip(npub1) const copyPubkey = () => clip(npub1)
const copyJson = () => clip(json) const copyJson = () => clip(json)
const back = () => history.back()
const formatter = new Intl.DateTimeFormat(LOCALE, { const formatter = new Intl.DateTimeFormat(LOCALE, {
dateStyle: "long", dateStyle: "long",
@ -110,6 +111,6 @@
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="button button-primary grow" onclick={() => history.back()}>Got it</Button> <Button class="button button-primary grow" onclick={back}>Got it</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View file

@ -6,6 +6,8 @@
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import {PLATFORM_NAME} from "@app/env" import {PLATFORM_NAME} from "@app/env"
const back = () => history.back()
</script> </script>
<Modal <Modal
@ -37,5 +39,5 @@
href="https://nostrapps.com#signers">nostrapps.com</Link href="https://nostrapps.com#signers">nostrapps.com</Link
>. >.
</p> </p>
<Button class="button button-primary" onclick={() => history.back()}>Got it</Button> <Button class="button button-primary" onclick={back}>Got it</Button>
</ModalBody></Modal> </ModalBody></Modal>

View file

@ -6,6 +6,8 @@
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import {PLATFORM_NAME} from "@app/env" import {PLATFORM_NAME} from "@app/env"
const back = () => history.back()
</script> </script>
<Modal <Modal
@ -27,5 +29,5 @@
class="underline">nostr.how</Link class="underline">nostr.how</Link
>. >.
</p> </p>
<Button class="button button-primary" onclick={() => history.back()}>Got it</Button> <Button class="button button-primary" onclick={back}>Got it</Button>
</ModalBody></Modal> </ModalBody></Modal>

View file

@ -5,6 +5,8 @@
import ModalBody from "@lib/components/ModalBody.svelte" import ModalBody from "@lib/components/ModalBody.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
const back = () => history.back()
</script> </script>
<Modal <Modal
@ -28,5 +30,5 @@
To learn more about how to manage your keys, or to set up an account, try To learn more about how to manage your keys, or to set up an account, try
<Link external class="link" href="https://nosta.me/">nosta.me</Link>. <Link external class="link" href="https://nosta.me/">nosta.me</Link>.
</p> </p>
<Button class="button button-primary" onclick={() => history.back()}>Got it</Button> <Button class="button button-primary" onclick={back}>Got it</Button>
</ModalBody></Modal> </ModalBody></Modal>

View file

@ -7,6 +7,8 @@
import Modal from "@lib/components/Modal.svelte" import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte" import ModalBody from "@lib/components/ModalBody.svelte"
import {PLATFORM_NAME} from "@app/env" import {PLATFORM_NAME} from "@app/env"
const back = () => history.back()
</script> </script>
<Modal> <Modal>
@ -27,6 +29,6 @@
</p> </p>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="button button-primary" onclick={() => history.back()}>Got it</Button> <Button class="button button-primary" onclick={back}>Got it</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View file

@ -88,10 +88,7 @@
Title Title
{/snippet} {/snippet}
{#snippet input()} {#snippet input()}
<input <input bind:value={title} class="input w-full" placeholder="Optional title" />
bind:value={title}
class="input input-bordered w-full"
placeholder="Optional title" />
{/snippet} {/snippet}
</Field> </Field>
<Field> <Field>
@ -133,7 +130,7 @@
<p>A URL or nostr link (note, nevent, naddr, npub, or nprofile).</p> <p>A URL or nostr link (note, nevent, naddr, npub, or nprofile).</p>
{/snippet} {/snippet}
{#snippet input()} {#snippet input()}
<input bind:value class="input input-bordered w-full" placeholder="URL or nevent..." /> <input bind:value class="input w-full" placeholder="URL or nevent..." />
{/snippet} {/snippet}
</Field> </Field>
</ModalBody> </ModalBody>

View file

@ -65,7 +65,7 @@
Title Title
{/snippet} {/snippet}
{#snippet input()} {#snippet input()}
<input bind:value={title} class="input input-bordered w-full" placeholder="Shelf title" /> <input bind:value={title} class="input w-full" placeholder="Shelf title" />
{/snippet} {/snippet}
</Field> </Field>
<Field> <Field>
@ -75,7 +75,7 @@
{#snippet input()} {#snippet input()}
<input <input
bind:value={description} bind:value={description}
class="input input-bordered w-full" class="input w-full"
placeholder="What's this shelf about?" /> placeholder="What's this shelf about?" />
{/snippet} {/snippet}
</Field> </Field>

View file

@ -12,7 +12,7 @@
import {canEnforceNip70} from "@app/relays" import {canEnforceNip70} from "@app/relays"
import {makePollPath, makeSpacePath} from "@app/routes" import {makePollPath, makeSpacePath} from "@app/routes"
interface Props { type Props = {
url: string url: string
event: TrustedEvent event: TrustedEvent
showRoom?: boolean showRoom?: boolean

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import cx from "classnames"
import {userProfile} from "@welshman/app" import {userProfile} from "@welshman/app"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
@ -33,7 +34,7 @@
) )
</script> </script>
<div class="primary-nav" class:justify-between={PLATFORM_RELAYS.length === 0}> <div class={cx("primary-nav", {"justify-between": PLATFORM_RELAYS.length === 0})}>
<PrimaryNavSpaces /> <PrimaryNavSpaces />
{#if PLATFORM_RELAYS.length > 0} {#if PLATFORM_RELAYS.length > 0}
<Divider /> <Divider />

View file

@ -41,6 +41,7 @@
const copyLink = () => clip(nprofile1) const copyLink = () => clip(nprofile1)
const copyPubkey = () => clip(npub1) const copyPubkey = () => clip(npub1)
const copyJson = () => clip(json) const copyJson = () => clip(json)
const back = () => history.back()
const formatter = new Intl.DateTimeFormat(LOCALE, { const formatter = new Intl.DateTimeFormat(LOCALE, {
dateStyle: "long", dateStyle: "long",
@ -116,6 +117,6 @@
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="button button-primary grow" onclick={() => history.back()}>Got it</Button> <Button class="button button-primary grow" onclick={back}>Got it</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View file

@ -16,6 +16,8 @@
} }
const {subtitle = "", pubkeys, url, ...restProps}: Props = $props() const {subtitle = "", pubkeys, url, ...restProps}: Props = $props()
const back = () => history.back()
</script> </script>
<Modal> <Modal>
@ -31,6 +33,6 @@
{/each} {/each}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="button button-primary" onclick={() => history.back()}>Got it</Button> <Button class="button button-primary" onclick={back}>Got it</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View file

@ -515,8 +515,9 @@
{#if type === "new-messages"} {#if type === "new-messages"}
<div <div
{id} {id}
class="flex items-center py-2 text-xs transition-colors" class={cx("flex items-center py-2 text-xs transition-colors", {
class:opacity-0={showFixedNewMessages}> "opacity-0": showFixedNewMessages,
})}>
<div class="h-px grow bg-primary text-primary-content"></div> <div class="h-px grow bg-primary text-primary-content"></div>
<p <p
class="rounded-full bg-primary text-primary-content px-2 py-1" class="rounded-full bg-primary text-primary-content px-2 py-1"

View file

@ -131,7 +131,7 @@
</Tippy> </Tippy>
</Button> </Button>
</div> </div>
<div class="chat-editor grow overflow-hidden rounded-box"> <div class="chat-editor grow overflow-hidden">
<EditorContent {autofocus} {editor} /> <EditorContent {autofocus} {editor} />
</div> </div>
<Button <Button

View file

@ -161,8 +161,10 @@
<strong class="flex items-center gap-1 relative"> <strong class="flex items-center gap-1 relative">
<RelayName {url} class="truncate min-w-0" /> <RelayName {url} class="truncate min-w-0" />
<div <div
class="absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary text-primary-content transition-all opacity-0" class={cx(
class:opacity-100={$userIsAdmin && $actionItems.length > 0}> "absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary text-primary-content transition-all",
$userIsAdmin && $actionItems.length > 0 ? "opacity-100" : "opacity-0",
)}>
</div> </div>
{#if $notificationSettings.push && !$shouldNotify} {#if $notificationSettings.push && !$shouldNotify}
<Icon icon={BellOff} size={3} class="opacity-50" /> <Icon icon={BellOff} size={3} class="opacity-50" />

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames"
import {parse, renderAsHtml} from "@welshman/content" import {parse, renderAsHtml} from "@welshman/content"
import Close from "@assets/icons/close.svg?dataurl" import Close from "@assets/icons/close.svg?dataurl"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
@ -84,16 +85,16 @@
{#key $toast.id} {#key $toast.id}
<div <div
role="alert" role="alert"
class="card relative flex justify-center whitespace-normal text-left" class={cx("card relative flex justify-center whitespace-normal text-left", {
class:text-content={theme === "info"} "text-content": theme === "info",
class:text-error={theme === "error"} "border-error text-error": theme === "error",
style={theme === "error" ? "border-color: var(--error)" : ""}> })}>
<Button <Button
class="button button-neutral button-xs button-circle absolute -top-2 -right-2 hidden md:inline-flex flex justify-center items-center" class="button button-neutral button-xs button-circle absolute -top-2 -right-2 hidden md:inline-flex flex justify-center items-center"
onclick={onClose}> onclick={onClose}>
<Icon icon={Close} size={4} /> <Icon icon={Close} size={4} />
</Button> </Button>
<p class="md:pr-6" class:welshman-content-error={theme === "error"}> <p class={cx("md:pr-6", {"welshman-content-error": theme === "error"})}>
{#if $toast.message} {#if $toast.message}
{@html renderAsHtml(parse({content: $toast.message}))} {@html renderAsHtml(parse({content: $toast.message}))}
{#if $toast.action} {#if $toast.action}

View file

@ -247,7 +247,7 @@
</div> </div>
{/if} {/if}
<span <span
class="pointer-events-none absolute bottom-1 left-1 max-w-[calc(100%-0.5rem)] truncate rounded bg-base-100/80 px-1.5 py-0.5 text-xs"> class="pointer-events-none absolute bottom-1 left-1 max-w-[calc(100%-0.5rem)] truncate rounded bg-surface/80 px-1.5 py-0.5 text-xs">
{labelFor(tile.liveKitIdentity, tile.source)}{tile.isLocal ? " (you)" : ""} {labelFor(tile.liveKitIdentity, tile.source)}{tile.isLocal ? " (you)" : ""}
</span> </span>
{#if videoTiles.length > 1} {#if videoTiles.length > 1}

View file

@ -1,18 +1,9 @@
import {deriveItems, deriveItemsByKey, getter, makeDeriveItem, makeLoadItem} from "@welshman/store" import {repository, tracker} from "@welshman/app"
import { import {writable} from "svelte/store"
ensurePlaintext,
makeOutboxLoader,
makeUserData,
makeUserLoader,
repository,
tracker,
} from "@welshman/app"
import {derived, writable} from "svelte/store"
import { import {
batch, batch,
between, between,
call, call,
groupBy,
insertAt, insertAt,
int, int,
now, now,
@ -22,87 +13,13 @@ import {
MONTH, MONTH,
YEAR, YEAR,
} from "@welshman/lib" } from "@welshman/lib"
import { import {EVENT_TIME, getAddress, getTagValue, matchFilters} from "@welshman/util"
Address, import type {Filter, TrustedEvent} from "@welshman/util"
EVENT_TIME, import {mergeRepositoryUpdates, request} from "@welshman/net"
FEED,
FEEDS,
asDecryptedEvent,
getAddress,
getIdFilters,
getTagValue,
matchFilters,
readList,
} from "@welshman/util"
import type {Filter, PublishedList, TrustedEvent} from "@welshman/util"
import {load, mergeRepositoryUpdates, request} from "@welshman/net"
import type {RepositoryUpdate} from "@welshman/net" import type {RepositoryUpdate} from "@welshman/net"
import {Router} from "@welshman/router"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
import {readFeed} from "@lib/feeds"
import {getEventsForUrl} from "@app/repository" import {getEventsForUrl} from "@app/repository"
export const feedsByAddress = deriveItemsByKey({
repository,
getKey: feed => getAddress(feed.event),
filters: [{kinds: [FEED]}],
eventToItem: readFeed,
})
export const getFeedsByAddress = getter(feedsByAddress)
export const feeds = deriveItems(feedsByAddress)
export const getFeeds = getter(feeds)
export const getFeed = (address: string) => getFeedsByAddress().get(address)
export const fetchFeed = (address: string) => {
const {pubkey} = Address.from(address)
return load({
relays: Router.get().FromPubkey(pubkey).getUrls(),
filters: getIdFilters([address]),
})
}
export const loadFeed = makeLoadItem(fetchFeed, getFeed)
export const deriveFeed = makeDeriveItem(feedsByAddress, loadFeed)
export const feedsByPubkey = derived(feeds, $feeds => groupBy(f => f.event.pubkey, $feeds))
export const getFeedsByPubkey = getter(feedsByPubkey)
export const getFeedsForPubkey = (pubkey: string) => getFeedsByPubkey().get(pubkey)
export const loadFeedsForPubkey = makeLoadItem(makeOutboxLoader(FEED), getFeedsForPubkey)
export const userFeeds = makeUserData(feedsByPubkey, loadFeedsForPubkey)
export const loadUserFeeds = makeUserLoader(loadFeedsForPubkey)
export const feedFavoritesByPubkey = deriveItemsByKey<PublishedList>({
repository,
getKey: list => list.event.pubkey,
filters: [{kinds: [FEEDS]}],
eventToItem: async event =>
readList(
asDecryptedEvent(event, {
content: await ensurePlaintext(event),
}),
),
})
export const getFeedFavoritesByPubkey = getter(feedFavoritesByPubkey)
export const getFeedFavorites = (pubkey: string) => getFeedFavoritesByPubkey().get(pubkey)
export const loadFeedFavorites = makeLoadItem(makeOutboxLoader(FEEDS), getFeedFavorites)
export const userFeedFavorites = makeUserData(feedFavoritesByPubkey, loadFeedFavorites)
export const loadUserFeedFavorites = makeUserLoader(loadFeedFavorites)
export const makeFeed = ({ export const makeFeed = ({
relays, relays,

View file

@ -19,7 +19,7 @@ import {
getTagValues, getTagValues,
sortEventsAsc, sortEventsAsc,
} from "@welshman/util" } from "@welshman/util"
import type {Filter, PublishedRoomMeta, TrustedEvent} from "@welshman/util" import type {Filter, ManagementRequest, PublishedRoomMeta, TrustedEvent} from "@welshman/util"
import {first, simpleCache, sortBy, spec, uniq} from "@welshman/lib" import {first, simpleCache, sortBy, spec, uniq} from "@welshman/lib"
import {addRoomMember, manageRelay, pubkey, waitForThunkError} from "@welshman/app" import {addRoomMember, manageRelay, pubkey, waitForThunkError} from "@welshman/app"
import {load} from "@welshman/net" import {load} from "@welshman/net"
@ -47,7 +47,7 @@ const DEFAULT_LIGHTNESS = 0.5
// Parse the hue from a ["color", hue] tag, falling back to 0. // Parse the hue from a ["color", hue] tag, falling back to 0.
export const parseRoleColor = (tags: string[][]): number => { export const parseRoleColor = (tags: string[][]): number => {
const hue = parseFloat(first(getTags("color", tags))?.[1] ?? "") const hue = parseFloat(getTagValue("color", tags) ?? "")
return isNaN(hue) ? 0 : hue return isNaN(hue) ? 0 : hue
} }
@ -105,6 +105,11 @@ export const deriveSpaceMemberRoles = (url: string) =>
const reloadRole = (url: string, id: string) => const reloadRole = (url: string, id: string) =>
load({relays: [url], filters: [{kinds: [RELAY_ROLE], "#d": [id]}]}) load({relays: [url], filters: [{kinds: [RELAY_ROLE], "#d": [id]}]})
// Roles extend NIP-86: these methods aren't in welshman's ManagementMethod, and hue and
// order go over the wire as JSON numbers, not strings, per the relay's implementation.
const manageRole = (url: string, method: string, params: (string | number)[]) =>
manageRelay(url, {method, params} as ManagementRequest)
export const createRole = async ( export const createRole = async (
url: string, url: string,
id: string, id: string,
@ -113,11 +118,7 @@ export const createRole = async (
color: number, color: number,
order: number, order: number,
): Promise<string | undefined> => { ): Promise<string | undefined> => {
const {error} = await manageRelay(url, { const {error} = await manageRole(url, "createrole", [id, label, description, color, order])
method: "createrole" as ManagementMethod,
// hue and order go over the wire as JSON numbers, not strings, per the relay's NIP-86 impl.
params: [id, label, description, color, order] as unknown as string[],
})
if (!error) { if (!error) {
await reloadRole(url, id) await reloadRole(url, id)
@ -134,11 +135,7 @@ export const editRole = async (
color: number, color: number,
order: number, order: number,
): Promise<string | undefined> => { ): Promise<string | undefined> => {
const {error} = await manageRelay(url, { const {error} = await manageRole(url, "editrole", [id, label, description, color, order])
method: "editrole" as ManagementMethod,
// hue and order go over the wire as JSON numbers, not strings, per the relay's NIP-86 impl.
params: [id, label, description, color, order] as unknown as string[],
})
if (!error) { if (!error) {
await reloadRole(url, id) await reloadRole(url, id)
@ -148,10 +145,7 @@ export const editRole = async (
} }
export const deleteRole = async (url: string, id: string): Promise<string | undefined> => { export const deleteRole = async (url: string, id: string): Promise<string | undefined> => {
const {error} = await manageRelay(url, { const {error} = await manageRole(url, "deleterole", [id])
method: "deleterole" as ManagementMethod,
params: [id],
})
return error return error
} }
@ -161,10 +155,7 @@ export const assignRole = async (
pubkey: string, pubkey: string,
roleId: string, roleId: string,
): Promise<string | undefined> => { ): Promise<string | undefined> => {
const {error} = await manageRelay(url, { const {error} = await manageRole(url, "assignrole", [pubkey, roleId])
method: "assignrole" as ManagementMethod,
params: [pubkey, roleId],
})
return error return error
} }
@ -174,10 +165,7 @@ export const unassignRole = async (
pubkey: string, pubkey: string,
roleId: string, roleId: string,
): Promise<string | undefined> => { ): Promise<string | undefined> => {
const {error} = await manageRelay(url, { const {error} = await manageRole(url, "unassignrole", [pubkey, roleId])
method: "unassignrole" as ManagementMethod,
params: [pubkey, roleId],
})
return error return error
} }

View file

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

View file

@ -1,7 +1,16 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import type {HTMLAttributes} from "svelte/elements"
import cx from "classnames" import cx from "classnames"
type Props = HTMLAttributes<HTMLElement> & {
children?: Snippet
tag?: string
sm?: boolean
flat?: boolean
interactive?: boolean
}
const { const {
children, children,
tag = "div", tag = "div",
@ -9,15 +18,7 @@
flat = false, flat = false,
interactive = false, interactive = false,
...restProps ...restProps
}: { }: Props = $props()
children?: Snippet
tag?: string
sm?: boolean
flat?: boolean
interactive?: boolean
class?: string
[key: string]: any
} = $props()
const className = $derived( const className = $derived(
cx( cx(

View file

@ -8,10 +8,9 @@
title?: Snippet title?: Snippet
info?: Snippet info?: Snippet
primary?: boolean primary?: boolean
[key: string]: any
} }
const {icon, title, info, primary, ...props}: Props = $props() const {icon, title, info, primary}: Props = $props()
</script> </script>
<div class="card-button {primary ? 'card-button-primary' : 'card-button-neutral'}"> <div class="card-button {primary ? 'card-button-primary' : 'card-button-neutral'}">

View file

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

View file

@ -7,7 +7,7 @@
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
interface Props { type Props = {
value?: number | undefined value?: number | undefined
} }

View file

@ -5,7 +5,7 @@
import {anchorDatepicker} from "@lib/html" import {anchorDatepicker} from "@lib/html"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
interface Props { type Props = {
start?: number | undefined start?: number | undefined
end?: number | undefined end?: number | undefined
} }

View file

@ -1,7 +1,15 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import type {HTMLInputAttributes} from "svelte/elements"
import cx from "classnames" import cx from "classnames"
type Props = Omit<HTMLInputAttributes, "value"> & {
value?: string
before?: Snippet
after?: Snippet
sm?: boolean
}
let { let {
value = $bindable(), value = $bindable(),
before, before,
@ -9,14 +17,7 @@
sm = false, sm = false,
class: className = "", class: className = "",
...restProps ...restProps
}: { }: Props = $props()
value?: any
before?: Snippet
after?: Snippet
sm?: boolean
class?: string
[key: string]: any
} = $props()
const wrapperClass = $derived(cx("input", "input-group", {"input-sm": sm}, className)) const wrapperClass = $derived(cx("input", "input-group", {"input-sm": sm}, className))
</script> </script>

View file

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts" generics="T">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
type Props = { type Props = {
items: any[] items: T[]
child: Snippet<[any]> child: Snippet<[T]>
getKey: (item: any) => PropertyKey getKey: (item: T) => PropertyKey
// Minimum column width, in tailwind spacing units (1 = 0.25rem). The browser // Minimum column width, in tailwind spacing units (1 = 0.25rem). The browser
// fits as many columns as the container allows, so the layout stays // fits as many columns as the container allows, so the layout stays
// responsive without media queries. // responsive without media queries.

View file

@ -2,7 +2,7 @@
import cx from "classnames" import cx from "classnames"
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
interface Props { type Props = {
element?: Element element?: Element
children?: Snippet children?: Snippet
noPad?: boolean noPad?: boolean

View file

@ -56,6 +56,14 @@
const back = () => history.back() const back = () => history.back()
const toggleMenu = () => {
menuOpen = !menuOpen
}
const closeMenu = () => {
menuOpen = false
}
const showInfo = () => { const showInfo = () => {
menuOpen = false menuOpen = false
@ -122,11 +130,11 @@
<Button <Button
class="button button-neutral button-sm button-square" class="button button-neutral button-sm button-square"
aria-label="More options" aria-label="More options"
onclick={() => (menuOpen = !menuOpen)}> onclick={toggleMenu}>
<Icon size={4} icon={MenuDots} /> <Icon size={4} icon={MenuDots} />
</Button> </Button>
{#if menuOpen} {#if menuOpen}
<Popover hideOnClick onClose={() => (menuOpen = false)}> <Popover hideOnClick onClose={closeMenu}>
<ul <ul
transition:fly transition:fly
class="menu bg-surface absolute right-0 z-popover mt-2 w-48 gap-1 rounded-2xl p-2"> class="menu bg-surface absolute right-0 z-popover mt-2 w-48 gap-1 rounded-2xl p-2">