Initial commit
This commit is contained in:
+23
@@ -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-*
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
bun.lock
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/static/
|
||||||
+16
@@ -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"
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+4860
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
allowBuilds:
|
||||||
|
'@tailwindcss/oxide': true
|
||||||
|
bufferutil: true
|
||||||
|
es5-ext: true
|
||||||
|
esbuild: true
|
||||||
|
svelte-preprocess: true
|
||||||
|
utf-8-validate: true
|
||||||
+123
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Vendored
+13
@@ -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 {};
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
@@ -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}
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 |
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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} />
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./input.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input,
|
||||||
|
};
|
||||||
@@ -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}
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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
Reference in New Issue
Block a user