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
+23
View File
@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+9
View File
@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/
+16
View File
@@ -0,0 +1,16 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/app.css"
}
+38
View File
@@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src/app.css",
"baseColor": "zinc"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
}
+40
View File
@@ -0,0 +1,40 @@
import prettier from 'eslint-config-prettier';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off'
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);
+69
View File
@@ -0,0 +1,69 @@
{
"name": "gugara-next",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"test:unit": "vitest",
"test": "npm run test:unit -- --run"
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@internationalized/date": "^3.9.0",
"@lucide/svelte": "^0.515.0",
"@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
"@vitest/browser": "^3.2.3",
"bits-ui": "^2.9.6",
"clsx": "^2.1.1",
"embla-carousel-svelte": "^8.6.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"playwright": "^1.53.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.3.8",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^7.0.4",
"vitest": "^3.2.3",
"vitest-browser-svelte": "^0.1.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
},
"dependencies": {
"@nostr-dev-kit/ndk": "^2.14.33",
"@nostr-dev-kit/ndk-cache-dexie": "^2.6.34",
"@nostr-dev-kit/ndk-svelte": "^2.4.38",
"@nostr-dev-kit/ndk-svelte-components": "^2.3.11",
"latlon-geohash": "^2.0.0",
"maplibre-gl": "^5.24.0",
"mode-watcher": "^1.1.0",
"pdfjs-dist": "^5.7.284",
"svelte-sonner": "^1.0.5"
}
}
+4860
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
allowBuilds:
'@tailwindcss/oxide': true
bufferutil: true
es5-ext: true
esbuild: true
svelte-preprocess: true
utf-8-validate: true
+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,
};

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