Compare commits

..

1 commit
main ... dev

Author SHA1 Message Date
ebb6d34f6e Merge pull request 'Update dev branch' (#29) from main into dev
Reviewed-on: #29
2024-11-25 21:37:00 +01:00
235 changed files with 4705 additions and 5809 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

View file

@ -1 +0,0 @@
API_URL=

13
.eslintignore Normal file
View file

@ -0,0 +1,13 @@
.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

30
.eslintrc.cjs Normal file
View file

@ -0,0 +1,30 @@
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'
}
}
]
};

22
.gitignore vendored
View file

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

1
.npmrc
View file

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

View file

@ -1,4 +1,13 @@
# Package Managers
package-lock.json
.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

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

View file

@ -1,3 +1,4 @@
# Base Stage
FROM node:21-slim AS base
ENV PNPM_HOME="/pnpm"
@ -9,10 +10,12 @@ 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
@ -21,12 +24,14 @@ 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
USER node
# Run as non-root user
# USER node
ARG PORT=3000

View file

@ -1,17 +1,14 @@
{
"$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"
"$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
}

View file

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

View file

@ -1,34 +0,0 @@
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,59 +1,55 @@
{
"name": "peer-at-code-web",
"private": true,
"name": "peer-at-code",
"type": "module",
"private": true,
"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 && npm run test:e2e",
"test:e2e": "playwright test"
},
"devDependencies": {
"@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",
"bits-ui": "1.0.0-next.79",
"clsx": "^2.1.1",
"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-boring-avatars": "^1.2.6",
"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",
"zod": "^3.24.1"
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .",
"test:integration": "playwright test",
"test:unit": "vitest"
},
"dependencies": {
"mdsvex": "^0.12.3"
"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"
},
"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",
"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"
}
}

View file

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

2912
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,68 +2,6 @@
@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 {
@ -109,26 +47,19 @@
--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));
}
@ -146,7 +77,9 @@
@apply rounded-sm !bg-muted-foreground/30;
}
html {
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: hsl(215.4 16.3% 46.9% / 0.3);
}
@ -166,7 +99,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://svelte.dev/docs/kit/types#app.d.ts
// See https://kit.svelte.dev/docs/types#app
import type { User } from "$lib/types";
import type { User } from '$lib/types';
// for information about these interfaces
declare global {
@ -9,14 +9,12 @@ declare global {
message: string;
errorId: string;
}
// interface Locals {}
interface Locals {
user: User | null;
user?: User;
}
interface PageData {
user: User | null;
user?: User;
}
// interface PageState {}
// interface Platform {}
}
}

View file

@ -1,11 +1,15 @@
<!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" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
<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>
</html>

View file

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

View file

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

View file

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

View file

@ -1,207 +0,0 @@
<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 Radio from 'lucide-svelte/icons/radio';
import ScrollText from 'lucide-svelte/icons/scroll-text';
import Settings from 'lucide-svelte/icons/settings';
let navigation = $state([
{
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: 'https://discord.gg/72vuHcwUkE',
icon: CircleHelp
}
]
},
{
title: 'Administration',
isAdmin: true,
items: [
{
title: 'Logs',
url: '/admin/logs',
icon: ScrollText
},
{
title: 'Puzzles',
url: '/admin/puzzles',
icon: Code
}
,
{
title: 'Broadcast',
url: '/admin/broadcast',
icon: Radio
}
]
}
]);
</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();
navigation = navigation.filter(
(nav) => !nav.isAdmin || page.data.user?.email.endsWith('@peerat.dev')
);
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

