account screen

This commit is contained in:
Aidan Bleser 2025-06-13 10:15:31 -05:00
parent 4233f4f17d
commit 61ba8c1b9f
27 changed files with 628 additions and 11 deletions

15
jsrepo.json Normal file
View file

@ -0,0 +1,15 @@
{
"$schema": "https://unpkg.com/jsrepo@2.3.1/schemas/project-config.json",
"repos": ["@ieedan/shadcn-svelte-extras"],
"includeTests": false,
"watermark": true,
"formatter": "prettier",
"configFiles": {},
"paths": {
"*": "$lib/blocks",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"actions": "$lib/actions",
"hooks": "$lib/hooks"
}
}

View file

@ -4,7 +4,7 @@
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "concurrently \"convex dev\" \"vite dev\"",
"dev": "concurrently -n \"convex,vite\" -c \"blue.bold,green.bold\" \"convex dev\" \"vite dev\"",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
@ -19,6 +19,7 @@
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@lucide/svelte": "^0.515.0",
"@playwright/test": "^1.49.1",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0",
@ -26,11 +27,17 @@
"@tailwindcss/vite": "^4.0.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/svelte": "^5.2.4",
"bits-ui": "^2.6.2",
"clsx": "^2.1.1",
"concurrently": "^9.1.2",
"convex": "^1.24.8",
"convex-svelte": "^0.0.11",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"jsdom": "^26.0.0",
"melt": "^0.35.0",
"mode-watcher": "^1.0.8",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
@ -38,15 +45,13 @@
"runed": "^0.28.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.2.6",
"vitest": "^3.2.3",
"concurrently": "^9.1.2",
"convex": "^1.24.8",
"convex-svelte": "^0.0.11",
"melt": "^0.35.0"
"vitest": "^3.2.3"
},
"pnpm": {
"onlyBuiltDependencies": [

96
pnpm-lock.yaml generated
View file

@ -14,6 +14,9 @@ importers:
'@eslint/js':
specifier: ^9.18.0
version: 9.28.0
'@lucide/svelte':
specifier: ^0.515.0
version: 0.515.0(svelte@5.34.1)
'@playwright/test':
specifier: ^1.49.1
version: 1.53.0
@ -35,6 +38,12 @@ importers:
'@testing-library/svelte':
specifier: ^5.2.4
version: 5.2.8(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1))(vitest@3.2.3(@types/node@24.0.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1))
bits-ui:
specifier: ^2.6.2
version: 2.6.2(@internationalized/date@3.8.2)(svelte@5.34.1)
clsx:
specifier: ^2.1.1
version: 2.1.1
concurrently:
specifier: ^9.1.2
version: 9.1.2
@ -83,6 +92,12 @@ importers:
svelte-check:
specifier: ^4.0.0
version: 4.2.1(picomatch@4.0.2)(svelte@5.34.1)(typescript@5.8.3)
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
tailwind-variants:
specifier: ^1.0.0
version: 1.0.0(tailwindcss@4.1.10)
tailwindcss:
specifier: ^4.0.0
version: 4.1.10
@ -531,6 +546,9 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@internationalized/date@3.8.2':
resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==}
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
@ -557,6 +575,11 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@lucide/svelte@0.515.0':
resolution: {integrity: sha512-CEAyqcZmNBfYzVgaRmK2RFJP5tnbXxekRyDk0XX/eZQRfsJmkDvmQwXNX8C869BgNeryzmrRyjHhUL6g9ZOHNA==}
peerDependencies:
svelte: ^5
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -714,6 +737,9 @@ packages:
svelte: ^5.0.0
vite: ^6.0.0
'@swc/helpers@0.5.17':
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
'@tailwindcss/node@4.1.10':
resolution: {integrity: sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==}
@ -991,6 +1017,13 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
bits-ui@2.6.2:
resolution: {integrity: sha512-OlPSUAT+ENhtRarPjABljca1cCljyoAqOZKfgjCB8PxQii2fL0AKnzObhnEdhZKwYdpXczEtNOYqUUNYwliaWA==}
engines: {node: '>=20', pnpm: '>=8.7.0'}
peerDependencies:
'@internationalized/date': ^3.8.1
svelte: ^5.33.0
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@ -1954,6 +1987,12 @@ packages:
peerDependencies:
svelte: ^5.0.0
svelte-toolbelt@0.9.1:
resolution: {integrity: sha512-wBX6MtYw/kpht80j5zLpxJyR9soLizXPIAIWEVd9llAi17SR44ZdG291bldjB7r/K5duC0opDFcuhk2cA1hb8g==}
engines: {node: '>=18', pnpm: '>=8.7.0'}
peerDependencies:
svelte: ^5.30.2
svelte@5.34.1:
resolution: {integrity: sha512-jWNnN2hZFNtnzKPptCcJHBWrD9CtbHPDwIRIODufOYaWkR0kLmAIlM384lMt4ucwuIRX4hCJwD2D8ZtEcGJQ0Q==}
engines: {node: '>=18'}
@ -1961,6 +2000,21 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
tailwind-merge@3.0.2:
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
tailwind-merge@3.3.1:
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
tailwind-variants@1.0.0:
resolution: {integrity: sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==}
engines: {node: '>=16.x', pnpm: '>=7.x'}
peerDependencies:
tailwindcss: '*'
tailwindcss@4.1.10:
resolution: {integrity: sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==}
@ -2493,6 +2547,10 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@internationalized/date@3.8.2':
dependencies:
'@swc/helpers': 0.5.17
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
@ -2518,6 +2576,10 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@lucide/svelte@0.515.0(svelte@5.34.1)':
dependencies:
svelte: 5.34.1
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -2647,6 +2709,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@swc/helpers@0.5.17':
dependencies:
tslib: 2.8.1
'@tailwindcss/node@4.1.10':
dependencies:
'@ampproject/remapping': 2.3.0
@ -2941,6 +3007,18 @@ snapshots:
balanced-match@1.0.2: {}
bits-ui@2.6.2(@internationalized/date@3.8.2)(svelte@5.34.1):
dependencies:
'@floating-ui/core': 1.7.1
'@floating-ui/dom': 1.7.1
'@internationalized/date': 3.8.2
css.escape: 1.5.1
esm-env: 1.2.2
runed: 0.28.0(svelte@5.34.1)
svelte: 5.34.1
svelte-toolbelt: 0.9.1(svelte@5.34.1)
tabbable: 6.2.0
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
@ -3836,6 +3914,13 @@ snapshots:
style-to-object: 1.0.9
svelte: 5.34.1
svelte-toolbelt@0.9.1(svelte@5.34.1):
dependencies:
clsx: 2.1.1
runed: 0.28.0(svelte@5.34.1)
style-to-object: 1.0.9
svelte: 5.34.1
svelte@5.34.1:
dependencies:
'@ampproject/remapping': 2.3.0
@ -3855,6 +3940,17 @@ snapshots:
symbol-tree@3.2.4: {}
tabbable@6.2.0: {}
tailwind-merge@3.0.2: {}
tailwind-merge@3.3.1: {}
tailwind-variants@1.0.0(tailwindcss@4.1.10):
dependencies:
tailwind-merge: 3.0.2
tailwindcss: 4.1.10
tailwindcss@4.1.10: {}
tapable@2.2.2: {}

