Clean up convention violations, dead code, and daisyUI leftovers
This commit is contained in:
parent
c0c8354a76
commit
a2c5a8e2ff
38 changed files with 120 additions and 224 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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/"],
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]]})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = () => {
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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'}">
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue