1
0
This commit is contained in:
2024-05-20 12:20:22 +03:00
parent e87b740e29
commit 13b6c19097
102 changed files with 1569 additions and 1346 deletions

View File

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

View File

@@ -3,10 +3,7 @@
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,
"plugins": [ "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss"
],
"overrides": [ "overrides": [
{ {
"files": "*.svelte", "files": "*.svelte",
@@ -15,4 +12,4 @@
} }
} }
] ]
} }

View File

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

View File

@@ -1,51 +1,51 @@
{ {
"name": "nir-messenger", "name": "nir-messenger",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .", "lint": "prettier --check . && eslint .",
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^8.56.0", "@types/eslint": "^8.56.0",
"@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.35.1",
"prettier": "^3.1.1", "prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.7", "svelte": "^4.2.7",
"svelte-check": "^3.6.0", "svelte-check": "^3.6.0",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vite": "^5.0.3", "vite": "^5.0.3",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"postcss": "^8.4.33", "postcss": "^8.4.33",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"prettier-plugin-tailwindcss": "^0.5.1" "prettier-plugin-tailwindcss": "^0.5.1"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"axios": "^1.6.8", "axios": "^1.6.8",
"bits-ui": "^0.21.7", "bits-ui": "^0.21.7",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"formsnap": "^1.0.0", "formsnap": "^1.0.0",
"lucide-svelte": "^0.378.0", "lucide-svelte": "^0.378.0",
"mode-watcher": "^0.3.0", "mode-watcher": "^0.3.0",
"svelte-persisted-store": "^0.9.2", "svelte-persisted-store": "^0.9.2",
"svelte-sonner": "^0.3.24", "svelte-sonner": "^0.3.24",
"sveltekit-superforms": "^2.13.1", "sveltekit-superforms": "^2.13.1",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.2.1", "tailwind-variants": "^0.2.1",
"zod": "^3.23.8" "zod": "^3.23.8"
} }
} }

View File

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

View File

@@ -1,78 +1,59 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 253 44% 98%;
--foreground: 222.2 84% 4.9%; --foreground: 253 58% 0%;
--muted: 253 7% 87%;
--muted: 210 40% 96.1%; --muted-foreground: 253 13% 37%;
--muted-foreground: 215.4 16.3% 46.9%; --popover: 253 44% 98%;
--popover-foreground: 253 58% 0%;
--popover: 0 0% 100%; --card: 253 44% 97%;
--popover-foreground: 222.2 84% 4.9%; --card-foreground: 0 0% 0%;
--border: 220 13% 91%;
--card: 0 0% 100%; --input: 220 13% 91%;
--card-foreground: 222.2 84% 4.9%; --primary: 253 91% 58%;
--primary-foreground: 253 91% 98%;
--border: 214.3 31.8% 91.4%; --secondary: 253 5% 89%;
--input: 214.3 31.8% 91.4%; --secondary-foreground: 253 5% 29%;
--accent: 253 12% 82%;
--primary: 222.2 47.4% 11.2%; --accent-foreground: 253 12% 22%;
--primary-foreground: 210 40% 98%; --destructive: 339.2 90.36% 51.18%;
--destructive-foreground: 0 0% 100%;
--secondary: 210 40% 96.1%; --ring: 253 91% 58%;
--secondary-foreground: 222.2 47.4% 11.2%; --radius: 0.5rem;
}
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%; .dark {
--background: 253 43% 3%;
--destructive: 0 72.2% 50.6%; --foreground: 253 31% 98%;
--destructive-foreground: 210 40% 98%; --muted: 253 7% 13%;
--muted-foreground: 253 13% 63%;
--ring: 222.2 84% 4.9%; --popover: 253 43% 3%;
--popover-foreground: 253 31% 98%;
--radius: 0.5rem; --card: 253 43% 4%;
} --card-foreground: 253 31% 99%;
--border: 215 27.9% 16.9%;
.dark { --input: 215 27.9% 16.9%;
--background: 222.2 84% 4.9%; --primary: 253 91% 58%;
--foreground: 210 40% 98%; --primary-foreground: 253 91% 98%;
--secondary: 253 7% 9%;
--muted: 217.2 32.6% 17.5%; --secondary-foreground: 253 7% 69%;
--muted-foreground: 215 20.2% 65.1%; --accent: 253 13% 14%;
--accent-foreground: 253 13% 74%;
--popover: 222.2 84% 4.9%; --destructive: 339.2 90.36% 51.18%;
--popover-foreground: 210 40% 98%; --destructive-foreground: 0 0% 100%;
--ring: 253 91% 58%;
--card: 222.2 84% 4.9%; }
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: hsl(212.7,26.8%,83.9);
}
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }

2
src/app.d.ts vendored
View File

@@ -10,4 +10,4 @@ declare global {
} }
} }
export { }; export {};

View File

@@ -1,28 +1,24 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height" />
%sveltekit.head%
</head>
<head> <style>
html,
body {
height: 100vh;
width: 100vw;
margin: 0;
background-color: black;
}
</style>
<meta charset="utf-8" /> <body data-sveltekit-preload-data="hover">
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <div style="display: contents">%sveltekit.body%</div>
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height" /> </body>
%sveltekit.head% </html>
</head>
<style>
html,
body {
height: 100vh;
width: 100vw;
margin: 0;
background-color: black;
}
</style>
<body data-sveltekit-preload-data="hover" class="dark">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,6 +1,6 @@
import type { Handle } from "@sveltejs/kit"; import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => { export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event); const response = await resolve(event);
return response; return response;
}; };

View File

