togglable sidebar

This commit is contained in:
Aidan Bleser 2025-06-16 07:01:15 -05:00
parent 1aaae9b8f8
commit 34b64420bf
10 changed files with 309 additions and 62 deletions

View file

@ -1,3 +1,3 @@
{ {
"functions": "src/lib/backend/convex" "functions": "src/lib/backend/convex"
} }

View file

@ -28,7 +28,6 @@
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/svelte": "^5.2.4", "@testing-library/svelte": "^5.2.4",
"bits-ui": "^2.6.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"convex": "^1.24.8", "convex": "^1.24.8",

54
pnpm-lock.yaml generated
View file

@ -48,9 +48,6 @@ importers:
'@testing-library/svelte': '@testing-library/svelte':
specifier: ^5.2.4 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)) 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: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
@ -596,9 +593,6 @@ packages:
'@iconify/utils@2.3.0': '@iconify/utils@2.3.0':
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
'@internationalized/date@3.8.2':
resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==}
'@isaacs/fs-minipass@4.0.1': '@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@ -814,9 +808,6 @@ packages:
svelte: ^5.0.0 svelte: ^5.0.0
vite: ^6.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': '@tailwindcss/node@4.1.10':
resolution: {integrity: sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==} resolution: {integrity: sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==}
@ -1104,13 +1095,6 @@ packages:
better-call@1.0.9: better-call@1.0.9:
resolution: {integrity: sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g==} resolution: {integrity: sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g==}
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: brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@ -2144,12 +2128,6 @@ packages:
peerDependencies: peerDependencies:
svelte: ^5.0.0 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: svelte@5.34.1:
resolution: {integrity: sha512-jWNnN2hZFNtnzKPptCcJHBWrD9CtbHPDwIRIODufOYaWkR0kLmAIlM384lMt4ucwuIRX4hCJwD2D8ZtEcGJQ0Q==} resolution: {integrity: sha512-jWNnN2hZFNtnzKPptCcJHBWrD9CtbHPDwIRIODufOYaWkR0kLmAIlM384lMt4ucwuIRX4hCJwD2D8ZtEcGJQ0Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -2157,9 +2135,6 @@ packages:
symbol-tree@3.2.4: symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
tailwind-merge@3.0.2: tailwind-merge@3.0.2:
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==} resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
@ -2789,10 +2764,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@internationalized/date@3.8.2':
dependencies:
'@swc/helpers': 0.5.17
'@isaacs/fs-minipass@4.0.1': '@isaacs/fs-minipass@4.0.1':
dependencies: dependencies:
minipass: 7.1.2 minipass: 7.1.2
@ -2998,10 +2969,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@swc/helpers@0.5.17':
dependencies:
tslib: 2.8.1
'@tailwindcss/node@4.1.10': '@tailwindcss/node@4.1.10':
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
@ -3324,18 +3291,6 @@ snapshots:
set-cookie-parser: 2.7.1 set-cookie-parser: 2.7.1
uncrypto: 0.1.3 uncrypto: 0.1.3
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: brace-expansion@1.1.12:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@ -4294,13 +4249,6 @@ snapshots:
style-to-object: 1.0.9 style-to-object: 1.0.9
svelte: 5.34.1 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: svelte@5.34.1:
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
@ -4320,8 +4268,6 @@ snapshots:
symbol-tree@3.2.4: {} symbol-tree@3.2.4: {}
tabbable@6.2.0: {}
tailwind-merge@3.0.2: {} tailwind-merge@3.0.2: {}
tailwind-merge@3.3.1: {} tailwind-merge@3.3.1: {}

View file

