diff --git a/.prettierrc b/.prettierrc index 8bc6e86..d173948 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,8 +1,18 @@ { - "useTabs": true, - "singleQuote": true, - "trailingComma": "none", - "printWidth": 100, - "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], - "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] -} + "tabWidth": 4, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": [ + "prettier-plugin-svelte", + "prettier-plugin-tailwindcss" + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 58960aa..9931be6 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 2f95a54..9c2defb 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,17 @@ }, "type": "module", "dependencies": { + "axios": "^1.6.8", + "bits-ui": "^0.21.7", "clsx": "^2.1.1", + "formsnap": "^1.0.0", + "lucide-svelte": "^0.378.0", + "mode-watcher": "^0.3.0", + "svelte-persisted-store": "^0.9.2", + "svelte-sonner": "^0.3.24", + "sveltekit-superforms": "^2.13.1", "tailwind-merge": "^2.3.0", - "tailwind-variants": "^0.2.1" + "tailwind-variants": "^0.2.1", + "zod": "^3.23.8" } } diff --git a/src/app.d.ts b/src/app.d.ts index 743f07b..d1b0cb7 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,13 +1,13 @@ // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } } -export {}; +export { }; diff --git a/src/app.html b/src/app.html index 77a5ff5..257c68a 100644 --- a/src/app.html +++ b/src/app.html @@ -1,12 +1,28 @@ + + - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - + + + + + + + %sveltekit.head% + + + + + +
%sveltekit.body%
+ + + \ No newline at end of file diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..f994583 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,6 @@ +import type { Handle } from "@sveltejs/kit"; + +export const handle: Handle = async ({ event, resolve }) => { + const response = await resolve(event); + return response; +}; \ No newline at end of file diff --git a/src/lib/api/channel.ts b/src/lib/api/channel.ts new file mode 100644 index 0000000..44f7b5d --- /dev/null +++ b/src/lib/api/channel.ts @@ -0,0 +1,30 @@ +import type { Channel, Message } from '../types'; +import { apiRequest } from './utils'; + +export async function getAllChannels() { + return await apiRequest('/channel', 'get'); +} + +export async function getChannelById(channelId: number) { + return await apiRequest(`/channel/${channelId}`, 'get'); +} + +export async function createChannel(name: string) { + return await apiRequest('/channel', 'post', { data: { name } }); +} + +export async function deleteChannel(channelId: number) { + return await apiRequest(`/channel/${channelId}`, 'delete'); +} + +export async function addUserToChannel(channelId: number, userId: number) { + return await apiRequest(`/channel/${channelId}/user/${userId}`, 'post'); +} + +export async function removeUserFromChannel(channelId: number, userId: number) { + return await apiRequest(`/channel/${channelId}/user/${userId}`, 'delete'); +} + +export async function getMessagesByChannelId(channelId: number, before_id: number, limit?: number) { + return await apiRequest(`/channel/${channelId}/message`, 'get', { data: { before_id, limit } }); +} \ No newline at end of file diff --git a/src/lib/api/message.ts b/src/lib/api/message.ts new file mode 100644 index 0000000..111f0f4 --- /dev/null +++ b/src/lib/api/message.ts @@ -0,0 +1,10 @@ +import type { Message } from '../types'; +import { apiRequest } from './utils'; + +export async function getMessageById(messageId: number) { + return await apiRequest(`/message/${messageId}`, 'get'); +} + +export async function createMessage(channelId: number, content: string) { + return await apiRequest('/message', 'post', { data: { channelId, content } }); +} \ No newline at end of file diff --git a/src/lib/api/user.ts b/src/lib/api/user.ts new file mode 100644 index 0000000..2763692 --- /dev/null +++ b/src/lib/api/user.ts @@ -0,0 +1,22 @@ +import type { Token, User } from '../types'; +import { apiRequest } from './utils'; + +export async function getByToken(token: string | undefined | null) { + return await apiRequest('/user/me', 'get', { token: token as string | undefined }); +} + +export async function getUserById(userId: number) { + return await apiRequest(`/user/${userId}`, 'get'); +} + +export async function getUserByUsername(username: string) { + return await apiRequest(`/user/username/${username}`, 'get'); +} + +export async function loginUser(username: string, password: string) { + return await apiRequest('/user/login', 'post', { data: { username, password } }); +} + +export async function registerUser(username: string, password: string) { + return await apiRequest('/user/register', 'post', { data: { username, password } }); +} \ No newline at end of file diff --git a/src/lib/api/utils.ts b/src/lib/api/utils.ts new file mode 100644 index 0000000..1e40da8 --- /dev/null +++ b/src/lib/api/utils.ts @@ -0,0 +1,31 @@ +import { API_URL } from "$lib/constants"; +import { getUserToken } from "$lib/stores/user"; +import type { ErrorResponse } from "$lib/types"; +import type { AxiosRequestConfig } from "axios"; +import axios from "axios"; + + + +export async function apiRequest(path: string, method: 'get' | 'post' | 'put' | 'delete', options?: AxiosRequestConfig & { token?: string }): Promise { + const url = API_URL + path; + + const token = options?.token || getUserToken(); + + options = { + url, + method, + headers: { + ...options?.headers, + 'Authorization': `Bearer ${token}` + }, + validateStatus: () => true, + ...options, + } + + const response = await axios.request(options); + + if (response.status === 200) + return response.data as T; + else + return response.data as ErrorResponse; +} \ No newline at end of file diff --git a/src/lib/components/ui/avatar/avatar-fallback.svelte b/src/lib/components/ui/avatar/avatar-fallback.svelte new file mode 100644 index 0000000..865fc40 --- /dev/null +++ b/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/lib/components/ui/avatar/avatar-image.svelte b/src/lib/components/ui/avatar/avatar-image.svelte new file mode 100644 index 0000000..6558dc4 --- /dev/null +++ b/src/lib/components/ui/avatar/avatar-image.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/ui/avatar/avatar.svelte b/src/lib/components/ui/avatar/avatar.svelte new file mode 100644 index 0000000..ba1379b --- /dev/null +++ b/src/lib/components/ui/avatar/avatar.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/src/lib/components/ui/avatar/index.ts b/src/lib/components/ui/avatar/index.ts new file mode 100644 index 0000000..d06457b --- /dev/null +++ b/src/lib/components/ui/avatar/index.ts @@ -0,0 +1,13 @@ +import Root from "./avatar.svelte"; +import Image from "./avatar-image.svelte"; +import Fallback from "./avatar-fallback.svelte"; + +export { + Root, + Image, + Fallback, + // + Root as Avatar, + Image as AvatarImage, + Fallback as AvatarFallback, +}; diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..86827f3 --- /dev/null +++ b/src/lib/components/ui/button/button.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/src/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..a927293 --- /dev/null +++ b/src/lib/components/ui/button/index.ts @@ -0,0 +1,49 @@ +import { type VariantProps, tv } from "tailwind-variants"; +import type { Button as ButtonPrimitive } from "bits-ui"; +import Root from "./button.svelte"; + +const buttonVariants = tv({ + base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, +}); + +type Variant = VariantProps["variant"]; +type Size = VariantProps["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, +}; diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..cbca3c5 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..a94b527 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..9a05d4b --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..43f1527 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..1c74ae1 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..79a48ee --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..e14d078 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..d8c7378 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..ff20507 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,30 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..9ba3916 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,32 @@ + + + + + + diff --git a/src/lib/components/ui/dropdown-menu/index.ts b/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..c1749e9 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/index.ts @@ -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, +}; diff --git a/src/lib/components/ui/form/form-button.svelte b/src/lib/components/ui/form/form-button.svelte new file mode 100644 index 0000000..087c839 --- /dev/null +++ b/src/lib/components/ui/form/form-button.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/src/lib/components/ui/form/form-description.svelte b/src/lib/components/ui/form/form-description.svelte new file mode 100644 index 0000000..7d36254 --- /dev/null +++ b/src/lib/components/ui/form/form-description.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/src/lib/components/ui/form/form-element-field.svelte b/src/lib/components/ui/form/form-element-field.svelte new file mode 100644 index 0000000..2de747e --- /dev/null +++ b/src/lib/components/ui/form/form-element-field.svelte @@ -0,0 +1,25 @@ + + + + + +
+ +
+
diff --git a/src/lib/components/ui/form/form-field-errors.svelte b/src/lib/components/ui/form/form-field-errors.svelte new file mode 100644 index 0000000..9395326 --- /dev/null +++ b/src/lib/components/ui/form/form-field-errors.svelte @@ -0,0 +1,26 @@ + + + + + {#each errors as error} +
{error}
+ {/each} +
+
diff --git a/src/lib/components/ui/form/form-field.svelte b/src/lib/components/ui/form/form-field.svelte new file mode 100644 index 0000000..6e958a3 --- /dev/null +++ b/src/lib/components/ui/form/form-field.svelte @@ -0,0 +1,25 @@ + + + + + +
+ +
+
diff --git a/src/lib/components/ui/form/form-fieldset.svelte b/src/lib/components/ui/form/form-fieldset.svelte new file mode 100644 index 0000000..81e8f1b --- /dev/null +++ b/src/lib/components/ui/form/form-fieldset.svelte @@ -0,0 +1,30 @@ + + + + + + + diff --git a/src/lib/components/ui/form/form-label.svelte b/src/lib/components/ui/form/form-label.svelte new file mode 100644 index 0000000..fcd1028 --- /dev/null +++ b/src/lib/components/ui/form/form-label.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/form/form-legend.svelte b/src/lib/components/ui/form/form-legend.svelte new file mode 100644 index 0000000..3b1387c --- /dev/null +++ b/src/lib/components/ui/form/form-legend.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/src/lib/components/ui/form/index.ts b/src/lib/components/ui/form/index.ts new file mode 100644 index 0000000..0713927 --- /dev/null +++ b/src/lib/components/ui/form/index.ts @@ -0,0 +1,33 @@ +import * as FormPrimitive from "formsnap"; +import Description from "./form-description.svelte"; +import Label from "./form-label.svelte"; +import FieldErrors from "./form-field-errors.svelte"; +import Field from "./form-field.svelte"; +import Fieldset from "./form-fieldset.svelte"; +import Legend from "./form-legend.svelte"; +import ElementField from "./form-element-field.svelte"; +import Button from "./form-button.svelte"; + +const Control = FormPrimitive.Control; + +export { + Field, + Control, + Label, + Button, + FieldErrors, + Description, + Fieldset, + Legend, + ElementField, + // + Field as FormField, + Control as FormControl, + Description as FormDescription, + Label as FormLabel, + FieldErrors as FormFieldErrors, + Fieldset as FormFieldset, + Legend as FormLegend, + ElementField as FormElementField, + Button as FormButton, +}; diff --git a/src/lib/components/ui/input/index.ts b/src/lib/components/ui/input/index.ts new file mode 100644 index 0000000..75e3bc2 --- /dev/null +++ b/src/lib/components/ui/input/index.ts @@ -0,0 +1,29 @@ +import Root from "./input.svelte"; + +export type FormInputEvent = T & { + currentTarget: EventTarget & HTMLInputElement; +}; +export type InputEvents = { + blur: FormInputEvent; + change: FormInputEvent; + click: FormInputEvent; + focus: FormInputEvent; + focusin: FormInputEvent; + focusout: FormInputEvent; + keydown: FormInputEvent; + keypress: FormInputEvent; + keyup: FormInputEvent; + mouseover: FormInputEvent; + mouseenter: FormInputEvent; + mouseleave: FormInputEvent; + mousemove: FormInputEvent; + paste: FormInputEvent; + input: FormInputEvent; + wheel: FormInputEvent; +}; + +export { + Root, + // + Root as Input, +}; diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte new file mode 100644 index 0000000..5824137 --- /dev/null +++ b/src/lib/components/ui/input/input.svelte @@ -0,0 +1,42 @@ + + + diff --git a/src/lib/components/ui/label/index.ts b/src/lib/components/ui/label/index.ts new file mode 100644 index 0000000..8bfca0b --- /dev/null +++ b/src/lib/components/ui/label/index.ts @@ -0,0 +1,7 @@ +import Root from "./label.svelte"; + +export { + Root, + // + Root as Label, +}; diff --git a/src/lib/components/ui/label/label.svelte b/src/lib/components/ui/label/label.svelte new file mode 100644 index 0000000..2a7d479 --- /dev/null +++ b/src/lib/components/ui/label/label.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/ui/popover/index.ts b/src/lib/components/ui/popover/index.ts new file mode 100644 index 0000000..63aecf9 --- /dev/null +++ b/src/lib/components/ui/popover/index.ts @@ -0,0 +1,17 @@ +import { Popover as PopoverPrimitive } from "bits-ui"; +import Content from "./popover-content.svelte"; +const Root = PopoverPrimitive.Root; +const Trigger = PopoverPrimitive.Trigger; +const Close = PopoverPrimitive.Close; + +export { + Root, + Content, + Trigger, + Close, + // + Root as Popover, + Content as PopoverContent, + Trigger as PopoverTrigger, + Close as PopoverClose, +}; diff --git a/src/lib/components/ui/popover/popover-content.svelte b/src/lib/components/ui/popover/popover-content.svelte new file mode 100644 index 0000000..794436c --- /dev/null +++ b/src/lib/components/ui/popover/popover-content.svelte @@ -0,0 +1,22 @@ + + + + + diff --git a/src/lib/components/ui/scroll-area/index.ts b/src/lib/components/ui/scroll-area/index.ts new file mode 100644 index 0000000..e86a25b --- /dev/null +++ b/src/lib/components/ui/scroll-area/index.ts @@ -0,0 +1,10 @@ +import Scrollbar from "./scroll-area-scrollbar.svelte"; +import Root from "./scroll-area.svelte"; + +export { + Root, + Scrollbar, + //, + Root as ScrollArea, + Scrollbar as ScrollAreaScrollbar, +}; diff --git a/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte b/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte new file mode 100644 index 0000000..a97f3bb --- /dev/null +++ b/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte @@ -0,0 +1,27 @@ + + + + + + diff --git a/src/lib/components/ui/scroll-area/scroll-area.svelte b/src/lib/components/ui/scroll-area/scroll-area.svelte new file mode 100644 index 0000000..d9f23c9 --- /dev/null +++ b/src/lib/components/ui/scroll-area/scroll-area.svelte @@ -0,0 +1,66 @@ + + + + + + + + + {#if orientation === 'vertical' || orientation === 'both'} + + {/if} + {#if orientation === 'horizontal' || orientation === 'both'} + + {/if} + + diff --git a/src/lib/components/ui/separator/index.ts b/src/lib/components/ui/separator/index.ts new file mode 100644 index 0000000..82442d2 --- /dev/null +++ b/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from "./separator.svelte"; + +export { + Root, + // + Root as Separator, +}; diff --git a/src/lib/components/ui/separator/separator.svelte b/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 0000000..be3843a --- /dev/null +++ b/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/ui/skeleton/index.ts b/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..186db21 --- /dev/null +++ b/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from "./skeleton.svelte"; + +export { + Root, + // + Root as Skeleton, +}; diff --git a/src/lib/components/ui/skeleton/skeleton.svelte b/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..640f112 --- /dev/null +++ b/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,11 @@ + + +
diff --git a/src/lib/components/ui/sonner/index.ts b/src/lib/components/ui/sonner/index.ts new file mode 100644 index 0000000..1ad9f4a --- /dev/null +++ b/src/lib/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./sonner.svelte"; diff --git a/src/lib/components/ui/sonner/sonner.svelte b/src/lib/components/ui/sonner/sonner.svelte new file mode 100644 index 0000000..7d5b2f1 --- /dev/null +++ b/src/lib/components/ui/sonner/sonner.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/switch/index.ts b/src/lib/components/ui/switch/index.ts new file mode 100644 index 0000000..f5533db --- /dev/null +++ b/src/lib/components/ui/switch/index.ts @@ -0,0 +1,7 @@ +import Root from "./switch.svelte"; + +export { + Root, + // + Root as Switch, +}; diff --git a/src/lib/components/ui/switch/switch.svelte b/src/lib/components/ui/switch/switch.svelte new file mode 100644 index 0000000..c6bcfd2 --- /dev/null +++ b/src/lib/components/ui/switch/switch.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/src/lib/components/ui/textarea/index.ts b/src/lib/components/ui/textarea/index.ts new file mode 100644 index 0000000..6eb6ba3 --- /dev/null +++ b/src/lib/components/ui/textarea/index.ts @@ -0,0 +1,28 @@ +import Root from "./textarea.svelte"; + +type FormTextareaEvent = T & { + currentTarget: EventTarget & HTMLTextAreaElement; +}; + +type TextareaEvents = { + blur: FormTextareaEvent; + change: FormTextareaEvent; + click: FormTextareaEvent; + focus: FormTextareaEvent; + keydown: FormTextareaEvent; + keypress: FormTextareaEvent; + keyup: FormTextareaEvent; + mouseover: FormTextareaEvent; + mouseenter: FormTextareaEvent; + mouseleave: FormTextareaEvent; + paste: FormTextareaEvent; + input: FormTextareaEvent; +}; + +export { + Root, + // + Root as Textarea, + type TextareaEvents, + type FormTextareaEvent, +}; diff --git a/src/lib/components/ui/textarea/textarea.svelte b/src/lib/components/ui/textarea/textarea.svelte new file mode 100644 index 0000000..d786257 --- /dev/null +++ b/src/lib/components/ui/textarea/textarea.svelte @@ -0,0 +1,38 @@ + + + diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..1c334e9 --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1 @@ +export const API_URL = 'http://localhost:1234'; \ No newline at end of file diff --git a/src/lib/stores/cache/messages.ts b/src/lib/stores/cache/messages.ts new file mode 100644 index 0000000..cbb0136 --- /dev/null +++ b/src/lib/stores/cache/messages.ts @@ -0,0 +1,42 @@ +import { getMessageById } from "$lib/api/message"; +import { isErrorResponse, type Message } from "$lib/types"; +import { get, writable, type Writable } from "svelte/store"; + +const messagesCache: Writable> = writable(new Map()); + +const runningCaches = new Set(); + +export function addMessageToCache(message: Message) { + messagesCache.update((messages) => messages.set(message.id, message)); +} + +export async function getCachedMessage(messageId: number): Promise { + const cached = get(messagesCache).get(messageId); + + if (cached) + return cached; + + if (runningCaches.has(messageId)) + return await new Promise((resolve) => { + messagesCache.subscribe((users) => { + + const user = users.get(messageId); + if (user) { + runningCaches.delete(messageId); + resolve(user); + } + }); + }); + + runningCaches.add(messageId); + + const response = await getMessageById(messageId); + if (isErrorResponse(response)) + return null; + + const message = response as Message; + + addMessageToCache(message); + + return message; +} \ No newline at end of file diff --git a/src/lib/stores/cache/users.ts b/src/lib/stores/cache/users.ts new file mode 100644 index 0000000..f9a469f --- /dev/null +++ b/src/lib/stores/cache/users.ts @@ -0,0 +1,47 @@ +import { getUserById } from "$lib/api/user"; +import { isErrorResponse, type User } from "$lib/types"; +import { get, writable, type Writable } from "svelte/store"; + +const usersCache: Writable> = writable(new Map()); + +const runningCaches = new Set(); + +usersCache.subscribe((users) => { + console.log(`Cached users: ${JSON.stringify([...users.values()])}`); +}) + +export function addUserToCache(user: User) { + usersCache.update((users) => users.set(user.id, user)); +} + +export async function getCachedUser(userId: number): Promise { + const cachedUser = get(usersCache).get(userId); + + if (cachedUser) + return cachedUser; + + if (runningCaches.has(userId)) { + return await new Promise((resolve) => { + usersCache.subscribe((users) => { + console.log(`subsribed called`); + const user = users.get(userId); + if (user) { + runningCaches.delete(userId); + resolve(user); + } + }); + }); + } + + runningCaches.add(userId); + + const response = await getUserById(userId); + if (isErrorResponse(response)) + return null; + + const user = response as User; + + addUserToCache(user); + + return user; +} \ No newline at end of file diff --git a/src/lib/stores/user.ts b/src/lib/stores/user.ts new file mode 100644 index 0000000..7b71f61 --- /dev/null +++ b/src/lib/stores/user.ts @@ -0,0 +1,22 @@ +import { derived, get, writable } from "svelte/store"; +import { isErrorResponse, type User } from "../types"; +import { getByToken } from "$lib/api/user"; + +export const token = writable(null); + +token.subscribe((token) => { + console.log(`updated token: ${JSON.stringify(token)}`); +}) + +export const user = derived(token, ($token, set) => { + getByToken($token).then((response) => { + if (isErrorResponse(response)) + set(null); + else + set(response); + }) +}); + +export function getUserToken(): string | null { + return get(token); +} \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..c71b2ff --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,36 @@ +export type ErrorResponse = { + error: string; +} + +export function isErrorResponse(data: unknown): data is ErrorResponse { + return ((data as ErrorResponse).error !== undefined); +} + +export type Token = { + token: string; + userId: number; + createdAt: Date; + expiresAt: Date; +} + +export type User = { + id: number; + username: string; + avatar: string | null; + createdAt: Date; +}; + +export type Message = { + id: number; + channelId: number; + authorId: number; + content: string; + createdAt: Date; +}; + +export type Channel = { + id: number; + name: string; + lastMessageId: number | null; + createdAt: Date; +}; \ No newline at end of file diff --git a/src/routes/(auth)/+layout.server.ts b/src/routes/(auth)/+layout.server.ts new file mode 100644 index 0000000..9c9f4fc --- /dev/null +++ b/src/routes/(auth)/+layout.server.ts @@ -0,0 +1,19 @@ +import { redirect } from '@sveltejs/kit'; +import type { LayoutServerLoad } from './$types'; +import { getByToken } from '$lib/api/user'; +import { isErrorResponse } from '$lib/types'; + +export const load = (async ({ cookies }) => { + const token = cookies.get('token'); + + if (!token) + return redirect(302, '/logout'); + + const user = await getByToken(token) + if (isErrorResponse(user)) + return redirect(302, '/logout'); + + console.log(`User: ${JSON.stringify(user)}`) + + return { token, user }; +}) satisfies LayoutServerLoad; \ No newline at end of file diff --git a/src/routes/(auth)/+layout.svelte b/src/routes/(auth)/+layout.svelte new file mode 100644 index 0000000..7fba21f --- /dev/null +++ b/src/routes/(auth)/+layout.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/routes/(auth)/+layout.ts b/src/routes/(auth)/+layout.ts new file mode 100644 index 0000000..e2d9be0 --- /dev/null +++ b/src/routes/(auth)/+layout.ts @@ -0,0 +1 @@ +export const ssr = false \ No newline at end of file diff --git a/src/routes/(auth)/chats/(components)/(chat)/chat.svelte b/src/routes/(auth)/chats/(components)/(chat)/chat.svelte new file mode 100644 index 0000000..f6839ca --- /dev/null +++ b/src/routes/(auth)/chats/(components)/(chat)/chat.svelte @@ -0,0 +1,44 @@ + + +
+
+
+ + + + {username[0].toUpperCase()} + + {username} +
+
+ +
+ +
+
+ { + messages = [ + ...messages, + { content: message, createdAt: new Date(), authorId: 1, id: 1 } + ]; + + setTimeout(() => { + messageArea.scroll('bottom'); + }, 100); + }} + /> +
+
diff --git a/src/routes/(auth)/chats/(components)/(chat)/message-area.svelte b/src/routes/(auth)/chats/(components)/(chat)/message-area.svelte new file mode 100644 index 0000000..0972917 --- /dev/null +++ b/src/routes/(auth)/chats/(components)/(chat)/message-area.svelte @@ -0,0 +1,27 @@ + + +
+ +
+ {#each messages as message} + + {/each} +
+
+
diff --git a/src/routes/(auth)/chats/(components)/(chat)/message.svelte b/src/routes/(auth)/chats/(components)/(chat)/message.svelte new file mode 100644 index 0000000..e4eb1ec --- /dev/null +++ b/src/routes/(auth)/chats/(components)/(chat)/message.svelte @@ -0,0 +1,46 @@ + + +
+
+ {#if !isSelf} + + {username[0].toUpperCase()} + + {/if} + +
+ + {message.content} + + {message.createdAt.toLocaleString()} + +
+ {#if isSelf} + + {username[0].toUpperCase()} + + {/if} +
+
diff --git a/src/routes/(auth)/chats/(components)/(chat)/text-field.svelte b/src/routes/(auth)/chats/(components)/(chat)/text-field.svelte new file mode 100644 index 0000000..25efcf0 --- /dev/null +++ b/src/routes/(auth)/chats/(components)/(chat)/text-field.svelte @@ -0,0 +1,56 @@ + + +
+ +
+
+ + +
diff --git a/src/routes/(auth)/chats/(components)/chat-list-item.svelte b/src/routes/(auth)/chats/(components)/chat-list-item.svelte new file mode 100644 index 0000000..3a0845d --- /dev/null +++ b/src/routes/(auth)/chats/(components)/chat-list-item.svelte @@ -0,0 +1,25 @@ + + + diff --git a/src/routes/(auth)/chats/(components)/chat-list.svelte b/src/routes/(auth)/chats/(components)/chat-list.svelte new file mode 100644 index 0000000..acdd58f --- /dev/null +++ b/src/routes/(auth)/chats/(components)/chat-list.svelte @@ -0,0 +1,32 @@ + + +
+ {#each chats as chat} + select(chat.username)} + /> + {/each} +
diff --git a/src/routes/(auth)/chats/(components)/sidebar-header.svelte b/src/routes/(auth)/chats/(components)/sidebar-header.svelte new file mode 100644 index 0000000..f8bbb81 --- /dev/null +++ b/src/routes/(auth)/chats/(components)/sidebar-header.svelte @@ -0,0 +1,43 @@ + + + + +
+ + + + + + My Account + + {#each menuItems as item} +
+ + {item.name} +
+ {/each} +
+
+
diff --git a/src/routes/(auth)/chats/(components)/sidebar.svelte b/src/routes/(auth)/chats/(components)/sidebar.svelte new file mode 100644 index 0000000..a37d560 --- /dev/null +++ b/src/routes/(auth)/chats/(components)/sidebar.svelte @@ -0,0 +1,18 @@ + + +
+
+ +
+
+ +
+ +
+
+
+
diff --git a/src/routes/(auth)/chats/+layout.svelte b/src/routes/(auth)/chats/+layout.svelte new file mode 100644 index 0000000..04f5c63 --- /dev/null +++ b/src/routes/(auth)/chats/+layout.svelte @@ -0,0 +1,67 @@ + + + + +
+ + +
+ +
+
diff --git a/src/routes/(auth)/chats/+layout.ts b/src/routes/(auth)/chats/+layout.ts new file mode 100644 index 0000000..c656668 --- /dev/null +++ b/src/routes/(auth)/chats/+layout.ts @@ -0,0 +1,11 @@ +import type { LayoutLoad } from './$types'; + +const chats = [ + { username: 'lionarius', lastMessage: 'Привет', avatarUrl: '/default-avatar.png' }, + { username: 'tenebris', lastMessage: 'Как дела', avatarUrl: null }, + { username: 'staheys', lastMessage: 'Hey', avatarUrl: '/default-avatar.png' }, +] + +export const load = (async () => { + return { chats }; +}) satisfies LayoutLoad; \ No newline at end of file diff --git a/src/routes/(auth)/chats/+page.svelte b/src/routes/(auth)/chats/+page.svelte new file mode 100644 index 0000000..246e86b --- /dev/null +++ b/src/routes/(auth)/chats/+page.svelte @@ -0,0 +1,5 @@ +
+
+

No chat selected

+
+
diff --git a/src/routes/(auth)/chats/[username]/+page.svelte b/src/routes/(auth)/chats/[username]/+page.svelte new file mode 100644 index 0000000..13a459a --- /dev/null +++ b/src/routes/(auth)/chats/[username]/+page.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/src/routes/(auth)/chats/[username]/+page.ts b/src/routes/(auth)/chats/[username]/+page.ts new file mode 100644 index 0000000..1a0525d --- /dev/null +++ b/src/routes/(auth)/chats/[username]/+page.ts @@ -0,0 +1,18 @@ +import type { PageLoad } from './$types'; + +const messages = [ + { content: 'Приветdsfsdfsdfsdf', senderId: 1, createdAt: new Date(Date.now() - 1000 * 60 * 25) }, + { content: 'Приветffffffffffdsfsdfsdfffffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsffffffffdsfsdfsdffвеffffffffffdsfsdfsdffвеffветdsfsdfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff дела?', senderId: 2, createdAt: new Date(Date.now() - 1000 * 60 * 20) }, + { content: 'Хорошо', senderId: 1, createdAt: new Date(Date.now() - 1000 * 60 * 15) }, + { content: 'Привет', senderId: 2, createdAt: new Date(Date.now() - 1000 * 60 * 10) }, + { content: 'Приветdsfsdfsdfsdf', senderId: 1, createdAt: new Date(Date.now() - 1000 * 60 * 25) }, + { content: 'fdsfdsfsdfКак дела?', senderId: 2, createdAt: new Date(Date.now() - 1000 * 60 * 20) }, + { content: 'Хорошо', senderId: 1, createdAt: new Date(Date.now() - 1000 * 60 * 15) }, + { content: 'Приветffffffffffdsfsdfsdfffffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsffffffffdsfsdfsdffвеffffffffffdsfsdfsdffвеffветdsfsdfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', senderId: 2, createdAt: new Date(Date.now() - 1000 * 60 * 10) }, + { content: 'Приветffffffffffdsfsdfsdfffffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsfffffffdsfsdfsdffffffffffdsffffffffdsfsdfsdffвеffffffffffdsfsdfsdffвеffветdsfsdfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', senderId: 1, createdAt: new Date(Date.now() - 1000 * 60 * 25) }, + +] + +export const load = (async () => { + return { messages }; +}) satisfies PageLoad; \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 07b516b..f5e8f79 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,7 +1,7 @@ - - - +
+ +
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..98b36e6 --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,6 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load = (async () => { + redirect(302, '/login') +}) satisfies PageServerLoad; \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte deleted file mode 100644 index 5982b0a..0000000 --- a/src/routes/+page.svelte +++ /dev/null @@ -1,2 +0,0 @@ -

Welcome to SvelteKit

-

Visit kit.svelte.dev to read the documentation

diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts new file mode 100644 index 0000000..0754c8a --- /dev/null +++ b/src/routes/login/+page.server.ts @@ -0,0 +1,35 @@ +import type { PageServerLoad } from './$types'; +import { zod } from "sveltekit-superforms/adapters"; +import { loginFormSchema } from './login-form.svelte'; +import { superValidate } from "sveltekit-superforms"; +import { type Actions, fail, redirect } from "@sveltejs/kit"; +import { loginUser } from '$lib/api/user'; +import { isErrorResponse } from '$lib/types'; + + +export const load = (async ({ cookies }) => { + if (cookies.get('token')) + throw redirect(302, '/chats'); + + return { form: await superValidate(zod(loginFormSchema)) }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + default: async (event) => { + const form = await superValidate(event, zod(loginFormSchema)); + + if (!form.valid) + return fail(400, { form }); + + const response = await loginUser(form.data.username, form.data.password); + + if (isErrorResponse(response)) { + form.errors.username = [response.error]; + return fail(400, { form }); + } + + event.cookies.set('token', response.token, { path: '/' }); + + return { form, token: response }; + }, +}; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte new file mode 100644 index 0000000..2baad88 --- /dev/null +++ b/src/routes/login/+page.svelte @@ -0,0 +1,15 @@ + + +
+ +
+

Login

+ +
+
diff --git a/src/routes/login/login-form.svelte b/src/routes/login/login-form.svelte new file mode 100644 index 0000000..4e3da05 --- /dev/null +++ b/src/routes/login/login-form.svelte @@ -0,0 +1,79 @@ + + + + +
+
+ + + Username + + + + + + + Password + + + + +
+ +
+ Login + +
+
diff --git a/src/routes/logout/+page.server.ts b/src/routes/logout/+page.server.ts new file mode 100644 index 0000000..9730af7 --- /dev/null +++ b/src/routes/logout/+page.server.ts @@ -0,0 +1,7 @@ +import type { PageServerLoad } from './$types'; + +export const load = (async ({ cookies }) => { + cookies.delete('token', { path: '/' }); + + return {}; +}) satisfies PageServerLoad; \ No newline at end of file diff --git a/src/routes/logout/+page.svelte b/src/routes/logout/+page.svelte new file mode 100644 index 0000000..18ec6d5 --- /dev/null +++ b/src/routes/logout/+page.svelte @@ -0,0 +1,11 @@ + diff --git a/src/routes/register/+page.server.ts b/src/routes/register/+page.server.ts new file mode 100644 index 0000000..dab1fd3 --- /dev/null +++ b/src/routes/register/+page.server.ts @@ -0,0 +1,29 @@ +import type { PageServerLoad } from './$types'; +import { superValidate } from "sveltekit-superforms"; +import { type Actions, fail } from "@sveltejs/kit"; +import { zod } from "sveltekit-superforms/adapters"; +import { registerFormSchema } from './register-form.svelte'; +import { registerUser } from '$lib/api/user'; +import { isErrorResponse } from '$lib/types'; + +export const load = (async () => { + return { form: await superValidate(zod(registerFormSchema)) }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + default: async (event) => { + const form = await superValidate(event, zod(registerFormSchema)); + + if (!form.valid) + return fail(400, { form }); + + const response = await registerUser(form.data.username, form.data.password); + + if (isErrorResponse(response)) { + form.errors.username = [response.error]; + return fail(400, { form }); + } + + return { form, success: true }; + } +} \ No newline at end of file diff --git a/src/routes/register/+page.svelte b/src/routes/register/+page.svelte new file mode 100644 index 0000000..e1c66ed --- /dev/null +++ b/src/routes/register/+page.svelte @@ -0,0 +1,14 @@ + + +
+
+

Register

+ +
+
diff --git a/src/routes/register/register-form.svelte b/src/routes/register/register-form.svelte new file mode 100644 index 0000000..e2b368e --- /dev/null +++ b/src/routes/register/register-form.svelte @@ -0,0 +1,95 @@ + + + + +
+
+ + + Username + + + + + + + Password + + + + + + + Confirm Password + + + + + +
+ Register + +
+
+
diff --git a/static/default-avatar.png b/static/default-avatar.png new file mode 100644 index 0000000..e397a40 Binary files /dev/null and b/static/default-avatar.png differ diff --git a/tailwind.config.ts b/tailwind.config.ts index c4bf235..1da57b6 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -37,7 +37,7 @@ const config: Config = { foreground: "hsl(var(--muted-foreground) / )" }, accent: { - DEFAULT: "hsl(var(--accent) / )", + DEFAULT: "#AD5CD6", foreground: "hsl(var(--accent-foreground) / )" }, popover: {