Initial commit

This commit is contained in:
2026-05-25 14:23:03 +02:00
commit ddd5c15cbe
131 changed files with 9643 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
--background: #fdba74;
--foreground: oklch(0.141 0.005 285.823);
--card: #fef9c3;
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: #ff9398;
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: #059669;
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: #422006;
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
<script lang="ts">
import { readingTime } from '$lib/utils';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import * as Card from '$lib/components/ui/card';
import Tags from './Tags.svelte';
import CalendarArrowUp from '@lucide/svelte/icons/calendar-arrow-up';
import BookOpenCheck from '@lucide/svelte/icons/book-open-check';
export let event: NDKEvent;
const longFormClient = 'https://grimoire.rocks/run?cmd=open%20';
</script>
<Card.Root
class="w-72 flex-none shadow-sm transition-shadow duration-200 hover:shadow-lg md:w-auto md:flex-auto"
>
<img
src={event.tagValue('image')}
class="max-h-[900px] w-full rounded-t-xl"
alt={event.tagValue('name') ? event.tagValue('name') : event.tagValue('title')}
/>
<Card.Header>
<Card.Title class="hover:text-neutral-400">
<a href="{longFormClient}{event.encode()}">
{event.tagValue('name') ? event.tagValue('name') : event.tagValue('title')}
</a>
</Card.Title>
<Card.Description>
<div class="flex items-center">
<CalendarArrowUp class="h-4" />
{new Date(Number(event.tagValue('published_at')) * 1000).toLocaleDateString('es-ES')}
</div>
<div class="flex items-center">
<BookOpenCheck class="h-4" />
{readingTime(event.content)} mins
</div>
</Card.Description>
</Card.Header>
<Card.Content class="relative max-h-32 overflow-hidden">
{event.tagValue('summary')}
<div
class="pointer-events-none absolute right-0 bottom-0 left-0 h-16 bg-gradient-to-t from-card to-transparent"
></div>
</Card.Content>
<Card.Footer class="relative max-h-24 items-start overflow-hidden">
<Tags tags={event.tags.filter((v) => v[0] === 't')} />
</Card.Footer>
</Card.Root>
+138
View File
@@ -0,0 +1,138 @@
<script lang="ts">
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import ndk from '$lib/stores/ndk';
import { parseDriveEvent, getBlobUrl, fileName, type BlossomFile } from '$lib/blossom';
import type { NDKFilter } from '@nostr-dev-kit/ndk';
import PdfCover from './PdfCover.svelte';
export let pubkey: string;
export let driveId: string;
let driveName = '';
let description = '';
let files: BlossomFile[] = [];
let servers: string[] = [];
let loading = true;
let error = '';
let activePdf: string | null = null;
onMount(() => {
const $ndk = get(ndk);
const filter: NDKFilter = {
kinds: [30563 as number],
authors: [pubkey],
'#d': [driveId]
};
$ndk
.fetchEvent(filter)
.then((event) => {
if (!event) {
error = 'Drive not found.';
return;
}
const drive = parseDriveEvent(event);
driveName = drive.name;
description = drive.description;
servers = drive.servers;
files = drive.files.filter(
(f) => f.mimeType === 'application/pdf' || f.path.endsWith('.pdf')
);
})
.catch((e: unknown) => {
error = e instanceof Error ? e.message : String(e);
})
.finally(() => {
loading = false;
});
});
function openPdf(file: BlossomFile) {
activePdf = getBlobUrl(file.sha256, servers, '.pdf');
}
function closePdf() {
activePdf = null;
}
</script>
{#if loading}
<p class="animate-pulse text-sm text-gray-500">Loading drive…</p>
{:else if error}
<p class="text-sm font-medium text-red-600">Error: {error}</p>
{:else}
<div class="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4">
{#each files as file (file.sha256)}
{@const blobUrl = getBlobUrl(file.sha256, servers, '.pdf')}
{@const name = fileName(file.path)}
<div class="group flex flex-col gap-2">
<!-- Cover — clicking opens the viewer -->
<button
on:click={() => openPdf(file)}
class="block w-full overflow-hidden rounded-lg shadow-md ring-2 ring-transparent transition-all duration-200 group-hover:-translate-y-1 group-hover:shadow-xl group-hover:ring-indigo-400"
aria-label="Read {name}"
>
<PdfCover url={blobUrl} alt={name} />
</button>
<!-- Title -->
<p
class="truncate px-1 text-center text-xs leading-tight font-medium text-gray-700 transition-colors group-hover:text-indigo-600"
title={name}
>
{name.replace(/\.pdf$/i, '')}
</p>
<!-- Download link -->
<a
href={blobUrl}
download={name}
target="_blank"
rel="noopener noreferrer"
class="text-center text-xs text-gray-400 transition-colors hover:text-indigo-500"
>
⬇ Download
</a>
</div>
{/each}
</div>
{/if}
<!-- PDF Viewer Modal -->
{#if activePdf}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
on:click={closePdf}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="relative flex w-[90vw] max-w-4xl flex-col rounded-xl bg-white p-4 shadow-2xl"
on:click|stopPropagation
>
<!-- Modal header -->
<div class="mb-3 flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">Fanzine Viewer</span>
<button
on:click={closePdf}
class="text-xl leading-none text-gray-400 transition-colors hover:text-gray-700"
aria-label="Close"
>
</button>
</div>
<!-- PDF iframe -->
<iframe
src={activePdf}
title="Fanzine PDF"
class="w-full rounded border border-gray-200"
style="height: 80vh;"
></iframe>
</div>
</div>
{/if}
+34
View File
@@ -0,0 +1,34 @@
<script lang="ts">
// import { _ } from '../../services/i18n'
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import * as Card from '$lib/components/ui/card';
import RsvpButton from './RsvpButton.svelte';
import CalendarEventInfo from './CalendarEventInfo.svelte';
export let event: NDKEvent;
</script>
<Card.Root
class="w-72 flex-none shadow-sm transition-shadow duration-200 hover:shadow-md md:w-auto md:flex-auto"
>
<div class="flex w-full items-start">
<img
src={event.tagValue('image')}
class="max-h-full max-w-full rounded-t-xl object-contain"
alt={event.tagValue('name') ? event.tagValue('name') : event.tagValue('title')}
/>
</div>
<Card.Header>
<Card.Title class="hover:text-neutral-300">
<a href="/calendar/{event.encode()}">
{event.tagValue('name') ? event.tagValue('name') : event.tagValue('title')}
</a>
</Card.Title>
<Card.Description>
<CalendarEventInfo {event} />
</Card.Description>
<Card.Action>
<RsvpButton {event} />
</Card.Action>
</Card.Header>
</Card.Root>
@@ -0,0 +1,125 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { writable } from 'svelte/store';
// import { _ } from '../services/i18n';
import ndk from '$lib/stores/ndk';
// import { Avatar, Popover } from 'flowbite-svelte';
import type { NDKEvent, NDKSubscription, NDKUser, NDKKind } from '@nostr-dev-kit/ndk';
import * as Avatar from '$lib/components/ui/avatar/index.js';
import type { NDKEventStore, ExtendedBaseType } from '@nostr-dev-kit/ndk-svelte';
export let event: NDKEvent;
interface Attendee {
user: NDKUser;
status: string;
rsvpEvent: NDKEvent;
}
const attendees = writable<Attendee[]>([]);
let subscription: NDKSubscription | null = null;
let isLoading = true;
onMount(async () => {
if (!$ndk) return;
// Subscribe to RSVP events for this specific event
subscription = $ndk.subscribe({
kinds: [31925 as NDKKind],
'#e': [event.id]
});
subscription.on('event', async (event: NDKEvent) => {
await handleRsvpEvent(event);
});
subscription.on('eose', () => {
isLoading = false;
});
// Start the subscription
subscription.start();
});
onDestroy(() => {
if (subscription) {
subscription.stop();
}
});
async function handleRsvpEvent(rsvpEvent: NDKEvent) {
// Get the status from the event tags
const statusTag = rsvpEvent.tags.find((tag) => tag[0] === 'status');
const status = statusTag?.[1];
if (!status) return;
// Get the user who made the RSVP
const user = rsvpEvent.author;
if (!user) return;
// Fetch user profile if not already loaded
if (!user.profile) {
await user.fetchProfile();
}
attendees.update((currentAttendees) => {
// Remove any existing RSVP from this user
const filtered = currentAttendees.filter((attendee) => attendee.user.pubkey !== user.pubkey);
// Only add if status is 'accepted'
if (status === 'accepted') {
return [...filtered, { user, status, rsvpEvent }];
}
return filtered;
});
}
$: acceptedAttendees = $attendees.filter((attendee) => attendee.status === 'accepted');
let responses: NDKEventStore<ExtendedBaseType<NDKEvent>>;
const filter = {
kinds: [31925 as number],
'#a': [`${event.kind}:${event.author.pubkey}:${event.dTag}`]
};
responses = $ndk.storeSubscribe(filter, { closeOnEose: true });
onMount(() => {
responses = $ndk.storeSubscribe(filter, { closeOnEose: true });
console.log(responses);
});
$: totalresponses = $responses.length;
onDestroy(() => responses?.unsubscribe());
</script>
<!-- <h3 class="text-lg font-semibold"> -->
<!-- Attending ({acceptedAttendees.length}) -->
<!-- </h3> -->
{#if isLoading}
<div class="flex items-center gap-2">
<div class="h-6 w-6 animate-spin rounded-full border-b-2 border-primary"></div>
<span class="text-sm text-muted-foreground">Cargando asistentes...</span>
</div>
{:else if acceptedAttendees.length === 0}
<p class="text-sm text-muted-foreground">Aún no se ha apuntado nadie</p>
{:else}
<div class="flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background">
{#each acceptedAttendees as attendee (attendee.user.pubkey)}
<Avatar.Root>
<Avatar.Image
src={attendee.user.profile?.image || attendee.user.profile?.picture}
alt={attendee.user.profile?.displayName || attendee.user.profile?.name}
/>
<Avatar.Fallback
>{attendee.user.profile?.displayName.slice(0, 2) ||
attendee.user.profile?.name.slice(0, 2)}</Avatar.Fallback
>
</Avatar.Root>
{/each}
</div>
{/if}
+47
View File
@@ -0,0 +1,47 @@
<script lang="ts">
// import { _ } from '../../services/i18n'
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import Calendar from '@lucide/svelte/icons/calendar';
import Clock from '@lucide/svelte/icons/clock';
import MapPin from '@lucide/svelte/icons/map-pin';
import Separator from '$lib/components/ui/separator/separator.svelte';
export let event: NDKEvent;
function getTagValue(tags: string[][], name: string, position: number = 0): string | null {
const found = tags.find((v) => v[0] === name);
if (!found) return null;
const [, ...values] = found;
return values[position];
}
</script>
<div class="flex items-center">
<Calendar class="h-4" />
{new Date(Number(event.tagValue('start')) * 1000).toLocaleDateString('es-ES', {
weekday: 'short',
day: 'numeric',
month: '2-digit',
year: '2-digit'
})}
</div>
<Separator orientation="vertical" />
<div class="flex items-center">
<Clock class="h-4" />
{new Date(Number(event.tagValue('start')) * 1000).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})}-{new Date(Number(event.tagValue('end')) * 1000).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})}
</div>
<Separator orientation="vertical" />
<div class="flex items-center">
<MapPin class="h-4" />
{#if event.tagValue('location')}
{getTagValue(event.tags, 'location', 1)
? getTagValue(event.tags, 'location', 1)
: getTagValue(event.tags, 'location')}
{/if}
</div>
+154
View File
@@ -0,0 +1,154 @@
<script lang="ts">
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import ndk from '$lib/stores/ndk';
import { parseDriveEvent, getBlobUrl, fileName, type BlossomFile } from '$lib/blossom';
import type { NDKFilter } from '@nostr-dev-kit/ndk';
import MoveRight from '@lucide/svelte/icons/move-right';
import PdfCover from './PdfCover.svelte';
export let pubkey: string;
export let driveId: string;
/** How many fanzines to show before "see all" — mirrors your products.slice(0, 3) */
export let limit: number = 3;
/** The href for the "see all" link */
export let href: string = '/fanzines';
/** Section heading */
export let heading: string = 'Fanzines';
let files: BlossomFile[] = [];
let servers: string[] = [];
let loading = true;
let error = '';
let activePdf: string | null = null;
let activeTitle: string = '';
onMount(() => {
const $ndk = get(ndk);
const filter: NDKFilter = {
kinds: [30563],
authors: [pubkey],
'#d': [driveId]
};
$ndk
.fetchEvent(filter)
.then((event) => {
if (!event) {
error = 'Drive not found.';
return;
}
const drive = parseDriveEvent(event);
servers = drive.servers;
files = drive.files.filter(
(f) => f.mimeType === 'application/pdf' || f.path.endsWith('.pdf')
);
})
.catch((e: unknown) => {
error = e instanceof Error ? e.message : String(e);
})
.finally(() => {
loading = false;
});
});
function openPdf(file: BlossomFile) {
activePdf = getBlobUrl(file.sha256, servers, '.pdf');
activeTitle = fileName(file.path);
}
function closePdf() {
activePdf = null;
activeTitle = '';
}
</script>
<section>
<a {href} class="mb-4 flex items-center gap-1 text-2xl font-bold hover:text-neutral-500">
{heading}<MoveRight />
</a>
{#if loading}
<!-- Skeleton row — same layout as the real cards -->
<div
class="flex gap-4 overflow-x-auto pb-4 md:grid md:grid-cols-2 md:overflow-visible md:pb-0 lg:grid-cols-3"
>
{#each Array(limit) as _}
<div class="flex w-40 shrink-0 flex-col gap-2 md:w-auto">
<div class="aspect-[3/4] animate-pulse rounded-lg bg-neutral-200" />
<div class="mx-auto h-3 w-3/4 animate-pulse rounded bg-neutral-200" />
</div>
{/each}
</div>
{:else if error}
<p class="text-sm text-red-500">{error}</p>
{:else if files.length === 0}
<p class="text-sm text-neutral-400">No fanzines found.</p>
{:else}
<!-- Horizontal scroll on mobile, grid on md+ — exactly your pattern -->
<div
class="flex gap-4 overflow-x-auto pb-4 md:grid md:grid-cols-2 md:overflow-visible md:pb-0 lg:grid-cols-3"
>
{#each files.slice(0, limit) as file (file.sha256)}
{@const blobUrl = getBlobUrl(file.sha256, servers, '.pdf')}
{@const name = fileName(file.path).replace(/\.pdf$/i, '')}
<!-- FanzineCard — mirrors your <ProductCard /> -->
<div
class="group flex w-40 shrink-0 cursor-pointer flex-col gap-2 md:w-auto"
on:click={() => openPdf(file)}
on:keydown={(e) => e.key === 'Enter' && openPdf(file)}
role="button"
tabindex="0"
aria-label="Read {name}"
>
<!-- Cover -->
<div
class="overflow-hidden rounded-lg shadow-md ring-2 ring-transparent transition-all duration-200 group-hover:-translate-y-1 group-hover:shadow-lg group-hover:ring-neutral-400"
>
<PdfCover url={blobUrl} alt={name} />
</div>
<!-- Title -->
<p
class="truncate px-1 text-center text-sm font-medium text-neutral-700 transition-colors group-hover:text-neutral-500"
title={name}
>
{name}
</p>
</div>
{/each}
</div>
{/if}
</section>
<!-- PDF Viewer Modal -->
{#if activePdf}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
on:click={closePdf}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="relative flex w-[92vw] max-w-4xl flex-col overflow-hidden rounded-2xl bg-white shadow-2xl"
style="height: 90vh;"
on:click|stopPropagation
>
<div class="flex shrink-0 items-center justify-between border-b border-neutral-200 px-4 py-3">
<span class="truncate text-sm font-semibold text-neutral-800">
📄 {activeTitle.replace(/\.pdf$/i, '')}
</span>
<button
on:click={closePdf}
class="ml-4 text-xl leading-none text-neutral-400 transition-colors hover:text-neutral-700"
aria-label="Close viewer"
>
</button>
</div>
<iframe src={activePdf} title="Fanzine PDF" class="w-full flex-1 border-none"></iframe>
</div>
</div>
{/if}
+34
View File
@@ -0,0 +1,34 @@
<script>
import FooterButton from './FooterButton.svelte';
</script>
<section class="mx-auto mb-2 flex justify-center pl-10">
<FooterButton
url="https://chachi.chat/community.bitcointxoko.com/txoko"
w="20"
h="18"
p="3"
icon="M0 0 C2.3925 -0.0825 4.785 -0.165 7.25 -0.25 C7.99668945 -0.28641602 8.74337891 -0.32283203 9.51269531 -0.36035156 C12.69907309 -0.41348643 14.73390382 -0.16167869 17.46875 1.5 C19.62741002 5.0243429 19.43342869 8.25689854 19.25 12.25 C19.23195313 12.99507812 19.21390625 13.74015625 19.1953125 14.5078125 C19.14835649 16.33909701 19.07661652 18.16971649 19 20 C17.52093108 20.02689216 16.04172517 20.04634621 14.5625 20.0625 C13.32693359 20.07990234 13.32693359 20.07990234 12.06640625 20.09765625 C10 20 10 20 9 19 C8.73706944 14.96839809 8.70595156 13.44107266 11 10 C14.625 9.8125 14.625 9.8125 18 10 C18 9.34 18 8.68 18 8 C17.21625 7.896875 16.4325 7.79375 15.625 7.6875 C13 7 13 7 11 4 C9.02 4 7.04 4 5 4 C5 9.28 5 14.56 5 20 C3.35 20 1.7 20 0 20 C0 13.4 0 6.8 0 0 Z "
/>
<FooterButton
url="https://matrix.to/#/#community:bitcointxoko.com"
w="32"
h="32"
p="6"
icon="M0.844 0.735v30.531h2.197v0.735h-3.041v-32h3.041v0.735zM10.235 10.412v1.547h0.041c0.412-0.595 0.912-1.047 1.489-1.371 0.579-0.323 1.251-0.484 2-0.484 0.719 0 1.38 0.141 1.975 0.417 0.599 0.281 1.047 0.776 1.359 1.479 0.339-0.5 0.803-0.943 1.38-1.323 0.579-0.38 1.267-0.573 2.063-0.573 0.604 0 1.161 0.073 1.677 0.224 0.521 0.145 0.959 0.38 1.328 0.703 0.365 0.329 0.651 0.751 0.86 1.272 0.203 0.52 0.307 1.151 0.307 1.891v7.635h-3.129v-6.468c0-0.381-0.016-0.745-0.048-1.084-0.020-0.307-0.099-0.604-0.239-0.88-0.131-0.251-0.333-0.459-0.584-0.593-0.255-0.152-0.609-0.224-1.047-0.224-0.443 0-0.797 0.083-1.068 0.249-0.265 0.167-0.489 0.396-0.64 0.667-0.161 0.287-0.265 0.604-0.308 0.927-0.052 0.349-0.077 0.699-0.083 1.048v6.359h-3.131v-6.401c0-0.339-0.005-0.672-0.025-1-0.011-0.317-0.073-0.624-0.193-0.916-0.104-0.281-0.301-0.516-0.552-0.672-0.255-0.167-0.636-0.255-1.136-0.255-0.151 0-0.348 0.031-0.588 0.099-0.24 0.067-0.479 0.192-0.703 0.375-0.229 0.188-0.428 0.453-0.589 0.797-0.161 0.343-0.239 0.796-0.239 1.359v6.62h-3.131v-11.421zM31.156 31.265v-30.531h-2.197v-0.735h3.041v32h-3.041v-0.735z"
/>
<FooterButton
url="https://simplex.bitcointxoko.com"
w="34"
h="34"
p="5"
icon="M3.02972 8.59396L8.62219 14.186L14.3703 8.43848L17.1668 11.2346L11.4182 16.982L17.0112 22.5742L14.1371 25.448L8.5441 19.8557L2.79651 25.6035L0 22.8074L5.74813 17.0597L0.155656 11.4678L3.02972 8.59396Z M14.0922 25.5L16.9434 22.6486L16.9423 22.6478L22.6464 16.9456L17.0512 11.3519L17.0518 11.3514L14.2542 8.55418L8.65961 2.95973L11.5114 0.108337L17.106 5.70288L22.8095 0L25.607 2.79722L19.903 8.5L25.4981 14.0943L31.2022 8.39169L33.9997 11.1889L28.2957 16.8914L33.8914 22.4861L31.0396 25.3375L25.4439 19.7428L19.7404 25.4454L25.3361 31.0403L22.4843 33.8917L16.8887 28.2968L11.1862 34L8.38867 31.2028L14.0922 25.5Z"
/>
<FooterButton
url="https://github.com/bitcointxoko"
w="496"
h="512"
p="6"
icon="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"
/>
</section>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
export let url: string;
export let icon: string;
export let w: string;
export let h: string;
</script>
<a href={url} class="inline-flex items-center align-middle">
<button class="flex h-16 w-16 cursor-pointer items-center gap-2 text-gray-700 dark:text-gray-50">
<svg
fill="#ffffff"
class="h-5 w-5 fill-current"
viewBox={`0 0 ${w} ${h}`}
xmlns="http://www.w3.org/2000/svg"
>
<path d={icon} />
</svg>
</button>
</a>
+165
View File
@@ -0,0 +1,165 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { initializeNDK, signInWithNIP07, signOut, user, isAuthenticated } from '$lib/utils/auth';
import Menu from '@lucide/svelte/icons/menu';
import SunIcon from '@lucide/svelte/icons/sun';
import MoonIcon from '@lucide/svelte/icons/moon';
import { toggleMode } from 'mode-watcher';
import * as Avatar from '$lib/components/ui/avatar/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
onMount(async () => {
await initializeNDK();
});
async function handleSignIn() {
try {
await signInWithNIP07();
} catch (error) {
alert('Failed to sign in. Make sure you have a NIP-07 extension installed.');
}
}
function handleSignOut() {
signOut();
}
const tabs = [
{
url: '/',
label: 'Inicio'
},
{
url: '/calendar',
label: 'Calendario'
},
{
url: '/articles',
label: 'Artículos'
},
{
url: '/fanzines',
label: 'Fanzines'
},
{
url: '/shop',
label: 'Tienda'
},
{
url: '/apps',
label: 'Apps'
}
];
</script>
<div class="mx-3 flex items-center justify-between">
<a href="/">
<img src="/logo.webp" class="me-3 h-12 sm:h-14" alt="Bitcoin Txoko Logo" />
</a>
<div class="flex items-center space-x-2">
{#if $isAuthenticated && $user}
{#await $user.fetchProfile() then profile}
{#if profile}
<DropdownMenu.Root>
<DropdownMenu.Trigger
class="flex items-center gap-2 rounded-full bg-foreground px-4 py-2 shadow-sm hover:shadow-lg"
>
<Avatar.Root>
<Avatar.Image
src={profile.image}
alt={profile.displayName ? profile.displayName : profile.name}
/>
<Avatar.Fallback>
{profile.displayName
? profile.displayName.slice(0, 2)
: profile.name?.slice(0, 2)}
</Avatar.Fallback>
</Avatar.Root>
<Menu class="text-background" />
</DropdownMenu.Trigger>
<DropdownMenu.Content class="bg-card">
<DropdownMenu.Group>
<DropdownMenu.Label>
{profile.displayName ? profile.displayName : profile.name}
</DropdownMenu.Label>
<DropdownMenu.Separator />
{#each tabs as tab (tab.url)}
<DropdownMenu.Item>
<a href={tab.url}>{tab.label}</a>
</DropdownMenu.Item>
{/each}
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={toggleMode}>
<SunIcon
class="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 !transition-all dark:scale-0 dark:-rotate-90"
/>
<MoonIcon
class="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 !transition-all dark:scale-100 dark:rotate-0"
/>
<span class="text-foreground">Tema</span>
</DropdownMenu.Item>
<DropdownMenu.Item>
<button on:click={handleSignOut} class="text-destructive">Cerrar sesión</button>
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
{/if}
{/await}
{:else}
<div class="flex items-center gap-2 rounded-full bg-foreground p-3 shadow-md">
<Dialog.Root>
<Dialog.Trigger class="border-r border-r-black px-2 font-semibold text-background">
Empezar
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Iniciar sesión</Dialog.Title>
<Dialog.Description>
Iniciar sesión con una extensión de navegador NIP-07, por ejemplo Alby o nos2x.
</Dialog.Description>
</Dialog.Header>
<button
class={buttonVariants({ variant: 'default' })}
on:click={handleSignIn}
role="menuitem"
tabindex="-1"
id="user-menu-item-1"
>
Conectarse con extensión
</button>
</Dialog.Content>
</Dialog.Root>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Menu class="text-background" />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
{#each tabs as tab (tab.url)}
<DropdownMenu.Item>
<a href={tab.url}>{tab.label}</a>
</DropdownMenu.Item>
{/each}
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={toggleMode}>
<SunIcon
class="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 !transition-all dark:scale-0 dark:-rotate-90"
/>
<MoonIcon
class="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 !transition-all dark:scale-100 dark:rotate-0"
/>
<span class="text-foreground">Tema</span>
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{/if}
</div>
</div>
+41
View File
@@ -0,0 +1,41 @@
<script>
import { onMount } from 'svelte';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css'; // Import CSS here instead of <style>
import geohash from 'latlon-geohash';
export let geohashString; // e.g., "u4pruydqqvj"
let mapContainer;
onMount(() => {
// Decode Geohash to [lat, lon]
const { lat, lon } = geohash.decode(geohashString);
const map = new maplibregl.Map({
container: mapContainer,
style: 'https://tiles.openfreemap.org/styles/liberty',
center: [lon, lat], // [longitude, latitude]
zoom: 12
});
// Wait for the map to fully load before adding the marker
map.on('load', () => {
// Force the map to recalculate its container size
map.resize();
// Add marker and ensure it's centered
new maplibregl.Marker().setLngLat([lon, lat]).addTo(map);
// Explicitly fly to the location to guarantee centering
map.flyTo({
center: [lon, lat],
zoom: 12,
essential: true
});
});
return () => map.remove(); // Clean up on unmount
});
</script>
<div bind:this={mapContainer} class="h-96 w-full rounded-md"></div>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import SunIcon from '@lucide/svelte/icons/sun';
import MoonIcon from '@lucide/svelte/icons/moon';
import { toggleMode } from 'mode-watcher';
import { Button } from '$lib/components/ui/button/index.js';
</script>
<Button onclick={toggleMode} variant="outline" size="icon">
<SunIcon
class="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 !transition-all dark:scale-0 dark:-rotate-90"
/>
<MoonIcon
class="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 !transition-all dark:scale-100 dark:rotate-0"
/>
<span class="sr-only">Toggle theme</span>
</Button>
+64
View File
@@ -0,0 +1,64 @@
<!-- src/lib/PdfCover.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
export let url: string;
export let alt: string = 'PDF cover';
let canvas: HTMLCanvasElement;
let loading = true;
let failed = false;
onMount(() => {
// Dynamic import — only runs in the browser, never on the server
import('pdfjs-dist')
.then((pdfjsLib) => {
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.mjs',
import.meta.url
).toString();
return pdfjsLib.getDocument(url).promise;
})
.then((pdf) => pdf.getPage(1))
.then((page) => {
const desiredWidth = 240;
const viewport = page.getViewport({ scale: 1 });
const scale = desiredWidth / viewport.width;
const scaled = page.getViewport({ scale });
canvas.width = scaled.width;
canvas.height = scaled.height;
return page.render({
canvasContext: canvas.getContext('2d')!,
viewport: scaled
}).promise;
})
.then(() => {
loading = false;
})
.catch(() => {
loading = false;
failed = true;
});
});
</script>
<div class="relative aspect-[3/4] w-full overflow-hidden rounded bg-gray-100">
{#if loading}
<div class="absolute inset-0 animate-pulse bg-gradient-to-br from-gray-200 to-gray-300" />
{:else if failed}
<div class="absolute inset-0 flex flex-col items-center justify-center gap-2 text-gray-400">
<span class="text-4xl">📄</span>
<span class="text-xs">{alt}</span>
</div>
{/if}
<canvas
bind:this={canvas}
class="h-full w-full object-contain"
class:hidden={loading || failed}
aria-label={alt}
/>
</div>
+58
View File
@@ -0,0 +1,58 @@
<script lang="ts">
// import { _ } from '../../services/i18n'
import * as Card from '$lib/components/ui/card';
import Button from '$lib/components/ui/button/button.svelte';
export let product;
</script>
<Card.Root class="w-full max-w-sm">
<div class="flex h-48 w-full items-start justify-center rounded-md">
<img
src={product.images[0].src}
class="max-h-full max-w-full rounded-md object-contain"
alt={product.images[0].alt}
/>
</div>
<Card.Header>
<Card.Title class="hover:text-neutral-200">
<a href="/shop/{product.choiceKey}">
{product.name}
</a>
</Card.Title>
</Card.Header>
<Card.Footer class="flex-col">
<form class="w-full" method="POST" action={product.action}>
{#if product.disabled}
<Button class="w-full" disabled>agotado</Button>
{:else}
<Button class="w-full justify-center" value={product.choiceKey}>
<button class="flex justify-center gap-1" name="choiceKey" value={product.choiceKey}>
Comprar por
<span class="flex">
{#if product.currency === 'sats'}
{product.price}
<svg
data-v-52a72b4a=""
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="mt-0.5 h-8"
>
<path
fill-rule="evenodd"
d="M12.75 18.5V21h-1.5v-2.5h1.5zM17 16.75H7v-1.5h10v1.5zM17 12.75H7v-1.5h10v1.5zM17 8.75H7v-1.5h10v1.5zM12.75 3v2.5h-1.5V3h1.5z"
clip-rule="evenodd"
>
</path>
</svg>
{:else}
{product.price}{product.currency}
{/if}
</span>
</button>
</Button>
{/if}
</form>
</Card.Footer>
</Card.Root>
+115
View File
@@ -0,0 +1,115 @@
<script lang="ts">
// import { _ } from '../../services/i18n'
import Check from '@lucide/svelte/icons/check';
import MailQuestion from '@lucide/svelte/icons/mail-question';
import X from '@lucide/svelte/icons/x';
import * as ToggleGroup from '$lib/components/ui/toggle-group/index.js';
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { buttonVariants } from '$lib/components/ui/button/button.svelte';
import Button from '$lib/components/ui/button/button.svelte';
import * as Dialog from '$lib/components/ui/dialog';
import { ndk, user, isAuthenticated } from '$lib/utils/auth';
import { toast } from 'svelte-sonner';
export let event: NDKEvent;
function canRsvp(event: NDKEvent) {
const date = (new Date().getTime() / 1000).toFixed(0);
if (Number(event.tagValue('end')) > Number(date)) return true;
else return false;
}
let selectedResponse = '';
let isSubmitting = false;
// RSVP response options for kind 31925
const rsvpOptions = [
{ value: 'accepted', label: 'Yes, I will attend', emoji: '✅' },
{ value: 'declined', label: 'No, I cannot attend', emoji: '❌' },
{ value: 'tentative', label: 'Maybe, not sure yet', emoji: '❓' }
];
async function handleSubmit(event: NDKEvent) {
if (!selectedResponse) {
toast.error('Please select an RSVP response');
return;
}
if (!$ndk || !$user) {
toast.error('Please sign in first');
return;
}
isSubmitting = true;
try {
// Create kind 31925 RSVP event
const rsvpEvent = new NDKEvent($ndk);
rsvpEvent.kind = 31925;
rsvpEvent.content = '';
// Add required tags for RSVP
rsvpEvent.tags = [
['e', event.id], // Reference to the event being responded to
['status', selectedResponse], // The RSVP response
['a', `${event.kind}:${event.author.pubkey}:${event.dTag}`],
['p', `${event.author.pubkey}`]
];
// Sign and publish the event
await rsvpEvent.sign();
await rsvpEvent.publish();
toast.success(
`RSVP sent: ${rsvpOptions.find((opt) => opt.value === selectedResponse)?.label}`
);
// Reset form
selectedResponse = '';
} catch (error) {
console.error('Failed to send RSVP:', error);
toast.error('Failed to send RSVP. Please try again.');
} finally {
isSubmitting = false;
}
}
</script>
{#if canRsvp(event)}
{#if $isAuthenticated && $user}
<Dialog.Root>
<Dialog.Trigger class={buttonVariants({ variant: 'outline' })}>RSVP</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>RSVP</Dialog.Title>
<Dialog.Description>Haznos saber si vas a venir al evento.</Dialog.Description>
</Dialog.Header>
<Dialog.Footer>
<form on:submit={() => handleSubmit(event)} class="flex items-center gap-2">
<ToggleGroup.Root
bind:value={selectedResponse}
size="lg"
variant="outline"
type="single"
>
<ToggleGroup.Item value="accepted" aria-label="Toggle accepted">
<Check class="size-4" />
</ToggleGroup.Item>
<ToggleGroup.Item value="tentative" aria-label="Toggle tentative">
<MailQuestion class="size-4" />
</ToggleGroup.Item>
<ToggleGroup.Item value="declined" aria-label="Toggle declined">
<X class="size-4" />
</ToggleGroup.Item>
</ToggleGroup.Root>
<Button type="submit">{isSubmitting ? 'Enviando...' : 'Enviar'}</Button>
</form>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
{:else}
<Button disabled>RSVP</Button>
{/if}
{:else}
<Button disabled variant="secondary">terminado</Button>
{/if}
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import { Badge } from '$lib/components/ui/badge';
import Hash from '@lucide/svelte/icons/hash';
// import { PUBLIC_NOSTR_LONG_FORM_CLIENT } from '$env/static/public';
export let tags: string[] | string[][] = [];
const isNostr = Array.isArray(tags[0]) && tags[0][0] === `t`;
const longFormClient: string = 'https://habla.news';
</script>
<div class="flex flex-wrap gap-2 font-mono">
{#each tags as tag, i (tag)}
<Badge variant="secondary" href="{longFormClient}/t/{tag[1]}">
<Hash />{isNostr ? tag[1] : tag}
</Badge>
{#if tags[i + 1]}
<span class="text-muted-bright"> </span>
{/if}
{/each}
</div>
+7
View File
@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+41
View File
@@ -0,0 +1,41 @@
import type { NDKEvent } from '@nostr-dev-kit/ndk';
export interface BlossomFile {
sha256: string;
path: string;
size: number;
mimeType: string | undefined;
}
export interface BlossomDrive {
name: string;
description: string;
servers: string[];
files: BlossomFile[];
}
export function parseDriveEvent(event: NDKEvent): BlossomDrive {
const name = event.tags.find((t) => t[0] === 'name')?.[1] ?? 'Untitled Drive';
const description = event.tags.find((t) => t[0] === 'description')?.[1] ?? '';
const servers = event.tags.filter((t) => t[0] === 'server').map((t) => t[1]);
const files: BlossomFile[] = event.tags
.filter((t) => t[0] === 'x')
.map((t) => ({
sha256: t[1],
path: t[2],
size: parseInt(t[3] ?? '0', 10),
mimeType: t[4]
}));
return { name, description, servers, files };
}
export function getBlobUrl(sha256: string, servers: string[], ext = '.pdf'): string {
const base = servers[0] ?? 'https://cdn.satellite.earth';
return `${base.replace(/\/$/, '')}/${sha256}${ext}`;
}
export function fileName(path: string): string {
return path.split('/').pop() ?? path;
}
@@ -0,0 +1,17 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.FallbackProps = $props();
</script>
<AvatarPrimitive.Fallback
bind:ref
data-slot="avatar-fallback"
class={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
{...restProps}
/>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.ImageProps = $props();
</script>
<AvatarPrimitive.Image
bind:ref
data-slot="avatar-image"
class={cn("aspect-square size-full", className)}
{...restProps}
/>
@@ -0,0 +1,19 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
loadingStatus = $bindable("loading"),
class: className,
...restProps
}: AvatarPrimitive.RootProps = $props();
</script>
<AvatarPrimitive.Root
bind:ref
bind:loadingStatus
data-slot="avatar"
class={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
{...restProps}
/>
+13
View File
@@ -0,0 +1,13 @@
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
};
+50
View File
@@ -0,0 +1,50 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const badgeVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
variants: {
variant: {
default:
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
destructive:
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
href,
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>
+2
View File
@@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
@@ -0,0 +1,80 @@
<script lang="ts" module>
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
outline:
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
disabled,
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
{/if}
+17
View File
@@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-action"
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
>
{@render children?.()}
</p>
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-title"
class={cn("font-semibold leading-none", className)}
{...restProps}
>
{@render children?.()}
</div>
+23
View File
@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card"
class={cn(
'flex flex-col gap-6 rounded-xl border bg-card pb-6 text-card-foreground shadow-sm',
className
)}
{...restProps}
>
{@render children?.()}
</div>
+25
View File
@@ -0,0 +1,25 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};
@@ -0,0 +1,43 @@
<script lang="ts">
import emblaCarouselSvelte from "embla-carousel-svelte";
import type { HTMLAttributes } from "svelte/elements";
import { getEmblaContext } from "./context.js";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
const emblaCtx = getEmblaContext("<Carousel.Content/>");
</script>
<div
data-slot="carousel-content"
class="overflow-hidden"
use:emblaCarouselSvelte={{
options: {
container: "[data-embla-container]",
slides: "[data-embla-slide]",
...emblaCtx.options,
axis: emblaCtx.orientation === "horizontal" ? "x" : "y",
},
plugins: emblaCtx.plugins,
}}
onemblaInit={emblaCtx.onInit}
>
<div
bind:this={ref}
class={cn(
"flex",
emblaCtx.orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
data-embla-container=""
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -0,0 +1,30 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { getEmblaContext } from "./context.js";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
const emblaCtx = getEmblaContext("<Carousel.Item/>");
</script>
<div
bind:this={ref}
data-slot="carousel-item"
role="group"
aria-roledescription="slide"
class={cn(
"min-w-0 shrink-0 grow-0 basis-full",
emblaCtx.orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
data-embla-slide=""
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,38 @@
<script lang="ts">
import ArrowRightIcon from "@lucide/svelte/icons/arrow-right";
import type { WithoutChildren } from "bits-ui";
import { getEmblaContext } from "./context.js";
import { cn } from "$lib/utils.js";
import { Button, type Props } from "$lib/components/ui/button/index.js";
let {
ref = $bindable(null),
class: className,
variant = "outline",
size = "icon",
...restProps
}: WithoutChildren<Props> = $props();
const emblaCtx = getEmblaContext("<Carousel.Next/>");
</script>
<Button
data-slot="carousel-next"
{variant}
{size}
aria-disabled={!emblaCtx.canScrollNext}
class={cn(
"absolute size-8 rounded-full",
emblaCtx.orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
onclick={emblaCtx.scrollNext}
onkeydown={emblaCtx.handleKeyDown}
bind:ref
{...restProps}
>
<ArrowRightIcon class="size-4" />
<span class="sr-only">Next slide</span>
</Button>
@@ -0,0 +1,38 @@
<script lang="ts">
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
import type { WithoutChildren } from "bits-ui";
import { getEmblaContext } from "./context.js";
import { cn } from "$lib/utils.js";
import { Button, type Props } from "$lib/components/ui/button/index.js";
let {
ref = $bindable(null),
class: className,
variant = "outline",
size = "icon",
...restProps
}: WithoutChildren<Props> = $props();
const emblaCtx = getEmblaContext("<Carousel.Previous/>");
</script>
<Button
data-slot="carousel-previous"
{variant}
{size}
aria-disabled={!emblaCtx.canScrollPrev}
class={cn(
"absolute size-8 rounded-full",
emblaCtx.orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
onclick={emblaCtx.scrollPrev}
onkeydown={emblaCtx.handleKeyDown}
{...restProps}
bind:ref
>
<ArrowLeftIcon class="size-4" />
<span class="sr-only">Previous slide</span>
</Button>
@@ -0,0 +1,93 @@
<script lang="ts">
import {
type CarouselAPI,
type CarouselProps,
type EmblaContext,
setEmblaContext,
} from "./context.js";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
opts = {},
plugins = [],
setApi = () => {},
orientation = "horizontal",
class: className,
children,
...restProps
}: WithElementRef<CarouselProps> = $props();
let carouselState = $state<EmblaContext>({
api: undefined,
scrollPrev,
scrollNext,
orientation,
canScrollNext: false,
canScrollPrev: false,
handleKeyDown,
options: opts,
plugins,
onInit,
scrollSnaps: [],
selectedIndex: 0,
scrollTo,
});
setEmblaContext(carouselState);
function scrollPrev() {
carouselState.api?.scrollPrev();
}
function scrollNext() {
carouselState.api?.scrollNext();
}
function scrollTo(index: number, jump?: boolean) {
carouselState.api?.scrollTo(index, jump);
}
function onSelect() {
if (!carouselState.api) return;
carouselState.selectedIndex = carouselState.api.selectedScrollSnap();
carouselState.canScrollNext = carouselState.api.canScrollNext();
carouselState.canScrollPrev = carouselState.api.canScrollPrev();
}
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "ArrowLeft") {
e.preventDefault();
scrollPrev();
} else if (e.key === "ArrowRight") {
e.preventDefault();
scrollNext();
}
}
function onInit(event: CustomEvent<CarouselAPI>) {
carouselState.api = event.detail;
setApi(carouselState.api);
carouselState.scrollSnaps = carouselState.api.scrollSnapList();
carouselState.api.on("select", onSelect);
onSelect();
}
$effect(() => {
return () => {
carouselState.api?.off("select", onSelect);
};
});
</script>
<div
bind:this={ref}
data-slot="carousel"
class={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...restProps}
>
{@render children?.()}
</div>
+58
View File
@@ -0,0 +1,58 @@
import type { WithElementRef } from "$lib/utils.js";
import type {
EmblaCarouselSvelteType,
default as emblaCarouselSvelte,
} from "embla-carousel-svelte";
import { getContext, hasContext, setContext } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
export type CarouselAPI =
NonNullable<NonNullable<EmblaCarouselSvelteType["$$_attributes"]>["on:emblaInit"]> extends (
evt: CustomEvent<infer CarouselAPI>
) => void
? CarouselAPI
: never;
type EmblaCarouselConfig = NonNullable<Parameters<typeof emblaCarouselSvelte>[1]>;
export type CarouselOptions = EmblaCarouselConfig["options"];
export type CarouselPlugins = EmblaCarouselConfig["plugins"];
////
export type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugins;
setApi?: (api: CarouselAPI | undefined) => void;
orientation?: "horizontal" | "vertical";
} & WithElementRef<HTMLAttributes<HTMLDivElement>>;
const EMBLA_CAROUSEL_CONTEXT = Symbol("EMBLA_CAROUSEL_CONTEXT");
export type EmblaContext = {
api: CarouselAPI | undefined;
orientation: "horizontal" | "vertical";
scrollNext: () => void;
scrollPrev: () => void;
canScrollNext: boolean;
canScrollPrev: boolean;
handleKeyDown: (e: KeyboardEvent) => void;
options: CarouselOptions;
plugins: CarouselPlugins;
onInit: (e: CustomEvent<CarouselAPI>) => void;
scrollTo: (index: number, jump?: boolean) => void;
scrollSnaps: number[];
selectedIndex: number;
};
export function setEmblaContext(config: EmblaContext): EmblaContext {
setContext(EMBLA_CAROUSEL_CONTEXT, config);
return config;
}
export function getEmblaContext(name = "This component") {
if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) {
throw new Error(`${name} must be used within a <Carousel.Root> component`);
}
return getContext<ReturnType<typeof setEmblaContext>>(EMBLA_CAROUSEL_CONTEXT);
}
+19
View File
@@ -0,0 +1,19 @@
import Root from "./carousel.svelte";
import Content from "./carousel-content.svelte";
import Item from "./carousel-item.svelte";
import Previous from "./carousel-previous.svelte";
import Next from "./carousel-next.svelte";
export {
Root,
Content,
Item,
Previous,
Next,
//
Root as Carousel,
Content as CarouselContent,
Item as CarouselItem,
Previous as CarouselPrevious,
Next as CarouselNext,
};
@@ -0,0 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
</script>
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
@@ -0,0 +1,43 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import XIcon from "@lucide/svelte/icons/x";
import type { Snippet } from "svelte";
import * as Dialog from "./index.js";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
portalProps,
children,
showCloseButton = true,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
showCloseButton?: boolean;
} = $props();
</script>
<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
data-slot="dialog-content"
class={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...restProps}
>
{@render children?.()}
{#if showCloseButton}
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute end-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
>
<XIcon />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content>
</Dialog.Portal>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script>
<DialogPrimitive.Description
bind:ref
data-slot="dialog-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="dialog-footer"
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="dialog-header"
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>
<DialogPrimitive.Overlay
bind:ref
data-slot="dialog-overlay"
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...restProps}
/>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.TitleProps = $props();
</script>
<DialogPrimitive.Title
bind:ref
data-slot="dialog-title"
class={cn("text-lg font-semibold leading-none", className)}
{...restProps}
/>
@@ -0,0 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
</script>
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
+37
View File
@@ -0,0 +1,37 @@
import { Dialog as DialogPrimitive } from "bits-ui";
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
import Trigger from "./dialog-trigger.svelte";
import Close from "./dialog-close.svelte";
const Root = DialogPrimitive.Root;
const Portal = DialogPrimitive.Portal;
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};
@@ -0,0 +1,41 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
data-slot="dropdown-menu-checkbox-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<MinusIcon class="size-4" />
{:else}
<CheckIcon class={cn("size-4", !checked && "text-transparent")} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>
@@ -0,0 +1,27 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
data-slot="dropdown-menu-content"
{sideOffset}
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-dropdown-menu-content-available-height) origin-(--bits-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md outline-none",
className
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>
@@ -0,0 +1,22 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
bind:ref
data-slot="dropdown-menu-group-heading"
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
{...restProps}
/>
@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
</script>
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
@@ -0,0 +1,27 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
inset,
variant = "default",
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
variant?: "default" | "destructive";
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
class={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>
@@ -0,0 +1,24 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
data-slot="dropdown-menu-label"
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: DropdownMenuPrimitive.RadioGroupProps = $props();
</script>
<DropdownMenuPrimitive.RadioGroup
bind:ref
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>
@@ -0,0 +1,31 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
data-slot="dropdown-menu-radio-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<CircleIcon class="size-2 fill-current" />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>
@@ -0,0 +1,17 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
data-slot="dropdown-menu-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
/>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="dropdown-menu-shortcut"
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...restProps}
>
{@render children?.()}
</span>
@@ -0,0 +1,20 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
bind:ref
data-slot="dropdown-menu-sub-content"
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...restProps}
/>
@@ -0,0 +1,29 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
class={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRightIcon class="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
</script>
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
@@ -0,0 +1,49 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import Group from "./dropdown-menu-group.svelte";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import Trigger from "./dropdown-menu-trigger.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import GroupHeading from "./dropdown-menu-group-heading.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
export {
CheckboxItem,
Content,
Root as DropdownMenu,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
GroupHeading as DropdownMenuGroupHeading,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger,
};
+7
View File
@@ -0,0 +1,7 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};
+52
View File
@@ -0,0 +1,52 @@
<script lang="ts">
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
type Props = WithElementRef<
Omit<HTMLInputAttributes, "type"> &
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
>;
let {
ref = $bindable(null),
value = $bindable(),
type,
files = $bindable(),
class: className,
"data-slot": dataSlot = "input",
...restProps
}: Props = $props();
</script>
{#if type === "file"}
<input
bind:this={ref}
data-slot={dataSlot}
class={cn(
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{:else}
<input
bind:this={ref}
data-slot={dataSlot}
class={cn(
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{type}
bind:value
{...restProps}
/>
{/if}
+25
View File
@@ -0,0 +1,25 @@
import Root from "./pagination.svelte";
import Content from "./pagination-content.svelte";
import Item from "./pagination-item.svelte";
import Link from "./pagination-link.svelte";
import PrevButton from "./pagination-prev-button.svelte";
import NextButton from "./pagination-next-button.svelte";
import Ellipsis from "./pagination-ellipsis.svelte";
export {
Root,
Content,
Item,
Link,
PrevButton,
NextButton,
Ellipsis,
//
Root as Pagination,
Content as PaginationContent,
Item as PaginationItem,
Link as PaginationLink,
PrevButton as PaginationPrevButton,
NextButton as PaginationNextButton,
Ellipsis as PaginationEllipsis,
};
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
</script>
<ul
bind:this={ref}
data-slot="pagination-content"
class={cn("flex flex-row items-center gap-1", className)}
{...restProps}
>
{@render children?.()}
</ul>
@@ -0,0 +1,22 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
</script>
<span
bind:this={ref}
aria-hidden="true"
data-slot="pagination-ellipsis"
class={cn("flex size-9 items-center justify-center", className)}
{...restProps}
>
<EllipsisIcon class="size-4" />
<span class="sr-only">More pages</span>
</span>
@@ -0,0 +1,14 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import type { WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
children,
...restProps
}: WithElementRef<HTMLLiAttributes> = $props();
</script>
<li bind:this={ref} data-slot="pagination-item" {...restProps}>
{@render children?.()}
</li>
@@ -0,0 +1,39 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { type Props, buttonVariants } from "$lib/components/ui/button/index.js";
let {
ref = $bindable(null),
class: className,
size = "icon",
isActive,
page,
children,
...restProps
}: PaginationPrimitive.PageProps &
Props & {
isActive: boolean;
} = $props();
</script>
{#snippet Fallback()}
{page.value}
{/snippet}
<PaginationPrimitive.Page
bind:ref
{page}
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
class={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
children={children || Fallback}
{...restProps}
/>
@@ -0,0 +1,33 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: PaginationPrimitive.NextButtonProps = $props();
</script>
{#snippet Fallback()}
<span>Next</span>
<ChevronRightIcon class="size-4" />
{/snippet}
<PaginationPrimitive.NextButton
bind:ref
aria-label="Go to next page"
class={cn(
buttonVariants({
size: "default",
variant: "ghost",
class: "gap-1 px-2.5 sm:pr-2.5",
}),
className
)}
children={children || Fallback}
{...restProps}
/>
@@ -0,0 +1,33 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: PaginationPrimitive.PrevButtonProps = $props();
</script>
{#snippet Fallback()}
<ChevronLeftIcon class="size-4" />
<span>Previous</span>
{/snippet}
<PaginationPrimitive.PrevButton
bind:ref
aria-label="Go to previous page"
class={cn(
buttonVariants({
size: "default",
variant: "ghost",
class: "gap-1 px-2.5 sm:pl-2.5",
}),
className
)}
children={children || Fallback}
{...restProps}
/>
@@ -0,0 +1,28 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
count = 0,
perPage = 10,
page = $bindable(1),
siblingCount = 1,
...restProps
}: PaginationPrimitive.RootProps = $props();
</script>
<PaginationPrimitive.Root
bind:ref
bind:page
role="navigation"
aria-label="pagination"
data-slot="pagination"
class={cn("mx-auto flex w-full justify-center", className)}
{count}
{perPage}
{siblingCount}
{...restProps}
/>
+17
View File
@@ -0,0 +1,17 @@
import { Popover as PopoverPrimitive } from "bits-ui";
import Content from "./popover-content.svelte";
import Trigger from "./popover-trigger.svelte";
const Root = PopoverPrimitive.Root;
const Close = PopoverPrimitive.Close;
export {
Root,
Content,
Trigger,
Close,
//
Root as Popover,
Content as PopoverContent,
Trigger as PopoverTrigger,
Close as PopoverClose,
};
@@ -0,0 +1,29 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Popover as PopoverPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
sideOffset = 4,
align = "center",
portalProps,
...restProps
}: PopoverPrimitive.ContentProps & {
portalProps?: PopoverPrimitive.PortalProps;
} = $props();
</script>
<PopoverPrimitive.Portal {...portalProps}>
<PopoverPrimitive.Content
bind:ref
data-slot="popover-content"
{sideOffset}
{align}
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-popover-content-transform-origin) outline-hidden z-50 w-72 rounded-md border p-4 shadow-md",
className
)}
{...restProps}
/>
</PopoverPrimitive.Portal>
@@ -0,0 +1,17 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Popover as PopoverPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
...restProps
}: PopoverPrimitive.TriggerProps = $props();
</script>
<PopoverPrimitive.Trigger
bind:ref
data-slot="popover-trigger"
class={cn("", className)}
{...restProps}
/>
+7
View File
@@ -0,0 +1,7 @@
import Root from "./progress.svelte";
export {
Root,
//
Root as Progress,
};
@@ -0,0 +1,27 @@
<script lang="ts">
import { Progress as ProgressPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
max = 100,
value,
...restProps
}: WithoutChildrenOrChild<ProgressPrimitive.RootProps> = $props();
</script>
<ProgressPrimitive.Root
bind:ref
data-slot="progress"
class={cn("bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", className)}
{value}
{max}
{...restProps}
>
<div
data-slot="progress-indicator"
class="bg-primary h-full w-full flex-1 transition-all"
style="transform: translateX(-{100 - (100 * (value ?? 0)) / (max ?? 1)}%)"
></div>
</ProgressPrimitive.Root>
+7
View File
@@ -0,0 +1,7 @@
import Root from "./separator.svelte";
export {
Root,
//
Root as Separator,
};
@@ -0,0 +1,20 @@
<script lang="ts">
import { Separator as SeparatorPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SeparatorPrimitive.RootProps = $props();
</script>
<SeparatorPrimitive.Root
bind:ref
data-slot="separator"
class={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
className
)}
{...restProps}
/>
+7
View File
@@ -0,0 +1,7 @@
import Root from "./skeleton.svelte";
export {
Root,
//
Root as Skeleton,
};
@@ -0,0 +1,17 @@
<script lang="ts">
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
</script>
<div
bind:this={ref}
data-slot="skeleton"
class={cn("bg-accent animate-pulse rounded-md", className)}
{...restProps}
></div>
@@ -0,0 +1,10 @@
import Root from "./toggle-group.svelte";
import Item from "./toggle-group-item.svelte";
export {
Root,
Item,
//
Root as ToggleGroup,
Item as ToggleGroupItem,
};
@@ -0,0 +1,34 @@
<script lang="ts">
import { ToggleGroup as ToggleGroupPrimitive } from "bits-ui";
import { getToggleGroupCtx } from "./toggle-group.svelte";
import { cn } from "$lib/utils.js";
import { type ToggleVariants, toggleVariants } from "$lib/components/ui/toggle/index.js";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
size,
variant,
...restProps
}: ToggleGroupPrimitive.ItemProps & ToggleVariants = $props();
const ctx = getToggleGroupCtx();
</script>
<ToggleGroupPrimitive.Item
bind:ref
data-slot="toggle-group-item"
data-variant={ctx.variant || variant}
data-size={ctx.size || size}
class={cn(
toggleVariants({
variant: ctx.variant || variant,
size: ctx.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className
)}
{value}
{...restProps}
/>
@@ -0,0 +1,47 @@
<script lang="ts" module>
import { getContext, setContext } from "svelte";
import type { ToggleVariants } from "$lib/components/ui/toggle/index.js";
export function setToggleGroupCtx(props: ToggleVariants) {
setContext("toggleGroup", props);
}
export function getToggleGroupCtx() {
return getContext<ToggleVariants>("toggleGroup");
}
</script>
<script lang="ts">
import { ToggleGroup as ToggleGroupPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
size = "default",
variant = "default",
...restProps
}: ToggleGroupPrimitive.RootProps & ToggleVariants = $props();
setToggleGroupCtx({
variant,
size,
});
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<ToggleGroupPrimitive.Root
bind:value={value as never}
bind:ref
data-slot="toggle-group"
data-variant={variant}
data-size={size}
class={cn(
"group/toggle-group data-[variant=outline]:shadow-xs flex w-fit items-center rounded-md",
className
)}
{...restProps}
/>
+13
View File
@@ -0,0 +1,13 @@
import Root from "./toggle.svelte";
export {
toggleVariants,
type ToggleSize,
type ToggleVariant,
type ToggleVariants,
} from "./toggle.svelte";
export {
Root,
//
Root as Toggle,
};
@@ -0,0 +1,52 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const toggleVariants = tv({
base: "hover:bg-muted hover:text-muted-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-transparent",
outline:
"border-input shadow-xs hover:bg-accent hover:text-accent-foreground border bg-transparent",
},
size: {
default: "h-9 min-w-9 px-2",
sm: "h-8 min-w-8 px-1.5",
lg: "h-10 min-w-10 px-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ToggleVariant = VariantProps<typeof toggleVariants>["variant"];
export type ToggleSize = VariantProps<typeof toggleVariants>["size"];
export type ToggleVariants = VariantProps<typeof toggleVariants>;
</script>
<script lang="ts">
import { Toggle as TogglePrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
pressed = $bindable(false),
class: className,
size = "default",
variant = "default",
...restProps
}: TogglePrimitive.RootProps & {
variant?: ToggleVariant;
size?: ToggleSize;
} = $props();
</script>
<TogglePrimitive.Root
bind:ref
bind:pressed
data-slot="toggle"
class={cn(toggleVariants({ variant, size }), className)}
{...restProps}
/>
+18
View File
@@ -0,0 +1,18 @@
[
{
"name": "LNbits",
"url": "https://bitcointxoko.org"
},
{
"name": "BTCPay",
"url": "https://btcpay.bitcointxoko.org"
},
{
"name": "Blossom Server",
"url": "https://loratu.bitcointxoko.org"
},
{
"name": "Blossom Drive",
"url": "https://blossom.bitcointxoko.org"
}
]
+128
View File
@@ -0,0 +1,128 @@
[
{
"name": "Una Posada para Caminantes",
"images": [
{
"alt": "una-posada-para-caminantes",
"src": "https://loratu.bitcointxoko.org/7228b931ae8bda272498df8d40433bc9d4fbafc938f0d4f2ed5baa5a34f3ab6b.png",
"title": "una-posada-para-caminantes"
}
],
"description": "Antes de que existieran Bitcoin, los mercados libres digitales o la conversación global sobre soberanía tecnológica, Una posada para caminantes anticipó —con una precisión sorprendente— la aparición de redes paralelas fuera del alcance de los gobiernos tradicionales. En esta novela provocadora, Paul Rosenberg entrelaza ciencia, filosofía, criptografía y política para seguir a un puñado de personajes —científicos, hackers, periodistas, inversores— que se apartan de las instituciones del mundo oficial y comienzan a construir el suyo propio. Lo que descubren no es solo una nueva forma de pensar, sino una nueva forma de existir: libre, voluntaria y cifrada. A medio camino entre el relato de ideas y la advertencia visionaria, esta obra funciona como un tratado encubierto sobre soberanía individual, mercados libres y resistencia pacífica al poder centralizado.",
"action": "https://btcpay.bitcointxoko.org/apps/4xToJK4VnWhw5sWAGiS1rxZdCW6/pos",
"choiceKey": "una-posada-para-caminantes",
"price": "25,00",
"currency": "€",
"disabled": false
},
{
"name": "Criptoria (Tapa Blanda)",
"images": [
{
"alt": "criptoria-tapa-blanda",
"src": "https://loratu.bitcointxoko.org/0a4492c4394b67793228d93f82883ff8c84f70f3cd67588b21bf2ab4efd54760.png",
"title": "criptoria-tapa-blanda"
}
],
"description": "Criptoria: de Turing a Nakamoto es una madriguera de madrigueras sobre las computadoras, las redes, la ciberseguridad y el dinero digital. Desde Grecia hasta Bitcoin, Criptoria narra una selección de hitos acontecidos antes y después de Alan Turing. El libro expone una amplia colección de personas, organizaciones, conceptos e inventos que evidencian la profunda relación histórica y cultural entre la matemática, la criptografía, la informática y la lucha por la soberanía individual. Criptoria está dirigido a todas las personas convencidas de que, más allá de las vías heredadas o impuestas, hay otras formas de organización social y económica que pueden y deben ser exploradas.",
"action": "https://btcpay.bitcointxoko.org/apps/4xToJK4VnWhw5sWAGiS1rxZdCW6/pos",
"choiceKey": "criptoria-tapa-blanda",
"price": "25,00",
"currency": "€",
"disabled": false
},
{
"name": "Criptoria (Tapa Dura)",
"images": [
{
"alt": "criptoria-tapa-dura",
"src": "https://loratu.bitcointxoko.org/0a4492c4394b67793228d93f82883ff8c84f70f3cd67588b21bf2ab4efd54760.png",
"title": "criptoria-tapa-dura"
}
],
"description": "Criptoria: de Turing a Nakamoto es una madriguera de madrigueras sobre las computadoras, las redes, la ciberseguridad y el dinero digital. Desde Grecia hasta Bitcoin, Criptoria narra una selección de hitos acontecidos antes y después de Alan Turing. El libro expone una amplia colección de personas, organizaciones, conceptos e inventos que evidencian la profunda relación histórica y cultural entre la matemática, la criptografía, la informática y la lucha por la soberanía individual. Criptoria está dirigido a todas las personas convencidas de que, más allá de las vías heredadas o impuestas, hay otras formas de organización social y económica que pueden y deben ser exploradas.",
"action": "https://btcpay.bitcointxoko.org/apps/4xToJK4VnWhw5sWAGiS1rxZdCW6/pos",
"choiceKey": "criptoria-tapa-dura",
"price": "43,00",
"currency": "€",
"disabled": false
},
{
"name": "BBQ en Eibar",
"images": [
{
"alt": "bbq",
"src": "https://loratu.bitcointxoko.com/9119d4e30ab94f352ae748daf6d849811d7af24e10c345489631c5638c5ba060.webp",
"title": "bbq"
}
],
"description": "Bote para cubrir los gastos de carne, carbón, bebidas, etc. Uno de nosotros en Eibar se encargará de hacer la compra antes del meetup.",
"action": "https://btcpay.bitcointxoko.org/apps/41wJSH8CL4aMBzUa7UpnAKH1NcEz/pos",
"choiceKey": "bbq-eibar",
"price": "20.000",
"currency": "sats",
"disabled": true
},
{
"name": "Calcetines",
"images": [
{
"alt": "socks",
"src": "https://loratu.bitcointxoko.org/6f5f418ac2dc5b67b00d2c19a829bfd77d5eda23240d87f9b35d4489e7cdec85.webp",
"title": "socks"
},
{
"alt": "socks",
"src": "https://loratu.bitcointxoko.org/5ff548bdbf323bd56846775a5f86c9892197e867b3e4f545602d55217bcb293f.webp",
"title": "socks"
},
{
"alt": "socks",
"src": "https://loratu.bitcointxoko.org/e8424ad97d9c2e852b5438fdc5dac6d38a0091d672da173fc96f1c75870ceabd.webp",
"title": "socks"
},
{
"alt": "socks",
"src": "https://loratu.bitcointxoko.org/f1b028dfc2365f81a8fa3c3d3169bc3b532a61b759949279f66776363b3fcc66.webp",
"title": "socks"
}
],
"description": "¡Ponte a la moda Bitcoin con nuestros calcetines de Bitcoin! 🧦 ¡Da pasos seguros con estilo mientras muestras tu amor por Bitcoin! 🚀",
"action": "https://btcpay.bitcointxoko.org/apps/4xToJK4VnWhw5sWAGiS1rxZdCW6/pos",
"choiceKey": "calcetines",
"price": "10,00",
"currency": "€",
"disabled": false
},
{
"name": "Calcetines x 3",
"images": [
{
"alt": "socks",
"src": "https://loratu.bitcointxoko.org/3199d0c2c665f7eec0e2df89eb9d5cae7da5e09de566c95449b82dcd7081bc16.webp",
"title": "socks"
}
],
"description": "Presentamos nuestros nuevos calcetines Bitcoin, la forma más cómoda de caminar hacia el futuro financiero. 🧦 ¡Lleva consigo el espíritu de Satoshi en cada paso! 🚀 ",
"action": "https://btcpay.bitcointxoko.org/apps/4xToJK4VnWhw5sWAGiS1rxZdCW6/pos",
"choiceKey": "calcetines-x-3",
"price": "25,00",
"currency": "€",
"disabled": false
},
{
"name": "Tarjeta NFC",
"images": [
{
"alt": "tarjeta nfc",
"src": "https://loratu.bitcointxoko.org/c6ec6f400675cdccb43829c1f41f1a1261b28cae0c85107765caa6382dee1254.jpg"
}
],
"description": "Un chip NXP NTAG424 DNA dentro de una tarjeta NFC PVC blanca. ",
"action": "https://btcpay.bitcointxoko.org/apps/41wJSH8CL4aMBzUa7UpnAKH1NcEz/pos",
"choiceKey": "boltcard",
"price": "1.000",
"currency": "sats",
"disabled": false
}
]
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+53
View File
@@ -0,0 +1,53 @@
import { type NDKUser, NDKKind, NDKNip07Signer, NDKEvent } from '@nostr-dev-kit/ndk';
import { writable } from 'svelte/store';
const currentUser = writable<NDKUser | null>(null);
// Store the user's current follower list as an array of pubkeys
// export const currentUserFollows = writable<string[]>([]);
// Store the user's app settings for Listr
// export const currentUserSettings = writable<App.UserSettings | null>(null);
/**
* Fetch the follows for a user, formatted as an array of pubkeys
* @param user the user who you want to fetch follows for
* @returns an array of pubkeys of all the users they follow
*/
// export async function fetchUserFollows(user: NDKUser): Promise<string[]> {
// const followsSet = await user.follows();
// return Array.from(followsSet).map((user) => user.pubkey);
// }
/**
* Fetch the app specific settings for Listr
* @param user the user you want to fetch settings for
* @returns A promise object with the settings or null
*/
// export async function fetchUserSettings(user: NDKUser): Promise<App.UserSettings> {
// if (!user || !user.ndk) throw new Error("No logged in user or NDK instance");
// const ndk = user.ndk;
// const settingsEvents = await ndk.fetchEvents({
// kinds: [NDKKind.AppSpecificData],
// authors: [user.pubkey],
// "#d": ["listr/settings/v1"],
// });
// const eventsArray = Array.from(settingsEvents);
// let settings: App.UserSettings = { devMode: false };
// if (eventsArray.length === 1) {
// const event: NDKEvent = eventsArray[0] as NDKEvent;
// let signer: NDKNip07Signer;
// if (!ndk.signer) {
// signer = new NDKNip07Signer();
// ndk.signer = signer;
// }
// await event.decrypt(user);
// settings = JSON.parse(event.content);
// } else if (eventsArray.length > 1) {
// console.error("Many settings events", eventsArray);
// }
// return settings;
// }
export default currentUser;
+14
View File
@@ -0,0 +1,14 @@
// stores.ts
import { derived } from 'svelte/store';
import type { NDKEventStore, ExtendedBaseType } from '@nostr-dev-kit/ndk-svelte';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
export function createRecentEventsStore(
events: NDKEventStore<ExtendedBaseType<NDKEvent>>,
count: number = 5
) {
return derived(events, ($events) => {
if (!$events) return [];
return [...$events].sort((a, b) => b.created_at - a.created_at).slice(0, count);
});
}
+83
View File
@@ -0,0 +1,83 @@
// import { PUBLIC_PUBKEY, PUBLIC_RELAYS } from '$env/static/public';
import { browser } from '$app/environment';
import type { NDKCacheAdapter } from '@nostr-dev-kit/ndk';
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie';
import NDKSvelte from '@nostr-dev-kit/ndk-svelte';
import NDK, { NDKRelayAuthPolicies } from '@nostr-dev-kit/ndk';
import { writable } from 'svelte/store';
const relayUrls: string[] = [
'wss://nos.lol',
'wss://nostr.wine',
'wss://relay.damus.io',
'wss://koru.bitcointxoko.org'
];
// let relayUrls: string[];
// relayUrls = PUBLIC_RELAYS.split(',');
// Create a new NDK instance with explicit relays
// const ndk = new NDK({
// explicitRelayUrls: relayUrls
// });
// Now connect to specified relays
// if (browser) {
// ndk.connect().then(() => {
// console.log('Connected');
// });
// }
let cacheAdapter: NDKCacheAdapter | undefined;
if (browser) {
cacheAdapter = new NDKCacheAdapterDexie({ dbName: 'gugara' });
}
// export const ndkStore = new NDKSvelte({
// explicitRelayUrls: [
// "wss://purplepag.es",
// "wss://relay.nostr.band",
// "wss://nos.lol",
// 'wss://relay.snort.social',
// "wss://relay.damus.io",
// "wss://nostr.wine",
// "wss://bostr.bitcointxoko.com",
// "ws://localhost:8080",
// ],
// explicitRelayUrls: relayUrls,
// cacheAdapter: cacheAdapter,
// });
// ndkStore.connect().then(() => console.log("NDK Connected"));
// Create a singleton instance that is the default export
// const ndk = writable(ndkStore);
const ndkStore = new NDKSvelte({
explicitRelayUrls: relayUrls,
cacheAdapter: cacheAdapter,
enableOutboxModel: true
});
ndkStore.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({
ndk: ndkStore
});
ndkStore
.connect()
.catch((error: Error) => {
console.error('Failed to connect to relays:', error);
})
.then(() => console.log('NDK Connected'));
const ndk = writable(ndkStore);
export const bunkerNDKStore = new NDK({
explicitRelayUrls: ['wss://relay.nsec.app'],
enableOutboxModel: true
});
bunkerNDKStore.connect().then(() => console.log('Bunker NDK Connected'));
export const bunkerNdk = writable(bunkerNDKStore);
// export const user = ndk.getUser({ npub: PUBLIC_PUBKEY });
export default ndk;
+17
View File
@@ -0,0 +1,17 @@
export interface BunkerConnection {
pubkey: string;
relayUrl: string;
isConnected: boolean;
}
export interface SignInError {
message: string;
code?: string;
}
export interface UserProfile {
pubkey: string;
displayName?: string;
name?: string;
picture?: string;
}
+19
View File
@@ -0,0 +1,19 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
export function readingTime(text: string): number {
const wpm = 225;
const words = text.trim().split(/\s+/).length;
return Math.ceil(words / wpm);
}
+147
View File
@@ -0,0 +1,147 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
import NDK, {
NDKNip07Signer,
NDKNip46Signer,
NDKPrivateKeySigner,
NDKUser
} from '@nostr-dev-kit/ndk';
export const ndk = writable<NDK | null>(null);
export const user = writable<NDKUser | null>(null);
export const isAuthenticated = writable<boolean>(false);
const STORAGE_KEY = 'nostr_pubkey';
export enum SigninMethod {
Nip07 = 'nip07',
Nip46 = 'nip46',
PK = 'pk'
}
export async function initializeNDK() {
const ndkInstance = new NDK({
explicitRelayUrls: ['wss://nostr.wine', 'wss://nos.lol']
});
await ndkInstance.connect();
ndk.set(ndkInstance);
console.log('NDK Connected');
// Check for stored public key
await checkStoredUser(ndkInstance);
}
async function checkStoredUser(ndkInstance: NDK) {
const storedPubkey = localStorage.getItem(STORAGE_KEY);
if (storedPubkey) {
try {
// Create a new NDK instance WITH signer for authenticated users
const signerNDK = new NDK({
explicitRelayUrls: ['wss://nostr.wine', 'wss://nos.lol'],
signer: new NDKNip07Signer()
});
await signerNDK.connect();
// Create user from stored pubkey
const ndkUser = new NDKUser({ pubkey: storedPubkey });
ndkUser.ndk = signerNDK;
// Set the NDK instance with signer
ndk.set(signerNDK);
user.set(ndkUser);
isAuthenticated.set(true);
console.log('User restored with signer:', storedPubkey);
} catch (error) {
console.error('Failed to restore user with signer:', error);
// Fall back to non-authenticated state
ndk.set(ndkInstance);
localStorage.removeItem(STORAGE_KEY);
}
} else {
// No stored user, use NDK without signer
ndk.set(ndkInstance);
}
}
export async function signInWithNIP07() {
try {
const signerNDK = new NDK({
explicitRelayUrls: ['wss://nostr.wine', 'wss://nos.lol'],
signer: new NDKNip07Signer()
});
await signerNDK.connect();
// This will prompt the user for permission
const ndkUser = await signerNDK.signer!.user();
// Store the public key for future sessions
localStorage.setItem(STORAGE_KEY, ndkUser.pubkey);
ndk.set(signerNDK);
user.set(ndkUser);
isAuthenticated.set(true);
console.log('User signed in:', ndkUser.pubkey);
localStorage.setItem('nostrSigninMethod', SigninMethod.Nip07);
return ndkUser;
} catch (error) {
console.error('NIP-07 sign in failed:', error);
throw error;
}
}
export async function userFromNip46(
ndk: NDK,
bunkerNdk: NDK,
token?: string
): Promise<NDKUser | null> {
let localSigner: NDKPrivateKeySigner;
let user: NDKUser | null = null;
if (browser) {
const storedKey = localStorage.getItem('Nip46LocalSignerPK');
const targetNpub = localStorage.getItem('Nip46TargetNpub');
// If we have a local PK and a target npub, try and sign in.
if (storedKey && targetNpub) {
console.log('stored key and target npub');
localSigner = new NDKPrivateKeySigner(storedKey);
const targetUser = ndk.getUser({ npub: targetNpub });
const remoteSigner = new NDKNip46Signer(bunkerNdk, targetUser!.pubkey, localSigner);
ndk.signer = remoteSigner;
await remoteSigner.blockUntilReady();
user = await remoteSigner.user();
}
// If we're missing one of the above but we have a token, try and create a new nsecBunker connection
else if (token) {
const localSigner = NDKPrivateKeySigner.generate();
localStorage.setItem('Nip46LocalSignerPK', localSigner.privateKey as string);
const remoteSigner = new NDKNip46Signer(bunkerNdk, token, localSigner);
ndk.signer = remoteSigner;
try {
await remoteSigner.blockUntilReady();
user = await remoteSigner.user();
localStorage.setItem('Nip46TargetNpub', user.npub);
} catch (error) {
// toast.error(error as string);
console.error(error);
}
}
localStorage.setItem('nostrSigninMethod', SigninMethod.Nip46);
}
return user;
}
export function signOut() {
localStorage.removeItem(STORAGE_KEY);
user.set(null);
isAuthenticated.set(false);
console.log('User signed out');
// Reinitialize NDK without signer
initializeNDK();
console.log('Reinitalised NDK');
}

Some files were not shown because too many files have changed in this diff Show More