@@ -1,20 +1,31 @@
import { messagesCache, channelsCache } from '$lib/stores/cache';
import type { Channel, Message } from '../types'; import type { Channel, Message } from '../types';
import { apiRequest } from './utils'; import { apiRequest } from './utils';
export async function getAllChannels() { export async function getAllChannels() {
return await apiRequest<Channel[]>('/channel', 'get'); return await apiRequest<Channel[]>('/channel', 'get', undefined, (data) => {
data.forEach((channel) => {
channelsCache.set(channel.id, channel);
});
});
} }
export async function getChannelById(channelId: number) { export async function getChannelById(channelId: number) {
return await apiRequest<Channel>(`/channel/${channelId}`, 'get'); return await apiRequest<Channel>(`/channel/${channelId}`, 'get', undefined, (data) => {
channelsCache.set(data.id, data);
});
} }
export async function createChannel(name: string) { export async function createChannel(name: string) {
return await apiRequest<Channel>('/channel', 'post', { data: { name } }); return await apiRequest<Channel>('/channel', 'post', { data: { name } }, (data) => {
channelsCache.set(data.id, data);
});
} }
export async function deleteChannel(channelId: number) { export async function deleteChannel(channelId: number) {
return await apiRequest<Channel>(`/channel/${channelId}`, 'delete'); return await apiRequest<Channel>(`/channel/${channelId}`, 'delete', undefined, (data) => {
channelsCache.remove(data.id);
});
} }
export async function addUserToChannel(channelId: number, userId: number) { export async function addUserToChannel(channelId: number, userId: number) {
@@ -26,5 +37,14 @@ export async function removeUserFromChannel(channelId: number, userId: number) {
} }
export async function getMessagesByChannelId(channelId: number, beforeId?: number, limit?: number) { export async function getMessagesByChannelId(channelId: number, beforeId?: number, limit?: number) {
return await apiRequest<Message[]>(`/channel/${channelId}/message${beforeId || limit ? '?' : ''}${beforeId ? `before=${beforeId}` : ''}${limit ? `&limit=${limit}` : ''}`, 'get'); return await apiRequest<Message[]>(
} `/channel/${channelId}/message${beforeId || limit ? '?' : ''}${beforeId ? `before=${beforeId}` : ''}${limit ? `&limit=${limit}` : ''}`,
'get',
undefined,
(data) => {
data.forEach((message) => {
messagesCache.set(message.id, message);
});
}
);
}

View File

@@ -1,10 +1,20 @@
import { messagesCache } from '$lib/stores/cache';
import type { Message } from '../types'; import type { Message } from '../types';
import { apiRequest } from './utils'; import { apiRequest } from './utils';
export async function getMessageById(messageId: number) { export async function getMessageById(messageId: number) {
return await apiRequest<Message>(`/message/${messageId}`, 'get'); return await apiRequest<Message>(`/message/${messageId}`, 'get', undefined, (data) => {
messagesCache.set(data.id, data);
});
} }
export async function createMessage(channelId: number, content: string) { export async function createMessage(channelId: number, content: string) {
return await apiRequest<Message>('/message', 'post', { data: { channelId, content } }); return await apiRequest<Message>(
} '/message',
'post',
{ data: { channelId, content } },
(data) => {
messagesCache.set(data.id, data);
}
);
}

View File

@@ -1,16 +1,28 @@
import { usersCache } from '$lib/stores/cache';
import type { Token, User } from '../types'; import type { Token, User } from '../types';
import { apiRequest } from './utils'; import { apiRequest } from './utils';
export async function getByToken(token: string | undefined | null) { export async function getByToken(token: string | undefined | null) {
return await apiRequest<User>('/user/me', 'get', { token: token as string | undefined }); return await apiRequest<User>(
'/user/me',
'get',
{ token: token as string | undefined },
(data) => {
usersCache.set(data.id, data);
}
);
} }
export async function getUserById(userId: number) { export async function getUserById(userId: number) {
return await apiRequest<User>(`/user/${userId}`, 'get'); return await apiRequest<User>(`/user/${userId}`, 'get', undefined, (data) => {
usersCache.set(data.id, data);
});
} }
export async function getUserByUsername(username: string) { export async function getUserByUsername(username: string) {
return await apiRequest<User>(`/user/username/${username}`, 'get'); return await apiRequest<User>(`/user/username/${username}`, 'get', undefined, (data) => {
usersCache.set(data.id, data);
});
} }
export async function loginUser(username: string, password: string) { export async function loginUser(username: string, password: string) {
@@ -19,4 +31,4 @@ export async function loginUser(username: string, password: string) {
export async function registerUser(username: string, password: string) { export async function registerUser(username: string, password: string) {
return await apiRequest<Token>('/user/register', 'post', { data: { username, password } }); return await apiRequest<Token>('/user/register', 'post', { data: { username, password } });
} }

View File

@@ -1,14 +1,19 @@
import { API_URL } from "$lib/constants"; import { API_URL } from '$lib/constants';
import { getUserToken } from "$lib/stores/user"; import { getUserToken } from '$lib/stores/user';
import type { ErrorResponse } from "$lib/types"; import type { ErrorResponse } from '$lib/types';
import type { AxiosRequestConfig } from "axios"; import type { AxiosRequestConfig } from 'axios';
import axios from "axios"; import axios from 'axios';
export async function apiRequest<T>(
path: string,
export async function apiRequest<T>(path: string, method: 'get' | 'post' | 'put' | 'delete', options?: AxiosRequestConfig & { token?: string }): Promise<T | ErrorResponse> { method: 'get' | 'post' | 'put' | 'delete',
options?: AxiosRequestConfig & { token?: string },
cacheCallback?: (data: T) => void
): Promise<T | ErrorResponse> {
const url = API_URL + path; const url = API_URL + path;
console.log(`[API] ${method.toUpperCase()} ${url}`);
const token = options?.token || getUserToken(); const token = options?.token || getUserToken();
options = { options = {
@@ -16,16 +21,18 @@ export async function apiRequest<T>(path: string, method: 'get' | 'post' | 'put'
method, method,
headers: { headers: {
...options?.headers, ...options?.headers,
'Authorization': `Bearer ${token}` Authorization: `Bearer ${token}`
}, },
validateStatus: () => true, validateStatus: () => true,
...options, ...options
} };
const response = await axios.request(options); const response = await axios.request(options);
if (response.status === 200) if (response.status === 200) {
return response.data as T; const data = response.data as T;
else if (cacheCallback) cacheCallback(data);
return response.data as ErrorResponse;
} return data;
} else return response.data as ErrorResponse;
}

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Moon, Sun } from 'lucide-svelte';
import { Button } from './ui/button';
import { theme } from '$lib/stores/theme';
import { cn } from '$lib/utils';
let className = '';
export { className as class };
function switchTheme() {
$theme = $theme === 'light' ? 'dark' : 'light';
}
</script>
<Button variant="ghost" class={cn('h-10 w-10 p-0', className)} on:click={() => switchTheme()}>
<Sun class="hidden dark:block" />
<Moon class="block dark:hidden" />
</Button>

View File

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

View File

@@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui"; import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = AvatarPrimitive.ImageProps; type $$Props = AvatarPrimitive.ImageProps;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let src: $$Props["src"] = undefined; export let src: $$Props['src'] = undefined;
export let alt: $$Props["alt"] = undefined; export let alt: $$Props['alt'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<AvatarPrimitive.Image <AvatarPrimitive.Image
{src} {src}
{alt} {alt}
class={cn("aspect-square h-full w-full", className)} class={cn('aspect-square h-full w-full', className)}
{...$$restProps} {...$$restProps}
/> />

View File

@@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui"; import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = AvatarPrimitive.Props; type $$Props = AvatarPrimitive.Props;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let delayMs: $$Props["delayMs"] = undefined; export let delayMs: $$Props['delayMs'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<AvatarPrimitive.Root <AvatarPrimitive.Root
{delayMs} {delayMs}
class={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)} class={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
{...$$restProps} {...$$restProps}
> >
<slot /> <slot />
</AvatarPrimitive.Root> </AvatarPrimitive.Root>

View File

@@ -1,13 +1,13 @@
import Root from "./avatar.svelte"; import Root from './avatar.svelte';
import Image from "./avatar-image.svelte"; import Image from './avatar-image.svelte';
import Fallback from "./avatar-fallback.svelte"; import Fallback from './avatar-fallback.svelte';
export { export {
Root, Root,
Image, Image,
Fallback, Fallback,
// //
Root as Avatar, Root as Avatar,
Image as AvatarImage, Image as AvatarImage,
Fallback as AvatarFallback, Fallback as AvatarFallback
}; };

View File

@@ -1,25 +1,25 @@
<script lang="ts"> <script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui"; import { Button as ButtonPrimitive } from 'bits-ui';
import { type Events, type Props, buttonVariants } from "./index.js"; import { type Events, type Props, buttonVariants } from './index.js';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = Props; type $$Props = Props;
type $$Events = Events; type $$Events = Events;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let variant: $$Props["variant"] = "default"; export let variant: $$Props['variant'] = 'default';
export let size: $$Props["size"] = "default"; export let size: $$Props['size'] = 'default';
export let builders: $$Props["builders"] = []; export let builders: $$Props['builders'] = [];
export { className as class }; export { className as class };
</script> </script>
<ButtonPrimitive.Root <ButtonPrimitive.Root
{builders} {builders}
class={cn(buttonVariants({ variant, size, className }))} class={cn(buttonVariants({ variant, size, className }))}
type="button" type="button"
{...$$restProps} {...$$restProps}
on:click on:click
on:keydown on:keydown
> >
<slot /> <slot />
</ButtonPrimitive.Root> </ButtonPrimitive.Root>

View File

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

View File

@@ -1,35 +1,35 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Check from "lucide-svelte/icons/check"; import Check from 'lucide-svelte/icons/check';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.CheckboxItemProps; type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents; type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let checked: $$Props["checked"] = undefined; export let checked: $$Props['checked'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
bind:checked bind:checked
class={cn( 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", '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 className
)} )}
{...$$restProps} {...$$restProps}
on:click on:click
on:keydown on:keydown
on:focusin on:focusin
on:focusout on:focusout
on:pointerdown on:pointerdown
on:pointerleave on:pointerleave
on:pointermove on:pointermove
> >
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.CheckboxIndicator> <DropdownMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" /> <Check class="h-4 w-4" />
</DropdownMenuPrimitive.CheckboxIndicator> </DropdownMenuPrimitive.CheckboxIndicator>
</span> </span>
<slot /> <slot />
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>

View File

@@ -1,27 +1,27 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn, flyAndScale } from "$lib/utils.js"; import { cn, flyAndScale } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.ContentProps; type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents; type $$Events = DropdownMenuPrimitive.ContentEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let sideOffset: $$Props["sideOffset"] = 4; export let sideOffset: $$Props['sideOffset'] = 4;
export let transition: $$Props["transition"] = flyAndScale; export let transition: $$Props['transition'] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined; export let transitionConfig: $$Props['transitionConfig'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
{transition} {transition}
{transitionConfig} {transitionConfig}
{sideOffset} {sideOffset}
class={cn( class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none", 'z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none',
className className
)} )}
{...$$restProps} {...$$restProps}
on:keydown on:keydown
> >
<slot /> <slot />
</DropdownMenuPrimitive.Content> </DropdownMenuPrimitive.Content>

View File

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

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.LabelProps & { type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean; inset?: boolean;
}; };
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let inset: $$Props["inset"] = undefined; export let inset: $$Props['inset'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
{...$$restProps} {...$$restProps}
> >
<slot /> <slot />
</DropdownMenuPrimitive.Label> </DropdownMenuPrimitive.Label>

View File

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

View File

@@ -1,35 +1,35 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Circle from "lucide-svelte/icons/circle"; import Circle from 'lucide-svelte/icons/circle';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.RadioItemProps; type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents; type $$Events = DropdownMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let value: $$Props["value"]; export let value: $$Props['value'];
export { className as class }; export { className as class };
</script> </script>
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
class={cn( 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", '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 className
)} )}
{value} {value}
{...$$restProps} {...$$restProps}
on:click on:click
on:keydown on:keydown
on:focusin on:focusin
on:focusout on:focusout
on:pointerdown on:pointerdown
on:pointerleave on:pointerleave
on:pointermove on:pointermove
> >
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.RadioIndicator> <DropdownMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" /> <Circle class="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.RadioIndicator> </DropdownMenuPrimitive.RadioIndicator>
</span> </span>
<slot /> <slot />
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>

View File

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

View File

@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement>; type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<span class={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...$$restProps}> <span class={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...$$restProps}>
<slot /> <slot />
</span> </span>

View File

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

View File

@@ -1,32 +1,32 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import ChevronRight from "lucide-svelte/icons/chevron-right"; import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = DropdownMenuPrimitive.SubTriggerProps & { type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean; inset?: boolean;
}; };
type $$Events = DropdownMenuPrimitive.SubTriggerEvents; type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let inset: $$Props["inset"] = undefined; export let inset: $$Props['inset'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
class={cn( 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", '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", inset && 'pl-8',
className className
)} )}
{...$$restProps} {...$$restProps}
on:click on:click
on:keydown on:keydown
on:focusin on:focusin
on:focusout on:focusout
on:pointerleave on:pointerleave
on:pointermove on:pointermove
> >
<slot /> <slot />
<ChevronRight class="ml-auto h-4 w-4" /> <ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,25 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms"; import type { FormPathLeaves, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>; type T = Record<string, unknown>;
type U = FormPathLeaves<T>; type U = FormPathLeaves<T>;
</script> </script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>"> <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
import * as FormPrimitive from "formsnap"; import * as FormPrimitive from 'formsnap';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>; type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>; export let form: SuperForm<T>;
export let name: U; export let name: U;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<FormPrimitive.ElementField {form} {name} let:constraints let:errors let:tainted let:value> <FormPrimitive.ElementField {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}> <div class={cn('space-y-2', className)}>
<slot {constraints} {errors} {tainted} {value} /> <slot {constraints} {errors} {tainted} {value} />
</div> </div>
</FormPrimitive.ElementField> </FormPrimitive.ElementField>

