feat: svelte 5

This commit is contained in:
glazk0 2025-01-28 21:14:10 +01:00
parent f8f460fe74
commit 886ad26687
No known key found for this signature in database
225 changed files with 5429 additions and 4677 deletions

View file

@ -1,8 +1,8 @@
# build folder
# Build folder
dist/
build/
# logs
# Logs
logs/
.cache
.DS_Store
@ -10,7 +10,7 @@ npm-debug.log
yarn-debug.log
yarn-error.log
# process data
# Process data
.pid
pids
.pid-lock

1
.env.example Normal file
View file

@ -0,0 +1 @@
API_URL=

View file

@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View file

@ -1,30 +0,0 @@
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

20
.gitignore vendored
View file

@ -1,10 +1,24 @@
.DS_Store
test-results
node_modules
/build
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/package
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc
View file

@ -1,2 +1 @@
engine-strict=true
resolution-mode=highest

View file

@ -1,13 +1,4 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

View file

@ -4,6 +4,12 @@
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

View file

@ -1,4 +1,3 @@
# Base Stage
FROM node:21-slim AS base
ENV PNPM_HOME="/pnpm"
@ -10,12 +9,10 @@ WORKDIR /app
COPY package.json pnpm-lock.yaml ./
# Dependencies Stage for Production
FROM base AS deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
# Build Stage
FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
@ -24,14 +21,12 @@ COPY . .
RUN pnpm run build
# Deploy Stage
FROM base AS deploy
COPY --from=deps /app/node_modules /app/node_modules
COPY --from=build /app/build /app/build
# Run as non-root user
# USER node
USER node
ARG PORT=3000

View file

@ -1,14 +1,17 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": true
"$schema": "https://next.shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "neutral"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
}

6
e2e/demo.test.ts Normal file
View file

@ -0,0 +1,6 @@
import { expect, test } from '@playwright/test';
test('home page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});

34
eslint.config.js Normal file
View file

@ -0,0 +1,34 @@
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
}
);

View file

@ -1,55 +1,59 @@
{
"name": "peer-at-code",
"type": "module",
"name": "peer-at-code-web",
"private": true,
"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",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .",
"test:integration": "playwright test",
"test:unit": "vitest"
},
"dependencies": {
"bits-ui": "^0.20.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"formsnap": "^0.5.1",
"lucide-svelte": "^0.363.0",
"mdsvex": "^0.11.2",
"mode-watcher": "^0.3.1",
"svelte-boring-avatars": "^1.2.6",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.2.1",
"vaul-svelte": "^0.3.2"
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"test:unit": "vitest",
"test": "npm run test:unit -- --run && npm run test:e2e",
"test:e2e": "playwright test"
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.2.7",
"@sveltejs/kit": "^2.7.1",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@tailwindcss/typography": "^0.5.15",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.49.1",
"@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"autoprefixer": "^10.4.20",
"boring-avatars": "^1.11.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.44.1",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"prettier-plugin-tailwindcss": "^0.5.14",
"svelte": "^4.2.19",
"svelte-check": "^3.8.6",
"sveltekit-superforms": "^2.19.1",
"tailwindcss": "^3.4.14",
"tslib": "^2.8.0",
"typescript": "^5.6.3",
"vite": "^5.4.9",
"zod": "^3.23.8"
"bits-ui": "1.0.0-next.78",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^2.46.1",
"formsnap": "^2.0.0",
"globals": "^15.14.0",
"lucide-svelte": "^0.474.0",
"mode-watcher": "^0.5.1",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.10",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-sonner": "^0.3.28",
"sveltekit-superforms": "^2.23.1",
"tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.0.0",
"vitest": "^3.0.0",
"svelte-boring-avatars": "^1.2.6",
"clsx": "^2.1.1",
"zod": "^3.24.1"
},
"dependencies": {
"mdsvex": "^0.12.3"
}
}

View file