View file

@ -1,5 +1,7 @@
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
:root {
--background: oklch(0.9754 0.0084 325.6414);
--foreground: oklch(0.3257 0.1161 325.0372);

View file

@ -0,0 +1,110 @@
/*
Installed from @ieedan/shadcn-svelte-extras
*/
import { page } from '$app/state';
import { untrack } from 'svelte';
import { createAttachmentKey } from 'svelte/attachments';
export type Options = {
/** Determines if the route should be active for subdirectories.
*
* @default true
*/
activeForSubdirectories?: boolean;
/** Determines if the href of the `<a/>` tag is a `#` route
*
* @default false
*/
isHash?: boolean;
/** Determines if the href of the `<a/>` tag is a search route
*
* @default false
*/
isSearch?: boolean;
url: URL;
};
/** Sets the `data-active` attribute on an `<a/>` tag based on its 'active' state.
*
* @param node
* @param opts
*
* ## Usage
* ```svelte
* <a href="/" use:active>Route</a>
* ```
*/
export function active(node: HTMLAnchorElement, opts: Omit<Options, 'url'> = {}) {
checkIsActive(node.href, { ...opts, url: page.url }).toString();
$effect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
page.url;
untrack(() => {
node.setAttribute(
'data-active',
checkIsActive(node.href, { ...opts, url: page.url }).toString()
);
});
});
}
/** Sets the `data-active` attribute on an `<a/>` tag based on its 'active' state.
*
* @param opts
* @returns
*
* ## Usage
* ```svelte
* <a href="/" {...attachActive()}>Route</a>
* ```
*/
export function attachActive(opts: Omit<Options, 'url'> = {}) {
return {
[createAttachmentKey()]: (node: HTMLAnchorElement) => active(node, opts)
};
}
export const checkIsActive = (
nodeHref: string,
{ activeForSubdirectories, url, isHash, isSearch }: Options
): boolean => {
let href: string = new URL(nodeHref).pathname;
if (isHash) {
href = new URL(nodeHref).hash;
}
let searchParamName: string | undefined = undefined;
let searchParamValue: string | undefined = undefined;
if (isSearch) {
const tempUrl = new URL(nodeHref);
for (const [key, value] of tempUrl.searchParams.entries()) {
searchParamName = key;
searchParamValue = value;
}
href = new URL(nodeHref).search;
}
const samePath = href === url.pathname;
const isParentRoute: boolean =
(activeForSubdirectories == undefined || activeForSubdirectories) &&
url.pathname.startsWith(href ?? '');
const isHashRoute: boolean =
isHash == true && (url.hash == href || ((href == '#' || href == '#/') && url.hash == ''));
const isSearchRoute: boolean =
isSearch === true &&
searchParamName !== undefined &&
searchParamValue !== undefined &&
(url.searchParams.get(searchParamName) ?? '/') === searchParamValue;
return samePath || isParentRoute || isHashRoute || isSearchRoute;
};