View File

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

View File

@@ -1,25 +1,25 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { FormPath, SuperForm } from "sveltekit-superforms"; import type { FormPath, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>; type T = Record<string, unknown>;
type U = FormPath<T>; type U = FormPath<T>;
</script> </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 type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
import * as FormPrimitive from "formsnap"; import * as FormPrimitive from 'formsnap';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>; type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>; export let form: SuperForm<T>;
export let name: U; export let name: U;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<FormPrimitive.Field {form} {name} let:constraints let:errors let:tainted let:value> <FormPrimitive.Field {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}> <div class={cn('space-y-2', className)}>
<slot {constraints} {errors} {tainted} {value} /> <slot {constraints} {errors} {tainted} {value} />
</div> </div>
</FormPrimitive.Field> </FormPrimitive.Field>

View File

@@ -1,30 +1,30 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { FormPath, SuperForm } from "sveltekit-superforms"; import type { FormPath, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>; type T = Record<string, unknown>;
type U = FormPath<T>; type U = FormPath<T>;
</script> </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 * as FormPrimitive from 'formsnap';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = FormPrimitive.FieldsetProps<T, U>; type $$Props = FormPrimitive.FieldsetProps<T, U>;
export let form: SuperForm<T>; export let form: SuperForm<T>;
export let name: U; export let name: U;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<FormPrimitive.Fieldset <FormPrimitive.Fieldset
{form} {form}
{name} {name}
let:constraints let:constraints
let:errors let:errors
let:tainted let:tainted
let:value let:value
class={cn("space-y-2", className)} class={cn('space-y-2', className)}
> >
<slot {constraints} {errors} {tainted} {value} /> <slot {constraints} {errors} {tainted} {value} />
</FormPrimitive.Fieldset> </FormPrimitive.Fieldset>

View File

@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { Label as LabelPrimitive } from "bits-ui"; import type { Label as LabelPrimitive } from 'bits-ui';
import { getFormControl } from "formsnap"; import { getFormControl } from 'formsnap';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
import { Label } from "$lib/components/ui/label/index.js"; import { Label } from '$lib/components/ui/label/index.js';
type $$Props = LabelPrimitive.Props; type $$Props = LabelPrimitive.Props;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export { className as class }; export { className as class };
const { labelAttrs } = getFormControl(); const { labelAttrs } = getFormControl();
</script> </script>
<Label {...$labelAttrs} class={cn("data-[fs-error]:text-destructive", className)} {...$$restProps}> <Label {...$labelAttrs} class={cn('data-[fs-error]:text-destructive', className)} {...$$restProps}>
<slot {labelAttrs} /> <slot {labelAttrs} />
</Label> </Label>

View File

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

View File

@@ -1,33 +1,33 @@
import * as FormPrimitive from "formsnap"; import * as FormPrimitive from 'formsnap';
import Description from "./form-description.svelte"; import Description from './form-description.svelte';
import Label from "./form-label.svelte"; import Label from './form-label.svelte';
import FieldErrors from "./form-field-errors.svelte"; import FieldErrors from './form-field-errors.svelte';
import Field from "./form-field.svelte"; import Field from './form-field.svelte';
import Fieldset from "./form-fieldset.svelte"; import Fieldset from './form-fieldset.svelte';
import Legend from "./form-legend.svelte"; import Legend from './form-legend.svelte';
import ElementField from "./form-element-field.svelte"; import ElementField from './form-element-field.svelte';
import Button from "./form-button.svelte"; import Button from './form-button.svelte';
const Control = FormPrimitive.Control; const Control = FormPrimitive.Control;
export { export {
Field, Field,
Control, Control,
Label, Label,
Button, Button,
FieldErrors, FieldErrors,
Description, Description,
Fieldset, Fieldset,
Legend, Legend,
ElementField, ElementField,
// //
Field as FormField, Field as FormField,
Control as FormControl, Control as FormControl,
Description as FormDescription, Description as FormDescription,
Label as FormLabel, Label as FormLabel,
FieldErrors as FormFieldErrors, FieldErrors as FormFieldErrors,
Fieldset as FormFieldset, Fieldset as FormFieldset,
Legend as FormLegend, Legend as FormLegend,
ElementField as FormElementField, ElementField as FormElementField,
Button as FormButton, Button as FormButton
}; };

View File

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

View File

@@ -1,42 +1,42 @@
<script lang="ts"> <script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements"; import type { HTMLInputAttributes } from 'svelte/elements';
import type { InputEvents } from "./index.js"; import type { InputEvents } from './index.js';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = HTMLInputAttributes; type $$Props = HTMLInputAttributes;
type $$Events = InputEvents; type $$Events = InputEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let value: $$Props["value"] = undefined; export let value: $$Props['value'] = undefined;
export { className as class }; export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305 // Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x. // Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined; export let readonly: $$Props['readonly'] = undefined;
</script> </script>
<input <input
class={cn( 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", '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 className
)} )}
bind:value bind:value
{readonly} {readonly}
on:blur on:blur
on:change on:change
on:click on:click
on:focus on:focus
on:focusin on:focusin
on:focusout on:focusout
on:keydown on:keydown
on:keypress on:keypress
on:keyup on:keyup
on:mouseover on:mouseover
on:mouseenter on:mouseenter
on:mouseleave on:mouseleave
on:mousemove on:mousemove
on:paste on:paste
on:input on:input
on:wheel|passive on:wheel|passive
{...$$restProps} {...$$restProps}
/> />

View File

@@ -1,7 +1,7 @@
import Root from "./label.svelte"; import Root from './label.svelte';
export { export {
Root, Root,
// //
Root as Label, Root as Label
}; };

View File

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

View File

@@ -1,17 +1,17 @@
import { Popover as PopoverPrimitive } from "bits-ui"; import { Popover as PopoverPrimitive } from 'bits-ui';
import Content from "./popover-content.svelte"; import Content from './popover-content.svelte';
const Root = PopoverPrimitive.Root; const Root = PopoverPrimitive.Root;
const Trigger = PopoverPrimitive.Trigger; const Trigger = PopoverPrimitive.Trigger;
const Close = PopoverPrimitive.Close; const Close = PopoverPrimitive.Close;
export { export {
Root, Root,
Content, Content,
Trigger, Trigger,
Close, Close,
// //
Root as Popover, Root as Popover,
Content as PopoverContent, Content as PopoverContent,
Trigger as PopoverTrigger, Trigger as PopoverTrigger,
Close as PopoverClose, Close as PopoverClose
}; };

View File

@@ -1,22 +1,22 @@
<script lang="ts"> <script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui"; import { Popover as PopoverPrimitive } from 'bits-ui';
import { cn, flyAndScale } from "$lib/utils.js"; import { cn, flyAndScale } from '$lib/utils.js';
type $$Props = PopoverPrimitive.ContentProps; type $$Props = PopoverPrimitive.ContentProps;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let transition: $$Props["transition"] = flyAndScale; export let transition: $$Props['transition'] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined; export let transitionConfig: $$Props['transitionConfig'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<PopoverPrimitive.Content <PopoverPrimitive.Content
{transition} {transition}
{transitionConfig} {transitionConfig}
class={cn( class={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none", 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none',
className className
)} )}
{...$$restProps} {...$$restProps}
> >
<slot /> <slot />
</PopoverPrimitive.Content> </PopoverPrimitive.Content>

View File

@@ -1,10 +1,10 @@
import Scrollbar from "./scroll-area-scrollbar.svelte"; import Scrollbar from './scroll-area-scrollbar.svelte';
import Root from "./scroll-area.svelte"; import Root from './scroll-area.svelte';
export { export {
Root, Root,
Scrollbar, Scrollbar,
//, //,
Root as ScrollArea, Root as ScrollArea,
Scrollbar as ScrollAreaScrollbar, Scrollbar as ScrollAreaScrollbar
}; };

View File

@@ -1,27 +1,27 @@
<script lang="ts"> <script lang="ts">
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui"; import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = ScrollAreaPrimitive.ScrollbarProps & { type $$Props = ScrollAreaPrimitive.ScrollbarProps & {
orientation?: "vertical" | "horizontal"; orientation?: 'vertical' | 'horizontal';
}; };
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let orientation: $$Props["orientation"] = "vertical"; export let orientation: $$Props['orientation'] = 'vertical';
export { className as class }; export { className as class };
</script> </script>
<ScrollAreaPrimitive.Scrollbar <ScrollAreaPrimitive.Scrollbar
{orientation} {orientation}
class={cn( class={cn(
"flex touch-none select-none transition-colors", 'flex touch-none select-none transition-colors',
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-px", orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-px',
orientation === "horizontal" && "h-2.5 w-full border-t border-t-transparent p-px", orientation === 'horizontal' && 'h-2.5 w-full border-t border-t-transparent p-px',
className className
)} )}
> >
<slot /> <slot />
<ScrollAreaPrimitive.Thumb <ScrollAreaPrimitive.Thumb
class={cn("relative rounded-full bg-border", orientation === "vertical" && "flex-1")} class={cn('relative rounded-full bg-border', orientation === 'vertical' && 'flex-1')}
/> />
</ScrollAreaPrimitive.Scrollbar> </ScrollAreaPrimitive.Scrollbar>

View File

