build: Install and configure ShadCN

This commit is contained in:
April Hall 2025-02-05 18:06:49 -05:00
parent 25246247cc
commit abecae1fee
Signed by: arithefirst
GPG Key ID: 4508A15C4DB91C5B
45 changed files with 1062 additions and 163 deletions

BIN
bun.lockb

Binary file not shown.

14
components.json Normal file
View 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
}

View File

@ -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"

View File

@ -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;
}
} }

View File

@ -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>

View 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>

View 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>

View 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>

View 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'];

View 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>

View 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,
};

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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';

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -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} />

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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,
};

View 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,
};

View 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} />

View 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'];

View 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>

View 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>

View 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>

View 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>

View 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} />

View 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>

View 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>

View File

@ -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
View 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,
};
};

View File

@ -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>

View File

@ -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>

View File

@ -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/*',
},
}, },
}; };

View File

@ -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',
}, },
fontFamily: { extend: {
heading: 'IBM Plex Sans', colors: {
body: 'IBM Plex Sans', border: 'hsl(var(--border) / <alpha-value>)',
}, input: 'hsl(var(--input) / <alpha-value>)',
fontWeight: { ring: 'hsl(var(--ring) / <alpha-value>)',
normal: '400', background: 'hsl(var(--background) / <alpha-value>)',
bold: '700', foreground: 'hsl(var(--foreground) / <alpha-value>)',
}, primary: {
}, DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
daisyui: { foreground: 'hsl(var(--primary-foreground) / <alpha-value>)',
themes: [
{
light: {
"primary": "#5fb979",
"secondary": "#9fdfb3",
"accent": "#66db89",
"neutral": "#1b3222",
"base-100": "#f2f8f4",
}, },
dark: { secondary: {
"primary": "#46a05f", DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
"secondary": "#206034", foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)',
"accent": "#249947", },
"neutral": "#1b3222", destructive: {
"base-100": "#2D2D2D", 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: {
sans: [...fontFamily.sans],
},
},
}, },
} satisfies Config; };
export default config;