build: Install and configure ShadCN
This commit is contained in:
parent
25246247cc
commit
abecae1fee
14
components.json
Normal file
14
components.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "stone"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils"
|
||||||
|
},
|
||||||
|
"typescript": true
|
||||||
|
}
|
41
package.json
41
package.json
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "chatapp",
|
"name": "svchat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@ -7,38 +7,43 @@
|
|||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "PORT=3005 tsm ./prodServer.ts",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"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",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "prettier --check . && eslint ."
|
"lint": "prettier --check . && eslint ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.3",
|
"@eslint/compat": "^1.2.5",
|
||||||
"@sveltejs/adapter-node": "^5.2.11",
|
"@eslint/js": "^9.18.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/adapter-auto": "^4.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/kit": "^2.16.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.7.0",
|
"bits-ui": "^0.22.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"clsx": "^2.1.1",
|
||||||
"eslint-plugin-svelte": "^2.36.0",
|
"eslint": "^9.18.0",
|
||||||
"globals": "^15.0.0",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"prettier": "^3.3.2",
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"globals": "^15.14.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.10",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tailwindcss": "^3.4.9",
|
"tailwind-merge": "^3.0.1",
|
||||||
|
"tailwind-variants": "^0.3.1",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.20.0",
|
||||||
"vite": "^5.4.11"
|
"vite": "^6.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lucide-svelte": "^0.474.0",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"cassandra-driver": "^4.7.2",
|
"cassandra-driver": "^4.7.2",
|
||||||
"daisyui": "^4.12.23",
|
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"socket.io": "^4.8.1",
|
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"tsm": "^2.3.0",
|
"tsm": "^2.3.0",
|
||||||
"uuid": "^11.0.4"
|
"uuid": "^11.0.4"
|
||||||
|
64
src/app.css
64
src/app.css
@ -1,10 +1,58 @@
|
|||||||
@import 'tailwindcss/base';
|
@tailwind base;
|
||||||
@import 'tailwindcss/components';
|
@tailwind components;
|
||||||
@import 'tailwindcss/utilities';
|
@tailwind utilities;
|
||||||
|
|
||||||
.abs-centered {
|
@layer base {
|
||||||
position: absolute;
|
:root {
|
||||||
left: 50%;
|
--background: 0 0% 100%;
|
||||||
top: 50%;
|
--foreground: 240 10% 3.9%;
|
||||||
transform: translate(-50%, -50%);
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary: 142.1 76.2% 36.3%;
|
||||||
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 72.22% 50.59%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 142.1 76.2% 36.3%;
|
||||||
|
--radius: 0.3rem;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--foreground: 0 0% 95%;
|
||||||
|
--card: 24 9.8% 10%;
|
||||||
|
--card-foreground: 0 0% 95%;
|
||||||
|
--popover: 0 0% 9%;
|
||||||
|
--popover-foreground: 0 0% 95%;
|
||||||
|
--primary: 142.1 70.6% 45.3%;
|
||||||
|
--primary-foreground: 144.9 80.4% 10%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 15%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 85.7% 97.3%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 142.4 71.8% 29.2%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
27
src/app.html
27
src/app.html
@ -1,31 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" data-theme="light">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<!--
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
SVChat -- A is a simple chat app built with SvelteKit, Drizzle ORM, and PostgreSQL
|
|
||||||
Copyright (C) 2025 April Hall (ari@arithefirst.com)
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
-->
|
|
||||||
<body data-sveltekit-preload-data="hover" class="h-screen">
|
|
||||||
<div style="display: contents;">%sveltekit.body%</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
20
src/lib/components/channel.svelte
Normal file
20
src/lib/components/channel.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import MessageSquare from 'lucide-svelte/icons/message-square';
|
||||||
|
import MessageUnread from 'lucide-svelte/icons/message-square-dot';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
channelName: string;
|
||||||
|
unread?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { channelName, unread = false }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href={`/channel/${channelName}`} class="text-muted-foreground hover:text-primary flex items-center gap-3 rounded-lg px-3 py-2 transition-all">
|
||||||
|
{#if unread}
|
||||||
|
<MessageUnread class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<MessageSquare class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
{channelName}
|
||||||
|
</a>
|
40
src/lib/components/mainLayout.svelte
Normal file
40
src/lib/components/mainLayout.svelte
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import MessagesSquare from 'lucide-svelte/icons/messages-square';
|
||||||
|
import Channel from './channel.svelte';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
channels: string[];
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { channels, children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
|
||||||
|
<div class="bg-muted/40 hidden border-r md:block">
|
||||||
|
<div class="flex h-full max-h-screen flex-col gap-2">
|
||||||
|
<div class="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
|
||||||
|
<a href="/" class="flex items-center gap-2 font-semibold">
|
||||||
|
<MessagesSquare class="h-6 w-6" />
|
||||||
|
<span class="">SVChat</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<nav class="grid items-start px-2 text-sm font-medium lg:px-4">
|
||||||
|
{#each channels as channelName}
|
||||||
|
<Channel {channelName} />
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<main class="flex flex-1 flex-col gap-4 p-4 lg:gap-6 lg:p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h1 class="text-lg font-semibold md:text-2xl">bello</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 items-center justify-center rounded-lg border border-dashed border-black shadow-sm"></div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
13
src/lib/components/ui/badge/badge.svelte
Normal file
13
src/lib/components/ui/badge/badge.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type Variant, badgeVariants } from './index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let className: string | undefined | null = undefined;
|
||||||
|
export let href: string | undefined = undefined;
|
||||||
|
export let variant: Variant = 'default';
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element this={href ? 'a' : 'span'} {href} class={cn(badgeVariants({ variant, className }))} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</svelte:element>
|
19
src/lib/components/ui/badge/index.ts
Normal file
19
src/lib/components/ui/badge/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export { default as Badge } from './badge.svelte';
|
||||||
|
export const badgeVariants = tv({
|
||||||
|
base: 'focus:ring-ring inline-flex select-none items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground hover:bg-primary/80 border-transparent shadow',
|
||||||
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent',
|
||||||
|
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent shadow',
|
||||||
|
outline: 'text-foreground',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Variant = VariantProps<typeof badgeVariants>['variant'];
|
18
src/lib/components/ui/button/button.svelte
Normal file
18
src/lib/components/ui/button/button.svelte
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button as ButtonPrimitive } from 'bits-ui';
|
||||||
|
import { type Events, type Props, buttonVariants } from './index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = Props;
|
||||||
|
type $$Events = Events;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export let variant: $$Props['variant'] = 'default';
|
||||||
|
export let size: $$Props['size'] = 'default';
|
||||||
|
export let builders: $$Props['builders'] = [];
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ButtonPrimitive.Root {builders} class={cn(buttonVariants({ variant, size, className }))} type="button" {...$$restProps} on:click on:keydown>
|
||||||
|
<slot />
|
||||||
|
</ButtonPrimitive.Root>
|
48
src/lib/components/ui/button/index.ts
Normal file
48
src/lib/components/ui/button/index.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { Button as ButtonPrimitive } from 'bits-ui';
|
||||||
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
|
import Root from './button.svelte';
|
||||||
|
|
||||||
|
const buttonVariants = tv({
|
||||||
|
base: 'focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow',
|
||||||
|
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm',
|
||||||
|
outline: 'border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm',
|
||||||
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm',
|
||||||
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-9 px-4 py-2',
|
||||||
|
sm: 'h-8 rounded-md px-3 text-xs',
|
||||||
|
lg: 'h-10 rounded-md px-8',
|
||||||
|
icon: 'h-9 w-9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type Variant = VariantProps<typeof buttonVariants>['variant'];
|
||||||
|
type Size = VariantProps<typeof buttonVariants>['size'];
|
||||||
|
|
||||||
|
type Props = ButtonPrimitive.Props & {
|
||||||
|
variant?: Variant;
|
||||||
|
size?: Size;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Events = ButtonPrimitive.Events;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type Props,
|
||||||
|
type Events,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
type Props as ButtonProps,
|
||||||
|
type Events as ButtonEvents,
|
||||||
|
buttonVariants,
|
||||||
|
};
|
13
src/lib/components/ui/card/card-content.svelte
Normal file
13
src/lib/components/ui/card/card-content.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn('p-6', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
13
src/lib/components/ui/card/card-description.svelte
Normal file
13
src/lib/components/ui/card/card-description.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLParagraphElement>;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p class={cn('text-muted-foreground text-sm', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</p>
|
13
src/lib/components/ui/card/card-footer.svelte
Normal file
13
src/lib/components/ui/card/card-footer.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn('flex items-center p-6 pt-0', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
13
src/lib/components/ui/card/card-header.svelte
Normal file
13
src/lib/components/ui/card/card-header.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn('flex flex-col space-y-1.5 p-6 pb-0', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
17
src/lib/components/ui/card/card-title.svelte
Normal file
17
src/lib/components/ui/card/card-title.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import type { HeadingLevel } from './index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
||||||
|
tag?: HeadingLevel;
|
||||||
|
};
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export let tag: $$Props['tag'] = 'h3';
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element this={tag} class={cn('font-semibold leading-none tracking-tight', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</svelte:element>
|
21
src/lib/components/ui/card/card.svelte
Normal file
21
src/lib/components/ui/card/card.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<div
|
||||||
|
class={cn('bg-card text-card-foreground rounded-xl border shadow', className)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:mouseenter
|
||||||
|
on:mouseleave>
|
||||||
|
<slot />
|
||||||
|
</div>
|
24
src/lib/components/ui/card/index.ts
Normal file
24
src/lib/components/ui/card/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import Root from './card.svelte';
|
||||||
|
import Content from './card-content.svelte';
|
||||||
|
import Description from './card-description.svelte';
|
||||||
|
import Footer from './card-footer.svelte';
|
||||||
|
import Header from './card-header.svelte';
|
||||||
|
import Title from './card-title.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Title,
|
||||||
|
//
|
||||||
|
Root as Card,
|
||||||
|
Content as CardContent,
|
||||||
|
Description as CardDescription,
|
||||||
|
Footer as CardFooter,
|
||||||
|
Header as CardHeader,
|
||||||
|
Title as CardTitle,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import Check from 'svelte-radix/Check.svelte';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
|
||||||
|
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export let checked: $$Props['checked'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
bind:checked
|
||||||
|
class={cn(
|
||||||
|
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerdown
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove>
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.CheckboxIndicator>
|
||||||
|
<Check class="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.CheckboxIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import { cn, flyAndScale } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.ContentProps;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export let sideOffset: $$Props['sideOffset'] = 4;
|
||||||
|
export let transition: $$Props['transition'] = flyAndScale;
|
||||||
|
export let transitionConfig: $$Props['transitionConfig'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
{transition}
|
||||||
|
{transitionConfig}
|
||||||
|
{sideOffset}
|
||||||
|
class={cn('bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-md focus:outline-none', className)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:keydown>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.Content>
|
@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.ItemProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
};
|
||||||
|
type $$Events = DropdownMenuPrimitive.ItemEvents;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export let inset: $$Props['inset'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
class={cn(
|
||||||
|
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground 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-[disabled]:opacity-50',
|
||||||
|
inset && 'pl-8',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerdown
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove
|
||||||
|
{...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.Item>
|
@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.LabelProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export let inset: $$Props['inset'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Label class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.Label>
|
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
|
||||||
|
|
||||||
|
export let value: $$Props['value'] = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.RadioGroup>
|
@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import DotFilled from 'svelte-radix/DotFilled.svelte';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.RadioItemProps;
|
||||||
|
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export let value: DropdownMenuPrimitive.RadioItemProps['value'];
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
class={cn(
|
||||||
|
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{value}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerdown
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove>
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.RadioIndicator>
|
||||||
|
<DotFilled class="h-4 w-4 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.RadioIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.SeparatorProps;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Separator class={cn('bg-muted -mx-1 my-1 h-px', className)} {...$$restProps} />
|
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</span>
|
@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import { cn, flyAndScale } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.SubContentProps;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export let transition: $$Props['transition'] = flyAndScale;
|
||||||
|
export let transitionConfig: $$Props['transitionConfig'] = {
|
||||||
|
x: -10,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
{transition}
|
||||||
|
{transitionConfig}
|
||||||
|
class={cn('bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none', className)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:keydown
|
||||||
|
on:focusout
|
||||||
|
on:pointermove>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.SubContent>
|
@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import ChevronRight from 'svelte-radix/ChevronRight.svelte';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
};
|
||||||
|
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export let inset: $$Props['inset'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
class={cn(
|
||||||
|
'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',
|
||||||
|
inset && 'pl-8',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove>
|
||||||
|
<slot />
|
||||||
|
<ChevronRight class="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
48
src/lib/components/ui/dropdown-menu/index.ts
Normal file
48
src/lib/components/ui/dropdown-menu/index.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import Item from './dropdown-menu-item.svelte';
|
||||||
|
import Label from './dropdown-menu-label.svelte';
|
||||||
|
import Content from './dropdown-menu-content.svelte';
|
||||||
|
import Shortcut from './dropdown-menu-shortcut.svelte';
|
||||||
|
import RadioItem from './dropdown-menu-radio-item.svelte';
|
||||||
|
import Separator from './dropdown-menu-separator.svelte';
|
||||||
|
import RadioGroup from './dropdown-menu-radio-group.svelte';
|
||||||
|
import SubContent from './dropdown-menu-sub-content.svelte';
|
||||||
|
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
|
||||||
|
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
|
||||||
|
|
||||||
|
const Sub = DropdownMenuPrimitive.Sub;
|
||||||
|
const Root = DropdownMenuPrimitive.Root;
|
||||||
|
const Trigger = DropdownMenuPrimitive.Trigger;
|
||||||
|
const Group = DropdownMenuPrimitive.Group;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sub,
|
||||||
|
Root,
|
||||||
|
Item,
|
||||||
|
Label,
|
||||||
|
Group,
|
||||||
|
Trigger,
|
||||||
|
Content,
|
||||||
|
Shortcut,
|
||||||
|
Separator,
|
||||||
|
RadioItem,
|
||||||
|
SubContent,
|
||||||
|
SubTrigger,
|
||||||
|
RadioGroup,
|
||||||
|
CheckboxItem,
|
||||||
|
//
|
||||||
|
Root as DropdownMenu,
|
||||||
|
Sub as DropdownMenuSub,
|
||||||
|
Item as DropdownMenuItem,
|
||||||
|
Label as DropdownMenuLabel,
|
||||||
|
Group as DropdownMenuGroup,
|
||||||
|
Content as DropdownMenuContent,
|
||||||
|
Trigger as DropdownMenuTrigger,
|
||||||
|
Shortcut as DropdownMenuShortcut,
|
||||||
|
RadioItem as DropdownMenuRadioItem,
|
||||||
|
Separator as DropdownMenuSeparator,
|
||||||
|
RadioGroup as DropdownMenuRadioGroup,
|
||||||
|
SubContent as DropdownMenuSubContent,
|
||||||
|
SubTrigger as DropdownMenuSubTrigger,
|
||||||
|
CheckboxItem as DropdownMenuCheckboxItem,
|
||||||
|
};
|
29
src/lib/components/ui/input/index.ts
Normal file
29
src/lib/components/ui/input/index.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import Root from './input.svelte';
|
||||||
|
|
||||||
|
export type FormInputEvent<T extends Event = Event> = T & {
|
||||||
|
currentTarget: EventTarget & HTMLInputElement;
|
||||||
|
};
|
||||||
|
export type InputEvents = {
|
||||||
|
blur: FormInputEvent<FocusEvent>;
|
||||||
|
change: FormInputEvent<Event>;
|
||||||
|
click: FormInputEvent<MouseEvent>;
|
||||||
|
focus: FormInputEvent<FocusEvent>;
|
||||||
|
focusin: FormInputEvent<FocusEvent>;
|
||||||
|
focusout: FormInputEvent<FocusEvent>;
|
||||||
|
keydown: FormInputEvent<KeyboardEvent>;
|
||||||
|
keypress: FormInputEvent<KeyboardEvent>;
|
||||||
|
keyup: FormInputEvent<KeyboardEvent>;
|
||||||
|
mouseover: FormInputEvent<MouseEvent>;
|
||||||
|
mouseenter: FormInputEvent<MouseEvent>;
|
||||||
|
mouseleave: FormInputEvent<MouseEvent>;
|
||||||
|
mousemove: FormInputEvent<MouseEvent>;
|
||||||
|
paste: FormInputEvent<ClipboardEvent>;
|
||||||
|
input: FormInputEvent<InputEvent>;
|
||||||
|
wheel: FormInputEvent<WheelEvent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input,
|
||||||
|
};
|
41
src/lib/components/ui/input/input.svelte
Normal file
41
src/lib/components/ui/input/input.svelte
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||||
|
import type { InputEvents } from './index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = HTMLInputAttributes;
|
||||||
|
type $$Events = InputEvents;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export let value: $$Props['value'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
|
// Workaround for https://github.com/sveltejs/svelte/issues/9305
|
||||||
|
// Fixed in Svelte 5, but not backported to 4.x.
|
||||||
|
export let readonly: $$Props['readonly'] = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class={cn(
|
||||||
|
'border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
bind:value
|
||||||
|
{readonly}
|
||||||
|
on:blur
|
||||||
|
on:change
|
||||||
|
on:click
|
||||||
|
on:focus
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:keydown
|
||||||
|
on:keypress
|
||||||
|
on:keyup
|
||||||
|
on:mouseover
|
||||||
|
on:mouseenter
|
||||||
|
on:mouseleave
|
||||||
|
on:mousemove
|
||||||
|
on:paste
|
||||||
|
on:input
|
||||||
|
on:wheel|passive
|
||||||
|
{...$$restProps} />
|
106
src/lib/components/ui/sheet/index.ts
Normal file
106
src/lib/components/ui/sheet/index.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||||
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
import Portal from './sheet-portal.svelte';
|
||||||
|
import Overlay from './sheet-overlay.svelte';
|
||||||
|
import Content from './sheet-content.svelte';
|
||||||
|
import Header from './sheet-header.svelte';
|
||||||
|
import Footer from './sheet-footer.svelte';
|
||||||
|
import Title from './sheet-title.svelte';
|
||||||
|
import Description from './sheet-description.svelte';
|
||||||
|
|
||||||
|
const Root = SheetPrimitive.Root;
|
||||||
|
const Close = SheetPrimitive.Close;
|
||||||
|
const Trigger = SheetPrimitive.Trigger;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Close,
|
||||||
|
Trigger,
|
||||||
|
Portal,
|
||||||
|
Overlay,
|
||||||
|
Content,
|
||||||
|
Header,
|
||||||
|
Footer,
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
//
|
||||||
|
Root as Sheet,
|
||||||
|
Close as SheetClose,
|
||||||
|
Trigger as SheetTrigger,
|
||||||
|
Portal as SheetPortal,
|
||||||
|
Overlay as SheetOverlay,
|
||||||
|
Content as SheetContent,
|
||||||
|
Header as SheetHeader,
|
||||||
|
Footer as SheetFooter,
|
||||||
|
Title as SheetTitle,
|
||||||
|
Description as SheetDescription,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sheetVariants = tv({
|
||||||
|
base: 'bg-background fixed z-50 gap-4 p-6 shadow-lg',
|
||||||
|
variants: {
|
||||||
|
side: {
|
||||||
|
top: 'inset-x-0 top-0 border-b ',
|
||||||
|
bottom: 'inset-x-0 bottom-0 border-t',
|
||||||
|
left: 'inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||||
|
right: 'inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
side: 'right',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sheetTransitions = {
|
||||||
|
top: {
|
||||||
|
in: {
|
||||||
|
y: '-100%',
|
||||||
|
duration: 500,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
out: {
|
||||||
|
y: '-100%',
|
||||||
|
duration: 300,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
in: {
|
||||||
|
y: '100%',
|
||||||
|
duration: 500,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
out: {
|
||||||
|
y: '100%',
|
||||||
|
duration: 300,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
in: {
|
||||||
|
x: '-100%',
|
||||||
|
duration: 500,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
out: {
|
||||||
|
x: '-100%',
|
||||||
|
duration: 300,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
in: {
|
||||||
|
x: '100%',
|
||||||
|
duration: 500,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
out: {
|
||||||
|
x: '100%',
|
||||||
|
duration: 300,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Side = VariantProps<typeof sheetVariants>['side'];
|
37
src/lib/components/ui/sheet/sheet-content.svelte
Normal file
37
src/lib/components/ui/sheet/sheet-content.svelte
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||||
|
import Cross2 from 'svelte-radix/Cross2.svelte';
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
import { SheetOverlay, SheetPortal, type Side, sheetTransitions, sheetVariants } from './index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = SheetPrimitive.ContentProps & {
|
||||||
|
side?: Side;
|
||||||
|
};
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export let side: $$Props['side'] = 'right';
|
||||||
|
export { className as class };
|
||||||
|
export let inTransition: $$Props['inTransition'] = fly;
|
||||||
|
export let inTransitionConfig: $$Props['inTransitionConfig'] = sheetTransitions[side ?? 'right'].in;
|
||||||
|
export let outTransition: $$Props['outTransition'] = fly;
|
||||||
|
export let outTransitionConfig: $$Props['outTransitionConfig'] = sheetTransitions[side ?? 'right'].out;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
{inTransition}
|
||||||
|
{inTransitionConfig}
|
||||||
|
{outTransition}
|
||||||
|
{outTransitionConfig}
|
||||||
|
class={cn(sheetVariants({ side }), className)}
|
||||||
|
{...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
<SheetPrimitive.Close
|
||||||
|
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
||||||
|
<Cross2 class="h-4 w-4" />
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
13
src/lib/components/ui/sheet/sheet-description.svelte
Normal file
13
src/lib/components/ui/sheet/sheet-description.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = SheetPrimitive.DescriptionProps;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SheetPrimitive.Description class={cn('text-muted-foreground text-sm', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</SheetPrimitive.Description>
|
13
src/lib/components/ui/sheet/sheet-footer.svelte
Normal file
13
src/lib/components/ui/sheet/sheet-footer.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
13
src/lib/components/ui/sheet/sheet-header.svelte
Normal file
13
src/lib/components/ui/sheet/sheet-header.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
20
src/lib/components/ui/sheet/sheet-overlay.svelte
Normal file
20
src/lib/components/ui/sheet/sheet-overlay.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = SheetPrimitive.OverlayProps;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
export let transition: $$Props['transition'] = fade;
|
||||||
|
export let transitionConfig: $$Props['transitionConfig'] = {
|
||||||
|
duration: 150,
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
{transition}
|
||||||
|
{transitionConfig}
|
||||||
|
class={cn('bg-background/80 fixed inset-0 z-50 backdrop-blur-sm', className)}
|
||||||
|
{...$$restProps} />
|
13
src/lib/components/ui/sheet/sheet-portal.svelte
Normal file
13
src/lib/components/ui/sheet/sheet-portal.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = SheetPrimitive.PortalProps;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SheetPrimitive.Portal class={cn(className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</SheetPrimitive.Portal>
|
13
src/lib/components/ui/sheet/sheet-title.svelte
Normal file
13
src/lib/components/ui/sheet/sheet-title.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type $$Props = SheetPrimitive.TitleProps;
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SheetPrimitive.Title class={cn('text-foreground text-lg font-semibold', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</SheetPrimitive.Title>
|
@ -1,17 +1,5 @@
|
|||||||
import cassandra from 'cassandra-driver';
|
import cassandra from 'cassandra-driver';
|
||||||
|
|
||||||
function reverseArray(array: cassandra.types.Row[]) {
|
|
||||||
let left = null;
|
|
||||||
let right = null;
|
|
||||||
const length = array.length;
|
|
||||||
for (left = 0, right = length - 1; left < right; left += 1, right -= 1) {
|
|
||||||
const temporary = array[left];
|
|
||||||
array[left] = array[right];
|
|
||||||
array[right] = temporary;
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createChannel(client: cassandra.Client, channelName: string) {
|
async function createChannel(client: cassandra.Client, channelName: string) {
|
||||||
try {
|
try {
|
||||||
await client.execute(`
|
await client.execute(`
|
||||||
@ -40,7 +28,7 @@ async function storeMessage(client: cassandra.Client, channelName: string, conte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMessages(client: cassandra.Client, channelName: string, limit: number) {
|
async function getMessages(client: cassandra.Client, channelName: string, limit: number): Promise<cassandra.types.Row[] | undefined> {
|
||||||
try {
|
try {
|
||||||
const res = await client.execute(
|
const res = await client.execute(
|
||||||
`SELECT * FROM channels.channel_${channelName} WHERE channel_name = '${channelName}' ORDER BY timestamp DESC LIMIT ${limit}`,
|
`SELECT * FROM channels.channel_${channelName} WHERE channel_name = '${channelName}' ORDER BY timestamp DESC LIMIT ${limit}`,
|
||||||
@ -49,6 +37,7 @@ async function getMessages(client: cassandra.Client, channelName: string, limit:
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// @ts-expect-error I don't like this thing yelling at me
|
// @ts-expect-error I don't like this thing yelling at me
|
||||||
console.log(`Error fetching messages: ${e.message}`);
|
console.log(`Error fetching messages: ${e.message}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
53
src/lib/utils.ts
Normal file
53
src/lib/utils.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { type ClassValue, clsx } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import { cubicOut } from 'svelte/easing';
|
||||||
|
import type { TransitionConfig } from 'svelte/transition';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlyAndScaleParams = {
|
||||||
|
y?: number;
|
||||||
|
x?: number;
|
||||||
|
start?: number;
|
||||||
|
duration?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const flyAndScale = (node: Element, params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }): TransitionConfig => {
|
||||||
|
const style = getComputedStyle(node);
|
||||||
|
const transform = style.transform === 'none' ? '' : style.transform;
|
||||||
|
|
||||||
|
const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => {
|
||||||
|
const [minA, maxA] = scaleA;
|
||||||
|
const [minB, maxB] = scaleB;
|
||||||
|
|
||||||
|
const percentage = (valueA - minA) / (maxA - minA);
|
||||||
|
const valueB = percentage * (maxB - minB) + minB;
|
||||||
|
|
||||||
|
return valueB;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styleToString = (style: Record<string, number | string | undefined>): string => {
|
||||||
|
return Object.keys(style).reduce((str, key) => {
|
||||||
|
if (style[key] === undefined) return str;
|
||||||
|
return str + `${key}:${style[key]};`;
|
||||||
|
}, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
duration: params.duration ?? 200,
|
||||||
|
delay: 0,
|
||||||
|
css: (t) => {
|
||||||
|
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
|
||||||
|
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
|
||||||
|
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
|
||||||
|
|
||||||
|
return styleToString({
|
||||||
|
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
|
||||||
|
opacity: t,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
easing: cubicOut,
|
||||||
|
};
|
||||||
|
};
|
@ -1,6 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
import MainLayout from '$lib/components/mainLayout.svelte';
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
const channels: string[] = [];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@render children()}
|
<MainLayout {channels}>
|
||||||
|
{@render children()}
|
||||||
|
</MainLayout>
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { io } from 'socket.io-client';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import Message from '$lib/components/message.svelte';
|
|
||||||
import SendIcon from '$lib/icons/SendIcon.svelte';
|
|
||||||
import { type TypeMessage } from '$lib';
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
|
||||||
let user: string | undefined;
|
|
||||||
let socket: ReturnType<typeof io> | null = null;
|
|
||||||
let log: TypeMessage[] = $state([]);
|
|
||||||
let msg: string = $state('');
|
|
||||||
|
|
||||||
function logEvent(newMsg: TypeMessage) {
|
|
||||||
log = [newMsg, ...log];
|
|
||||||
}
|
|
||||||
|
|
||||||
function establishSocketIOConnection() {
|
|
||||||
if (socket) return;
|
|
||||||
socket = io();
|
|
||||||
|
|
||||||
socket.on('message', (data: TypeMessage) => {
|
|
||||||
console.log('[ws] message received', data);
|
|
||||||
logEvent(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendMessage() {
|
|
||||||
if (!socket) return;
|
|
||||||
socket.emit('message', { id: user, content: msg });
|
|
||||||
msg = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
establishSocketIOConnection();
|
|
||||||
user = uuidv4();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#snippet message(messages: TypeMessage[])}
|
|
||||||
{#each messages as message}
|
|
||||||
<Message imageSrc={message.imageSrc} user={message.user} message={message.message} />
|
|
||||||
{/each}
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<main class="h-full flex flex-col">
|
|
||||||
<div class="w-full">
|
|
||||||
<h1 class="text-center text-base-content text-2xl"><span class="text-primary">SV</span>Chat</h1>
|
|
||||||
<hr class="w-11/12 border-primary border-1 mx-auto" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex-grow flex-col-reverse flex flex-auto overflow-y-scroll overflow-x-hidden mx-2 mt-2 mb-1 rounded-lg bg-base-200 border-2 border-base-300">
|
|
||||||
{@render message(log)}
|
|
||||||
{@render message(data.messages)}
|
|
||||||
</div>
|
|
||||||
<form class="flex mb-2 mx-2 mt-1" onsubmit={sendMessage}>
|
|
||||||
<input type="text" placeholder="Type here" class="input border-base-300 w-5/6 mr-1 border-2" bind:value={msg} />
|
|
||||||
<button aria-label="send message" class="btn w-1/6 btn-primary" type="submit"><SendIcon /></button>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
@ -1,4 +1,4 @@
|
|||||||
import adapter from '@sveltejs/adapter-node';
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
@ -12,6 +12,9 @@ const config = {
|
|||||||
// 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://svelte.dev/docs/kit/adapters for more information about adapters.
|
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
|
alias: {
|
||||||
|
'@/*': './path/to/lib/*',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,46 +1,64 @@
|
|||||||
|
import { fontFamily } from 'tailwindcss/defaultTheme';
|
||||||
import type { Config } from 'tailwindcss';
|
import type { Config } from 'tailwindcss';
|
||||||
import daisyui from 'daisyui';
|
|
||||||
|
|
||||||
export default {
|
const config: Config = {
|
||||||
|
darkMode: ['class'],
|
||||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
plugins: [daisyui],
|
safelist: ['dark'],
|
||||||
theme: {
|
theme: {
|
||||||
fontSize: {
|
container: {
|
||||||
sm: '0.750rem',
|
center: true,
|
||||||
base: '1rem',
|
padding: '2rem',
|
||||||
xl: '1.333rem',
|
screens: {
|
||||||
'2xl': '1.777rem',
|
'2xl': '1400px',
|
||||||
'3xl': '2.369rem',
|
},
|
||||||
'4xl': '3.158rem',
|
},
|
||||||
'5xl': '4.210rem',
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: 'hsl(var(--border) / <alpha-value>)',
|
||||||
|
input: 'hsl(var(--input) / <alpha-value>)',
|
||||||
|
ring: 'hsl(var(--ring) / <alpha-value>)',
|
||||||
|
background: 'hsl(var(--background) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--foreground) / <alpha-value>)',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--card-foreground) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
heading: 'IBM Plex Sans',
|
sans: [...fontFamily.sans],
|
||||||
body: 'IBM Plex Sans',
|
|
||||||
},
|
|
||||||
fontWeight: {
|
|
||||||
normal: '400',
|
|
||||||
bold: '700',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
daisyui: {
|
|
||||||
themes: [
|
|
||||||
{
|
|
||||||
light: {
|
|
||||||
"primary": "#5fb979",
|
|
||||||
"secondary": "#9fdfb3",
|
|
||||||
"accent": "#66db89",
|
|
||||||
"neutral": "#1b3222",
|
|
||||||
"base-100": "#f2f8f4",
|
|
||||||
},
|
},
|
||||||
dark: {
|
};
|
||||||
"primary": "#46a05f",
|
|
||||||
"secondary": "#206034",
|
export default config;
|
||||||
"accent": "#249947",
|
|
||||||
"neutral": "#1b3222",
|
|
||||||
"base-100": "#2D2D2D",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
} satisfies Config;
|
|
||||||
|
Loading…
Reference in New Issue
Block a user