@ -0,0 +1,211 @@
/*
Installed from @ieedan/shadcn-svelte-extras
*/
import { createAttachmentKey } from 'svelte/attachments';
export type Options = {
/** Event to use to detect the shortcut @default 'keydown' */
event?: 'keydown' | 'keyup' | 'keypress';
/** Function to be called when the shortcut is pressed */
callback: (e: KeyboardEvent) => void;
/** Should the `Shift` key be pressed */
shift?: boolean;
/** Should the `Ctrl` / `Command` key be pressed */
ctrl?: boolean;
/** Should the `Alt` key be pressed */
alt?: boolean;
/** Which key should be pressed */
key: Key;
/** Control whether or not the shortcut prevents default behavior @default true */
preventDefault?: boolean;
/** Control whether or not the shortcut stops propagation @default false */
stopPropagation?: boolean;
};
/** Allows you to configure one or more shortcuts based on the key events of an element.
*
* ## Usage
* ```svelte
* <!-- Ctrl + K Shortcut -->
* <svelte:window use:shortcut={
* {
* ctrl: true,
* key: 'k',
* callback: commandMenu.toggle
* }
* }
* />
* ```
*/
export const shortcut = (node: HTMLElement, options: Options[] | Options) => {
const handleKeydown = (e: KeyboardEvent, options: Options) => {
if (options.ctrl && !e.ctrlKey && !e.metaKey) return;
if (options.alt && !e.altKey) return;
if (options.shift && !e.shiftKey) return;
if (e.key.toLocaleLowerCase() !== options.key.toLocaleLowerCase()) return;
if (options.preventDefault === undefined || options.preventDefault) {
e.preventDefault();
}
if (options.stopPropagation) {
e.stopPropagation();
}
options.callback(e);
};
$effect(() => {
let optionsArr: Options[] = [];
if (Array.isArray(options)) {
optionsArr = options;
} else {
optionsArr = [options];
}
for (const opt of optionsArr) {
node.addEventListener(opt.event ?? 'keydown', (e) => handleKeydown(e, opt));
}
return () => {
for (const opt of optionsArr) {
node.removeEventListener(opt.event ?? 'keydown', (e) => handleKeydown(e, opt));
}
};
});
};
/** Allows you to configure one or more shortcuts based on the key events of an element.
*
* ## Usage
* ```svelte
* <!-- Ctrl + K Shortcut -->
* <svelte:window
* {...attachShortcut({
* ctrl: true,
* key: 'k',
* callback: commandMenu.toggle
* })}
* />
* ```
*/
export function attachShortcut(opts: Options[] | Options) {
return {
[createAttachmentKey()]: (node: HTMLElement) => shortcut(node, opts),
};
}
export type Key =
| 'backspace'
| 'tab'
| 'enter'
| 'shift(left)'
| 'shift(right)'
| 'ctrl(left)'
| 'ctrl(right)'
| 'alt(left)'
| 'alt(right)'
| 'pause/break'
| 'caps lock'
| 'escape'
| 'space'
| 'page up'
| 'page down'
| 'end'
| 'home'
| 'left arrow'
| 'up arrow'
| 'right arrow'
| 'down arrow'
| 'print screen'
| 'insert'
| 'delete'
| '0'
| '1'
| '2'
| '3'
| '4'
| '5'
| '6'
| '7'
| '8'
| '9'
| 'a'
| 'b'
| 'c'
| 'd'
| 'e'
| 'f'
| 'g'
| 'h'
| 'i'
| 'j'
| 'k'
| 'l'
| 'm'
| 'n'
| 'o'
| 'p'
| 'q'
| 'r'
| 's'
| 't'
| 'u'
| 'v'
| 'w'
| 'x'
| 'y'
| 'z'
| 'left window key'
| 'right window key'
| 'select key (Context Menu)'
| 'numpad 0'
| 'numpad 1'
| 'numpad 2'
| 'numpad 3'
| 'numpad 4'
| 'numpad 5'
| 'numpad 6'
| 'numpad 7'
| 'numpad 8'
| 'numpad 9'
| 'multiply'
| 'add'
| 'subtract'
| 'decimal point'
| 'divide'
| 'f1'
| 'f2'
| 'f3'
| 'f4'
| 'f5'
| 'f6'
| 'f7'
| 'f8'
| 'f9'
| 'f10'
| 'f11'
| 'f12'
| 'num lock'
| 'scroll lock'
| 'audio volume mute'
| 'audio volume down'
| 'audio volume up'
| 'media player'
| 'launch application 1'
| 'launch application 2'
| 'semi-colon'
| 'equal sign'
| 'comma'
| 'dash'
| 'period'
| 'forward slash'
| 'Backquote/Grave accent'
| 'open bracket'
| 'back slash'
| 'close bracket'
| 'single quote';