@@ -22,32 +22,34 @@
let viewport: HTMLDivElement; let viewport: HTMLDivElement;
const scrollBottom = (node: HTMLDivElement, top: number) => { const scrollTo = (node: HTMLDivElement, top: number, behavior: ScrollBehavior) => {
const scroll = () => const scrollNode = () => {
node.scroll({ node.scrollTo({ top: top, behavior });
top, node.scrollTop = top;
behavior: 'instant' };
}); scrollNode();
scroll();
return { update: scroll };
}; };
onMount(() => { onMount(() => {
if (scrollToBottom) { if (scrollToBottom) {
scrollBottom(viewport, viewport.scrollHeight); scrollTo(viewport, viewport.scrollHeight, 'instant');
} }
}); });
export const scroll = (anchor: 'top' | 'bottom') => { export const scroll = (anchor: 'top' | 'bottom', behavior: ScrollBehavior = 'smooth') => {
if (anchor === 'bottom') { if (anchor === 'bottom') {
scrollBottom(viewport, viewport.scrollHeight); scrollTo(viewport, viewport.scrollHeight, behavior);
} }
if (anchor === 'top') { if (anchor === 'top') {
scrollBottom(viewport, 0); scrollTo(viewport, 0, behavior);
} }
}; };
export const getScrollPercent = () => {
const { scrollTop, scrollHeight, clientHeight } = viewport;
return scrollTop / (scrollHeight - clientHeight);
};
</script> </script>
<ScrollAreaPrimitive.Root {...$$restProps} class={cn('relative overflow-hidden', className)}> <ScrollAreaPrimitive.Root {...$$restProps} class={cn('relative overflow-hidden', className)}>

View File

@@ -1,7 +1,7 @@
import Root from "./separator.svelte"; import Root from './separator.svelte';
export { export {
Root, Root,
// //
Root as Separator, Root as Separator
}; };

View File

@@ -1,22 +1,22 @@
<script lang="ts"> <script lang="ts">
import { Separator as SeparatorPrimitive } from "bits-ui"; import { Separator as SeparatorPrimitive } from 'bits-ui';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = SeparatorPrimitive.Props; type $$Props = SeparatorPrimitive.Props;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let orientation: $$Props["orientation"] = "horizontal"; export let orientation: $$Props['orientation'] = 'horizontal';
export let decorative: $$Props["decorative"] = undefined; export let decorative: $$Props['decorative'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
class={cn( class={cn(
"shrink-0 bg-border", 'shrink-0 bg-border',
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className className
)} )}
{orientation} {orientation}
{decorative} {decorative}
{...$$restProps} {...$$restProps}
/> />

View File

@@ -1,7 +1,7 @@
import Root from "./skeleton.svelte"; import Root from './skeleton.svelte';
export { export {
Root, Root,
// //
Root as Skeleton, Root as Skeleton
}; };

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLDivElement>; type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<div class={cn("animate-pulse rounded-md bg-muted", className)} {...$$restProps}></div> <div class={cn('animate-pulse rounded-md bg-muted', className)} {...$$restProps}></div>

View File

@@ -1 +1 @@
export { default as Toaster } from "./sonner.svelte"; export { default as Toaster } from './sonner.svelte';

View File

@@ -1,20 +1,20 @@
<script lang="ts"> <script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner"; import { Toaster as Sonner, type ToasterProps as SonnerProps } from 'svelte-sonner';
import { mode } from "mode-watcher"; import { mode } from 'mode-watcher';
type $$Props = SonnerProps; type $$Props = SonnerProps;
</script> </script>
<Sonner <Sonner
theme={$mode} theme={$mode}
class="toaster group" class="toaster group"
toastOptions={{ toastOptions={{
classes: { classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: "group-[.toast]:text-muted-foreground", description: 'group-[.toast]:text-muted-foreground',
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground'
}, }
}} }}
{...$$restProps} {...$$restProps}
/> />

View File

@@ -1,7 +1,7 @@
import Root from "./switch.svelte"; import Root from './switch.svelte';
export { export {
Root, Root,
// //
Root as Switch, Root as Switch
}; };

View File

@@ -1,28 +1,28 @@
<script lang="ts"> <script lang="ts">
import { Switch as SwitchPrimitive } from "bits-ui"; import { Switch as SwitchPrimitive } from 'bits-ui';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = SwitchPrimitive.Props; type $$Props = SwitchPrimitive.Props;
type $$Events = SwitchPrimitive.Events; type $$Events = SwitchPrimitive.Events;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let checked: $$Props["checked"] = undefined; export let checked: $$Props['checked'] = undefined;
export { className as class }; export { className as class };
</script> </script>
<SwitchPrimitive.Root <SwitchPrimitive.Root
bind:checked bind:checked
class={cn( class={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", 'peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className className
)} )}
{...$$restProps} {...$$restProps}
on:click on:click
on:keydown on:keydown
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
class={cn( class={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0" 'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
)} )}
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>

View File

@@ -1,28 +1,28 @@
import Root from "./textarea.svelte"; import Root from './textarea.svelte';
type FormTextareaEvent<T extends Event = Event> = T & { type FormTextareaEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLTextAreaElement; currentTarget: EventTarget & HTMLTextAreaElement;
}; };
type TextareaEvents = { type TextareaEvents = {
blur: FormTextareaEvent<FocusEvent>; blur: FormTextareaEvent<FocusEvent>;
change: FormTextareaEvent<Event>; change: FormTextareaEvent<Event>;
click: FormTextareaEvent<MouseEvent>; click: FormTextareaEvent<MouseEvent>;
focus: FormTextareaEvent<FocusEvent>; focus: FormTextareaEvent<FocusEvent>;
keydown: FormTextareaEvent<KeyboardEvent>; keydown: FormTextareaEvent<KeyboardEvent>;
keypress: FormTextareaEvent<KeyboardEvent>; keypress: FormTextareaEvent<KeyboardEvent>;
keyup: FormTextareaEvent<KeyboardEvent>; keyup: FormTextareaEvent<KeyboardEvent>;
mouseover: FormTextareaEvent<MouseEvent>; mouseover: FormTextareaEvent<MouseEvent>;
mouseenter: FormTextareaEvent<MouseEvent>; mouseenter: FormTextareaEvent<MouseEvent>;
mouseleave: FormTextareaEvent<MouseEvent>; mouseleave: FormTextareaEvent<MouseEvent>;
paste: FormTextareaEvent<ClipboardEvent>; paste: FormTextareaEvent<ClipboardEvent>;
input: FormTextareaEvent<InputEvent>; input: FormTextareaEvent<InputEvent>;
}; };
export { export {
Root, Root,
// //
Root as Textarea, Root as Textarea,
type TextareaEvents, type TextareaEvents,
type FormTextareaEvent, type FormTextareaEvent
}; };

View File

@@ -1,38 +1,38 @@
<script lang="ts"> <script lang="ts">
import type { HTMLTextareaAttributes } from "svelte/elements"; import type { HTMLTextareaAttributes } from 'svelte/elements';
import type { TextareaEvents } from "./index.js"; import type { TextareaEvents } from './index.js';
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js';
type $$Props = HTMLTextareaAttributes; type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents; type $$Events = TextareaEvents;
let className: $$Props["class"] = undefined; let className: $$Props['class'] = undefined;
export let value: $$Props["value"] = undefined; export let value: $$Props['value'] = undefined;
export { className as class }; export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305 // Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x. // Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined; export let readonly: $$Props['readonly'] = undefined;
</script> </script>
<textarea <textarea
class={cn( class={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background 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", 'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background 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 className
)} )}
bind:value bind:value
{readonly} {readonly}
on:blur on:blur
on:change on:change
on:click on:click
on:focus on:focus
on:keydown on:keydown
on:keypress on:keypress
on:keyup on:keyup
on:mouseover on:mouseover
on:mouseenter on:mouseenter
on:mouseleave on:mouseleave
on:paste on:paste
on:input on:input
{...$$restProps} {...$$restProps}
></textarea> ></textarea>

View File

@@ -1 +1,2 @@
export const API_URL = 'http://localhost:1234'; export const BASE_API_URL = 'localhost:1234';
export const API_URL = `http://${BASE_API_URL}`;

39
src/lib/event.ts Normal file
View File

@@ -0,0 +1,39 @@
export interface Listener<D> {
(event: D): unknown;
}
export interface Disposable {
dispose(): void;
}
export class EventEmitter<E, D> {
private listeners: Map<E, Listener<D>[]> = new Map();
on = (event: E, listener: Listener<D>): Disposable => {
if (!this.listeners.has(event)) this.listeners.set(event, []);
this.listeners.get(event)?.push(listener);
return {
dispose: () => this.off(event, listener)
};
};
off = (event: E, listener: Listener<D>) => {
if (!this.listeners.has(event)) return;
const listeners = this.listeners.get(event)?.filter((l) => l !== listener);
this.listeners.set(event, listeners || []);
};
emit = (event: E, data: D) => {
if (!this.listeners.has(event)) return;
this.listeners.get(event)?.forEach((listener) => setTimeout(() => listener(data), 0));
};
pipe = (event: E, te: EventEmitter<E, D>): Disposable => {
return this.on(event, (e) => te.emit(event, e));
};
}

View File

@@ -1,41 +0,0 @@
import { getChannelById } from "$lib/api/channel";
import { isErrorResponse, type Channel } from "$lib/types";
import { get, writable, type Writable } from "svelte/store";
const channelsCache: Writable<Map<number, Channel>> = writable(new Map<number, Channel>());
const runningCaches = new Set();
export function addChannelToCache(channel: Channel) {
channelsCache.update((channels) => channels.set(channel.id, channel));
}
export async function getCachedChannel(channelId: number): Promise<Channel | null> {
const cached = get(channelsCache).get(channelId);
if (cached)
return cached;
if (runningCaches.has(channelId))
return await new Promise((resolve) => {
channelsCache.subscribe((channels) => {
const channel = channels.get(channelId);
if (channel) {
runningCaches.delete(channelId);
resolve(channel);
}
});
});
runningCaches.add(channelId);
const response = await getChannelById(channelId);
if (isErrorResponse(response))
return null;
const channel = response as Channel;
addChannelToCache(channel);
return channel;
}