@ -1,12 +1,10 @@
import type { PlaywrightTestConfig } from '@playwright/test';
import { defineConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
export default defineConfig({
webServer: {
command: 'pnpm build && pnpm preview',
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default config;
testDir: 'e2e'
});

2902
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View file

@ -2,6 +2,68 @@
@tailwind components;
@tailwind utilities;
/* @layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
} */
@layer base {
@font-face {
@ -47,19 +109,26 @@
--ring: 258 85% 67%;
--radius: 0.5rem;
--sidebar-background: 255 14.81% 10.59%;
--sidebar-foreground: 284 4.7% 97.05%;
--sidebar-primary: 258 85% 67%;
--sidebar-primary-foreground: 284 4.7% 97.05%;
--sidebar-accent: 258 60% 40%;
--sidebar-accent-foreground: 284 4.7% 97.05%;
--sidebar-border: 258 15% 17%;
--sidebar-ring: 258 85% 67%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
::selection {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
::selection {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
@ -77,9 +146,7 @@
@apply rounded-sm !bg-muted-foreground/30;
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
html {
scrollbar-color: hsl(215.4 16.3% 46.9% / 0.3);
}
@ -99,7 +166,7 @@
}
@layer components {
.bg-gradient {
background: var(--gradient);
}
.bg-gradient {
background: var(--gradient);
}
}

10
src/app.d.ts vendored
View file

@ -1,6 +1,6 @@
// See https://kit.svelte.dev/docs/types#app
// See https://svelte.dev/docs/kit/types#app.d.ts
import type { User } from '$lib/types';
import type { User } from "$lib/types";
// for information about these interfaces
declare global {
@ -9,12 +9,14 @@ declare global {
message: string;
errorId: string;
}
// interface Locals {}
interface Locals {
user?: User;
user: User | null;
}
interface PageData {
user?: User;
user: User | null;
}
// interface PageState {}
// interface Platform {}
}
}

View file

@ -1,15 +1,12 @@
<!DOCTYPE html>
<!doctype html>
<html lang="fr" style="color-scheme: dark;">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<script data-domain="app.peerat.dev" src="https://plosibl.peerat.dev/js/script.js" defer></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="min-h-screen antialiased font-sans text-base">
<div style="display: contents" class="relative flex min-h-screen flex-col">%sveltekit.body%</div>
</body>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<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>

7
src/demo.spec.ts Normal file
View file

@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View file

@ -6,7 +6,8 @@ export const handleError: HandleClientError = ({ error, status }) => {
console.error(`Client - Error ID: ${errorId} - Status: ${status} - ${error}`);
return {
message: 'Whoops!',
message: 'An error occurred',
status,
errorId,
};
};

View file

@ -1,63 +1,67 @@
import type { Handle, HandleFetch, HandleServerError } from '@sveltejs/kit';
import { API_URL } from '$env/static/private';
import type { User } from '$lib/types';
import type { Handle, HandleFetch, HandleServerError } from '@sveltejs/kit';
import { COOKIES } from '$lib/constants';
export const handle: Handle = async ({ event, resolve }) => {
const session = event.cookies.get('session');
const session = event.cookies.get(COOKIES.SESSION);
if (!session) {
event.locals.user = undefined;
return await resolve(event);
}
if (!session) {
event.locals.user = null;
return resolve(event);
}
const res = await fetch(`${API_URL}/player/`, {
headers: {
Authorization: `Bearer ${session}`
}
});
try {
const res = await fetch(API_URL + '/player/', {
headers: {
Authorization: `Bearer ${session}`
}
});
if (!res.ok) {
event.locals.user = undefined;
event.cookies.delete('session', { path: '/' });
return await resolve(event);
}
if (!res.ok) {
event.locals.user = null;
event.cookies.delete(COOKIES.SESSION, { path: '/' });
return resolve(event);
}
try {
const user = await res.json() as User;
event.locals.user = user;
} catch (error) {
event.locals.user = undefined;
event.cookies.delete('session', { path: '/' });
}
const user = await res.json();
event.locals.user = user;
return await resolve(event);
return resolve(event);
} catch (error) {
console.error(error);
event.locals.user = null;
event.cookies.delete(COOKIES.SESSION, { path: '/' });
return resolve(event);
}
};
export const handleFetch: HandleFetch = ({ request, fetch, event: { cookies } }) => {
const session = cookies.get('session');
export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
const session = event.cookies.get(COOKIES.SESSION);
if (!session) {
return fetch(request);
}
if (!session) {
return fetch(request);
}
request = new Request(request, {
headers: {
Authorization: `Bearer ${session}`
},
});
const headers = {
...request.headers,
Authorization: `Bearer ${session}`
};
return fetch(request);
}
request = new Request(request, { headers });
export const handleError: HandleServerError = ({ error, status }) => {
const errorId = crypto.randomUUID();
console.error(`Server - Error ID: ${errorId} - Status: ${status} - ${error}`);
return {
message: 'Whoops!',
errorId,
};
return fetch(request);
};
export const handleError: HandleServerError = async ({ error, status }) => {
const errorId = crypto.randomUUID();
console.error(`Error ID: ${errorId} - Status: ${status} - ${error}`);
return {
message: 'Internal server error',
status,
errorId
}
};

View file

@ -0,0 +1,196 @@
<script lang="ts" module>
import ChartNoAxesColumn from 'lucide-svelte/icons/chart-no-axes-column';
import ChevronsUpDown from 'lucide-svelte/icons/chevrons-up-down';
import CircleHelp from 'lucide-svelte/icons/circle-help';
import Code from 'lucide-svelte/icons/code';
import GitBranch from 'lucide-svelte/icons/git-branch';
import LayoutDashboard from 'lucide-svelte/icons/layout-dashboard';
import LogOut from 'lucide-svelte/icons/log-out';
import ScrollText from 'lucide-svelte/icons/scroll-text';
import Settings from 'lucide-svelte/icons/settings';
const navigation = [
{
title: null,
isAdmin: false,
items: [
{
title: 'Dashboard',
url: '/',
icon: LayoutDashboard
},
{
title: 'Classement',
url: '/leaderboard',
icon: ChartNoAxesColumn
},
{
title: 'Chapitres',
url: '/chapters',
icon: Code
}
]
},
{
title: 'Documentation',
isAdmin: false,
items: [
{
title: 'Git',
url: 'https://git.peerat.dev',
icon: GitBranch
},
{
title: 'Discord',
url: '#',
icon: CircleHelp
}
]
},
{
title: 'Administration',
isAdmin: true,
items: [
{
title: 'Logs',
url: '/admin/logs',
icon: ScrollText
},
{
title: 'Puzzles',
url: '/admin/puzzles',
icon: Code
}
]
}
];
</script>
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/state';
import type { ComponentProps } from 'svelte';
import * as Avatar from '$lib/components/ui/avatar';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Sidebar from '$lib/components/ui/sidebar';
import { useSidebar } from '$lib/components/ui/sidebar';
let {
ref = $bindable(null),
collapsible = 'icon',
...restProps
}: ComponentProps<typeof Sidebar.Root> = $props();
const sidebar = useSidebar();
afterNavigate(() => {
if (sidebar.isMobile) {
sidebar.setOpenMobile(false);
}
});
</script>
<Sidebar.Root bind:ref {collapsible} {...restProps}>
<Sidebar.Header>
<Sidebar.Menu>
<a href="/">
<Sidebar.MenuItem>
<Sidebar.MenuButton
size="lg"
class="rounded data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div class="flex aspect-square size-8 items-center justify-center rounded">
<img src="/favicon.ico" alt="Peer-at Code" class="size-6" />
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold"> Peer-at Code </span>
</div>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</a>
</Sidebar.Menu>
</Sidebar.Header>
<Sidebar.Content>
{#each navigation as navigation}
<Sidebar.Group>
{#if navigation.title}
<Sidebar.GroupLabel>{navigation.title}</Sidebar.GroupLabel>
{/if}
<Sidebar.Menu>
{#each navigation.items as item (item.title)}
<Sidebar.MenuItem>
<Sidebar.MenuButton class="rounded" isActive={page.url.pathname === item.url}>
{#snippet child({ props })}
<a href={item.url} {...props}>
<item.icon />
<span>{item.title}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.Group>
{/each}
</Sidebar.Content>
<Sidebar.Footer>
<Sidebar.Menu>
<Sidebar.MenuItem>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Sidebar.MenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
{...props}
>
<Avatar.Root class="h-8 w-8 rounded">
<Avatar.Image src={page.data.user?.avatar} alt={page.data.user?.pseudo} />
<Avatar.Fallback class="rounded">CN</Avatar.Fallback>
</Avatar.Root>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{page.data.user?.pseudo}</span>
<span class="truncate text-xs">{page.data.user?.email}</span>
</div>
<ChevronsUpDown class="ml-auto size-4" />
</Sidebar.MenuButton>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-[--bits-dropdown-menu-anchor-width] min-w-56 rounded"
side={sidebar.isMobile ? 'bottom' : 'right'}
align="end"
sideOffset={4}
>
<DropdownMenu.Label class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar.Root class="h-8 w-8 rounded">
<Avatar.Image src={page.data.user?.avatar} alt={page.data.user?.pseudo} />
<Avatar.Fallback class="rounded">CN</Avatar.Fallback>
</Avatar.Root>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{page.data.user?.pseudo}</span>
<span class="truncate text-xs">{page.data.user?.email}</span>
</div>
</div>
</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item disabled>
<Settings />
Paramètres
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={() => goto('/logout')}>
<LogOut />
Se déconnecter
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Footer>
<Sidebar.Rail />
</Sidebar.Root>

View file

@ -1,23 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
export let name: string;
export let src: string;
export let alt: string;
export let level: number;
</script>
<div class="flex w-24 flex-col space-y-2 text-center">
<img
src={`data:image;base64,${src}`}
{alt}
class={cn(`rounded-full border-2 lg:border-4`, {
'border-green-600': level === 1,
'border-yellow-600': level === 2,
'border-red-600': level === 3
})}
width={500}
height={500}
/>
<span class="text-sm font-semibold break-all">{name}</span>
</div>

View file

@ -1,19 +1,17 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import * as Breadcrumb from '$lib/components/ui/breadcrumb';
export let breadcrumb: { name: string; href: string }[] = [];
$: page.subscribe(({ url: { pathname } }) => {
breadcrumb = pathname
const breadcrumb = $derived(
page.url.pathname
.split('/')
.slice(1)
.map((segment, index, segments) => ({
name: segment.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
href: '/' + segments.slice(0, index + 1).join('/')
}));
});
}))
);
</script>
<Breadcrumb.Root>

View file

@ -1,16 +0,0 @@
<script lang="ts">
export let title: string;
export let data: any;
</script>
<div
class="flex w-full items-center space-x-4 border rounded border-border bg-card p-4 shadow-md"
>
<!-- <Icon class="text-muted-foreground" size={30} /> -->
<div class="flex w-full items-center justify-between">
<div class="">
<h2 class="text-xl font-semibold">{data}</h2>
<p class="text-muted-foreground">{title}</p>
</div>
</div>
</div>

View file

@ -1,46 +1,41 @@
<script lang="ts">
import Swords from 'lucide-svelte/icons/swords';
import Book from 'lucide-svelte/icons/book';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import Trophy from 'lucide-svelte/icons/trophy';
import type { Chapter } from '$lib/types';
import { cn } from '$lib/utils';
export let chapter: Chapter;
type Props = { chapter: Chapter };
let { chapter }: Props = $props();
</script>
<li
class={cn(
'group relative flex h-full w-full flex-col rounded border border-border bg-card transition-colors duration-150',
{
'hover:bg-card/80': chapter.show || (chapter.start && chapter.end),
'opacity-50': !chapter.show && !(chapter.start && chapter.end)
}
)}
>
{#if chapter.show || (chapter.start && chapter.end)}
<a
class="flex h-full w-full items-center justify-between gap-4 p-4"
href={chapter.show || (chapter.start && chapter.end) ? `/chapters/${chapter.id}` : null}
>
{#snippet item({ chapter }: { chapter: Chapter })}
<div class="group overflow-hidden rounded bg-card shadow-sm hover:bg-card/80">
<div class="flex w-full items-center justify-between p-4">
<div class="flex items-center gap-2">
<span class="font-semibold">
{chapter.name}
</span>
{#if chapter.start && chapter.end}
<Swords class="stroke-muted-foreground" />
<Trophy class="size-6 stroke-muted-foreground" />
{:else}
<Book class="size-6 stroke-muted-foreground" />
{/if}
</div>
<span class="translate-x-0 transform-gpu duration-300 group-hover:translate-x-2">
<ChevronRight />
</span>
</a>
{:else}
<span class="flex h-full w-full items-center gap-4 p-4">
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
<h2 class="font-semibold">
<h2 class="text-lg font-semibold">
{chapter.name}
</h2>
</div>
</span>
{/if}
</li>
{#if chapter.show || (chapter.start && chapter.end)}
<ChevronRight
class="size-6 stroke-muted-foreground transition-transform duration-300 group-hover:translate-x-2"
/>
{/if}
</div>
</div>
{/snippet}
{#if chapter.show || (chapter.start && chapter.end)}
<a href="/chapters/{chapter.id}">
{@render item({ chapter })}
</a>
{:else}
{@render item({ chapter })}
{/if}

View file

@ -1,32 +0,0 @@
<script lang="ts">
import Check from 'lucide-svelte/icons/check';
import Copy from 'lucide-svelte/icons/copy';
let element: HTMLButtonElement;
let copying = false;
const copy = () => {
const sibling = element.nextElementSibling as HTMLElement;
const code = sibling.innerText;
navigator.clipboard.writeText(code);
copying = true;
setTimeout(() => (copying = false), 1000);
};
</script>
<button
class="absolute right-2 top-2 rounded-md p-2"
bind:this={element}
on:click={copy}
{...$$restProps}
>
{#if copying}
<Check color="green" />
{:else}
<Copy />
{/if}
</button>

View file

@ -1,47 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import CopyCodeButton from './copy-code-button.svelte';
onMount(() => {
const preTags: HTMLCollectionOf<HTMLPreElement> = document.getElementsByTagName('pre');
for (let preTag of preTags) {
const classList = Array.from(preTag.classList);
const isCodeBlock = classList.some((className) => className.startsWith('language-'));
if (isCodeBlock) {
const preTagParent = preTag.parentNode;
const newCodeBlockWrapper = document.createElement('div');
newCodeBlockWrapper.className = 'relative';
new CopyCodeButton({
target: newCodeBlockWrapper
});
if (preTagParent) {
preTagParent.replaceChild(newCodeBlockWrapper, preTag);
newCodeBlockWrapper.appendChild(preTag);
}
}
}
return () => {
for (let preTag of preTags) {
const preTagParent = preTag.parentNode;
if (preTagParent) {
const newCodeBlockWrapper = preTagParent.parentNode;
if (newCodeBlockWrapper) {
preTagParent.replaceChild(preTag, newCodeBlockWrapper);
}
}
}
};
});
</script>
<slot />

View file

@ -1,6 +0,0 @@
export { default as Badge } from './badge.svelte';
export { default as Breadcrumb } from './breadcrumb.svelte';
export { default as Chapter } from './chapter.svelte';
export { default as CopyCodeInjector } from './copy-code-injector.svelte';
export { default as Metadata } from './metadata.svelte';
export { default as Puzzle } from './puzzle.svelte';

View file

@ -1,3 +0,0 @@
export { default as Loader } from './loader/loader.svelte';
export { default as Navbar } from './navbar/navbar.svelte';
export { default as Sidenav } from './sidenav/sidenav.svelte';

View file

@ -1,42 +0,0 @@
<div class="loading-bar">
<div class="progress-bar">
<div class="progress-bar-value" />
</div>
</div>
<style>
.loading-bar {
width: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 100000;
}
.progress-bar {
@apply bg-muted;
height: 2px;
width: 100%;
overflow: hidden;
}
.progress-bar-value {
@apply bg-primary;
width: 100%;
height: 100%;
animation: loadingBarAnimation 2s infinite linear;
transform-origin: 0% 50%;
}
@keyframes loadingBarAnimation {
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
}
</style>

View file

@ -1,63 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import type { NavItemWithChildren } from '$lib/config';
import { cn } from '$lib/utils';
import MobileNavLink from './mobile-nav-link.svelte';
export let navItem: NavItemWithChildren;
</script>
{#if navItem.children?.length}
<h5 class="mb-2 font-medium">{navItem.name}</h5>
<ul class="space-y-2">
{#each navItem.children as item}
{@const isActive = $page.url.pathname === item.href}
<li>
<MobileNavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': isActive,
'hover:border-opacity-100 hover:bg-card hover:text-foreground': !isActive
}
)}
href={item.href}
external={item.external}
>
<svelte:component
this={item.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{item.name}</span>
</MobileNavLink>
</li>
{/each}
</ul>
{:else}
<MobileNavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': $page.url.pathname === navItem.href,
'hover:border-opacity-100 hover:bg-card hover:text-foreground':
$page.url.pathname !== navItem.href
}
)}
href={navItem.href}
external={navItem.external}
>
<svelte:component
this={navItem.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{navItem.name}</span>
</MobileNavLink>
{/if}

View file

@ -1,32 +0,0 @@
<script lang="ts">
import ArrowUpRight from 'lucide-svelte/icons/arrow-up-right';
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { Drawer } from 'vaul-svelte';
import { cn } from '$lib/utils';
type $$Props = HTMLAnchorAttributes & {
external?: boolean;
};
let className: string | undefined | null = undefined;
export { className as class };
export let href: $$Props['href'] = '';
export let external = false;
</script>
<Drawer.Close asChild let:builder>
<a
use:builder.action
{href}
target={external ? '_blank' : undefined}
class={cn(external && 'flex items-center gap-0.5', className)}
{...$$restProps}
>
<slot />
{#if external}
<ArrowUpRight class="h-3 w-3" />
{/if}
</a>
</Drawer.Close>

View file

@ -1,32 +0,0 @@
<script lang="ts">
import Menu from 'lucide-svelte/icons/menu';
import { navigation } from '$lib/config';
import MobileNavItem from './mobile-nav-item.svelte';
import * as Drawer from '$lib/components/ui/drawer';
import Button from '$lib/components/ui/button/button.svelte';
</script>
<Drawer.Root>
<Drawer.Trigger asChild let:builder aria-label="open mobile menu">
<Button class="sm:hidden" builders={[builder]} variant="outline" size="icon">
<Menu class="h-4 w-4" />
</Button>
</Drawer.Trigger>
<Drawer.Content>
<div class="container mx-auto">
<Drawer.Title class="sr-only mb-4 font-medium">Navigation</Drawer.Title>
<nav class="py-4">
<ul class="flex w-full flex-col justify-center gap-4">
{#each navigation as navItem}
<li>
<MobileNavItem {navItem} />
</li>
{/each}
</ul>
</nav>
</div>
</Drawer.Content>
</Drawer.Root>

View file

@ -1,84 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import Boring from 'svelte-boring-avatars';
import Award from 'lucide-svelte/icons/award';
import Code from 'lucide-svelte/icons/code';
import GitBranch from 'lucide-svelte/icons/git-branch';
import LifeBuoy from 'lucide-svelte/icons/life-buoy';
import LogOut from 'lucide-svelte/icons/log-out';
import RectangleEllipsis from 'lucide-svelte/icons/rectangle-ellipsis';
import ScrollText from 'lucide-svelte/icons/scroll-text';
import Settings from 'lucide-svelte/icons/settings';
import Shield from 'lucide-svelte/icons/shield';
import * as Avatar from '$lib/components/ui/avatar';
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button builders={[builder]} variant="plain" size="icon">
<Avatar.Root>
<Avatar.Image
src="data:image;base64,{$page.data.user?.avatar}"
alt={$page.data.user?.pseudo}
/>
<Avatar.Fallback>
<Boring name={$page.data.user?.pseudo} variant="beam" />
</Avatar.Fallback>
</Avatar.Root>
<span class="sr-only">Menu utilisateur</span>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-56">
<DropdownMenu.Label
>Salutation, <span class="text-primary">{$page.data.user?.pseudo}</span></DropdownMenu.Label
>
{#if $page.data.user?.email.endsWith('@peerat.dev')}
<DropdownMenu.Separator />
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>
<Shield class="mr-2 h-4 w-4" />
<span>Administration</span>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item href="/admin/logs">
<ScrollText class="mr-2 h-4 w-4" />
<span>Logs</span>
</DropdownMenu.Item>
<DropdownMenu.Item href="/admin/puzzles">
<Code class="mr-2 h-4 w-4" />
<span>Puzzles</span>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{/if}
<DropdownMenu.Separator />
<DropdownMenu.Item href="/settings">
<Settings class="mr-2 h-4 w-4" />
<span>Paramètres</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/badges">
<Award class="mr-2 h-4 w-4" />
<span>Mes badges</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/git" target="_blank">
<GitBranch class="mr-2 h-4 w-4" />
<span>Git</span>
</DropdownMenu.Item>
<DropdownMenu.Item href="/discord" target="_blank">
<LifeBuoy class="mr-2 h-4 w-4" />
<span>Discord</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/logout" class="text-destructive data-[highlighted]:bg-destructive">
<LogOut class="mr-2 h-4 w-4" />
Se déconnecter
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -1,15 +0,0 @@
<script lang="ts">
import Breadcrumb from '$lib/components/breadcrumb.svelte';
import MobileNav from '../mobile-nav/mobile-nav.svelte';
import NavbarUser from './navbar-user.svelte';
</script>
<nav class="w-full border-b border-muted p-4">
<div class="flex items-center justify-between">
<Breadcrumb />
<div class="flex items-center gap-4">
<NavbarUser />
<MobileNav />
</div>
</div>
</nav>

View file

@ -1,63 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import type { NavItemWithChildren } from '$lib/config';
import { cn } from '$lib/utils';
import SidenavLink from './sidenav-link.svelte';
export let navItem: NavItemWithChildren;
</script>
{#if navItem.children?.length}
<h5 class="mb-2 font-medium">{navItem.name}</h5>
<ul class="space-y-2">
{#each navItem.children as item}
{@const isActive = $page.url.pathname === item.href}
<li>
<SidenavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': isActive,
'hover:border-opacity-100 hover:bg-card hover:text-foreground': !isActive
}
)}
href={item.href}
external={item.external}
>
<svelte:component
this={item.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{item.name}</span>
</SidenavLink>
</li>
{/each}
</ul>
{:else}
<SidenavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': $page.url.pathname === navItem.href,
'hover:border-opacity-100 hover:bg-card hover:text-foreground':
$page.url.pathname !== navItem.href
}
)}
href={navItem.href}
external={navItem.external}
>
<svelte:component
this={navItem.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{navItem.name}</span>
</SidenavLink>
{/if}

View file

@ -1,28 +0,0 @@
<script lang="ts">
import ArrowUpRight from 'lucide-svelte/icons/arrow-up-right';
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
type $$Props = HTMLAnchorAttributes & {
external?: boolean;
};
let className: string | undefined | null = undefined;
export { className as class };
export let href: $$Props['href'] = '';
export let external = false;
</script>
<a
{href}
target={external ? '_blank' : undefined}
class={cn(external && 'flex items-center gap-0.5', className)}
{...$$restProps}
>
<slot />
{#if external}
<ArrowUpRight class="h-3 w-3" />
{/if}
</a>

View file

@ -1,27 +0,0 @@
<script lang="ts">
import { navigation } from '$lib/config';
import SidenavItem from './sidenav-item.svelte';
</script>
<aside
class="hidden min-w-60 overflow-hidden border-r border-border transition-all duration-300 ease-in-out sm:flex sm:flex-col"
>
<div class="flex flex-col p-4">
<img
src="/assets/brand/peerat.webp"
alt="Logo"
width="50"
height="50"
loading="eager"
draggable="false"
/>
<ul class="flex flex-col gap-2 pt-4">
{#each navigation as navItem}
<li>
<SidenavItem {navItem} />
</li>
{/each}
</ul>
</div>
</aside>

View file

@ -1,50 +1,52 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { siteConfig } from '$lib/config/site';
import { SITE_CONFIG } from '$lib/constants';
export let title = siteConfig.name;
$: title = $page.data?.title ? `${$page.data.title} | ${siteConfig.name}` : siteConfig.name;
const title = $derived(
page.data.title ? `${page.data.title} | ${SITE_CONFIG.name}` : SITE_CONFIG.name
);
</script>
<svelte:head>
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={siteConfig.description} />
<meta name="keywords" content={siteConfig.keywords.join(',')} />
<meta name="author" content={siteConfig.author} />
<meta name="description" content={SITE_CONFIG.description} />
<meta name="keywords" content={SITE_CONFIG.keywords.join(',')} />
<meta name="author" content={SITE_CONFIG.author} />
<meta name="theme-color" content={siteConfig.themeColor} />
<meta name="theme-color" content={SITE_CONFIG.themeColor} />
<meta name="robots" content="noindex,nofollow" />
<meta itemprop="name" content={title} />
<meta itemprop="description" content={siteConfig.description} />
<meta itemprop="image" content={siteConfig.imageUrl} />
<meta itemprop="description" content={SITE_CONFIG.description} />
<meta itemprop="image" content={SITE_CONFIG.imageUrl} />
<meta property="og:site_name" content={siteConfig.name} />
<meta property="og:site_name" content={SITE_CONFIG.name} />
<meta property="og:title" content={title} />
<meta property="og:description" content={siteConfig.description} />
<meta property="og:description" content={SITE_CONFIG.description} />
<meta property="og:type" content="website" />
<meta property="og:url" content={siteConfig.url + $page.url.pathname} />
<meta property="og:image" content={siteConfig.imageUrl} />
<meta property="og:url" content={SITE_CONFIG.url + page.url.pathname} />
<meta property="og:image" content={SITE_CONFIG.imageUrl} />
<meta property="og:image:alt" content={title} />
<meta property="og:locale" content="fr" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={siteConfig.url} />
<meta name="twitter:site" content={SITE_CONFIG.url} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={siteConfig.description} />
<meta name="twitter:image" content={siteConfig.imageUrl} />
<meta name="twitter:description" content={SITE_CONFIG.description} />
<meta name="twitter:image" content={SITE_CONFIG.imageUrl} />
<meta name="twitter:image:alt" content={title} />
<meta name="twitter:creator" content={`@${siteConfig.author}`} />
<meta name="twitter:creator" content={`@${SITE_CONFIG.author}`} />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/icons/android-chrome-192x192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="/icons/android-chrome-512x512.png" />
</svelte:head>

View file

@ -1,17 +1,60 @@
<script lang="ts">
import { page } from '$app/stores';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import CircleCheckBig from 'lucide-svelte/icons/circle-check-big';
import type { Puzzle } from '$lib/types';
import { cn } from '$lib/utils';
export let puzzle: Puzzle;
type Props = { puzzle: Puzzle; chapterId: number };
const chapterId = $page.params.chapterId;
let { puzzle, chapterId }: Props = $props();
</script>
<li
{#snippet item({ puzzle }: { puzzle: Puzzle })}
<div class="group overflow-hidden rounded bg-card shadow-sm hover:bg-card/80">
<div class="flex w-full items-center justify-between p-4">
<div class="flex items-center gap-2">
<h2 class="text-lg font-semibold">
{puzzle.name}
</h2>
<span class="text-muted-foreground">({puzzle.score ?? 0}/{puzzle.scoreMax} points)</span>
{#if puzzle.tags?.length}
<div class="flex items-center gap-x-6">
<div class="flex gap-x-2 text-sm">
{#each puzzle.tags.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name.toLowerCase())) as tag}
<span class="inline-block rounded bg-muted px-2 py-1 text-muted-foreground">
{tag.name}
</span>
{/each}
</div>
</div>
{/if}
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 text-sm">
{#if puzzle.score}
<div class="flex items-center gap-1">
<CircleCheckBig class="size-4 stroke-green-500" />
<span class="text-green-500">Complété</span>
</div>
{/if}
</div>
<ChevronRight
class="size-6 stroke-muted-foreground transition-transform duration-300 group-hover:translate-x-2"
/>
</div>
</div>
</div>
{/snippet}
{#if puzzle.show}
<a href="/chapters/{chapterId}/puzzle/{puzzle.id}">
{@render item({ puzzle })}
</a>
{:else}
{@render item({ puzzle })}
{/if}
<!-- <li
class={cn(
'group relative flex h-full w-full rounded border border-border bg-card transition-colors duration-150',
{
@ -74,4 +117,4 @@
</div>
</span>
{/if}
</li>
</li> -->

View file

@ -2,15 +2,15 @@
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.FallbackProps;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.FallbackProps = $props();
</script>
<AvatarPrimitive.Fallback
class={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
{...$$restProps}
>
<slot />
</AvatarPrimitive.Fallback>
bind:ref
class={cn("bg-muted flex h-full w-full items-center justify-center rounded-full", className)}
{...restProps}
/>

View file

@ -2,17 +2,15 @@
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.ImageProps;
let className: $$Props["class"] = undefined;
export let src: $$Props["src"] = undefined;
export let alt: $$Props["alt"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.ImageProps = $props();
</script>
<AvatarPrimitive.Image
{src}
{alt}
bind:ref
class={cn("aspect-square h-full w-full", className)}
{...$$restProps}
{...restProps}
/>

View file

@ -2,17 +2,15 @@
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.Props;
let className: $$Props["class"] = undefined;
export let delayMs: $$Props["delayMs"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.RootProps = $props();
</script>
<AvatarPrimitive.Root
{delayMs}
class={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...$$restProps}
>
<slot />
</AvatarPrimitive.Root>
bind:ref
class={cn("relative flex size-10 shrink-0 overflow-hidden rounded-full", className)}
{...restProps}
/>

View file

@ -1,24 +1,23 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import Ellipsis from "lucide-svelte/icons/ellipsis";
import type { WithElementRef, WithoutChildren } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
</script>
<span
bind:this={el}
bind:this={ref}
role="presentation"
aria-hidden="true"
class={cn("flex h-9 w-9 items-center justify-center", className)}
{...$$restProps}
class={cn("flex size-9 items-center justify-center", className)}
{...restProps}
>
<Ellipsis class="h-4 w-4" />
<Ellipsis class="size-4" />
<span class="sr-only">More</span>
</span>

View file

@ -1,16 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLLiAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLLiAttributes> = $props();
</script>
<li bind:this={el} class={cn("inline-flex items-center gap-1.5", className)}>
<slot />
<li bind:this={ref} class={cn("inline-flex items-center gap-1.5", className)} {...restProps}>
{@render children?.()}
</li>

View file

@ -1,31 +1,31 @@
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import type { Snippet } from "svelte";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAnchorAttributes & {
el?: HTMLAnchorElement;
asChild?: boolean;
};
let {
ref = $bindable(null),
class: className,
href = undefined,
child,
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
child?: Snippet<[{ props: HTMLAnchorAttributes }]>;
} = $props();
export let href: $$Props["href"] = undefined;
export let el: $$Props["el"] = undefined;
export let asChild: $$Props["asChild"] = false;
let className: $$Props["class"] = undefined;
export { className as class };
let attrs: Record<string, unknown>;
$: attrs = {
class: cn("transition-colors hover:text-foreground", className),
const attrs = $derived({
class: cn("hover:text-foreground transition-colors", className),
href,
...$$restProps,
};
...restProps,
});
</script>
{#if asChild}
<slot {attrs} />
{#if child}
{@render child({ props: attrs })}
{:else}
<a bind:this={el} {...attrs} {href}>
<slot {attrs} />
<a bind:this={ref} {...attrs}>
{@render children?.()}
</a>
{/if}

View file

@ -1,23 +1,23 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLOlAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLOlAttributes & {
el?: HTMLOListElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLOlAttributes> = $props();
</script>
<ol
bind:this={el}
bind:this={ref}
class={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
"text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5",
className
)}
{...$$restProps}
{...restProps}
>
<slot />
{@render children?.()}
</ol>

View file

@ -1,23 +1,23 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
export let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={el}
bind:this={ref}
role="link"
aria-disabled="true"
aria-current="page"
class={cn("font-normal text-foreground", className)}
{...$$restProps}
class={cn("text-foreground font-normal", className)}
{...restProps}
>
<slot />
{@render children?.()}
</span>

View file

@ -1,25 +1,27 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import type { WithElementRef } from "bits-ui";
import type { HTMLLiAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLLiAttributes> = $props();
</script>
<li
role="presentation"
aria-hidden="true"
class={cn("[&>svg]:size-3.5", className)}
bind:this={el}
{...$$restProps}
bind:this={ref}
{...restProps}
>
<slot>
{#if children}
{@render children?.()}
{:else}
<ChevronRight />
</slot>
{/if}
</li>

View file

@ -1,15 +1,15 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLElement> & {
el?: HTMLElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<nav class={className} bind:this={el} aria-label="breadcrumb" {...$$restProps}>
<slot />
<nav bind:this={ref} class={className} aria-label="breadcrumb" {...restProps}>
{@render children?.()}
</nav>

View file

@ -1,25 +1,74 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { buttonVariants, type Props, type Events } from "./index.js";
<script lang="ts" module>
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
type $$Props = Props;
type $$Events = Events;
export const buttonVariants = tv({
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
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>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>
<script lang="ts">
import { cn } from "$lib/utils.js";
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View file

@ -1,50 +1,17 @@
import Root from "./button.svelte";
import { tv, type VariantProps } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
const buttonVariants = tv({
base: "inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
plain: "hover:bg-transparent hover:text-primary",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["size"];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type Props,
type Events,
type ButtonProps as Props,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} class={cn("p-6", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p bind:this={ref} class={cn("text-muted-foreground text-sm", className)} {...restProps}>
{@render children?.()}
</p>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} class={cn("flex items-center p-6 pt-0", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} class={cn("flex flex-col space-y-1.5 p-6 pb-0", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,25 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
level = 3,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
level?: 1 | 2 | 3 | 4 | 5 | 6;
} = $props();
</script>
<div
role="heading"
aria-level={level}
bind:this={ref}
class={cn("text-2xl font-semibold leading-none tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("bg-card text-card-foreground rounded-lg border shadow-sm", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,22 @@
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";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
};

View file

@ -0,0 +1,15 @@
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
const Root = CollapsiblePrimitive.Root;
const Trigger = CollapsiblePrimitive.Trigger;
const Content = CollapsiblePrimitive.Content;
export {
Root,
Content,
Trigger,
//
Root as Collapsible,
Content as CollapsibleContent,
Trigger as CollapsibleTrigger,
};

View file

@ -1,24 +0,0 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import DrawerOverlay from "./drawer-overlay.svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Portal>
<DrawerOverlay />
<DrawerPrimitive.Content
class={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...$$restProps}
>
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
<slot />
</DrawerPrimitive.Content>
</DrawerPrimitive.Portal>

View file

@ -1,18 +0,0 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.DescriptionProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Description
bind:el
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Description>

View file

@ -1,16 +0,0 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLDivElement> & {
el?: HTMLDivElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div bind:this={el} class={cn("mt-auto flex flex-col gap-2 p-4", className)} {...$$restProps}>
<slot />
</div>

View file

@ -1,19 +0,0 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLDivElement> & {
el?: HTMLDivElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
bind:this={el}
class={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...$$restProps}
>
<slot />
</div>

View file

@ -1,12 +0,0 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
type $$Props = DrawerPrimitive.Props;
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
export let open: $$Props["open"] = false;
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
</script>
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
<slot />
</DrawerPrimitive.NestedRoot>

View file

@ -1,18 +0,0 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.OverlayProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Overlay
bind:el
class={cn("fixed inset-0 z-50 bg-black/80", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Overlay>

View file

@ -1,18 +0,0 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.TitleProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Title
bind:el
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Title>

View file

@ -1,12 +0,0 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
type $$Props = DrawerPrimitive.Props;
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
export let open: $$Props["open"] = false;
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
</script>
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
<slot />
</DrawerPrimitive.Root>

View file

@ -1,41 +0,0 @@
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import Root from "./drawer.svelte";
import Content from "./drawer-content.svelte";
import Description from "./drawer-description.svelte";
import Overlay from "./drawer-overlay.svelte";
import Footer from "./drawer-footer.svelte";
import Header from "./drawer-header.svelte";
import Title from "./drawer-title.svelte";
import NestedRoot from "./drawer-nested.svelte";
const Trigger = DrawerPrimitive.Trigger;
const Portal = DrawerPrimitive.Portal;
const Close = DrawerPrimitive.Close;
export {
Root,
NestedRoot,
Content,
Description,
Overlay,
Footer,
Header,
Title,
Trigger,
Portal,
Close,
//
Root as Drawer,
NestedRoot as DrawerNestedRoot,
Content as DrawerContent,
Description as DrawerDescription,
Overlay as DrawerOverlay,
Footer as DrawerFooter,
Header as DrawerHeader,
Title as DrawerTitle,
Trigger as DrawerTrigger,
Portal as DrawerPortal,
Close as DrawerClose,
};

View file

@ -1,35 +1,40 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import Minus from "lucide-svelte/icons/minus";
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
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
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
{...restProps}
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</DropdownMenuPrimitive.CheckboxIndicator>
</span>
<slot />
{#snippet children({ checked, indeterminate })}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<Minus class="size-4" />
{:else}
<Check class={cn("size-4", !checked && "text-transparent")} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>

View file

@ -1,27 +1,26 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents;
let className: $$Props["class"] = undefined;
export let sideOffset: $$Props["sideOffset"] = 4;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Content
{transition}
{transitionConfig}
{sideOffset}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
className
)}
{...$$restProps}
on:keydown
>
<slot />
</DropdownMenuPrimitive.Content>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
{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 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md outline-none",
className
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: DropdownMenuPrimitive.GroupHeadingProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
bind:ref
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...restProps}
/>

View file

@ -1,31 +1,23 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type $$Props = DropdownMenuPrimitive.ItemProps & {
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
class={cn(
"relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<slot />
</DropdownMenuPrimitive.Item>
{...restProps}
/>

View file

@ -1,19 +1,23 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { type WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = DropdownMenuPrimitive.LabelProps & {
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
};
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
} = $props();
</script>
<DropdownMenuPrimitive.Label
<div
bind:this={ref}
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...$$restProps}
{...restProps}
>
<slot />
</DropdownMenuPrimitive.Label>
{@render children?.()}
</div>

View file

@ -1,11 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
export let value: $$Props["value"] = undefined;
</script>
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
<slot />
</DropdownMenuPrimitive.RadioGroup>

View file

@ -1,35 +1,30 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
{...restProps}
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.RadioIndicator>
</span>
<slot />
{#snippet children({ checked })}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<Circle class="size-2 fill-current" />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>

View file

@ -2,13 +2,15 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SeparatorProps;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", className)}
{...$$restProps}
bind:ref
class={cn("bg-muted -mx-1 my-1 h-px", className)}
{...restProps}
/>

View file

@ -1,13 +1,20 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { type WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span class={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...$$restProps}>
<slot />
<span
bind:this={ref}
class={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...restProps}
>
{@render children?.()}
</span>

View file

@ -1,30 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubContentProps;
type $$Events = DropdownMenuPrimitive.SubContentEvents;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
x: -10,
y: 0,
};
export { className as class };
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
{transition}
{transitionConfig}
bind:ref
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none",
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none",
className
)}
{...$$restProps}
on:keydown
on:focusout
on:pointermove
>
<slot />
</DropdownMenuPrimitive.SubContent>
{...restProps}
/>

View file

@ -3,30 +3,26 @@
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
"data-[highlighted]:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
{...restProps}
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
{@render children?.()}
<ChevronRight class="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>

View file

@ -1,48 +1,50 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import GroupHeading from "./dropdown-menu-group-heading.svelte";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import Content from "./dropdown-menu-content.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
const Trigger = DropdownMenuPrimitive.Trigger;
const Group = DropdownMenuPrimitive.Group;
const RadioGroup = DropdownMenuPrimitive.RadioGroup;
export {
Sub,
Root,
Item,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Content,
Root as DropdownMenu,
Sub as DropdownMenuSub,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
GroupHeading as DropdownMenuGroupHeading,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
Group as DropdownMenuGroup,
Content as DropdownMenuContent,
Trigger as DropdownMenuTrigger,
Shortcut as DropdownMenuShortcut,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
RadioGroup as DropdownMenuRadioGroup,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
CheckboxItem as DropdownMenuCheckboxItem,
Trigger as DropdownMenuTrigger,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger,
};

View file

@ -1,10 +1,7 @@
<script lang="ts">
import * as Button from "$lib/components/ui/button/index.js";
type $$Props = Button.Props;
type $$Events = Button.Events;
let { ref = $bindable(null), ...restProps }: Button.Props = $props();
</script>
<Button.Root type="submit" on:click on:keydown {...$$restProps}>
<slot />
</Button.Root>
<Button.Root bind:ref type="submit" {...restProps} />

View file

@ -1,17 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import type { WithoutChild } from "bits-ui";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChild<FormPrimitive.DescriptionProps> = $props();
</script>
<FormPrimitive.Description
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
let:descriptionAttrs
>
<slot {descriptionAttrs} />
</FormPrimitive.Description>
bind:ref
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

View file

@ -1,26 +1,30 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
<script lang="ts" module>
import type { FormPathLeaves as _FormPathLeaves } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
type U = _FormPathLeaves<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements";
<script lang="ts" generics="T extends Record<string, unknown>, U extends _FormPathLeaves<T>">
import * as FormPrimitive from "formsnap";
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef, WithoutChildren } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
form,
name,
children: childrenProp,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> &
FormPrimitive.ElementFieldProps<T, U> = $props();
</script>
<FormPrimitive.ElementField {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
<FormPrimitive.ElementField {form} {name}>
{#snippet children({ constraints, errors, tainted, value })}
<div bind:this={ref} class={cn("space-y-2", className)} {...restProps}>
{@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
</div>
{/snippet}
</FormPrimitive.ElementField>

View file

@ -1,26 +1,31 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import type { WithoutChild } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldErrorsProps & {
let {
ref = $bindable(null),
class: className,
errorClasses,
children: childrenProp,
...restProps
}: WithoutChild<FormPrimitive.FieldErrorsProps> & {
errorClasses?: string | undefined | null;
};
let className: $$Props["class"] = undefined;
export { className as class };
export let errorClasses: $$Props["class"] = undefined;
} = $props();
</script>
<FormPrimitive.FieldErrors
class={cn("text-sm font-medium text-destructive", className)}
{...$$restProps}
let:errors
let:fieldErrorsAttrs
let:errorAttrs
bind:ref
class={cn("text-destructive text-sm font-medium", className)}
{...restProps}
>
<slot {errors} {fieldErrorsAttrs} {errorAttrs}>
{#each errors as error}
<div {...errorAttrs} class={cn(errorClasses)}>{error}</div>
{/each}
</slot>
{#snippet children({ errors, errorProps })}
{#if childrenProp}
{@render childrenProp({ errors, errorProps })}
{:else}
{#each errors as error}
<div {...errorProps} class={cn(errorClasses)}>{error}</div>
{/each}
{/if}
{/snippet}
</FormPrimitive.FieldErrors>

View file

@ -1,26 +1,30 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
<script lang="ts" module>
import type { FormPath as _FormPath } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
type U = _FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements";
<script lang="ts" generics="T extends Record<string, unknown>, U extends _FormPath<T>">
import * as FormPrimitive from "formsnap";
import type { WithoutChildren, WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
form,
name,
children: childrenProp,
...restProps
}: FormPrimitive.FieldProps<T, U> &
WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
</script>
<FormPrimitive.Field {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
<FormPrimitive.Field {form} {name}>
{#snippet children({ constraints, errors, tainted, value })}
<div bind:this={ref} class={cn("space-y-2", className)} {...restProps}>
{@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
</div>
{/snippet}
</FormPrimitive.Field>

View file

@ -1,31 +1,21 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
<script lang="ts" module>
import type { FormPath as _FormPath } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
type U = _FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
<script lang="ts" generics="T extends Record<string, unknown>, U extends _FormPath<T>">
import * as FormPrimitive from "formsnap";
import type { WithoutChild } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldsetProps<T, U>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
form,
name,
...restProps
}: WithoutChild<FormPrimitive.FieldsetProps<T, U>> = $props();
</script>
<FormPrimitive.Fieldset
{form}
{name}
let:constraints
let:errors
let:tainted
let:value
class={cn("space-y-2", className)}
>
<slot {constraints} {errors} {tainted} {value} />
</FormPrimitive.Fieldset>
<FormPrimitive.Fieldset bind:ref {form} {name} class={cn("space-y-2", className)} {...restProps} />

View file

@ -1,17 +1,21 @@
<script lang="ts">
import type { Label as LabelPrimitive } from 'bits-ui';
import { getFormControl } from 'formsnap';
import { cn } from '$lib/utils.js';
import { Label } from '$lib/components/ui/label/index.js';
import type { WithoutChild } from "bits-ui";
import * as FormPrimitive from "formsnap";
import { Label } from "$lib/components/ui/label/index.js";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
let className: $$Props['class'] = undefined;
export { className as class };
const { labelAttrs } = getFormControl();
let {
ref = $bindable(null),
children,
class: className,
...restProps
}: WithoutChild<FormPrimitive.LabelProps> = $props();
</script>
<Label {...$labelAttrs} class={cn('data-[fs-error]:text-destructive', className)} {...$$restProps}>
<slot {labelAttrs} />
</Label>
<FormPrimitive.Label {...restProps} bind:ref>
{#snippet child({ props })}
<Label {...props} class={cn("data-[fs-error]:text-destructive", className)}>
{@render children?.()}
</Label>
{/snippet}
</FormPrimitive.Label>

View file

@ -1,17 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import type { WithoutChild } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.LegendProps;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChild<FormPrimitive.LegendProps> = $props();
</script>
<FormPrimitive.Legend
{...$$restProps}
class={cn("text-sm font-medium leading-none data-[fs-error]:text-destructive", className)}
let:legendAttrs
>
<slot {legendAttrs} />
</FormPrimitive.Legend>
bind:ref
class={cn("data-[fs-error]:text-destructive text-sm font-medium leading-none", className)}
{...restProps}
/>

View file

@ -0,0 +1,15 @@
import Root from "./input-otp.svelte";
import Group from "./input-otp-group.svelte";
import Slot from "./input-otp-slot.svelte";
import Separator from "./input-otp-separator.svelte";
export {
Root,
Group,
Slot,
Separator,
Root as InputOTP,
Group as InputOTPGroup,
Slot as InputOTPSlot,
Separator as InputOTPSeparator,
};

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} class={cn("flex items-center", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import Dot from "lucide-svelte/icons/dot";
let {
ref = $bindable(null),
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} role="separator" {...restProps}>
{#if children}
{@render children?.()}
{:else}
<Dot />
{/if}
</div>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { PinInput as InputOTPPrimitive } from "bits-ui";
import type { ComponentProps } from "svelte";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
cell,
class: className,
...restProps
}: ComponentProps<typeof InputOTPPrimitive.Cell> = $props();
</script>
<InputOTPPrimitive.Cell
{cell}
bind:ref
class={cn(
"border-input relative flex h-10 w-10 items-center justify-center border-y border-r text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
cell.isActive && "ring-ring ring-offset-background z-10 ring-2",
className
)}
{...restProps}
>
{cell.char}
{#if cell.hasFakeCaret}
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
<div class="animate-caret-blink bg-foreground h-4 w-px duration-1000"></div>
</div>
{/if}
</InputOTPPrimitive.Cell>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { PinInput as InputOTPPrimitive } from "bits-ui";
import type { ComponentProps } from "svelte";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value = $bindable(""),
...restProps
}: ComponentProps<typeof InputOTPPrimitive.Root> = $props();
</script>
<InputOTPPrimitive.Root
bind:ref
bind:value
class={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50 [&_input]:disabled:cursor-not-allowed",
className
)}
{...restProps}
/>

View file

@ -1,25 +1,5 @@
import Root from "./input.svelte";
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
};
export {
Root,
//

View file

@ -1,35 +1,22 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
import type { InputEvents } from "./index.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...restProps
}: WithElementRef<HTMLInputAttributes> = $props();
</script>
<input
bind:this={ref}
class={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
bind:value
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
{...restProps}
/>

View file

@ -2,20 +2,18 @@
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
let className: $$Props["class"] = undefined;
export { className as class };
let {
ref = $bindable(null),
class: className,
...restProps
}: LabelPrimitive.RootProps = $props();
</script>
<LabelPrimitive.Root
bind:ref
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
on:mousedown
>
<slot />
</LabelPrimitive.Root>
{...restProps}
/>

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