├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── bun.lockb ├── components.json ├── eslint.config.js ├── package.json ├── postcss.config.js ├── src ├── app.css ├── app.d.ts ├── app.html ├── hooks.server.ts ├── lib │ ├── components │ │ ├── create-repo-dialog.svelte │ │ ├── file-browser.svelte │ │ ├── file-upload-dialog.svelte │ │ ├── github-app-prompt.svelte │ │ ├── repo-context-menu.svelte │ │ ├── ui │ │ │ ├── button │ │ │ │ ├── button.svelte │ │ │ │ └── index.ts │ │ │ ├── card │ │ │ │ ├── card-content.svelte │ │ │ │ ├── card-description.svelte │ │ │ │ ├── card-footer.svelte │ │ │ │ ├── card-header.svelte │ │ │ │ ├── card-title.svelte │ │ │ │ ├── card.svelte │ │ │ │ └── index.ts │ │ │ ├── context-menu │ │ │ │ ├── context-menu-checkbox-item.svelte │ │ │ │ ├── context-menu-content.svelte │ │ │ │ ├── context-menu-group-heading.svelte │ │ │ │ ├── context-menu-item.svelte │ │ │ │ ├── context-menu-radio-item.svelte │ │ │ │ ├── context-menu-separator.svelte │ │ │ │ ├── context-menu-shortcut.svelte │ │ │ │ ├── context-menu-sub-content.svelte │ │ │ │ ├── context-menu-sub-trigger.svelte │ │ │ │ └── index.ts │ │ │ ├── dialog │ │ │ │ ├── dialog-content.svelte │ │ │ │ ├── dialog-description.svelte │ │ │ │ ├── dialog-footer.svelte │ │ │ │ ├── dialog-header.svelte │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ ├── dialog-title.svelte │ │ │ │ └── index.ts │ │ │ ├── dropdown-menu │ │ │ │ ├── dropdown-menu-checkbox-item.svelte │ │ │ │ ├── dropdown-menu-content.svelte │ │ │ │ ├── dropdown-menu-group-heading.svelte │ │ │ │ ├── dropdown-menu-item.svelte │ │ │ │ ├── dropdown-menu-label.svelte │ │ │ │ ├── dropdown-menu-radio-item.svelte │ │ │ │ ├── dropdown-menu-separator.svelte │ │ │ │ ├── dropdown-menu-shortcut.svelte │ │ │ │ ├── dropdown-menu-sub-content.svelte │ │ │ │ ├── dropdown-menu-sub-trigger.svelte │ │ │ │ └── index.ts │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ └── input.svelte │ │ │ ├── separator │ │ │ │ ├── index.ts │ │ │ │ └── separator.svelte │ │ │ ├── sheet │ │ │ │ ├── index.ts │ │ │ │ ├── sheet-content.svelte │ │ │ │ ├── sheet-description.svelte │ │ │ │ ├── sheet-footer.svelte │ │ │ │ ├── sheet-header.svelte │ │ │ │ ├── sheet-overlay.svelte │ │ │ │ └── sheet-title.svelte │ │ │ ├── sidebar │ │ │ │ ├── constants.ts │ │ │ │ ├── context.svelte.ts │ │ │ │ ├── index.ts │ │ │ │ ├── sidebar-content.svelte │ │ │ │ ├── sidebar-footer.svelte │ │ │ │ ├── sidebar-group-action.svelte │ │ │ │ ├── sidebar-group-content.svelte │ │ │ │ ├── sidebar-group-label.svelte │ │ │ │ ├── sidebar-group.svelte │ │ │ │ ├── sidebar-header.svelte │ │ │ │ ├── sidebar-input.svelte │ │ │ │ ├── sidebar-inset.svelte │ │ │ │ ├── sidebar-menu-action.svelte │ │ │ │ ├── sidebar-menu-badge.svelte │ │ │ │ ├── sidebar-menu-button.svelte │ │ │ │ ├── sidebar-menu-item.svelte │ │ │ │ ├── sidebar-menu-skeleton.svelte │ │ │ │ ├── sidebar-menu-sub-button.svelte │ │ │ │ ├── sidebar-menu-sub-item.svelte │ │ │ │ ├── sidebar-menu-sub.svelte │ │ │ │ ├── sidebar-menu.svelte │ │ │ │ ├── sidebar-provider.svelte │ │ │ │ ├── sidebar-rail.svelte │ │ │ │ ├── sidebar-separator.svelte │ │ │ │ ├── sidebar-trigger.svelte │ │ │ │ └── sidebar.svelte │ │ │ ├── skeleton │ │ │ │ ├── index.ts │ │ │ │ └── skeleton.svelte │ │ │ └── tooltip │ │ │ │ ├── index.ts │ │ │ │ └── tooltip-content.svelte │ │ └── user-menu.svelte │ ├── hooks │ │ └── is-mobile.svelte.ts │ ├── services │ │ ├── github.ts │ │ └── service.ts │ ├── stores │ │ ├── reload.ts │ │ ├── repositories.ts │ │ ├── service-config.ts │ │ └── theme.ts │ ├── supabaseClient.ts │ ├── types.ts │ └── utils.ts └── routes │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── +layout.ts │ ├── +page.svelte │ ├── api │ └── github │ │ └── check-installation │ │ └── +server.ts │ └── auth │ ├── auth-code-error │ └── +page.svelte │ └── callback │ └── +server.ts ├── static ├── BasierSquareMono.woff2 └── favicon.png ├── svelte.config.js ├── tailwind.config.ts ├── tsconfig.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3db 2 | 3 | A UI for using GitHub repositories as a CDN/file store. Pretty flaky, but works. 4 | 5 | 6 | 7 | https://github.com/user-attachments/assets/29c5a822-ea87-41ea-94e0-a5555aa45c93 8 | 9 | 10 | 11 | > [!WARNING] 12 | > This project is experimental and unstable. Use at your own risk and only for non-critical data. 13 | 14 | ## How it Works 15 | 16 | 3db provides a simple interface to: 17 | 18 | 1. Create and manage GitHub repositories as storage 19 | 2. Upload, browse and manage files 20 | 3. Get direct CDN links to your files 21 | 22 | When you first authenticate: 23 | 24 | 1. A repository called '3db-service' is created to store metadata 25 | 2. You can create new repos or connect existing ones 26 | 3. Files uploaded are stored in public GitHub repositories 27 | 4. Direct CDN links are provided via GitHub's raw content URLs 28 | 29 | ## Features 30 | 31 | - GitHub OAuth authentication 32 | - Create and connect multiple repositories 33 | - ️ Upload and delete files 34 | - Browse files and folders 35 | - Copy direct CDN links 36 | - ️ Delete files and folders 37 | 38 | ## Limitations 39 | 40 | - Only public repositories are supported 41 | - GitHub has a soft limit of 5GB per repository 42 | - Files are publicly accessible 43 | - API rate limits may apply 44 | - Not suitable for sensitive data 45 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tnixc/3db/c8683196ee815be5215f3ba109a205091c5fc9c0/bun.lockb -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://next.shadcn-svelte.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.ts", 6 | "css": "src/app.css", 7 | "baseColor": "stone" 8 | }, 9 | "aliases": { 10 | "components": "$lib/components", 11 | "utils": "$lib/utils", 12 | "ui": "$lib/components/ui", 13 | "hooks": "$lib/hooks" 14 | }, 15 | "typescript": true, 16 | "registry": "https://next.shadcn-svelte.com/registry" 17 | } 18 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import globals from 'globals'; 5 | import ts from 'typescript-eslint'; 6 | 7 | export default ts.config( 8 | js.configs.recommended, 9 | ...ts.configs.recommended, 10 | ...svelte.configs['flat/recommended'], 11 | prettier, 12 | ...svelte.configs['flat/prettier'], 13 | { 14 | languageOptions: { 15 | globals: { 16 | ...globals.browser, 17 | ...globals.node 18 | } 19 | } 20 | }, 21 | { 22 | files: ['**/*.svelte'], 23 | 24 | languageOptions: { 25 | parserOptions: { 26 | parser: ts.parser 27 | } 28 | } 29 | }, 30 | { 31 | ignores: ['build/', '.svelte-kit/', 'dist/'] 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3db", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "format": "prettier --write .", 12 | "lint": "prettier --check . && eslint ." 13 | }, 14 | "devDependencies": { 15 | "@iconify/svelte": "^4.0.2", 16 | "@sveltejs/adapter-auto": "^3.0.0", 17 | "@sveltejs/kit": "^2.0.0", 18 | "@sveltejs/vite-plugin-svelte": "^4.0.0", 19 | "@types/eslint": "^9.6.0", 20 | "autoprefixer": "^10.4.20", 21 | "bits-ui": "^1.0.0-next.58", 22 | "clsx": "^2.1.1", 23 | "eslint": "^9.7.0", 24 | "eslint-config-prettier": "^9.1.0", 25 | "eslint-plugin-svelte": "^2.36.0", 26 | "globals": "^15.0.0", 27 | "lucide-svelte": "^0.460.1", 28 | "prettier": "^3.3.2", 29 | "prettier-plugin-svelte": "^3.2.6", 30 | "prettier-plugin-tailwindcss": "^0.6.5", 31 | "svelte": "^5.0.0", 32 | "svelte-check": "^4.0.0", 33 | "tailwind-merge": "^2.5.4", 34 | "tailwind-variants": "^0.3.0", 35 | "tailwindcss": "^3.4.9", 36 | "tailwindcss-animate": "^1.0.7", 37 | "typescript": "^5.0.0", 38 | "typescript-eslint": "^8.0.0", 39 | "vite": "^5.0.3" 40 | }, 41 | "dependencies": { 42 | "@supabase/auth-helpers-sveltekit": "^0.13.0", 43 | "@supabase/supabase-js": "^2.46.1", 44 | "@sveltejs/adapter-vercel": "^5.4.7" 45 | }, 46 | "overrides": { 47 | "string-width": "^4.2.3" 48 | }, 49 | "resolutions": { 50 | "string-width": "^4.2.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | @layer base { 7 | :root { 8 | --flexoki-bg: 48 100% 97%; 9 | --flexoki-bg-2: 51 33% 92%; 10 | 11 | --flexoki-ui: 51 21% 88%; 12 | --flexoki-ui-2: 50 14% 83%; 13 | --flexoki-ui-3: 55 10% 79%; 14 | 15 | --flexoki-tx: 0 3% 6%; 16 | --flexoki-tx-2: 50 3% 42%; 17 | --flexoki-tx-3: 49 7% 70%; 18 | 19 | --flexoki-re: 3 62% 42%; 20 | } 21 | 22 | .dark { 23 | --flexoki-bg: 0 3% 6%; 24 | --flexoki-bg-2: 30 4% 11%; 25 | 26 | --flexoki-ui: 30 3% 15%; 27 | --flexoki-ui-2: 40 3% 20%; 28 | --flexoki-ui-3: 30 3% 24%; 29 | 30 | --flexoki-tx: 55 10% 79%; 31 | --flexoki-tx-2: 43 3% 52%; 32 | --flexoki-tx-3: 45 2% 33%; 33 | 34 | --flexoki-re: 5 61% 54%; 35 | } 36 | 37 | :root { 38 | --background: var(--flexoki-bg); 39 | --foreground: var(--flexoki-tx); 40 | --card: var(--flexoki-bg); 41 | --card-foreground: var(--flexoki-tx); 42 | --popover: var(--flexoki-bg); 43 | --popover-foreground: var(--flexoki-tx); 44 | --primary: var(--flexoki-tx); 45 | --primary-foreground: var(--flexoki-bg); 46 | --secondary: var(--flexoki-ui-2); 47 | --secondary-foreground: var(--flexoki-tx-2); 48 | --muted: var(--flexoki-bg-2); 49 | --muted-foreground: var(--flexoki-tx-3); 50 | --accent: var(--flexoki-ui); 51 | --accent-foreground: var(--flexoki-tx-2); 52 | --destructive: var(--flexoki-re); 53 | --destructive-foreground: var(--flexoki-bg); 54 | --border: var(--flexoki-ui-2); 55 | --input: var(--flexoki-ui-2); 56 | --ring: var(--flexoki-ui-3); 57 | --radius: 0rem; 58 | } 59 | 60 | :root { 61 | --sidebar-background: 0 0% 98%; 62 | --sidebar-foreground: 240 5.3% 26.1%; 63 | --sidebar-primary: 240 5.9% 10%; 64 | --sidebar-primary-foreground: 0 0% 98%; 65 | --sidebar-accent: 240 4.8% 95.9%; 66 | --sidebar-accent-foreground: 240 5.9% 10%; 67 | --sidebar-border: 220 13% 91%; 68 | --sidebar-ring: 217.2 91.2% 59.8%; 69 | } 70 | 71 | .dark { 72 | --sidebar-background: 240 5.9% 10%; 73 | --sidebar-foreground: 240 4.8% 95.9%; 74 | --sidebar-primary: 224.3 76.3% 48%; 75 | --sidebar-primary-foreground: 0 0% 100%; 76 | --sidebar-accent: 240 3.7% 15.9%; 77 | --sidebar-accent-foreground: 240 4.8% 95.9%; 78 | --sidebar-border: 240 3.7% 15.9%; 79 | --sidebar-ring: 217.2 91.2% 59.8%; 80 | } 81 | } 82 | } 83 | 84 | @font-face { 85 | font-family: BasierSquareMono; 86 | font-style: normal; 87 | font-weight: 400; 88 | font-display: swap; 89 | src: url(/BasierSquareMono.woff2) format('woff2'); 90 | } 91 | 92 | @layer base { 93 | * { 94 | @apply border-border; 95 | font-family: 'BasierSquareMono', monospace; 96 | } 97 | 98 | body { 99 | @apply bg-background text-foreground; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare global { 4 | namespace App { 5 | interface Locals { 6 | supabase: import('@supabase/supabase-js').SupabaseClient; 7 | getUser: () => Promise; 8 | } 9 | interface PageData { 10 | user: import('@supabase/supabase-js').User | null; 11 | } 12 | } 13 | } 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 16 | %sveltekit.head% 17 | 3db 18 | 19 | 20 |
%sveltekit.body%
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; 2 | import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit'; 3 | import type { Handle } from '@sveltejs/kit'; 4 | 5 | export const handle: Handle = async ({ event, resolve }) => { 6 | event.locals.supabase = createSupabaseServerClient({ 7 | supabaseUrl: PUBLIC_SUPABASE_URL, 8 | supabaseKey: PUBLIC_SUPABASE_ANON_KEY, 9 | event 10 | }); 11 | 12 | event.locals.getUser = async () => { 13 | const { 14 | data: { session } 15 | } = await event.locals.supabase.auth.getSession(); 16 | 17 | if (!session) { 18 | return null; 19 | } 20 | 21 | const { 22 | data: { user }, 23 | error 24 | } = await event.locals.supabase.auth.getUser(); 25 | 26 | if (error) { 27 | console.error('Error fetching user:', error); 28 | return null; 29 | } 30 | 31 | return user; 32 | }; 33 | 34 | return resolve(event, { 35 | filterSerializedResponseHeaders(name) { 36 | return name === 'content-range'; 37 | } 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/lib/components/create-repo-dialog.svelte: -------------------------------------------------------------------------------- 1 | 100 | 101 | 102 | 103 | 104 | Link or Create Repository 105 | Create a new GitHub repository to use as a database. 106 | 107 | 108 |
109 |
110 | 111 | 112 |
113 | {#if existingRepo} 114 |

115 | This repository already exists. Connecting it will give 3db access to its contents. 116 |

117 | {:else} 118 |

119 | NOTE: This will create a public repository. Your files will be publicly accessible. 120 |

121 | {/if} 122 | {#if error} 123 |

{error}

124 | {/if} 125 | 126 | 127 | 128 | 131 | 132 |
133 |
134 |
135 | -------------------------------------------------------------------------------- /src/lib/components/file-browser.svelte: -------------------------------------------------------------------------------- 1 | 131 | 132 |
133 | {#if !$currentRepository} 134 | 138 | {/if} 139 | 140 | {#if $currentRepository} 141 |
142 |
143 | 147 | 148 | {#each getBreadcrumbs() as crumb} 149 | 152 | {#if crumb.path !== getBreadcrumbs().slice(-1)[0].path} 153 | / 154 | {/if} 155 | {/each} 156 |
157 |
158 | 159 | {#if loading} 160 |
Loading...
161 | {:else if error} 162 |
163 | {error} 164 |
165 | {:else if contents.length === 0} 166 |
This folder is empty
167 | {:else} 168 |
169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | {#each contents.toSorted((a, b) => { 179 | if (a.type === 'dir' && b.type !== 'dir') return -1; 180 | if (b.type === 'dir' && a.type !== 'dir') return 1; 181 | if (a.name === 'index.json' && $currentRepository?.name === '3db-service') return 1; 182 | if (b.name === 'index.json' && $currentRepository?.name === '3db-service') return -1; 183 | return a.name.localeCompare(b.name); 184 | }) as item} 185 | 186 | 207 | 210 | 244 | 245 | {/each} 246 | 247 |
NameSizeActions
187 | 206 | 208 | {item.type === 'dir' ? '--' : formatBytes(item.size)} 209 | 211 | {#if item.type === 'file' || item.type === 'dir'} 212 |
213 | {#if item.type === 'file'} 214 | 222 | 232 | {/if} 233 | 241 |
242 | {/if} 243 |
248 |
249 | {/if} 250 | {:else} 251 |
252 | Select a repository to view its contents 253 |
254 | {/if} 255 |
256 | -------------------------------------------------------------------------------- /src/lib/components/file-upload-dialog.svelte: -------------------------------------------------------------------------------- 1 | 87 | 88 | 89 | 90 | 91 | Upload File 92 | Upload a file to the current directory. 93 | 94 | 95 |
96 |
97 | 107 |
108 | 109 |
110 | 121 |
122 | 123 | {#if error} 124 |

{error}

125 | {/if} 126 | 127 | 128 | 129 | 132 | 133 |
134 |
135 |
136 | -------------------------------------------------------------------------------- /src/lib/components/github-app-prompt.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 | 14 | 15 | Install 3db 16 | 17 | To use 3db, you need to install our GitHub App. This will allow us to manage your 18 | repositories. 19 | 20 | 21 | 22 | 23 |
24 |
25 |

The app will only use these permissions:

26 |
    27 |
  • 28 | 29 | Read and write access to repositories 30 |
  • 31 |
  • 32 | 33 | Create and delete repositories 34 |
  • 35 |
36 |

37 | Note: A repo called '3db-service' will be created. It holds the metadata for connected 38 | repos. 39 |

40 |
41 | 45 |
46 |
47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /src/lib/components/repo-context-menu.svelte: -------------------------------------------------------------------------------- 1 | 77 | 78 | 79 | 80 |
81 | {@render children?.()} 82 |
83 |
84 | 85 | 86 | 87 | Open on GitHub 88 | 89 | 90 | 91 | 92 | Disconnect Repository 93 | 94 | 95 | 96 | Delete Repository 97 | 98 | 99 |
100 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/button.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | 54 | 55 | {#if href} 56 | 57 | {@render children?.()} 58 | 59 | {:else} 60 | 68 | {/if} 69 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import Root, { 2 | type ButtonProps, 3 | type ButtonSize, 4 | type ButtonVariant, 5 | buttonVariants 6 | } from './button.svelte'; 7 | 8 | export { 9 | Root, 10 | type ButtonProps as Props, 11 | // 12 | Root as Button, 13 | buttonVariants, 14 | type ButtonProps, 15 | type ButtonSize, 16 | type ButtonVariant 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-content.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {@render children?.()} 16 |
17 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-description.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |

15 | {@render children?.()} 16 |

17 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {@render children?.()} 16 |
17 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-header.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {@render children?.()} 16 |
17 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-title.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
24 | {@render children?.()} 25 |
26 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './card.svelte'; 2 | import Content from './card-content.svelte'; 3 | import Description from './card-description.svelte'; 4 | import Footer from './card-footer.svelte'; 5 | import Header from './card-header.svelte'; 6 | import Title from './card-title.svelte'; 7 | 8 | export { 9 | Root, 10 | Content, 11 | Description, 12 | Footer, 13 | Header, 14 | Title, 15 | // 16 | Root as Card, 17 | Content as CardContent, 18 | Description as CardDescription, 19 | Footer as CardFooter, 20 | Header as CardHeader, 21 | Title as CardTitle 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | {#snippet children({ checked, indeterminate })} 31 | 32 | {#if indeterminate} 33 | 34 | {:else} 35 | 36 | {/if} 37 | 38 | {@render childrenProp?.()} 39 | {/snippet} 40 | 41 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-content.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-radio-item.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | {#snippet children({ checked })} 23 | 24 | {#if checked} 25 | 26 | {/if} 27 | 28 | {@render childrenProp?.({ checked })} 29 | {/snippet} 30 | 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | {@render children?.()} 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | {@render children?.()} 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'; 2 | 3 | import Item from './context-menu-item.svelte'; 4 | import GroupHeading from './context-menu-group-heading.svelte'; 5 | import Content from './context-menu-content.svelte'; 6 | import Shortcut from './context-menu-shortcut.svelte'; 7 | import RadioItem from './context-menu-radio-item.svelte'; 8 | import Separator from './context-menu-separator.svelte'; 9 | import SubContent from './context-menu-sub-content.svelte'; 10 | import SubTrigger from './context-menu-sub-trigger.svelte'; 11 | import CheckboxItem from './context-menu-checkbox-item.svelte'; 12 | 13 | const Sub = ContextMenuPrimitive.Sub; 14 | const Root = ContextMenuPrimitive.Root; 15 | const Trigger = ContextMenuPrimitive.Trigger; 16 | const Group = ContextMenuPrimitive.Group; 17 | const RadioGroup = ContextMenuPrimitive.RadioGroup; 18 | 19 | export { 20 | Sub, 21 | Root, 22 | Item, 23 | GroupHeading, 24 | Group, 25 | Trigger, 26 | Content, 27 | Shortcut, 28 | Separator, 29 | RadioItem, 30 | SubContent, 31 | SubTrigger, 32 | RadioGroup, 33 | CheckboxItem, 34 | // 35 | Root as ContextMenu, 36 | Sub as ContextMenuSub, 37 | Item as ContextMenuItem, 38 | GroupHeading as ContextMenuGroupHeading, 39 | Group as ContextMenuGroup, 40 | Content as ContextMenuContent, 41 | Trigger as ContextMenuTrigger, 42 | Shortcut as ContextMenuShortcut, 43 | RadioItem as ContextMenuRadioItem, 44 | Separator as ContextMenuSeparator, 45 | RadioGroup as ContextMenuRadioGroup, 46 | SubContent as ContextMenuSubContent, 47 | SubTrigger as ContextMenuSubTrigger, 48 | CheckboxItem as ContextMenuCheckboxItem 49 | }; 50 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 28 | {@render children?.()} 29 | 32 | 33 | Close 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as DialogPrimitive } from 'bits-ui'; 2 | 3 | import Title from './dialog-title.svelte'; 4 | import Footer from './dialog-footer.svelte'; 5 | import Header from './dialog-header.svelte'; 6 | import Overlay from './dialog-overlay.svelte'; 7 | import Content from './dialog-content.svelte'; 8 | import Description from './dialog-description.svelte'; 9 | 10 | const Root = DialogPrimitive.Root; 11 | const Trigger = DialogPrimitive.Trigger; 12 | const Close = DialogPrimitive.Close; 13 | const Portal = DialogPrimitive.Portal; 14 | 15 | export { 16 | Root, 17 | Title, 18 | Portal, 19 | Footer, 20 | Header, 21 | Trigger, 22 | Overlay, 23 | Content, 24 | Description, 25 | Close, 26 | // 27 | Root as Dialog, 28 | Title as DialogTitle, 29 | Portal as DialogPortal, 30 | Footer as DialogFooter, 31 | Header as DialogHeader, 32 | Trigger as DialogTrigger, 33 | Overlay as DialogOverlay, 34 | Content as DialogContent, 35 | Description as DialogDescription, 36 | Close as DialogClose 37 | }; 38 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | {#snippet children({ checked, indeterminate })} 31 | 32 | {#if indeterminate} 33 | 34 | {:else} 35 | 36 | {/if} 37 | 38 | {@render childrenProp?.()} 39 | {/snippet} 40 | 41 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
22 | {@render children?.()} 23 |
24 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | {#snippet children({ checked })} 23 | 24 | {#if checked} 25 | 26 | {/if} 27 | 28 | {@render childrenProp?.({ checked })} 29 | {/snippet} 30 | 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | {@render children?.()} 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | {@render children?.()} 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; 2 | import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; 3 | import Content from './dropdown-menu-content.svelte'; 4 | import GroupHeading from './dropdown-menu-group-heading.svelte'; 5 | import Item from './dropdown-menu-item.svelte'; 6 | import Label from './dropdown-menu-label.svelte'; 7 | import RadioItem from './dropdown-menu-radio-item.svelte'; 8 | import Separator from './dropdown-menu-separator.svelte'; 9 | import Shortcut from './dropdown-menu-shortcut.svelte'; 10 | import SubContent from './dropdown-menu-sub-content.svelte'; 11 | import SubTrigger from './dropdown-menu-sub-trigger.svelte'; 12 | 13 | const Sub = DropdownMenuPrimitive.Sub; 14 | const Root = DropdownMenuPrimitive.Root; 15 | const Trigger = DropdownMenuPrimitive.Trigger; 16 | const Group = DropdownMenuPrimitive.Group; 17 | const RadioGroup = DropdownMenuPrimitive.RadioGroup; 18 | 19 | export { 20 | CheckboxItem, 21 | Content, 22 | Root as DropdownMenu, 23 | CheckboxItem as DropdownMenuCheckboxItem, 24 | Content as DropdownMenuContent, 25 | Group as DropdownMenuGroup, 26 | GroupHeading as DropdownMenuGroupHeading, 27 | Item as DropdownMenuItem, 28 | Label as DropdownMenuLabel, 29 | RadioGroup as DropdownMenuRadioGroup, 30 | RadioItem as DropdownMenuRadioItem, 31 | Separator as DropdownMenuSeparator, 32 | Shortcut as DropdownMenuShortcut, 33 | Sub as DropdownMenuSub, 34 | SubContent as DropdownMenuSubContent, 35 | SubTrigger as DropdownMenuSubTrigger, 36 | Trigger as DropdownMenuTrigger, 37 | Group, 38 | GroupHeading, 39 | Item, 40 | Label, 41 | RadioGroup, 42 | RadioItem, 43 | Root, 44 | Separator, 45 | Shortcut, 46 | Sub, 47 | SubContent, 48 | SubTrigger, 49 | Trigger 50 | }; 51 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './input.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Input 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './separator.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Separator 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/separator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as SheetPrimitive } from 'bits-ui'; 2 | import Overlay from './sheet-overlay.svelte'; 3 | import Content from './sheet-content.svelte'; 4 | import Header from './sheet-header.svelte'; 5 | import Footer from './sheet-footer.svelte'; 6 | import Title from './sheet-title.svelte'; 7 | import Description from './sheet-description.svelte'; 8 | 9 | const Root = SheetPrimitive.Root; 10 | const Close = SheetPrimitive.Close; 11 | const Trigger = SheetPrimitive.Trigger; 12 | const Portal = SheetPrimitive.Portal; 13 | 14 | export { 15 | Root, 16 | Close, 17 | Trigger, 18 | Portal, 19 | Overlay, 20 | Content, 21 | Header, 22 | Footer, 23 | Title, 24 | Description, 25 | // 26 | Root as Sheet, 27 | Close as SheetClose, 28 | Trigger as SheetTrigger, 29 | Portal as SheetPortal, 30 | Overlay as SheetOverlay, 31 | Content as SheetContent, 32 | Header as SheetHeader, 33 | Footer as SheetFooter, 34 | Title as SheetTitle, 35 | Description as SheetDescription 36 | }; 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-content.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 41 | 42 | 43 | 44 | 45 | {@render children?.()} 46 | 49 | 50 | Close 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-description.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-header.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-overlay.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-title.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/constants.ts: -------------------------------------------------------------------------------- 1 | export const SIDEBAR_COOKIE_NAME = 'sidebar:state'; 2 | export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; 3 | export const SIDEBAR_WIDTH = '16rem'; 4 | export const SIDEBAR_WIDTH_MOBILE = '18rem'; 5 | export const SIDEBAR_WIDTH_ICON = '3rem'; 6 | export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; 7 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/context.svelte.ts: -------------------------------------------------------------------------------- 1 | import { IsMobile } from '$lib/hooks/is-mobile.svelte.js'; 2 | import { getContext, setContext } from 'svelte'; 3 | import { SIDEBAR_KEYBOARD_SHORTCUT } from './constants.js'; 4 | 5 | type Getter = () => T; 6 | 7 | export type SidebarStateProps = { 8 | /** 9 | * A getter function that returns the current open state of the sidebar. 10 | * We use a getter function here to support `bind:open` on the `Sidebar.Provider` 11 | * component. 12 | */ 13 | open: Getter; 14 | 15 | /** 16 | * A function that sets the open state of the sidebar. To support `bind:open`, we need 17 | * a source of truth for changing the open state to ensure it will be synced throughout 18 | * the sub-components and any `bind:` references. 19 | */ 20 | setOpen: (open: boolean) => void; 21 | }; 22 | 23 | class SidebarState { 24 | readonly props: SidebarStateProps; 25 | open = $derived.by(() => this.props.open()); 26 | openMobile = $state(false); 27 | setOpen: SidebarStateProps['setOpen']; 28 | #isMobile: IsMobile; 29 | state = $derived.by(() => (this.open ? 'expanded' : 'collapsed')); 30 | 31 | constructor(props: SidebarStateProps) { 32 | this.setOpen = props.setOpen; 33 | this.#isMobile = new IsMobile(); 34 | this.props = props; 35 | } 36 | 37 | // Convenience getter for checking if the sidebar is mobile 38 | // without this, we would need to use `sidebar.isMobile.current` everywhere 39 | get isMobile() { 40 | return this.#isMobile.current; 41 | } 42 | 43 | // Event handler to apply to the `` 44 | handleShortcutKeydown = (e: KeyboardEvent) => { 45 | if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) { 46 | e.preventDefault(); 47 | this.toggle(); 48 | } 49 | }; 50 | 51 | setOpenMobile = (value: boolean) => { 52 | this.openMobile = value; 53 | }; 54 | 55 | toggle = () => { 56 | return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open); 57 | }; 58 | } 59 | 60 | const SYMBOL_KEY = 'scn-sidebar'; 61 | 62 | /** 63 | * Instantiates a new `SidebarState` instance and sets it in the context. 64 | * 65 | * @param props The constructor props for the `SidebarState` class. 66 | * @returns The `SidebarState` instance. 67 | */ 68 | export function setSidebar(props: SidebarStateProps): SidebarState { 69 | return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props)); 70 | } 71 | 72 | /** 73 | * Retrieves the `SidebarState` instance from the context. This is a class instance, 74 | * so you cannot destructure it. 75 | * @returns The `SidebarState` instance. 76 | */ 77 | export function useSidebar(): SidebarState { 78 | return getContext(Symbol.for(SYMBOL_KEY)); 79 | } 80 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | import { useSidebar } from './context.svelte.js'; 2 | import Content from './sidebar-content.svelte'; 3 | import Footer from './sidebar-footer.svelte'; 4 | import GroupAction from './sidebar-group-action.svelte'; 5 | import GroupContent from './sidebar-group-content.svelte'; 6 | import GroupLabel from './sidebar-group-label.svelte'; 7 | import Group from './sidebar-group.svelte'; 8 | import Header from './sidebar-header.svelte'; 9 | import Input from './sidebar-input.svelte'; 10 | import Inset from './sidebar-inset.svelte'; 11 | import MenuAction from './sidebar-menu-action.svelte'; 12 | import MenuBadge from './sidebar-menu-badge.svelte'; 13 | import MenuButton from './sidebar-menu-button.svelte'; 14 | import MenuItem from './sidebar-menu-item.svelte'; 15 | import MenuSkeleton from './sidebar-menu-skeleton.svelte'; 16 | import MenuSubButton from './sidebar-menu-sub-button.svelte'; 17 | import MenuSubItem from './sidebar-menu-sub-item.svelte'; 18 | import MenuSub from './sidebar-menu-sub.svelte'; 19 | import Menu from './sidebar-menu.svelte'; 20 | import Provider from './sidebar-provider.svelte'; 21 | import Rail from './sidebar-rail.svelte'; 22 | import Separator from './sidebar-separator.svelte'; 23 | import Trigger from './sidebar-trigger.svelte'; 24 | import Root from './sidebar.svelte'; 25 | 26 | export { 27 | Content, 28 | Footer, 29 | Group, 30 | GroupAction, 31 | GroupContent, 32 | GroupLabel, 33 | Header, 34 | Input, 35 | Inset, 36 | Menu, 37 | MenuAction, 38 | MenuBadge, 39 | MenuButton, 40 | MenuItem, 41 | MenuSkeleton, 42 | MenuSub, 43 | MenuSubButton, 44 | MenuSubItem, 45 | Provider, 46 | Rail, 47 | Root, 48 | Separator, 49 | // 50 | Root as Sidebar, 51 | Content as SidebarContent, 52 | Footer as SidebarFooter, 53 | Group as SidebarGroup, 54 | GroupAction as SidebarGroupAction, 55 | GroupContent as SidebarGroupContent, 56 | GroupLabel as SidebarGroupLabel, 57 | Header as SidebarHeader, 58 | Input as SidebarInput, 59 | Inset as SidebarInset, 60 | Menu as SidebarMenu, 61 | MenuAction as SidebarMenuAction, 62 | MenuBadge as SidebarMenuBadge, 63 | MenuButton as SidebarMenuButton, 64 | MenuItem as SidebarMenuItem, 65 | MenuSkeleton as SidebarMenuSkeleton, 66 | MenuSub as SidebarMenuSub, 67 | MenuSubButton as SidebarMenuSubButton, 68 | MenuSubItem as SidebarMenuSubItem, 69 | Provider as SidebarProvider, 70 | Rail as SidebarRail, 71 | Separator as SidebarSeparator, 72 | Trigger as SidebarTrigger, 73 | Trigger, 74 | useSidebar 75 | }; 76 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-content.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
23 | {@render children?.()} 24 |
25 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
20 | {@render children?.()} 21 |
22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group-action.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | {#if child} 31 | {@render child({ props: propObj })} 32 | {:else} 33 | 36 | {/if} 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group-content.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
20 | {@render children?.()} 21 |
22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group-label.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | {#if child} 29 | {@render child({ props: mergedProps })} 30 | {:else} 31 |
32 | {@render children?.()} 33 |
34 | {/if} 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
20 | {@render children?.()} 21 |
22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-header.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
20 | {@render children?.()} 21 |
22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-inset.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
23 | {@render children?.()} 24 |
25 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-action.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | {#if child} 38 | {@render child({ props: mergedProps })} 39 | {:else} 40 | 43 | {/if} 44 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-badge.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
28 | {@render children?.()} 29 |
30 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-button.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 66 | 67 | {#snippet Button({ props }: { props?: Record })} 68 | {@const mergedProps = mergeProps(buttonProps, props)} 69 | {#if child} 70 | {@render child({ props: mergedProps })} 71 | {:else} 72 | 75 | {/if} 76 | {/snippet} 77 | 78 | {#if !tooltipContent} 79 | {@render Button({})} 80 | {:else} 81 | 82 | 83 | {#snippet child({ props })} 84 | {@render Button({ props })} 85 | {/snippet} 86 | 87 | 95 | {/if} 96 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
  • 20 | {@render children?.()} 21 |
  • 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
    27 | {#if showIcon} 28 | 29 | {/if} 30 | 35 | {@render children?.()} 36 |
    37 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | {#if child} 38 | {@render child({ props: mergedProps })} 39 | {:else} 40 | 41 | {@render children?.()} 42 | 43 | {/if} 44 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
  • 13 | {@render children?.()} 14 |
  • 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-sub.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
      24 | {@render children?.()} 25 |
    26 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
      20 | {@render children?.()} 21 |
    22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-provider.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 | 46 | 47 | 48 |
    57 | {@render children?.()} 58 |
    59 |
    60 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-rail.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-separator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-trigger.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | {#if collapsible === 'none'} 27 |
    35 | {@render children?.()} 36 |
    37 | {:else if sidebar.isMobile} 38 | 44 | 51 |
    52 | {@render children?.()} 53 |
    54 |
    55 |
    56 | {:else} 57 | 98 | {/if} 99 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './skeleton.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Skeleton 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/skeleton.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    18 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip as TooltipPrimitive } from 'bits-ui'; 2 | import Content from './tooltip-content.svelte'; 3 | 4 | const Root = TooltipPrimitive.Root; 5 | const Trigger = TooltipPrimitive.Trigger; 6 | const Provider = TooltipPrimitive.Provider; 7 | 8 | export { 9 | Root, 10 | Trigger, 11 | Content, 12 | Provider, 13 | // 14 | Root as Tooltip, 15 | Content as TooltipContent, 16 | Trigger as TooltipTrigger, 17 | Provider as TooltipProvider 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/tooltip-content.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /src/lib/components/user-menu.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 25 | 36 | 37 | 38 | 39 | 40 | 3db source code 41 | 42 | 43 | 44 | 45 | Open My GitHub Repos 46 | 47 | theme.toggle()}> 48 | 49 | Toggle Theme 50 | 51 | 52 | 53 | 54 | Sign Out 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/lib/hooks/is-mobile.svelte.ts: -------------------------------------------------------------------------------- 1 | import { untrack } from 'svelte'; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export class IsMobile { 6 | #current = $state(false); 7 | 8 | constructor() { 9 | $effect(() => { 10 | return untrack(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | this.#current = window.innerWidth < MOBILE_BREAKPOINT; 14 | }; 15 | mql.addEventListener('change', onChange); 16 | onChange(); 17 | return () => { 18 | mql.removeEventListener('change', onChange); 19 | }; 20 | }); 21 | }); 22 | } 23 | 24 | get current() { 25 | return this.#current; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/services/github.ts: -------------------------------------------------------------------------------- 1 | import type { FileContent, Repository } from '$lib/types'; 2 | 3 | export type GitHubConfig = { 4 | token: string; 5 | userEmail: string; 6 | }; 7 | 8 | const createCommitter = (userEmail: string) => ({ 9 | name: 'db3 service', 10 | email: userEmail 11 | }); 12 | 13 | async function request( 14 | config: GitHubConfig, 15 | endpoint: string, 16 | options: RequestInit = {} 17 | ): Promise { 18 | const response = await fetch(`https://api.github.com${endpoint}`, { 19 | ...options, 20 | headers: { 21 | Authorization: `token ${config.token}`, 22 | Accept: 'application/vnd.github.v3+json', 23 | ...options.headers 24 | } 25 | }); 26 | 27 | if (!response.ok) { 28 | throw new Error(`GitHub API error: ${response.statusText}`); 29 | } 30 | 31 | return response.json(); 32 | } 33 | 34 | export async function createRepository(config: GitHubConfig, name: string): Promise { 35 | return request(config, '/user/repos', { 36 | method: 'POST', 37 | body: JSON.stringify({ 38 | name, 39 | private: false, 40 | auto_init: true 41 | }) 42 | }); 43 | } 44 | 45 | export async function getRepositories(config: GitHubConfig): Promise { 46 | let allRepos: Repository[] = []; 47 | let nextUrl = '/user/repos?per_page=100&sort=created'; 48 | 49 | while (nextUrl) { 50 | const response = await fetch(`https://api.github.com${nextUrl}`, { 51 | headers: { 52 | Authorization: `token ${config.token}`, 53 | Accept: 'application/vnd.github.v3+json' 54 | } 55 | }); 56 | 57 | if (!response.ok) { 58 | throw new Error(`GitHub API error: ${response.statusText}`); 59 | } 60 | 61 | const repos = await response.json(); 62 | allRepos = [...allRepos, ...repos]; 63 | 64 | // Check for Link header and extract next URL 65 | const linkHeader = response.headers.get('link'); 66 | if (!linkHeader) break; 67 | 68 | const links = linkHeader.split(','); 69 | const nextLink = links.find((link) => link.includes('rel="next"')); 70 | if (!nextLink) break; 71 | 72 | // Extract URL from next link 73 | const matches = nextLink.match(/<([^>]+)>/); 74 | nextUrl = matches ? matches[1].replace('https://api.github.com', '') : ''; 75 | } 76 | 77 | return allRepos; 78 | } 79 | 80 | export async function getContents( 81 | config: GitHubConfig, 82 | owner: string, 83 | repo: string, 84 | path = '' 85 | ): Promise { 86 | return request(config, `/repos/${owner}/${repo}/contents/${path}`); 87 | } 88 | 89 | // Add utility function for base64 encoding 90 | function bytesToBase64(bytes: Uint8Array): string { 91 | const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join(''); 92 | return btoa(binString); 93 | } 94 | 95 | export async function createFile( 96 | config: GitHubConfig, 97 | owner: string, 98 | repo: string, 99 | path: string, 100 | content: string | ArrayBuffer, 101 | message = 'Add file via db3', 102 | sha?: string 103 | ): Promise { 104 | const body: any = { 105 | message, 106 | committer: createCommitter(config.userEmail) 107 | }; 108 | 109 | // Handle both text and binary content 110 | if (typeof content === 'string') { 111 | body.content = btoa(content); // Handle UTF-8 text properly 112 | } else { 113 | body.content = bytesToBase64(new Uint8Array(content)); 114 | } 115 | 116 | // Only include SHA if updating an existing file 117 | if (sha) { 118 | body.sha = sha; 119 | } 120 | 121 | await request(config, `/repos/${owner}/${repo}/contents/${path}`, { 122 | method: 'PUT', 123 | body: JSON.stringify(body) 124 | }); 125 | } 126 | 127 | export async function deleteFile( 128 | config: GitHubConfig, 129 | owner: string, 130 | repo: string, 131 | path: string, 132 | sha: string, 133 | message = 'Delete file via db3' 134 | ): Promise { 135 | await request(config, `/repos/${owner}/${repo}/contents/${path}`, { 136 | method: 'DELETE', 137 | body: JSON.stringify({ 138 | message, 139 | sha, 140 | committer: createCommitter(config.userEmail) 141 | }) 142 | }); 143 | } 144 | 145 | export async function deleteRepository( 146 | config: GitHubConfig, 147 | owner: string, 148 | repo: string 149 | ): Promise { 150 | await request(config, `/repos/${owner}/${repo}`, { 151 | method: 'DELETE' 152 | }); 153 | } 154 | 155 | export async function checkRepo( 156 | config: GitHubConfig, 157 | owner: string, 158 | repo: string 159 | ): Promise { 160 | try { 161 | await request(config, `/repos/${owner}/${repo}`); 162 | return true; 163 | } catch (error) { 164 | return false; 165 | } 166 | } 167 | 168 | export async function deleteFolder( 169 | config: GitHubConfig, 170 | owner: string, 171 | repo: string, 172 | path: string 173 | ): Promise { 174 | const contents = await getContents(config, owner, repo, path); 175 | 176 | // Recursively delete all contents 177 | for (const item of contents) { 178 | if (item.type === 'dir') { 179 | await deleteFolder(config, owner, repo, item.path); 180 | } else { 181 | await deleteFile(config, owner, repo, item.path, item.sha); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/lib/services/service.ts: -------------------------------------------------------------------------------- 1 | import type { ServiceConfig } from '$lib/types'; 2 | import { DEFAULT_SERVICE_CONFIG, SERVICE_REPO_PATH, SERVICE_CONFIG_FILE } from '$lib/types'; 3 | import * as github from './github'; 4 | 5 | export async function initializeServiceRepo(config: github.GitHubConfig) { 6 | // First, try to find existing service repo 7 | const repos = await github.getRepositories(config); 8 | let serviceRepo = repos.find((repo) => repo.name === SERVICE_REPO_PATH); 9 | 10 | // If it doesn't exist, create it 11 | if (!serviceRepo) { 12 | serviceRepo = await github.createRepository(config, SERVICE_REPO_PATH); 13 | 14 | // Create initial config file immediately after creating repo 15 | await github.createFile( 16 | config, 17 | serviceRepo.owner.login, 18 | serviceRepo.name, 19 | SERVICE_CONFIG_FILE, 20 | JSON.stringify( 21 | { 22 | ...DEFAULT_SERVICE_CONFIG, 23 | connectedRepos: [`${serviceRepo.owner.login}/${SERVICE_REPO_PATH}`] 24 | }, 25 | null, 26 | 2 27 | ) 28 | ); 29 | 30 | return serviceRepo; 31 | } 32 | 33 | // Try to get existing config 34 | try { 35 | const contents = await github.getContents( 36 | config, 37 | serviceRepo.owner.login, 38 | serviceRepo.name, 39 | SERVICE_CONFIG_FILE 40 | ); 41 | 42 | if (!Array.isArray(contents)) { 43 | const fileContent = contents as { content: string }; 44 | const configContent = atob(fileContent.content); 45 | const existingConfig = JSON.parse(configContent); 46 | 47 | // Validate and clean connected repos 48 | const validRepos = await validateAndCleanConnectedRepos( 49 | config, 50 | existingConfig.connectedRepos 51 | ); 52 | 53 | // Update config if any repos were removed 54 | if (validRepos.length !== existingConfig.connectedRepos.length) { 55 | const updatedConfig = { ...existingConfig, connectedRepos: validRepos }; 56 | await github.createFile( 57 | config, 58 | serviceRepo.owner.login, 59 | serviceRepo.name, 60 | SERVICE_CONFIG_FILE, 61 | JSON.stringify(updatedConfig, null, 2), 62 | 'Clean up invalid repositories', 63 | fileContent.sha 64 | ); 65 | } 66 | } 67 | } catch (error) { 68 | if ((error as any).status === 404) { 69 | // Create new config file if it doesn't exist 70 | await github.createFile( 71 | config, 72 | serviceRepo.owner.login, 73 | serviceRepo.name, 74 | SERVICE_CONFIG_FILE, 75 | JSON.stringify( 76 | { 77 | ...DEFAULT_SERVICE_CONFIG, 78 | connectedRepos: [`${serviceRepo.owner.login}/${SERVICE_REPO_PATH}`] 79 | }, 80 | null, 81 | 2 82 | ) 83 | ); 84 | } else { 85 | throw error; 86 | } 87 | } 88 | 89 | return serviceRepo; 90 | } 91 | export async function getServiceConfig(config: github.GitHubConfig): Promise { 92 | const repos = await github.getRepositories(config); 93 | const serviceRepo = repos.find((repo) => repo.name === SERVICE_REPO_PATH); 94 | if (!serviceRepo) throw new Error('Service repository not found'); 95 | 96 | const contents = await github.getContents( 97 | config, 98 | serviceRepo.owner.login, 99 | serviceRepo.name, 100 | SERVICE_CONFIG_FILE 101 | ); 102 | 103 | if (Array.isArray(contents)) throw new Error('Unexpected contents format'); 104 | 105 | // Add a type assertion here 106 | const fileContent = contents as { content: string }; 107 | const configContent = atob(fileContent.content); 108 | return JSON.parse(configContent); 109 | } 110 | 111 | export async function updateServiceConfig( 112 | config: github.GitHubConfig, 113 | serviceConfig: ServiceConfig 114 | ): Promise { 115 | const repos = await github.getRepositories(config); 116 | const serviceRepo = repos.find((repo) => repo.name === SERVICE_REPO_PATH); 117 | if (!serviceRepo) throw new Error('Service repository not found'); 118 | 119 | try { 120 | // Get current file contents to get the SHA 121 | const contents = await github.getContents( 122 | config, 123 | serviceRepo.owner.login, 124 | serviceRepo.name, 125 | SERVICE_CONFIG_FILE 126 | ); 127 | 128 | if (Array.isArray(contents)) throw new Error('Unexpected contents format'); 129 | 130 | // Use the SHA from the existing file 131 | const fileContent = contents as { sha: string }; 132 | 133 | await github.createFile( 134 | config, 135 | serviceRepo.owner.login, 136 | serviceRepo.name, 137 | SERVICE_CONFIG_FILE, 138 | JSON.stringify(serviceConfig, null, 2), 139 | 'Update service config', 140 | fileContent.sha // Pass the SHA 141 | ); 142 | } catch (error) { 143 | // If file doesn't exist, create it without SHA 144 | if ((error as any).status === 404) { 145 | await github.createFile( 146 | config, 147 | serviceRepo.owner.login, 148 | serviceRepo.name, 149 | SERVICE_CONFIG_FILE, 150 | JSON.stringify(serviceConfig, null, 2), 151 | 'Create service config' 152 | ); 153 | } else { 154 | throw error; 155 | } 156 | } 157 | } 158 | 159 | export async function validateAndCleanConnectedRepos( 160 | config: github.GitHubConfig, 161 | connectedRepos: string[] 162 | ): Promise { 163 | const validRepos: string[] = []; 164 | 165 | for (const repoFullName of connectedRepos) { 166 | const [owner, repo] = repoFullName.split('/'); 167 | if (await github.checkRepo(config, owner, repo)) { 168 | validRepos.push(repoFullName); 169 | } else { 170 | console.log(`Repository ${repoFullName} no longer exists, removing from config`); 171 | } 172 | } 173 | 174 | return validRepos; 175 | } 176 | -------------------------------------------------------------------------------- /src/lib/stores/reload.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | export const needsReload = writable(false); 4 | -------------------------------------------------------------------------------- /src/lib/stores/repositories.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import type { Repository } from '$lib/types'; 3 | 4 | export const repositories = writable([]); 5 | export const currentRepository = writable(null); 6 | -------------------------------------------------------------------------------- /src/lib/stores/service-config.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import type { ServiceConfig } from '$lib/types'; 3 | import { DEFAULT_SERVICE_CONFIG } from '$lib/types'; 4 | 5 | export const serviceConfig = writable(DEFAULT_SERVICE_CONFIG); 6 | -------------------------------------------------------------------------------- /src/lib/stores/theme.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | type Theme = 'light' | 'dark'; 4 | 5 | function createThemeStore() { 6 | // Get initial theme from localStorage or system preference 7 | const getInitialTheme = (): Theme => { 8 | if (typeof window === 'undefined') return 'light'; 9 | 10 | const savedTheme = localStorage.getItem('theme') as Theme; 11 | if (savedTheme) return savedTheme; 12 | 13 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 14 | }; 15 | 16 | const { subscribe, set } = writable(getInitialTheme()); 17 | 18 | return { 19 | subscribe, 20 | toggle: () => { 21 | if (typeof window === 'undefined') return; 22 | 23 | const newTheme = document.documentElement.classList.contains('dark') ? 'light' : 'dark'; 24 | document.documentElement.classList.remove('light', 'dark'); 25 | document.documentElement.classList.add(newTheme); 26 | localStorage.setItem('theme', newTheme); 27 | set(newTheme); 28 | }, 29 | // Only used for initialization 30 | set: (theme: Theme) => { 31 | if (typeof window === 'undefined') return; 32 | 33 | document.documentElement.classList.remove('light', 'dark'); 34 | document.documentElement.classList.add(theme); 35 | localStorage.setItem('theme', theme); 36 | set(theme); 37 | } 38 | }; 39 | } 40 | 41 | export const theme = createThemeStore(); 42 | -------------------------------------------------------------------------------- /src/lib/supabaseClient.ts: -------------------------------------------------------------------------------- 1 | import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; 2 | import { createClient } from '@supabase/supabase-js'; 3 | 4 | export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY); 5 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type Repository = { 2 | id: string; 3 | name: string; 4 | full_name: string; 5 | owner: { 6 | login: string; 7 | avatar_url: string; 8 | }; 9 | default_branch: string; 10 | private: boolean; 11 | html_url: string; 12 | }; 13 | 14 | export type FileContent = { 15 | name: string; 16 | path: string; 17 | sha: string; 18 | size: number; 19 | url: string; 20 | html_url: string; 21 | git_url: string; 22 | download_url: string; 23 | type: 'file' | 'dir'; 24 | content?: string; 25 | encoding?: string; 26 | }; 27 | 28 | export type ServiceConfig = { 29 | connectedRepos: string[]; 30 | version: number; 31 | }; 32 | 33 | export const DEFAULT_SERVICE_CONFIG: ServiceConfig = { 34 | connectedRepos: [], 35 | version: 1 36 | }; 37 | 38 | export const SERVICE_REPO_PATH = '3db-service'; 39 | export const SERVICE_CONFIG_FILE = 'index.json'; 40 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export function formatBytes(bytes: number, decimals = 1): string { 9 | if (bytes === 0) return '0 B'; 10 | 11 | const k = 1024; 12 | const dm = decimals < 0 ? 0 : decimals; 13 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 14 | 15 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 16 | 17 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import type { LayoutServerLoad } from './$types'; 2 | 3 | export const load: LayoutServerLoad = async ({ locals: { supabase, getUser } }) => { 4 | const { 5 | data: { session } 6 | } = await supabase.auth.getSession(); 7 | 8 | const user = session ? await getUser() : null; 9 | 10 | return { 11 | user, 12 | session 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 167 | 168 | {#if !isLoading} 169 | {#if data.user} 170 | {#if !hasGithubApp} 171 | 174 | {:else} 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | {#if githubConfig} 183 | 184 | Repositories 185 | 186 | {#each $repositories as repo} 187 | 188 | 189 | currentRepository.set(repo)} 193 | > 194 | 195 | {repo.name} 196 | 197 | 198 | 199 | {/each} 200 | 201 | 202 | {:else} 203 |
    Loading repositories...
    204 | {/if} 205 |
    206 | 207 | 208 | {#if $needsReload} 209 |
    210 | 211 |

    Reload to see changes. You may need to wait a few seconds.

    212 |
    213 | {/if} 214 | 222 | 226 |
    227 |
    228 | 229 | 230 | {@render children()} 231 | 232 | {#if githubConfig} 233 | 238 | 239 | 246 | {/if} 247 | 248 |
    249 | {/if} 250 | {:else} 251 | {@render children()} 252 | {/if} 253 | {/if} 254 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; 2 | import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit'; 3 | import type { LoadEvent } from '@sveltejs/kit'; 4 | 5 | export const load = async ({ fetch, depends }: LoadEvent) => { 6 | depends('supabase:auth'); 7 | 8 | const supabase = createSupabaseLoadClient({ 9 | supabaseUrl: PUBLIC_SUPABASE_URL, 10 | supabaseKey: PUBLIC_SUPABASE_ANON_KEY, 11 | event: { fetch }, 12 | serverSession: null 13 | }); 14 | 15 | // First check session 16 | const { 17 | data: { session } 18 | } = await supabase.auth.getSession(); 19 | 20 | let user = null; 21 | if (session) { 22 | const { 23 | data: { user: userData }, 24 | error 25 | } = await supabase.auth.getUser(); 26 | if (error) { 27 | console.error('Error fetching user:', error); 28 | } else { 29 | user = userData; 30 | } 31 | } 32 | 33 | return { 34 | supabase, 35 | session, 36 | user 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 |
    57 | {#if !isLoading} 58 | {#if !data.user} 59 |
    60 |

    3db

    61 | 65 | 66 |
    67 |
    68 |

    Use GitHub as a CDN

    69 |

    70 | 3db provides a simple interface to use GitHub repositories as a content delivery 71 | network or file store. Pretty flaky, but works. 72 |

    73 |
    74 | 75 |
    76 |

    Features:

    77 |
      78 |
    • Create and connect multiple repositories
    • 79 |
    • Upload and delete files
    • 80 |
    • Browse files and folders
    • 81 |
    • Get direct CDN links
    • 82 |
    83 |
    84 | 85 |
    86 |

    How it works:

    87 |

    88 | A repository called '3db-service' will be created to store metadata. You can then 89 | create or connect repositories to use as storage. Files are stored in public GitHub 90 | repositories and served via GitHub's CDN. 91 |

    92 |
    93 | 94 |
    95 |

    Warning

    96 |

    97 | This is an experimental project. Only use for non-critical data. Files are public and 98 | GitHub's 5GB repository limit applies. 99 |

    100 |
    101 | 102 | 111 |
    112 |
    113 | {:else if !githubConfig} 114 |
    115 |

    116 | Unable to access GitHub. Please try logging in again. 117 |

    118 |
    119 | {:else} 120 |
    121 | 122 |
    123 | {/if} 124 | {/if} 125 |
    126 | -------------------------------------------------------------------------------- /src/routes/api/github/check-installation/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | 4 | export const GET: RequestHandler = async ({ locals: { supabase, getUser } }) => { 5 | const user = await getUser(); 6 | 7 | if (!user) { 8 | return json({ installed: false }); 9 | } 10 | 11 | try { 12 | const { 13 | data: { session } 14 | } = await supabase.auth.getSession(); 15 | 16 | if (!session?.provider_token) { 17 | return json({ installed: false }); 18 | } 19 | 20 | // Make a request to GitHub API to check if the app is installed 21 | const response = await fetch('https://api.github.com/user/installations', { 22 | headers: { 23 | Authorization: `Bearer ${session.provider_token}`, 24 | Accept: 'application/vnd.github.v3+json' 25 | } 26 | }); 27 | 28 | const data = await response.json(); 29 | const installed = data.total_count > 0; 30 | 31 | return json({ installed }); 32 | } catch (error) { 33 | console.error('Error checking GitHub app installation:', error); 34 | return json({ installed: false }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/routes/auth/auth-code-error/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 |

    Authentication Error

    7 |

    There was an error during authentication.

    8 | 9 |
    10 | -------------------------------------------------------------------------------- /src/routes/auth/callback/+server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | 4 | export const GET: RequestHandler = async ({ url, locals: { supabase } }) => { 5 | const code = url.searchParams.get('code'); 6 | const next = url.searchParams.get('next') ?? '/'; 7 | 8 | if (code) { 9 | const { error } = await supabase.auth.exchangeCodeForSession(code); 10 | if (error) { 11 | console.error('Auth error:', error); 12 | throw redirect(303, '/'); 13 | } 14 | } 15 | 16 | throw redirect(303, next); 17 | }; 18 | -------------------------------------------------------------------------------- /static/BasierSquareMono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tnixc/3db/c8683196ee815be5215f3ba109a205091c5fc9c0/static/BasierSquareMono.woff2 -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tnixc/3db/c8683196ee815be5215f3ba109a205091c5fc9c0/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | // import adapter from '@sveltejs/adapter-auto'; 2 | import adapter from '@sveltejs/adapter-vercel'; 3 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://svelte.dev/docs/kit/integrations 8 | // for more information about preprocessors 9 | preprocess: vitePreprocess(), 10 | 11 | kit: { 12 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 13 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 14 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 15 | adapter: adapter() 16 | } 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { fontFamily } from 'tailwindcss/defaultTheme'; 2 | import type { Config } from 'tailwindcss'; 3 | import tailwindcssAnimate from 'tailwindcss-animate'; 4 | 5 | const config: Config = { 6 | darkMode: ['class'], 7 | content: ['./src/**/*.{html,js,svelte,ts}'], 8 | safelist: ['dark'], 9 | theme: { 10 | container: { 11 | center: true, 12 | padding: '2rem', 13 | screens: { 14 | '2xl': '1400px' 15 | } 16 | }, 17 | extend: { 18 | colors: { 19 | border: 'hsl(var(--border) / )', 20 | input: 'hsl(var(--input) / )', 21 | ring: 'hsl(var(--ring) / )', 22 | background: 'hsl(var(--background) / )', 23 | foreground: 'hsl(var(--foreground) / )', 24 | primary: { 25 | DEFAULT: 'hsl(var(--primary) / )', 26 | foreground: 'hsl(var(--primary-foreground) / )' 27 | }, 28 | secondary: { 29 | DEFAULT: 'hsl(var(--secondary) / )', 30 | foreground: 'hsl(var(--secondary-foreground) / )' 31 | }, 32 | destructive: { 33 | DEFAULT: 'hsl(var(--destructive) / )', 34 | foreground: 'hsl(var(--destructive-foreground) / )' 35 | }, 36 | muted: { 37 | DEFAULT: 'hsl(var(--muted) / )', 38 | foreground: 'hsl(var(--muted-foreground) / )' 39 | }, 40 | accent: { 41 | DEFAULT: 'hsl(var(--accent) / )', 42 | foreground: 'hsl(var(--accent-foreground) / )' 43 | }, 44 | popover: { 45 | DEFAULT: 'hsl(var(--popover) / )', 46 | foreground: 'hsl(var(--popover-foreground) / )' 47 | }, 48 | card: { 49 | DEFAULT: 'hsl(var(--card) / )', 50 | foreground: 'hsl(var(--card-foreground) / )' 51 | }, 52 | sidebar: { 53 | DEFAULT: 'hsl(var(--sidebar-background))', 54 | foreground: 'hsl(var(--sidebar-foreground))', 55 | primary: 'hsl(var(--sidebar-primary))', 56 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', 57 | accent: 'hsl(var(--sidebar-accent))', 58 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', 59 | border: 'hsl(var(--sidebar-border))', 60 | ring: 'hsl(var(--sidebar-ring))' 61 | } 62 | }, 63 | borderRadius: { 64 | xl: 'calc(var(--radius) + 4px)', 65 | lg: 'var(--radius)', 66 | md: 'calc(var(--radius) - 2px)', 67 | sm: 'calc(var(--radius) - 4px)' 68 | }, 69 | fontFamily: { 70 | sans: [...fontFamily.sans] 71 | }, 72 | keyframes: { 73 | 'accordion-down': { 74 | from: { height: '0' }, 75 | to: { height: 'var(--bits-accordion-content-height)' } 76 | }, 77 | 'accordion-up': { 78 | from: { height: 'var(--bits-accordion-content-height)' }, 79 | to: { height: '0' } 80 | }, 81 | 'caret-blink': { 82 | '0%,70%,100%': { opacity: '1' }, 83 | '20%,50%': { opacity: '0' } 84 | } 85 | }, 86 | animation: { 87 | 'accordion-down': 'accordion-down 0.2s ease-out', 88 | 'accordion-up': 'accordion-up 0.2s ease-out', 89 | 'caret-blink': 'caret-blink 1.25s ease-out infinite' 90 | } 91 | } 92 | }, 93 | plugins: [tailwindcssAnimate] 94 | }; 95 | 96 | export default config; 97 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | // "checkJs": true, 5 | "allowJs": false, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | --------------------------------------------------------------------------------