23
src/lib/stores/cache/index.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
import { getChannelById } from '$lib/api/channel';
import { getMessageById } from '$lib/api/message';
import { getUserById } from '$lib/api/user';
import { isErrorResponse, type Channel, type Message, type User } from '$lib/types';
import { Cache } from './utils';
export const usersCache: Cache<number, User> = new Cache(async (id) => {
const response = await getUserById(id);
if (isErrorResponse(response)) return null;
return response;
});
export const messagesCache: Cache<number, Message> = new Cache(async (id) => {
const response = await getMessageById(id);
if (isErrorResponse(response)) return null;
return response;
});
export const channelsCache: Cache<number, Channel> = new Cache(async (id) => {
const response = await getChannelById(id);
if (isErrorResponse(response)) return null;
return response;
});

View File

@@ -1,42 +0,0 @@
import { getMessageById } from "$lib/api/message";
import { isErrorResponse, type Message } from "$lib/types";
import { get, writable, type Writable } from "svelte/store";
const messagesCache: Writable<Map<number, Message>> = writable(new Map<number, Message>());
const runningCaches = new Set();
export function addMessageToCache(message: Message) {
messagesCache.update((messages) => messages.set(message.id, message));
}
export async function getCachedMessage(messageId: number): Promise<Message | null> {
const cached = get(messagesCache).get(messageId);
if (cached)
return cached;
if (runningCaches.has(messageId))
return await new Promise((resolve) => {
messagesCache.subscribe((users) => {
const user = users.get(messageId);
if (user) {
runningCaches.delete(messageId);
resolve(user);
}
});
});
runningCaches.add(messageId);
const response = await getMessageById(messageId);
if (isErrorResponse(response))
return null;
const message = response as Message;
addMessageToCache(message);
return message;
}

View File

@@ -1,47 +0,0 @@
import { getUserById } from "$lib/api/user";
import { isErrorResponse, type User } from "$lib/types";
import { get, writable, type Writable } from "svelte/store";
const usersCache: Writable<Map<number, User>> = writable(new Map<number, User>());
const runningCaches = new Set();
usersCache.subscribe((users) => {
console.log(`Cached users: ${JSON.stringify([...users.values()])}`);
})
export function addUserToCache(user: User) {
usersCache.update((users) => users.set(user.id, user));
}
export async function getCachedUser(userId: number): Promise<User | null> {
const cachedUser = get(usersCache).get(userId);
if (cachedUser)
return cachedUser;
if (runningCaches.has(userId)) {
return await new Promise((resolve) => {
usersCache.subscribe((users) => {
console.log(`subsribed called`);
const user = users.get(userId);
if (user) {
runningCaches.delete(userId);
resolve(user);
}
});
});
}
runningCaches.add(userId);
const response = await getUserById(userId);
if (isErrorResponse(response))
return null;
const user = response as User;
addUserToCache(user);
return user;
}

57
src/lib/stores/cache/utils.ts vendored Normal file
View File

@@ -0,0 +1,57 @@
import { get, writable, type Writable } from 'svelte/store';
export class Cache<I, T> {
private data: Writable<Map<I, T>> = writable(new Map<I, T>());
private runningCaches: Set<I> = new Set();
private resolver: (data: I) => Promise<T | null>;
constructor(resolver: (data: I) => Promise<T | null>) {
this.resolver = resolver;
}
async get(key: I): Promise<T | null> {
const cached = get(this.data).get(key);
if (cached) {
console.log(`[Cache] Found in cache: `, cached);
return cached;
}
if (this.runningCaches.has(key)) {
return new Promise((resolve) => {
this.data.subscribe((data) => {
const value = data.get(key);
if (value) {
resolve(value);
}
});
});
}
this.runningCaches.add(key);
const data = await this.resolver(key);
this.runningCaches.delete(key);
if (data)
console.log(`[Cache] Added to cache: `, data);
return data;
}
set(key: I, value: T) {
console.log(`[Cache] Added to cache: `, value);
this.data.update((data) => data.set(key, value));
}
remove(key: I) {
console.log(`[Cache] Removed from cache: `, key);
this.data.update((data) => {
data.delete(key);
return data;
});
}
}

19
src/lib/stores/theme.ts Normal file
View File

@@ -0,0 +1,19 @@
import { browser } from '$app/environment';
import { persisted } from 'svelte-persisted-store';
import type { Writable } from 'svelte/store';
function initialTheme() {
if (!browser) return 'light';
if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark';
return 'light';
}
export const theme: Writable<'light' | 'dark'> = persisted('theme', initialTheme());
theme.subscribe((theme) => {
if (!browser) return;
if (theme === 'light') document.documentElement.classList.remove('dark');
else document.documentElement.classList.add('dark');
});

View File

@@ -1,23 +1,17 @@
import { derived, get } from "svelte/store"; import { derived, get } from 'svelte/store';
import { isErrorResponse, type User } from "../types"; import { isErrorResponse, type User } from '../types';
import { getByToken } from "$lib/api/user"; import { getByToken } from '$lib/api/user';
import { persisted } from "svelte-persisted-store"; import { persisted } from 'svelte-persisted-store';
export const token = persisted<string | null>('token', null); export const token = persisted<string | null>('token', null);
token.subscribe((token) => {
console.log(`updated token: ${JSON.stringify(token)}`);
})
export const user = derived<typeof token, User | null>(token, ($token, set) => { export const user = derived<typeof token, User | null>(token, ($token, set) => {
getByToken($token).then((response) => { getByToken($token).then((response) => {
if (isErrorResponse(response)) if (isErrorResponse(response)) set(null);
set(null); else set(response);
else });
set(response);
})
}); });
export function getUserToken(): string | null { export function getUserToken(): string | null {
return get(token); return get(token);
} }

View File

@@ -0,0 +1,98 @@
import { BASE_API_URL } from '$lib/constants';
import { EventEmitter } from '$lib/event';
import { derived, get } from 'svelte/store';
import { token as tokenStore } from './user';
import type { Channel, Message } from '$lib/types';
import { messagesCache, channelsCache } from './cache';
export type WebSocketMessageType =
| 'createMessage'
| 'updateChannel'
| 'createChannel'
| 'deleteChannel'
| 'connect'
| 'disconnect'
| 'any';
export type WebSocketMessageData =
| Message
| Channel
| { id: number }
| null
| {
type: WebSocketMessageType;
};
export type WebsoketMessage = {
type: WebSocketMessageType;
data: WebSocketMessageData;
};
export const appWebsocket = new EventEmitter<WebSocketMessageType, WebSocketMessageData>();
appWebsocket.on('any', (data) => {
console.log(`[WS] Recieved message: `, data);
});
function updateCache(type: WebSocketMessageType, data: WebSocketMessageData) {
switch (type) {
case 'createMessage':
messagesCache.set((data as Message).id, data as Message);
break;
case 'updateChannel':
channelsCache.set((data as Channel).id, data as Channel);
break;
case 'createChannel':
channelsCache.set((data as Channel).id, data as Channel);
break;
case 'deleteChannel':
channelsCache.remove((data as { id: number }).id);
break;
default:
break;
}
}
const connect = (token: string) => {
const websocket = new WebSocket(`ws://${BASE_API_URL}/ws/${token}`);
websocket.onopen = () => {
appWebsocket.emit('connect', null);
appWebsocket.emit('any', { type: 'connect' });
};
websocket.onmessage = (event) => {
const message: WebsoketMessage = JSON.parse(event.data);
updateCache(message.type, message.data);
appWebsocket.emit(message.type, message.data as WebSocketMessageData);
appWebsocket.emit('any', message);
};
websocket.onclose = () => {
appWebsocket.emit('disconnect', null);
appWebsocket.emit('any', { type: 'disconnect' });
setTimeout(() => {
const token = get(tokenStore);
if (token) connect(token);
}, 500);
};
return websocket;
};
const socket = derived<typeof tokenStore, WebSocket | null>(
tokenStore,
($token, set) => {
if ($token) {
set(connect($token));
} else {
set(null);
}
},
null
);
socket.subscribe((socket) => {
console.log(`[WS] Connected: `, socket);
});

View File

@@ -1,9 +1,9 @@
export type ErrorResponse = { export type ErrorResponse = {
error: string; error: string;
} };
export function isErrorResponse(data: unknown): data is ErrorResponse { export function isErrorResponse(data: unknown): data is ErrorResponse {
return ((data as ErrorResponse).error !== undefined); return (data as ErrorResponse).error !== undefined;
} }
export type Token = { export type Token = {
@@ -11,7 +11,7 @@ export type Token = {
userId: number; userId: number;
createdAt: string; createdAt: string;
expiresAt: string; expiresAt: string;
} };
export type User = { export type User = {
id: number; id: number;
@@ -33,4 +33,4 @@ export type Channel = {
name: string; name: string;
lastMessageId?: number; lastMessageId?: number;
createdAt: string; createdAt: string;
}; };

View File

@@ -1,62 +1,60 @@
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from 'clsx';
import { twMerge } from "tailwind-merge"; import { twMerge } from 'tailwind-merge';
import { cubicOut } from "svelte/easing"; import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from "svelte/transition"; import type { TransitionConfig } from 'svelte/transition';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
type FlyAndScaleParams = { type FlyAndScaleParams = {
y?: number; y?: number;
x?: number; x?: number;
start?: number; start?: number;
duration?: number; duration?: number;
}; };
export const flyAndScale = ( export const flyAndScale = (
node: Element, node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 } params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => { ): TransitionConfig => {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
const scaleConversion = ( const scaleConversion = (
valueA: number, valueA: number,
scaleA: [number, number], scaleA: [number, number],
scaleB: [number, number] scaleB: [number, number]
) => { ) => {
const [minA, maxA] = scaleA; const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB; const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA); const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB; const valueB = percentage * (maxB - minB) + minB;
return valueB; return valueB;
}; };
const styleToString = ( const styleToString = (style: Record<string, number | string | undefined>): string => {
style: Record<string, number | string | undefined> return Object.keys(style).reduce((str, key) => {
): string => { if (style[key] === undefined) return str;
return Object.keys(style).reduce((str, key) => { return str + `${key}:${style[key]};`;
if (style[key] === undefined) return str; }, '');
return str + `${key}:${style[key]};`; };
}, "");
};
return { return {
duration: params.duration ?? 200, duration: params.duration ?? 200,
delay: 0, delay: 0,
css: (t) => { css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({ return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t opacity: t
}); });
}, },
easing: cubicOut easing: cubicOut
}; };
}; };