@ -0,0 +1,23 @@
<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,17 +1,19 @@
<script lang="ts">
import { page } from '$app/state';
import { page } from '$app/stores';
import * as Breadcrumb from '$lib/components/ui/breadcrumb';
const breadcrumb = $derived(
page.url.pathname
export let breadcrumb: { name: string; href: string }[] = [];
$: page.subscribe(({ url: { pathname } }) => {
breadcrumb = 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

@ -0,0 +1,16 @@
<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,41 +1,46 @@
<script lang="ts">
import Book from 'lucide-svelte/icons/book';
import Swords from 'lucide-svelte/icons/swords';
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';
type Props = { chapter: Chapter };
let { chapter }: Props = $props();
export let chapter: Chapter;
</script>
{#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">
<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}
>
<div class="flex items-center gap-2">
<span class="font-semibold">
{chapter.name}
</span>
{#if chapter.start && chapter.end}
<Trophy class="size-6 stroke-muted-foreground" />
{:else}
<Book class="size-6 stroke-muted-foreground" />
<Swords class="stroke-muted-foreground" />
{/if}
<h2 class="text-lg font-semibold">
</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">
{chapter.name}
</h2>
</div>
{#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}
</span>
{/if}
</li>

View file

@ -0,0 +1,32 @@
<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

@ -0,0 +1,47 @@
<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

View file

@ -0,0 +1,6 @@
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

@ -0,0 +1,3 @@
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

@ -0,0 +1,42 @@
<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

@ -0,0 +1,63 @@
<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

@ -0,0 +1,32 @@
<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

@ -0,0 +1,32 @@
<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

@ -0,0 +1,84 @@
<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

@ -0,0 +1,15 @@
<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

@ -0,0 +1,63 @@
<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

@ -0,0 +1,28 @@
<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

@ -0,0 +1,27 @@
<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,52 +1,50 @@
<script lang="ts">
import { page } from '$app/state';
import { page } from '$app/stores';
import { SITE_CONFIG } from '$lib/constants';
import { siteConfig } from '$lib/config/site';
const title = $derived(
page.data.title ? `${page.data.title} | ${SITE_CONFIG.name}` : SITE_CONFIG.name
);
export let title = siteConfig.name;
$: title = $page.data?.title ? `${$page.data.title} | ${siteConfig.name}` : siteConfig.name;
</script>
<svelte:head>
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={SITE_CONFIG.description} />
<meta name="keywords" content={SITE_CONFIG.keywords.join(',')} />
<meta name="author" content={SITE_CONFIG.author} />
<meta name="description" content={siteConfig.description} />
<meta name="keywords" content={siteConfig.keywords.join(',')} />
<meta name="author" content={siteConfig.author} />
<meta name="theme-color" content={SITE_CONFIG.themeColor} />
<meta name="theme-color" content={siteConfig.themeColor} />
<meta name="robots" content="noindex,nofollow" />
<meta itemprop="name" content={title} />
<meta itemprop="description" content={SITE_CONFIG.description} />
<meta itemprop="image" content={SITE_CONFIG.imageUrl} />
<meta itemprop="description" content={siteConfig.description} />
<meta itemprop="image" content={siteConfig.imageUrl} />
<meta property="og:site_name" content={SITE_CONFIG.name} />
<meta property="og:site_name" content={siteConfig.name} />
<meta property="og:title" content={title} />
<meta property="og:description" content={SITE_CONFIG.description} />
<meta property="og:description" content={siteConfig.description} />
<meta property="og:type" content="website" />
<meta property="og:url" content={SITE_CONFIG.url + page.url.pathname} />
<meta property="og:image" content={SITE_CONFIG.imageUrl} />
<meta property="og:url" content={siteConfig.url + $page.url.pathname} />
<meta property="og:image" content={siteConfig.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={SITE_CONFIG.url} />
<meta name="twitter:site" content={siteConfig.url} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={SITE_CONFIG.description} />
<meta name="twitter:image" content={SITE_CONFIG.imageUrl} />
<meta name="twitter:description" content={siteConfig.description} />
<meta name="twitter:image" content={siteConfig.imageUrl} />
<meta name="twitter:image:alt" content={title} />
<meta name="twitter:creator" content={`@${SITE_CONFIG.author}`} />
<meta name="twitter:creator" content={`@${siteConfig.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="/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" />
<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" />
</svelte:head>

View file

@ -1,60 +1,17 @@
<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';
type Props = { puzzle: Puzzle; chapterId: number };
export let puzzle: Puzzle;
let { puzzle, chapterId }: Props = $props();
const chapterId = $page.params.chapterId;
</script>
{#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
<li
class={cn(
'group relative flex h-full w-full rounded border border-border bg-card transition-colors duration-150',
{
@ -117,4 +74,4 @@
</div>
</span>
{/if}
</li> -->
</li>

View file

@ -1,50 +0,0 @@
<script lang="ts">
import { onDestroy } from 'svelte';
type Props = {
date: string;
class?: string;
};
let { date, class: className }: Props = $props();
let days = $state(0);
let hours = $state(0);
let minutes = $state(0);
let seconds = $state(0);
let time = new Date(date).getTime();
let interval = setInterval(() => {
let now = new Date().getTime();
let distance = time - now;
days = Math.floor(distance / (1000 * 60 * 60 * 24));
hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
seconds = Math.floor((distance % (1000 * 60)) / 1000);
if (distance < 0) {
clearInterval(interval);
}
}, 1000);
onDestroy(() => {
clearInterval(interval);
});
</script>
<span class={className}>
{#if days > 0}
{days}d
{/if}
{#if hours > 0}
{hours}h
{/if}
{#if minutes > 0}
{minutes}m
{/if}
{#if seconds > 0}
{seconds}s
{/if}
</span>

View file

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

View file

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

View file

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

View file

@ -1,23 +1,24 @@
<script lang="ts">
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";
import Ellipsis from "lucide-svelte/icons/ellipsis";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
bind:this={ref}
bind:this={el}
role="presentation"
aria-hidden="true"
class={cn("flex size-9 items-center justify-center", className)}
{...restProps}
class={cn("flex h-9 w-9 items-center justify-center", className)}
{...$$restProps}
>
<Ellipsis class="size-4" />
<Ellipsis class="h-4 w-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";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLLiAttributes> = $props();
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<li bind:this={ref} class={cn("inline-flex items-center gap-1.5", className)} {...restProps}>
{@render children?.()}
<li bind:this={el} class={cn("inline-flex items-center gap-1.5", className)}>
<slot />
</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";
let {
ref = $bindable(null),
class: className,
href = undefined,
child,
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
child?: Snippet<[{ props: HTMLAnchorAttributes }]>;
} = $props();
type $$Props = HTMLAnchorAttributes & {
el?: HTMLAnchorElement;
asChild?: boolean;
};
const attrs = $derived({
class: cn("hover:text-foreground transition-colors", className),
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),
href,
...restProps,
});
...$$restProps,
};
</script>
{#if child}
{@render child({ props: attrs })}
{#if asChild}
<slot {attrs} />
{:else}
<a bind:this={ref} {...attrs}>
{@render children?.()}
<a bind:this={el} {...attrs} {href}>
<slot {attrs} />
</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";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLOlAttributes> = $props();
type $$Props = HTMLOlAttributes & {
el?: HTMLOListElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<ol
bind:this={ref}
bind:this={el}
class={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5",
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...restProps}
{...$$restProps}
>
{@render children?.()}
<slot />
</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";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
export let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
bind:this={ref}
bind:this={el}
role="link"
aria-disabled="true"
aria-current="page"
class={cn("text-foreground font-normal", className)}
{...restProps}
class={cn("font-normal text-foreground", className)}
{...$$restProps}
>
{@render children?.()}
<slot />
</span>

View file

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

View file

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

View file

@ -1,74 +1,25 @@
<script lang="ts" module>
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
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",
},
});
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">
import { Button as ButtonPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { buttonVariants, type Props, type Events } from "./index.js";
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
children,
...restProps
}: ButtonProps = $props();
type $$Props = Props;
type $$Events = Events;
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 };
</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}
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>

View file

@ -1,17 +1,50 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
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;
export {
Root,
type ButtonProps as Props,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View file

@ -1,16 +0,0 @@
<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

@ -1,16 +0,0 @@
<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

@ -1,16 +0,0 @@
<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

@ -1,16 +0,0 @@
<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

@ -1,25 +0,0 @@
<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

@ -1,20 +0,0 @@
<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

@ -1,22 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -0,0 +1,24 @@
<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

@ -0,0 +1,18 @@
<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

@ -0,0 +1,16 @@
<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

@ -0,0 +1,19 @@
<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

@ -0,0 +1,12 @@
<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

@ -0,0 +1,18 @@
<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

@ -0,0 +1,18 @@
<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

@ -0,0 +1,12 @@
<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

@ -0,0 +1,41 @@
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,40 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive } 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";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
class={cn(
"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",
"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",
className
)}
{...restProps}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
{#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}
<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 />
</DropdownMenuPrimitive.CheckboxItem>

View file

@ -1,26 +1,27 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
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 };
</script>
<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>
<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>

View file

@ -1,19 +0,0 @@
<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,23 +1,31 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: DropdownMenuPrimitive.ItemProps & {
type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
} = $props();
};
type $$Events = DropdownMenuPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Item
bind:ref
class={cn(
"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",
"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",
inset && "pl-8",
className
)}
{...restProps}
/>
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<slot />
</DropdownMenuPrimitive.Item>

View file

@ -1,23 +1,19 @@
<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";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean;
} = $props();
};
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<div
bind:this={ref}
<DropdownMenuPrimitive.Label
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...restProps}
{...$$restProps}
>
{@render children?.()}
</div>
<slot />
</DropdownMenuPrimitive.Label>

View file

@ -0,0 +1,11 @@
<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,30 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
class={cn(
"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",
"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",
className
)}
{...restProps}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
{#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}
<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 />
</DropdownMenuPrimitive.RadioItem>

View file

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

View file

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

View file

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

View file

@ -3,26 +3,30 @@
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
};
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
class={cn(
"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",
"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",
inset && "pl-8",
className
)}
{...restProps}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
{@render children?.()}
<ChevronRight class="ml-auto" />
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>

View file

@ -1,50 +1,48 @@
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 Shortcut from "./dropdown-menu-shortcut.svelte";
import RadioGroup from "./dropdown-menu-radio-group.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 {
CheckboxItem,
Content,
Root as DropdownMenu,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
GroupHeading as DropdownMenuGroupHeading,
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,
Group,
GroupHeading,
Sub,
Root,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Group,
Trigger,
Content,
Shortcut,
Sub,
Separator,
RadioItem,
SubContent,
SubTrigger,
Trigger,
RadioGroup,
CheckboxItem,
//
Root as DropdownMenu,
Sub as DropdownMenuSub,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
Group as DropdownMenuGroup,
Content as DropdownMenuContent,
Trigger as DropdownMenuTrigger,
Shortcut as DropdownMenuShortcut,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
RadioGroup as DropdownMenuRadioGroup,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
CheckboxItem as DropdownMenuCheckboxItem,
};

View file

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

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";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChild<FormPrimitive.DescriptionProps> = $props();
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Description
bind:ref
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
let:descriptionAttrs
>
<slot {descriptionAttrs} />
</FormPrimitive.Description>

View file

@ -1,30 +1,26 @@
<script lang="ts" module>
import type { FormPathLeaves as _FormPathLeaves } from "sveltekit-superforms";
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = _FormPathLeaves<T>;
type U = unknown;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends _FormPathLeaves<T>">
import * as FormPrimitive from "formsnap";
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef, WithoutChildren } from "bits-ui";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
form,
name,
children: childrenProp,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> &
FormPrimitive.ElementFieldProps<T, U> = $props();
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 };
</script>
<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 {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>

View file

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

View file

@ -1,30 +1,26 @@
<script lang="ts" module>
import type { FormPath as _FormPath } from "sveltekit-superforms";
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = _FormPath<T>;
type U = unknown;
</script>
<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";
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
form,
name,
children: childrenProp,
...restProps
}: FormPrimitive.FieldProps<T, U> &
WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
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 };
</script>
<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 {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>

View file

@ -1,21 +1,31 @@
<script lang="ts" module>
import type { FormPath as _FormPath } from "sveltekit-superforms";
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = _FormPath<T>;
type U = unknown;
</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";
let {
ref = $bindable(null),
class: className,
form,
name,
...restProps
}: WithoutChild<FormPrimitive.FieldsetProps<T, U>> = $props();
type $$Props = FormPrimitive.FieldsetProps<T, U>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Fieldset bind:ref {form} {name} class={cn("space-y-2", className)} {...restProps} />
<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>

View file

@ -1,21 +1,17 @@
<script lang="ts">
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";
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';
let {
ref = $bindable(null),
children,
class: className,
...restProps
}: WithoutChild<FormPrimitive.LabelProps> = $props();
type $$Props = LabelPrimitive.Props;
let className: $$Props['class'] = undefined;
export { className as class };
const { labelAttrs } = getFormControl();
</script>
<FormPrimitive.Label {...restProps} bind:ref>
{#snippet child({ props })}
<Label {...props} class={cn("data-[fs-error]:text-destructive", className)}>
{@render children?.()}
</Label>
{/snippet}
</FormPrimitive.Label>
<Label {...$labelAttrs} class={cn('data-[fs-error]:text-destructive', className)} {...$$restProps}>
<slot {labelAttrs} />
</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";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChild<FormPrimitive.LegendProps> = $props();
type $$Props = FormPrimitive.LegendProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Legend
bind:ref
class={cn("data-[fs-error]:text-destructive text-sm font-medium leading-none", className)}
{...restProps}
/>
{...$$restProps}
class={cn("text-sm font-medium leading-none data-[fs-error]:text-destructive", className)}
let:legendAttrs
>
<slot {legendAttrs} />
</FormPrimitive.Legend>

View file

@ -1,15 +0,0 @@
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

@ -1,16 +0,0 @@
<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

@ -1,19 +0,0 @@
<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

@ -1,30 +0,0 @@
<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

@ -1,22 +0,0 @@
<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,5 +1,25 @@
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,22 +1,35 @@
<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";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...restProps
}: WithElementRef<HTMLInputAttributes> = $props();
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
</script>
<input
bind:this={ref}
class={cn(
"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",
"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",
className
)}
bind:value
{...restProps}
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}
/>

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