View file

@ -0,0 +1,128 @@
<!--
Installed from @ieedan/shadcn-svelte-extras
-->
<script lang="ts" module>
import type { WithChildren, WithoutChildren } from 'bits-ui';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants';
export const buttonVariants = tv({
base: "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive focus-visible:border-ring focus-visible:ring-ring/50 relative inline-flex shrink-0 items-center justify-center gap-2 overflow-hidden rounded-md text-sm font-medium whitespace-nowrap outline-hidden transition-all select-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-2xs',
destructive:
'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 text-white shadow-2xs',
outline:
'bg-background hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 border shadow-2xs',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-2xs',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export type ButtonPropsWithoutHTML = WithChildren<{
ref?: HTMLElement | null;
variant?: ButtonVariant;
size?: ButtonSize;
loading?: boolean;
onClickPromise?: (
e: MouseEvent & {
currentTarget: EventTarget & HTMLButtonElement;
}
) => Promise<void>;
}>;
export type AnchorElementProps = ButtonPropsWithoutHTML &
WithoutChildren<Omit<HTMLAnchorAttributes, 'href' | 'type'>> & {
href: HTMLAnchorAttributes['href'];
type?: never;
disabled?: HTMLButtonAttributes['disabled'];
};
export type ButtonElementProps = ButtonPropsWithoutHTML &
WithoutChildren<Omit<HTMLButtonAttributes, 'type' | 'href'>> & {
type?: HTMLButtonAttributes['type'];
href?: never;
disabled?: HTMLButtonAttributes['disabled'];
};
export type ButtonProps = AnchorElementProps | ButtonElementProps;
</script>
<script lang="ts">
import { cn } from '$lib/utils/utils.js';
import { LoaderCircleIcon } from '@lucide/svelte';
let {
ref = $bindable(null),
variant = 'default',
size = 'default',
href = undefined,
type = 'button',
loading = false,
disabled = false,
tabindex = 0,
onclick,
onClickPromise,
class: className,
children,
...rest
}: ButtonProps = $props();
</script>
<!-- This approach to disabled links is inspired by bits-ui see: https://github.com/huntabyte/bits-ui/pull/1055 -->
<svelte:element
this={href ? 'a' : 'button'}
{...rest}
data-slot="button"
type={href ? undefined : type}
href={href && !disabled ? href : undefined}
disabled={href ? undefined : disabled || loading}
aria-disabled={href ? disabled : undefined}
role={href && disabled ? 'link' : undefined}
tabindex={href && disabled ? -1 : tabindex}
class={cn(buttonVariants({ variant, size }), className)}
bind:this={ref}
onclick={async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
e: any
) => {
onclick?.(e);
if (type === undefined) return;
if (onClickPromise) {
loading = true;
await onClickPromise(e);
loading = false;
}
}}
>
{#if type !== undefined && loading}
<div class="absolute flex size-full place-items-center justify-center bg-inherit">
<div class="flex animate-spin place-items-center justify-center">
<LoaderCircleIcon class="size-4" />
</div>
</div>
<span class="sr-only">Loading</span>
{/if}
{@render children?.()}
</svelte:element>

View file

@ -0,0 +1,27 @@
/*
Installed from @ieedan/shadcn-svelte-extras
*/
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
type AnchorElementProps,
type ButtonElementProps,
type ButtonPropsWithoutHTML,
buttonVariants
} from './button.svelte';
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
type AnchorElementProps,
type ButtonElementProps,
type ButtonPropsWithoutHTML
};