View File

@@ -1,2 +1,4 @@
import type { ParamMatcher } from '@sveltejs/kit'; import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => { return /^\d+$/.test(param); }; export const match: ParamMatcher = (param) => {
return /^\d+$/.test(param);
};

View File

@@ -3,20 +3,16 @@ import type { LayoutServerLoad } from './$types';
import { getByToken } from '$lib/api/user'; import { getByToken } from '$lib/api/user';
import { isErrorResponse } from '$lib/types'; import { isErrorResponse } from '$lib/types';
export const prerender = false; // export const prerender = false;
export const ssr = false; // export const ssr = false;
export const load = (async ({ cookies }) => { export const load = (async ({ cookies }) => {
const token = cookies.get('token'); const token = cookies.get('token');
if (!token) if (!token) return redirect(302, '/logout');
return redirect(302, '/logout');
const user = await getByToken(token) const user = await getByToken(token);
if (isErrorResponse(user)) if (isErrorResponse(user)) return redirect(302, '/logout');
return redirect(302, '/logout');
console.log(`User: ${JSON.stringify(user)}`)
return { token, user }; return { token, user };
}) satisfies LayoutServerLoad; }) satisfies LayoutServerLoad;

View File

@@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import type { LayoutData } from './$types'; import type { LayoutData } from './$types';
import { token, user } from '$lib/stores/user'; import { token } from '$lib/stores/user';
import { addUserToCache } from '$lib/stores/cache/users'; import { usersCache } from '$lib/stores/cache';
import { appWebsocket } from '$lib/stores/websocket';
export let data: LayoutData; export let data: LayoutData;
console.log(`loading`); token.update(() => data.token);
console.log(data);
$token = data.token; const user = data.user;
addUserToCache(data.user); usersCache.set(user.id, user);
</script> </script>
<slot /> <slot />

View File

@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import { createMessage } from '$lib/api/message'; import { createMessage } from '$lib/api/message';
import * as Avatar from '$lib/components/ui/avatar'; import * as Avatar from '$lib/components/ui/avatar';
import { ScrollArea } from '$lib/components/ui/scroll-area'; import { type Channel, type Message, type Message as MessageType } from '$lib/types';
import { isErrorResponse, type Channel, type Message as MessageType } from '$lib/types'; import { afterUpdate } from 'svelte';
import MessageArea from './message-area.svelte'; import MessageArea from './message-area.svelte';
import Message from './message.svelte';
import TextField from './text-field.svelte'; import TextField from './text-field.svelte';
export let channel: Channel | null = null; export let channel: Channel | null = null;
@@ -12,34 +11,40 @@
let messageArea: MessageArea; let messageArea: MessageArea;
const sendMessage = (message: string) => { const sendMessage = (content: string) => {
if (!channel) return; if (!channel) return;
createMessage(channel.id, message).then((message) => { createMessage(channel.id, content);
if (!isErrorResponse(message)) {
messages = [...messages, message];
messageArea.scroll('bottom');
}
});
}; };
export function updateMessages(newMessages: Message[]) {
messages = newMessages;
}
afterUpdate(() => {
if (!messageArea) return;
if (messageArea.getScrollPercent() > 0.95) messageArea.scroll('bottom', 'smooth');
});
</script> </script>
<div class="flex h-screen w-[95%] flex-col contain-strict"> <div class="flex h-screen w-full flex-col items-center contain-strict">
<div class="contents"> <div class="w-full">
<div class="flex items-center space-x-4"> <div class="flex items-center justify-center space-x-4 bg-secondary p-4">
<Avatar.Root class="h-12 w-12"> <Avatar.Root class="h-12 w-12">
<Avatar.Image src="/default-avatar.png" /> <Avatar.Image src="/default-avatar.png" />
<Avatar.Fallback>{channel?.name[0].toUpperCase() || ''}</Avatar.Fallback> <Avatar.Fallback>{channel?.name[0].toUpperCase() || ''}</Avatar.Fallback>
</Avatar.Root> </Avatar.Root>
<span class="text-xl font-bold">{channel?.name || ''}</span> <span class="text-3xl font-bold">{channel?.name || ''}</span>
</div> </div>
</div> </div>
<div class="contents flex-grow"> <div class="flex h-screen w-[95%] flex-col contain-strict">
<MessageArea {messages} bind:this={messageArea} /> <div class="z-[10000] contents flex-grow">
</div> <MessageArea {messages} bind:this={messageArea} />
<div class="m-4 mt-1 max-h-[40%] flex-grow"> </div>
<TextField onSend={sendMessage} /> <div class="relative bottom-0 left-0 right-0 m-4 mt-1 max-h-[40%] flex-grow">
<TextField onSend={sendMessage} />
</div>
</div> </div>
</div> </div>

View File

