diff --git a/convex.json b/convex.json
index d13b702..14af2a8 100644
--- a/convex.json
+++ b/convex.json
@@ -1,3 +1,3 @@
{
- "functions": "src/lib/backend/convex"
+ "functions": "src/lib/backend/convex"
}
diff --git a/package.json b/package.json
index 12b1975..dc7acaa 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,6 @@
"@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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 34f5499..b6fb3f1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -48,9 +48,6 @@ 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
@@ -596,9 +593,6 @@ packages:
'@iconify/utils@2.3.0':
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
- '@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'}
@@ -814,9 +808,6 @@ 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==}
@@ -1104,13 +1095,6 @@ packages:
better-call@1.0.9:
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:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@@ -2144,12 +2128,6 @@ 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'}
@@ -2157,9 +2135,6 @@ 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==}
@@ -2789,10 +2764,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@internationalized/date@3.8.2':
- dependencies:
- '@swc/helpers': 0.5.17
-
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
@@ -2998,10 +2969,6 @@ 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
@@ -3324,18 +3291,6 @@ snapshots:
set-cookie-parser: 2.7.1
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:
dependencies:
balanced-match: 1.0.2
@@ -4294,13 +4249,6 @@ 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
@@ -4320,8 +4268,6 @@ snapshots:
symbol-tree@3.2.4: {}
- tabbable@6.2.0: {}
-
tailwind-merge@3.0.2: {}
tailwind-merge@3.3.1: {}
diff --git a/src/lib/actions/shortcut.svelte.ts b/src/lib/actions/shortcut.svelte.ts
new file mode 100644
index 0000000..50c06a5
--- /dev/null
+++ b/src/lib/actions/shortcut.svelte.ts
@@ -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
+ *
+ *
+ * ```
+ */
+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
+ *
+ *
+ * ```
+ */
+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';
diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte
index a8db535..a9b71fe 100644
--- a/src/lib/components/ui/button/button.svelte
+++ b/src/lib/components/ui/button/button.svelte
@@ -3,7 +3,6 @@
-->
+
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte
index fd845a7..7cbd77c 100644
--- a/src/lib/components/ui/sidebar/sidebar.svelte
+++ b/src/lib/components/ui/sidebar/sidebar.svelte
@@ -2,12 +2,15 @@
import { cn } from '$lib/utils/utils';
import type { HTMLAttributes } from 'svelte/elements';
import { useSidebar } from './sidebar.svelte.js';
+ import { shortcut } from '$lib/actions/shortcut.svelte.js';
let { children, ...rest }: HTMLAttributes = $props();
const sidebar = useSidebar();
+
+