View file

@ -0,0 +1,10 @@
<script lang="ts">
import { cn } from '$lib/utils/utils';
import type { HTMLAttributes } from 'svelte/elements';
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
</script>
<div class={cn('flex flex-col gap-2', className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,10 @@
<script lang="ts">
import { cn } from '$lib/utils/utils';
import type { HTMLAttributes } from 'svelte/elements';
let { class: className, children, ...restProps }: HTMLAttributes<HTMLParagraphElement> = $props();
</script>
<p class={cn('text-muted-foreground text-sm', className)} {...restProps}>
{@render children?.()}
</p>

View file

@ -0,0 +1,10 @@
<script lang="ts">
import { cn } from '$lib/utils/utils';
import type { HTMLAttributes } from 'svelte/elements';
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
</script>
<div class={cn('flex flex-col gap-1', className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,10 @@
<script lang="ts">
import { cn } from '$lib/utils/utils';
import type { HTMLAttributes } from 'svelte/elements';
let { class: className, children, ...restProps }: HTMLAttributes<HTMLHeadingElement> = $props();
</script>
<h3 class={cn('text-lg font-semibold', className)} {...restProps}>
{@render children?.()}
</h3>

View file

@ -0,0 +1,10 @@
<script lang="ts">
import { cn } from '$lib/utils/utils';
import type { HTMLAttributes } from 'svelte/elements';
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
</script>
<div class={cn('bg-card flex flex-col gap-4 rounded-lg border p-4', className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,7 @@
import Root from './card.svelte';
import Header from './card-header.svelte';
import Title from './card-title.svelte';
import Description from './card-description.svelte';
import Content from './card-content.svelte';
export { Root, Header, Title, Description, Content };

View file

@ -0,0 +1,3 @@
import Input from './input.svelte';
export { Input };

View file

@ -0,0 +1,14 @@
<script lang="ts">
import { cn } from '$lib/utils/utils';
import type { HTMLInputAttributes } from 'svelte/elements';
let { class: className, ...restProps }: HTMLInputAttributes = $props();
</script>
<input
class={cn(
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...restProps}
/>

View file

@ -0,0 +1,7 @@
/*
Installed from @ieedan/shadcn-svelte-extras
*/
import LightSwitch from './light-switch.svelte';
export { LightSwitch };

View file

@ -0,0 +1,18 @@
<!--
Installed from @ieedan/shadcn-svelte-extras
-->
<script lang="ts">
import { SunIcon, MoonIcon } from '@lucide/svelte';
import { toggleMode } from 'mode-watcher';
import { Button } from '$lib/components/ui/button/index.js';
import type { LightSwitchProps } from './types';
let { variant = 'outline' }: LightSwitchProps = $props();
</script>
<Button onclick={toggleMode} {variant} size="icon">
<SunIcon class="absolute scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<MoonIcon class="scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<span class="sr-only">Toggle theme</span>
</Button>

View file

@ -0,0 +1,7 @@
/*
Installed from @ieedan/shadcn-svelte-extras
*/
export type LightSwitchProps = {
variant?: 'outline' | 'ghost';
};

View file

@ -0,0 +1,3 @@
import Link from './link.svelte';
export { Link };

View file

@ -0,0 +1,10 @@
<script lang="ts">
import { cn } from '$lib/utils/utils';
import type { HTMLAnchorAttributes } from 'svelte/elements';
let { class: className, children, ...restProps }: HTMLAnchorAttributes = $props();
</script>
<a class={cn('text-primary', className)} {...restProps}>
{@render children?.()}
</a>

View file

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

17
src/lib/utils/utils.ts Normal file
View file

@ -0,0 +1,17 @@
/*
Installed from @ieedan/shadcn-svelte-extras
*/
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };

View file

@ -1,4 +1,73 @@
<script lang="ts">
import { active } from '$lib/actions/active.svelte';
import Button from '$lib/components/ui/button/button.svelte';
import { LightSwitch } from '$lib/components/ui/light-switch';
import { ArrowLeftIcon } from '@lucide/svelte';
import { Avatar } from 'melt/components';
let { children } = $props();
const navigation: { title: string; href: string }[] = [
{
title: 'Account',
href: '/account'
},
{
title: 'Customization',
href: '/account/customization'
},
{
title: 'Models',
href: '/account/models'
},
{
title: 'API Keys',
href: '/account/api-keys'
}
];
</script>
<div class="container mx-auto max-w-[1200px] space-y-8 pt-6 pb-24">
<header class="flex place-items-center justify-between px-4">
<a href="/chat" class="flex place-items-center gap-2 text-sm">
<ArrowLeftIcon class="size-4" />
Back to Chat
</a>
<div class="flex place-items-center gap-2">
<LightSwitch variant="ghost" />
<Button variant="ghost">Sign out</Button>
</div>
</header>
<div class="px-4 md:grid md:grid-cols-[280px_1fr]">
<div class="hidden md:col-start-1 md:block">
<div class="flex flex-col place-items-center gap-2">
<Avatar src="https://github.com/shadcn.png">
{#snippet children(avatar)}
<img {...avatar.image} alt="Avatar" class="size-40 rounded-full" />
<span {...avatar.fallback}>JD</span>
{/snippet}
</Avatar>
<div class="flex flex-col gap-1">
<h1 class="text-center text-2xl font-bold">John Doe</h1>
<span class="text-muted-foreground text-center text-sm">m@example.com</span>
</div>
</div>
</div>
<div class="space-y-8 pl-12 md:col-start-2">
<div
class="bg-card text-muted-foreground gap-2 flex w-fit place-items-center rounded-lg p-1 text-sm"
>
{#each navigation as tab (tab)}
<a
href={tab.href}
use:active={{ activeForSubdirectories: false }}
class="data-[active=true]:bg-background data-[active=true]:text-foreground rounded-md px-2 py-1"
>
{tab.title}
</a>
{/each}
</div>
{@render children?.()}
</div>
</div>
</div>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { KeyIcon } from '@lucide/svelte';
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import { Link } from '$lib/components/ui/link';
</script>
<Card.Root>
<Card.Header>
<Card.Title>
<KeyIcon class="inline size-4" /> Open Router
</Card.Title>
<Card.Description>API Key for OpenRouter.</Card.Description>
</Card.Header>
<Card.Content>
<div class="flex flex-col gap-1">
<Input type="password" placeholder="sk-or-..." />
<span class="text-muted-foreground text-xs">
Get your API key from
<Link href="https://openrouter.ai/settings/keys" target="_blank" class="text-blue-500">
OpenRouter
</Link>
</span>
</div>
<div class="flex justify-end">
<Button type="submit">Save</Button>
</div>
</Card.Content>
</Card.Root>