@@ -7,21 +7,26 @@
let scrollArea: ScrollArea; let scrollArea: ScrollArea;
messages = [...messages].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); $: messages = [...messages].sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
export function scroll(anchor: 'top' | 'bottom') { export function scroll(anchor: 'top' | 'bottom', behavior: ScrollBehavior = 'smooth') {
if (scrollArea) { scrollArea.scroll(anchor, behavior);
scrollArea.scroll(anchor); }
}
export function getScrollPercent() {
return scrollArea.getScrollPercent();
} }
</script> </script>
<div class="h-full w-full overflow-y-hidden"> <div class="h-full w-full overflow-y-hidden">
<ScrollArea class="h-full w-full" scrollToBottom={true} bind:this={scrollArea}> <ScrollArea class="h-full w-full" scrollToBottom={true} bind:this={scrollArea}>
<div class="mx-4 flex flex-col gap-3"> <div class="mx-4 flex flex-col gap-3 pt-4">
{#each messages as message} {#each messages as message}
<Message {message} /> <Message {message} />
{/each} {/each}
</div> </div>
<!-- <div class="h-[6rem] invisible" /> -->
</ScrollArea> </ScrollArea>
</div> </div>

View File

@@ -3,15 +3,14 @@
import { user } from '$lib/stores/user'; import { user } from '$lib/stores/user';
import { cn } from '$lib/utils'; import { cn } from '$lib/utils';
import * as Avatar from '$lib/components/ui/avatar'; import * as Avatar from '$lib/components/ui/avatar';
import { getCachedUser } from '$lib/stores/cache/users'; import { usersCache } from '$lib/stores/cache';
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
import { Skeleton } from '$lib/components/ui/skeleton';
export let message: Message; export let message: Message;
let sender: Writable<User | null> = writable(null); let sender: Writable<User | null> = writable(null);
getCachedUser(message.authorId).then((user) => ($sender = user)); usersCache.get(message.authorId).then((user) => ($sender = user));
$: username = (isSelf ? $user?.username : $sender?.username) || 'N'; $: username = (isSelf ? $user?.username : $sender?.username) || 'N';
$: isSelf = $user?.id === message.authorId; $: isSelf = $user?.id === message.authorId;
@@ -33,7 +32,7 @@
<span class="whitespace-pre-line break-words break-all text-left text-xl font-bold"> <span class="whitespace-pre-line break-words break-all text-left text-xl font-bold">
{message.content} {message.content}
</span> </span>
<span class={cn('text-md', timestampPosition)} <span class={cn('text-sm', timestampPosition)}
>{new Date(message.createdAt).toLocaleString()} >{new Date(message.createdAt).toLocaleString()}
</span> </span>
</div> </div>

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { ScrollArea } from '$lib/components/ui/scroll-area'; import { ScrollArea } from '$lib/components/ui/scroll-area';
import { writable } from 'svelte/store';
import { Send } from 'lucide-svelte'; import { Send } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
@@ -15,20 +14,11 @@
const text = event.clipboardData?.getData('text/plain'); const text = event.clipboardData?.getData('text/plain');
if (text && browser) { if (text && browser) {
// if (window.getSelection) {
// var selObj = window.getSelection();
// var selRange = selObj?.getRangeAt(0);
// selRange?.deleteContents();
// selRange?.insertNode(document.createTextNode(text));
// selObj?.collapseToEnd();
// }
document.execCommand('insertText', false, text); document.execCommand('insertText', false, text);
} }
} }
function onSendClick() { function onSendClick() {
console.log(content);
if (content) { if (content) {
onSend(content); onSend(content);
content = ''; content = '';
@@ -36,7 +26,9 @@
} }
</script> </script>
<div class="flex h-full items-center gap-2 rounded-3xl bg-secondary p-2"> <div
class="flex h-full items-center gap-2 rounded-3xl bg-secondary p-2 shadow-md shadow-secondary-foreground dark:shadow-none"
>
<ScrollArea class="h-full w-full"> <ScrollArea class="h-full w-full">
<div <div
contenteditable="true" contenteditable="true"

View File

@@ -1,36 +1,48 @@
<script lang="ts"> <script lang="ts">
import { getMessageById } from '$lib/api/message';
import * as Avatar from '$lib/components/ui/avatar'; import * as Avatar from '$lib/components/ui/avatar';
import { isErrorResponse, type Channel, type Message } from '$lib/types'; import { usersCache, messagesCache } from '$lib/stores/cache';
import { type Channel, type Message, type User } from '$lib/types';
import { cn } from '$lib/utils'; import { cn } from '$lib/utils';
export let channel: Channel; export let channel: Channel;
let lastMessage: Message | null = null; let lastMessage: Message | null = null;
if (channel.lastMessageId) { let lastMessageAuthor: User | null = null;
getMessageById(channel.lastMessageId).then((message) => {
if (!isErrorResponse(message)) lastMessage = message; $: if (lastMessage) {
}); usersCache.get(lastMessage.authorId).then((user) => (lastMessageAuthor = user));
}
$: if (channel.lastMessageId) {
messagesCache.get(channel.lastMessageId).then((message) => (lastMessage = message));
} }
export let selected: boolean = false; export let selected: boolean = false;
export let onClick: () => void = () => {}; export let onClick: () => void = () => {};
$: className = selected ? 'bg-accent' : 'hover:bg-secondary'; $: buttonColor = selected ? 'bg-accent' : 'hover:bg-secondary-foreground';
</script> </script>
<button on:click={onClick} class={cn('flex w-full space-x-4 rounded-xl p-4', className)}> <button
on:click={onClick}
class={cn(
'flex w-full space-x-4 rounded-xl p-4 transition-colors contain-inline-size',
buttonColor
)}
>
<div> <div>
<Avatar.Root class="h-12 w-12"> <Avatar.Root class="h-12 w-12">
<Avatar.Image src="/default-avatar.png" /> <Avatar.Image src="/default-avatar.png" />
<Avatar.Fallback>{channel.name[0].toUpperCase()}</Avatar.Fallback> <Avatar.Fallback>{channel.name[0].toUpperCase()}</Avatar.Fallback>
</Avatar.Root> </Avatar.Root>
</div> </div>
<div class="flex flex-col overflow-auto"> <div class="flex flex-col overflow-hidden">
<span class="overflow-hidden text-ellipsis text-left text-xl font-bold">{channel.name}</span <span class="overflow-hidden text-ellipsis text-left text-xl font-bold">
> {channel.name}
<span class="overflow-hidden text-ellipsis text-left text-sm" </span>
>{lastMessage?.content || ''}</span <span class="overflow-hidden text-ellipsis whitespace-nowrap text-left text-sm">
> {#if lastMessage}
{`${lastMessageAuthor?.username}: ${lastMessage.content}`}
{/if}
</span>
</div> </div>
</button> </button>

View File

@@ -16,11 +16,13 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Separator } from '$lib/components/ui/separator'; import { Separator } from '$lib/components/ui/separator';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { user } from '$lib/stores/user';
import ThemeSwitch from '$lib/components/theme-switch.svelte';
export let menuItems: MenuItem[] = []; export let menuItems: MenuItem[] = [];
</script> </script>
<div class="mx-4 my-3 flex w-full"> <div class="mx-4 my-3 flex">
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder> <DropdownMenu.Trigger asChild let:builder>
<Button builders={[builder]} variant="ghost" class="h-10 w-10 p-0"> <Button builders={[builder]} variant="ghost" class="h-10 w-10 p-0">
@@ -40,4 +42,12 @@
{/each} {/each}
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
<div class="flex-grow pt-1 text-center text-2xl">
{$user?.username}
</div>
<div>
<ThemeSwitch />
</div>
</div> </div>

View File

@@ -1,11 +1,9 @@
<script lang="ts"> <script lang="ts">
import { ScrollArea, ScrollAreaScrollbar } from '$lib/components/ui/scroll-area'; import { ScrollArea } from '$lib/components/ui/scroll-area';
const tags = Array.from({ length: 50 }).map((_, i, a) => `v1.2.0-beta.${a.length - i}`);
</script> </script>
<div> <div class="light:shadow-secondary-foreground flex h-full w-full flex-col bg-secondary shadow-lg">
<div> <div class="p-0">
<slot name="header"></slot> <slot name="header"></slot>
</div> </div>
<div> <div>

View File

@@ -8,14 +8,15 @@
import type { Icon } from 'lucide-svelte'; import type { Icon } from 'lucide-svelte';
import Settings from 'lucide-svelte/icons/settings'; import Settings from 'lucide-svelte/icons/settings';
import LogOut from 'lucide-svelte/icons/log-out'; import LogOut from 'lucide-svelte/icons/log-out';
import type { ComponentType } from 'svelte'; import { onDestroy, onMount, type ComponentType } from 'svelte';
import type { MenuItem } from './(components)/sidebar-header.svelte'; import type { MenuItem } from './(components)/sidebar-header.svelte';
import ChannelList from './(components)/channel-list.svelte'; import ChannelList from './(components)/channel-list.svelte';
import { addChannelToCache } from '$lib/stores/cache/channels'; import { appWebsocket, type WebSocketMessageType } from '$lib/stores/websocket';
import type { Channel } from '$lib/types';
export let data: LayoutData; export let data: LayoutData;
for (const channel of data.channels) addChannelToCache(channel); let channels = data.channels;
let channelList: ChannelList | undefined; let channelList: ChannelList | undefined;
@@ -44,6 +45,51 @@
]; ];
$: channelId = parseInt($page.params.channel_id); $: channelId = parseInt($page.params.channel_id);
function handleChannelCreated(channel: unknown) {
const typedChannel = channel as Channel;
if (!channelList) return;
channels.push(typedChannel);
}
function handleChannelUpdated(channel: unknown) {
const typedChannel = channel as Channel;
if (!channelList) return;
for (let i = 0; i < channels.length; i++) {
if (channels[i].id == typedChannel.id) {
channels[i] = typedChannel;
break;
}
}
}
function handleChannelDeleted(channel_id: unknown) {
const id = (channel_id as { id: number }).id;
if (!channelList) return;
channels = channels.filter((c) => c.id != id);
}
const handlers = {
createChannel: handleChannelCreated,
updateChannel: handleChannelUpdated,
deleteChannel: handleChannelDeleted
};
onMount(() => {
for (const [key, value] of Object.entries(handlers))
appWebsocket.on(key as WebSocketMessageType, value);
});
onDestroy(() => {
for (const [key, value] of Object.entries(handlers))
appWebsocket.off(key as WebSocketMessageType, value);
});
</script> </script>
<svelte:window on:keyup={onKeyUp} /> <svelte:window on:keyup={onKeyUp} />
@@ -56,11 +102,7 @@
</div> </div>
<div slot="channels"> <div slot="channels">
<ChannelList <ChannelList {channels} defaultSelected={channelId} bind:this={channelList} />
channels={data.channels}
defaultSelected={channelId}
bind:this={channelList}
/>
</div> </div>
</Sidebar> </Sidebar>
</div> </div>

View File

@@ -2,13 +2,14 @@ import { getAllChannels } from '$lib/api/channel';
import type { Channel } from '$lib/types'; import type { Channel } from '$lib/types';
import type { LayoutLoad } from './$types'; import type { LayoutLoad } from './$types';
export const ssr = false;
export const load = (async ({ parent }) => { export const load = (async ({ parent }) => {
await parent(); await parent();
const response = await getAllChannels(); const response = await getAllChannels();
const channels = response as Channel[] const channels = response as Channel[];
return { channels }; return { channels };
}) satisfies LayoutLoad; }) satisfies LayoutLoad;

View File

@@ -2,21 +2,40 @@
import type { PageData } from './$types'; import type { PageData } from './$types';
import { page } from '$app/stores'; import { page } from '$app/stores';
import ChannelArea from '../(components)/(channel)/channel-area.svelte'; import ChannelArea from '../(components)/(channel)/channel-area.svelte';
import { getCachedChannel } from '$lib/stores/cache/channels'; import { channelsCache } from '$lib/stores/cache';
import type { Channel } from '$lib/types'; import type { Channel, Message } from '$lib/types';
import { addMessageToCache } from '$lib/stores/cache/messages'; import { appWebsocket } from '$lib/stores/websocket';
import { onDestroy, onMount } from 'svelte';
export let data: PageData; export let data: PageData;
let channel: Channel | null = null; let channel: Channel | null = null;
$: channelId = parseInt($page.params.channel_id); $: channelId = parseInt($page.params.channel_id);
$: getCachedChannel(channelId).then((c) => (channel = c)); $: channelsCache.get(channelId).then((c) => (channel = c));
for (const message of data.messages) const messages = data.messages;
addMessageToCache(message);
let channelArea: ChannelArea;
function handleCreateMessage(message: unknown) {
const typedMessage = message as Message;
if (!channel) return;
if (typedMessage.channelId != channel.id) return;
messages.push(typedMessage);
channelArea?.updateMessages(messages);
}
onMount(() => {
appWebsocket.on('createMessage', handleCreateMessage);
});
onDestroy(() => {
appWebsocket.off('createMessage', handleCreateMessage);
});
</script> </script>
<div class="flex h-screen w-full items-center justify-center"> <div class="flex h-screen w-full items-center justify-center">
<ChannelArea {channel} messages={data.messages} /> <ChannelArea {channel} {messages} bind:this={channelArea} />
</div> </div>

View File

@@ -1,5 +1,5 @@
import { getMessagesByChannelId } from '$lib/api/channel'; import { getMessagesByChannelId } from '$lib/api/channel';
import { getCachedChannel } from '$lib/stores/cache/channels'; import { channelsCache } from '$lib/stores/cache';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import { isErrorResponse } from '$lib/types'; import { isErrorResponse } from '$lib/types';
@@ -9,15 +9,13 @@ export const ssr = false;
export const load = (async ({ params, parent }) => { export const load = (async ({ params, parent }) => {
await parent(); await parent();
const channelId = parseInt(params.channel_id) const channelId = parseInt(params.channel_id);
const channel = await getCachedChannel(channelId); const channel = await channelsCache.get(channelId);
if (!channel) if (!channel) return redirect(302, '/channels');
return redirect(302, '/channels');
const messages = await getMessagesByChannelId(channel.id, channel.lastMessageId); const messages = await getMessagesByChannelId(channel.id, channel.lastMessageId);
if (isErrorResponse(messages)) if (isErrorResponse(messages)) return redirect(302, '/channels');
return redirect(302, '/channels');
return { messages }; return { messages };
}) satisfies PageLoad; }) satisfies PageLoad;

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import ThemeSwitch from '$lib/components/theme-switch.svelte';
import { Fan } from 'lucide-svelte';
</script>
<div class="flex h-full w-full items-center justify-center">
<ThemeSwitch class="absolute right-4 top-4" />
<div class="flex h-full w-full flex-col items-center justify-center gap-8">
<Fan class="h-32 w-32" />
<slot />
</div>
</div>