View file

@ -3,7 +3,6 @@
--> -->
<script lang="ts" module> <script lang="ts" module>
import type { WithChildren, WithoutChildren } from 'bits-ui';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'; import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants'; import { type VariantProps, tv } from 'tailwind-variants';
@ -36,7 +35,7 @@
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant']; export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size']; export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export type ButtonPropsWithoutHTML = WithChildren<{ export type ButtonPropsWithoutHTML = {
ref?: HTMLElement | null; ref?: HTMLElement | null;
variant?: ButtonVariant; variant?: ButtonVariant;
size?: ButtonSize; size?: ButtonSize;
@ -46,17 +45,18 @@
currentTarget: EventTarget & HTMLButtonElement; currentTarget: EventTarget & HTMLButtonElement;
} }
) => Promise<void>; ) => Promise<void>;
}>; children?: Snippet<[]>;
};
export type AnchorElementProps = ButtonPropsWithoutHTML & export type AnchorElementProps = ButtonPropsWithoutHTML &
WithoutChildren<Omit<HTMLAnchorAttributes, 'href' | 'type'>> & { Omit<HTMLAnchorAttributes, 'href' | 'type' | 'children'> & {
href: HTMLAnchorAttributes['href']; href: HTMLAnchorAttributes['href'];
type?: never; type?: never;
disabled?: HTMLButtonAttributes['disabled']; disabled?: HTMLButtonAttributes['disabled'];
}; };
export type ButtonElementProps = ButtonPropsWithoutHTML & export type ButtonElementProps = ButtonPropsWithoutHTML &
WithoutChildren<Omit<HTMLButtonAttributes, 'type' | 'href'>> & { Omit<HTMLButtonAttributes, 'type' | 'href' | 'children'> & {
type?: HTMLButtonAttributes['type']; type?: HTMLButtonAttributes['type'];
href?: never; href?: never;
disabled?: HTMLButtonAttributes['disabled']; disabled?: HTMLButtonAttributes['disabled'];
@ -68,6 +68,7 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils/utils.js'; import { cn } from '$lib/utils/utils.js';
import LoaderCircleIcon from '~icons/lucide/loader-circle'; import LoaderCircleIcon from '~icons/lucide/loader-circle';
import type { Snippet } from 'svelte';
let { let {
ref = $bindable(null), ref = $bindable(null),

View file

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

View file

@ -0,0 +1,53 @@
<!--
Installed from @ieedan/shadcn-svelte-extras
-->
<script lang="ts" module>
import { tv, type VariantProps } from 'tailwind-variants';
const style = tv({
base: 'inline-flex place-items-center justify-center gap-1 rounded-md p-0.5',
variants: {
variant: {
outline: 'border-border bg-background text-muted-foreground border',
secondary: 'bg-secondary text-muted-foreground',
primary: 'bg-primary text-primary-foreground',
},
size: {
sm: 'min-w-6 gap-1.5 p-0.5 px-1 text-sm',
default: 'min-w-8 gap-1.5 p-1 px-2',
lg: 'min-w-9 gap-2 p-1 px-3 text-lg',
},
},
});
type Size = VariantProps<typeof style>['size'];
type Variant = VariantProps<typeof style>['variant'];
export type KbdPropsWithoutHTML = {
ref?: HTMLElement | null;
class?: string;
size?: Size;
variant?: Variant;
children?: Snippet<[]>;
};
export type KbdProps = KbdPropsWithoutHTML;
</script>
<script lang="ts">
import { cn } from '$lib/utils/utils';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
class: className,
size = 'sm',
variant = 'outline',
children,
}: KbdProps = $props();
</script>
<kbd bind:this={ref} class={cn(style({ size, variant }), className)}>
{@render children?.()}
</kbd>

View file

@ -2,12 +2,15 @@
import { cn } from '$lib/utils/utils'; import { cn } from '$lib/utils/utils';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
import { useSidebar } from './sidebar.svelte.js'; import { useSidebar } from './sidebar.svelte.js';
import { shortcut } from '$lib/actions/shortcut.svelte.js';
let { children, ...rest }: HTMLAttributes<HTMLDivElement> = $props(); let { children, ...rest }: HTMLAttributes<HTMLDivElement> = $props();
const sidebar = useSidebar(); const sidebar = useSidebar();
</script> </script>
<svelte:window use:shortcut={{ key: 'b', ctrl: true, callback: sidebar.toggle }} />
<div <div
{...rest} {...rest}
class={cn('[--sidebar-width:0px] md:grid md:grid-cols-[var(--sidebar-width)_1fr]', { class={cn('[--sidebar-width:0px] md:grid md:grid-cols-[var(--sidebar-width)_1fr]', {

View file

@ -0,0 +1,10 @@
/*
Installed from @ieedan/shadcn-svelte-extras
*/
import { browser } from '$app/environment';
/** Attempts to determine if a user is on a Mac using `navigator.userAgent`. */
export class IsMac {
readonly current = $derived(browser ? navigator.userAgent.includes('Mac') : false);
}

View file

@ -6,6 +6,8 @@
import { LightSwitch } from '$lib/components/ui/light-switch'; import { LightSwitch } from '$lib/components/ui/light-switch';
import ArrowLeftIcon from '~icons/lucide/arrow-left'; import ArrowLeftIcon from '~icons/lucide/arrow-left';
import { Avatar } from 'melt/components'; import { Avatar } from 'melt/components';
import { Kbd } from '$lib/components/ui/kbd/index.js';
import { IsMac } from '$lib/hooks/is-mac.svelte.js';
let { data, children } = $props(); let { data, children } = $props();
@ -33,6 +35,8 @@
await goto('/login'); await goto('/login');
} }
const isMac = new IsMac();
</script> </script>
<div class="container mx-auto max-w-[1200px] space-y-8 pt-6 pb-24"> <div class="container mx-auto max-w-[1200px] space-y-8 pt-6 pb-24">
@ -46,7 +50,7 @@
<Button variant="ghost" onClickPromise={signOut}>Sign out</Button> <Button variant="ghost" onClickPromise={signOut}>Sign out</Button>
</div> </div>
</header> </header>
<div class="px-4 md:grid md:grid-cols-[280px_1fr]"> <div class="px-4 md:grid md:grid-cols-[255px_1fr]">
<div class="hidden md:col-start-1 md:block"> <div class="hidden md:col-start-1 md:block">
<div class="flex flex-col place-items-center gap-2"> <div class="flex flex-col place-items-center gap-2">
<Avatar src={data.session.user.image ?? undefined}> <Avatar src={data.session.user.image ?? undefined}>
@ -64,6 +68,19 @@
<p class="text-center text-2xl font-bold">{data.session.user.name}</p> <p class="text-center text-2xl font-bold">{data.session.user.name}</p>
<span class="text-muted-foreground text-center text-sm">{data.session.user.email}</span> <span class="text-muted-foreground text-center text-sm">{data.session.user.email}</span>
</div> </div>
<div class="mt-4 flex w-full flex-col gap-2">
<span class="text-sm font-medium">Keyboard Shortcuts</span>
<div class="flex flex-col gap-1">
<div class="flex place-items-center justify-between">
<span>Toggle Sidebar </span>
<div>
<Kbd>{isMac.current ? '⌘' : 'Ctrl'}</Kbd>
<Kbd>B</Kbd>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="pl-12 md:col-start-2"> <div class="pl-12 md:col-start-2">