View File

@@ -1,15 +1,13 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { zod } from "sveltekit-superforms/adapters"; import { zod } from 'sveltekit-superforms/adapters';
import { loginFormSchema } from './login-form.svelte'; import { loginFormSchema } from './login-form.svelte';
import { superValidate } from "sveltekit-superforms"; import { superValidate } from 'sveltekit-superforms';
import { type Actions, fail, redirect } from "@sveltejs/kit"; import { type Actions, fail, redirect } from '@sveltejs/kit';
import { loginUser } from '$lib/api/user'; import { loginUser } from '$lib/api/user';
import { isErrorResponse } from '$lib/types'; import { isErrorResponse } from '$lib/types';
export const load = (async ({ cookies }) => { export const load = (async ({ cookies }) => {
if (cookies.get('token')) if (cookies.get('token')) throw redirect(302, '/channels');
throw redirect(302, '/channels');
return { form: await superValidate(zod(loginFormSchema)) }; return { form: await superValidate(zod(loginFormSchema)) };
}) satisfies PageServerLoad; }) satisfies PageServerLoad;
@@ -18,8 +16,7 @@ export const actions: Actions = {
default: async (event) => { default: async (event) => {
const form = await superValidate(event, zod(loginFormSchema)); const form = await superValidate(event, zod(loginFormSchema));
if (!form.valid) if (!form.valid) return fail(400, { form });
return fail(400, { form });
const response = await loginUser(form.data.username, form.data.password); const response = await loginUser(form.data.username, form.data.password);
@@ -31,5 +28,5 @@ export const actions: Actions = {
event.cookies.set('token', response.token, { path: '/' }); event.cookies.set('token', response.token, { path: '/' });
return { form, token: response }; return { form, token: response };
}, }
}; };

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import type { PageData } from './$types';
import LoginForm from './login-form.svelte';
export let data: PageData;
</script>
<div class="w-[20%] min-w-[300px] space-y-4 rounded-lg bg-secondary p-4">
<h1 class="text-center text-4xl font-bold">Login</h1>
<LoginForm data={data.form}></LoginForm>
</div>

View File

@@ -1,7 +1,7 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { superValidate } from "sveltekit-superforms"; import { superValidate } from 'sveltekit-superforms';
import { type Actions, fail } from "@sveltejs/kit"; import { type Actions, fail } from '@sveltejs/kit';
import { zod } from "sveltekit-superforms/adapters"; import { zod } from 'sveltekit-superforms/adapters';
import { registerFormSchema } from './register-form.svelte'; import { registerFormSchema } from './register-form.svelte';
import { registerUser } from '$lib/api/user'; import { registerUser } from '$lib/api/user';
import { isErrorResponse } from '$lib/types'; import { isErrorResponse } from '$lib/types';
@@ -14,8 +14,7 @@ export const actions: Actions = {
default: async (event) => { default: async (event) => {
const form = await superValidate(event, zod(registerFormSchema)); const form = await superValidate(event, zod(registerFormSchema));
if (!form.valid) if (!form.valid) return fail(400, { form });
return fail(400, { form });
const response = await registerUser(form.data.username, form.data.password); const response = await registerUser(form.data.username, form.data.password);
@@ -26,4 +25,4 @@ export const actions: Actions = {
return { form, success: true }; return { form, success: true };
} }
} };

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import type { PageData } from './$types';
import RegisterForm from './register-form.svelte';
export let data: PageData;
</script>
<div class="w-[20%] min-w-[300px] space-y-4 rounded-lg bg-secondary p-4">
<h1 class="text-center text-4xl font-bold">Register</h1>
<RegisterForm data={data.form}></RegisterForm>
</div>

View File

@@ -25,9 +25,7 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Form from '$lib/components/ui/form'; import * as Form from '$lib/components/ui/form';
import { superForm, type SuperValidated } from 'sveltekit-superforms'; import { superForm, type SuperValidated } from 'sveltekit-superforms';
import { Label } from '$lib/components/ui/label';
import { zodClient } from 'sveltekit-superforms/adapters'; import { zodClient } from 'sveltekit-superforms/adapters';
import { goto } from '$app/navigation';
export let data: SuperValidated<RegisterFormSchema>; export let data: SuperValidated<RegisterFormSchema>;
export let loginUrl: string = '/login'; export let loginUrl: string = '/login';

View File

@@ -1,5 +1,7 @@
<script> <script>
import '../app.css'; import '../app.css';
import { theme } from '$lib/stores/theme';
</script> </script>
<div class="h-full w-full"> <div class="h-full w-full">

View File

@@ -2,5 +2,5 @@ import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
export const load = (async () => { export const load = (async () => {
redirect(302, '/login') redirect(302, '/login');
}) satisfies PageServerLoad; }) satisfies PageServerLoad;

View File

@@ -1,15 +0,0 @@
<script lang="ts">
import { Fan } from 'lucide-svelte';
import type { PageData } from './$types';
import LoginForm from './login-form.svelte';
export let data: PageData;
</script>
<div class="flex h-full w-full flex-col items-center justify-center gap-8">
<Fan class="h-32 w-32" />
<div class="w-[20%] min-w-[300px] space-y-4 rounded-lg bg-secondary p-4">
<h1 class="text-center text-4xl font-bold">Login</h1>
<LoginForm data={data.form}></LoginForm>
</div>
</div>

View File

@@ -4,4 +4,4 @@ export const load = (async ({ cookies }) => {
cookies.delete('token', { path: '/' }); cookies.delete('token', { path: '/' });
return {}; return {};
}) satisfies PageServerLoad; }) satisfies PageServerLoad;

View File

@@ -5,7 +5,7 @@
if (browser) { if (browser) {
$token = null; $token = null;
goto('/login'); goto('/login');
} }
</script> </script>

View File

@@ -1,14 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import type { PageData } from './$types';
import RegisterForm from './register-form.svelte';
export let data: PageData;
</script>
<div class="flex h-full w-full items-center justify-center">
<div class="w-[20%] min-w-[300px] space-y-4 rounded-lg bg-secondary p-4">
<h1 class="text-center text-4xl font-bold">Register</h1>
<RegisterForm data={data.form}></RegisterForm>
</div>
</div>

View File

@@ -3,16 +3,16 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors // Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter. // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters. // See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter() adapter: adapter()
} }
}; };
export default config; export default config;

View File

@@ -1,64 +1,64 @@
import { fontFamily } from "tailwindcss/defaultTheme"; import { fontFamily } from 'tailwindcss/defaultTheme';
import type { Config } from "tailwindcss"; import type { Config } from 'tailwindcss';
const config: Config = { const config: Config = {
darkMode: ["class"], darkMode: ['class'],
content: ["./src/**/*.{html,js,svelte,ts}"], content: ['./src/**/*.{html,js,svelte,ts}'],
safelist: ["dark"], safelist: ['dark'],
theme: { theme: {
container: { container: {
center: true, center: true,
padding: "2rem", padding: '2rem',
screens: { screens: {
"2xl": "1400px" '2xl': '1400px'
} }
}, },
extend: { extend: {
colors: { colors: {
border: "hsl(var(--border) / <alpha-value>)", border: 'hsl(var(--border) / <alpha-value>)',
input: "hsl(var(--input) / <alpha-value>)", input: 'hsl(var(--input) / <alpha-value>)',
ring: "hsl(var(--ring) / <alpha-value>)", ring: 'hsl(var(--ring) / <alpha-value>)',
background: "hsl(var(--background) / <alpha-value>)", background: 'hsl(var(--background) / <alpha-value>)',
foreground: "hsl(var(--foreground) / <alpha-value>)", foreground: 'hsl(var(--foreground) / <alpha-value>)',
primary: { primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)", DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
foreground: "hsl(var(--primary-foreground) / <alpha-value>)" foreground: 'hsl(var(--primary-foreground) / <alpha-value>)'
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)", DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)" foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)'
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)", DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)" foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)'
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)", DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
foreground: "hsl(var(--muted-foreground) / <alpha-value>)" foreground: 'hsl(var(--muted-foreground) / <alpha-value>)'
}, },
accent: { accent: {
DEFAULT: "#AD5CD6", DEFAULT: '#AD5CD6',
foreground: "hsl(var(--accent-foreground) / <alpha-value>)" foreground: 'hsl(var(--accent-foreground) / <alpha-value>)'
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)", DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
foreground: "hsl(var(--popover-foreground) / <alpha-value>)" foreground: 'hsl(var(--popover-foreground) / <alpha-value>)'
}, },
card: { card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)", DEFAULT: 'hsl(var(--card) / <alpha-value>)',
foreground: "hsl(var(--card-foreground) / <alpha-value>)" foreground: 'hsl(var(--card-foreground) / <alpha-value>)'
} }
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: 'var(--radius)',
md: "calc(var(--radius) - 2px)", md: 'calc(var(--radius) - 2px)',
sm: "calc(var(--radius) - 4px)" sm: 'calc(var(--radius) - 4px)'
}, },
fontFamily: { fontFamily: {
sans: [...fontFamily.sans] sans: [...fontFamily.sans]
} }
} }
}, }
}; };
export default config; export default config;

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