├── .npmrc ├── static ├── robots.txt ├── favicon.png └── fonts │ ├── Sora-VariableFont.ttf │ └── Montserrat-VariableFont.ttf ├── src ├── lib │ ├── components │ │ ├── ui │ │ │ ├── sonner │ │ │ │ ├── index.ts │ │ │ │ └── sonner.svelte │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ └── input.svelte │ │ │ ├── label │ │ │ │ ├── index.ts │ │ │ │ └── label.svelte │ │ │ ├── slider │ │ │ │ ├── index.ts │ │ │ │ └── slider.svelte │ │ │ ├── switch │ │ │ │ ├── index.ts │ │ │ │ └── switch.svelte │ │ │ ├── checkbox │ │ │ │ ├── index.ts │ │ │ │ └── checkbox.svelte │ │ │ ├── progress │ │ │ │ ├── index.ts │ │ │ │ └── progress.svelte │ │ │ ├── separator │ │ │ │ ├── index.ts │ │ │ │ └── separator.svelte │ │ │ ├── skeleton │ │ │ │ ├── index.ts │ │ │ │ └── skeleton.svelte │ │ │ ├── textarea │ │ │ │ ├── index.ts │ │ │ │ └── textarea.svelte │ │ │ ├── badge │ │ │ │ ├── index.ts │ │ │ │ └── badge.svelte │ │ │ ├── kbd │ │ │ │ ├── index.ts │ │ │ │ ├── kbd-group.svelte │ │ │ │ └── kbd.svelte │ │ │ ├── radio-group │ │ │ │ ├── index.ts │ │ │ │ ├── radio-group.svelte │ │ │ │ └── radio-group-item.svelte │ │ │ ├── toggle-group │ │ │ │ ├── index.ts │ │ │ │ ├── toggle-group-item.svelte │ │ │ │ └── toggle-group.svelte │ │ │ ├── toggle │ │ │ │ ├── index.ts │ │ │ │ └── toggle.svelte │ │ │ ├── form │ │ │ │ ├── form-button.svelte │ │ │ │ ├── form-legend.svelte │ │ │ │ ├── form-description.svelte │ │ │ │ ├── form-fieldset.svelte │ │ │ │ ├── form-label.svelte │ │ │ │ ├── form-field-errors.svelte │ │ │ │ ├── index.ts │ │ │ │ ├── form-field.svelte │ │ │ │ └── form-element-field.svelte │ │ │ ├── select │ │ │ │ ├── select-group.svelte │ │ │ │ ├── select-separator.svelte │ │ │ │ ├── select-label.svelte │ │ │ │ ├── select-group-heading.svelte │ │ │ │ ├── select-scroll-up-button.svelte │ │ │ │ ├── select-scroll-down-button.svelte │ │ │ │ ├── index.ts │ │ │ │ ├── select-item.svelte │ │ │ │ ├── select-trigger.svelte │ │ │ │ └── select-content.svelte │ │ │ ├── sheet │ │ │ │ ├── sheet-close.svelte │ │ │ │ ├── sheet-trigger.svelte │ │ │ │ ├── sheet-title.svelte │ │ │ │ ├── sheet-description.svelte │ │ │ │ ├── sheet-header.svelte │ │ │ │ ├── sheet-footer.svelte │ │ │ │ ├── sheet-overlay.svelte │ │ │ │ ├── index.ts │ │ │ │ └── sheet-content.svelte │ │ │ ├── dialog │ │ │ │ ├── dialog-close.svelte │ │ │ │ ├── dialog-trigger.svelte │ │ │ │ ├── dialog-title.svelte │ │ │ │ ├── dialog-description.svelte │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ ├── dialog-header.svelte │ │ │ │ ├── dialog-footer.svelte │ │ │ │ ├── index.ts │ │ │ │ └── dialog-content.svelte │ │ │ ├── tooltip │ │ │ │ ├── tooltip-trigger.svelte │ │ │ │ ├── index.ts │ │ │ │ └── tooltip-content.svelte │ │ │ ├── sidebar │ │ │ │ ├── constants.ts │ │ │ │ ├── sidebar-separator.svelte │ │ │ │ ├── sidebar-input.svelte │ │ │ │ ├── sidebar-footer.svelte │ │ │ │ ├── sidebar-header.svelte │ │ │ │ ├── sidebar-group-content.svelte │ │ │ │ ├── sidebar-group.svelte │ │ │ │ ├── sidebar-menu-item.svelte │ │ │ │ ├── sidebar-menu-sub-item.svelte │ │ │ │ ├── sidebar-menu.svelte │ │ │ │ ├── sidebar-content.svelte │ │ │ │ ├── sidebar-menu-sub.svelte │ │ │ │ ├── sidebar-inset.svelte │ │ │ │ ├── sidebar-trigger.svelte │ │ │ │ ├── sidebar-menu-badge.svelte │ │ │ │ ├── sidebar-menu-skeleton.svelte │ │ │ │ ├── sidebar-group-label.svelte │ │ │ │ ├── sidebar-group-action.svelte │ │ │ │ ├── sidebar-rail.svelte │ │ │ │ ├── sidebar-provider.svelte │ │ │ │ ├── sidebar-menu-sub-button.svelte │ │ │ │ ├── sidebar-menu-action.svelte │ │ │ │ └── index.ts │ │ │ ├── avatar │ │ │ │ ├── index.ts │ │ │ │ ├── avatar-image.svelte │ │ │ │ ├── avatar-fallback.svelte │ │ │ │ └── avatar.svelte │ │ │ ├── dropdown-menu │ │ │ │ ├── dropdown-menu-group.svelte │ │ │ │ ├── dropdown-menu-trigger.svelte │ │ │ │ ├── dropdown-menu-radio-group.svelte │ │ │ │ ├── dropdown-menu-separator.svelte │ │ │ │ ├── dropdown-menu-shortcut.svelte │ │ │ │ ├── dropdown-menu-group-heading.svelte │ │ │ │ ├── dropdown-menu-label.svelte │ │ │ │ ├── dropdown-menu-sub-content.svelte │ │ │ │ ├── dropdown-menu-sub-trigger.svelte │ │ │ │ ├── dropdown-menu-content.svelte │ │ │ │ ├── dropdown-menu-item.svelte │ │ │ │ ├── dropdown-menu-radio-item.svelte │ │ │ │ ├── dropdown-menu-checkbox-item.svelte │ │ │ │ └── index.ts │ │ │ ├── alert-dialog │ │ │ │ ├── alert-dialog-trigger.svelte │ │ │ │ ├── alert-dialog-title.svelte │ │ │ │ ├── alert-dialog-description.svelte │ │ │ │ ├── alert-dialog-action.svelte │ │ │ │ ├── alert-dialog-cancel.svelte │ │ │ │ ├── alert-dialog-overlay.svelte │ │ │ │ ├── alert-dialog-header.svelte │ │ │ │ ├── alert-dialog-footer.svelte │ │ │ │ ├── index.ts │ │ │ │ └── alert-dialog-content.svelte │ │ │ ├── accordion │ │ │ │ ├── accordion.svelte │ │ │ │ ├── accordion-root.svelte │ │ │ │ ├── index.ts │ │ │ │ ├── accordion-item.svelte │ │ │ │ ├── accordion-content.svelte │ │ │ │ └── accordion-trigger.svelte │ │ │ ├── button │ │ │ │ └── index.ts │ │ │ ├── tabs │ │ │ │ ├── index.ts │ │ │ │ ├── tabs-content.svelte │ │ │ │ ├── tabs.svelte │ │ │ │ ├── tabs-list.svelte │ │ │ │ └── tabs-trigger.svelte │ │ │ ├── alert │ │ │ │ ├── index.ts │ │ │ │ ├── alert-title.svelte │ │ │ │ ├── alert-description.svelte │ │ │ │ └── alert.svelte │ │ │ ├── popover │ │ │ │ ├── popover-trigger.svelte │ │ │ │ ├── index.ts │ │ │ │ └── popover-content.svelte │ │ │ ├── breadcrumb │ │ │ │ ├── breadcrumb.svelte │ │ │ │ ├── breadcrumb-item.svelte │ │ │ │ ├── breadcrumb-list.svelte │ │ │ │ ├── breadcrumb-page.svelte │ │ │ │ ├── index.ts │ │ │ │ ├── breadcrumb-separator.svelte │ │ │ │ ├── breadcrumb-ellipsis.svelte │ │ │ │ └── breadcrumb-link.svelte │ │ │ ├── card │ │ │ │ ├── card-content.svelte │ │ │ │ ├── card-title.svelte │ │ │ │ ├── card-description.svelte │ │ │ │ ├── card-footer.svelte │ │ │ │ ├── card-action.svelte │ │ │ │ ├── card.svelte │ │ │ │ ├── index.ts │ │ │ │ └── card-header.svelte │ │ │ └── table │ │ │ │ ├── table-header.svelte │ │ │ │ ├── table-body.svelte │ │ │ │ ├── table-cell.svelte │ │ │ │ ├── table-caption.svelte │ │ │ │ ├── table-footer.svelte │ │ │ │ ├── table.svelte │ │ │ │ ├── table-head.svelte │ │ │ │ ├── index.ts │ │ │ │ └── table-row.svelte │ │ ├── placeholder-pattern.svelte │ │ ├── app-sidebar-header.svelte │ │ ├── app-sidebar.svelte │ │ ├── settings-navbar.svelte │ │ ├── app-head.svelte │ │ ├── app-sidebar-content.svelte │ │ └── theme-switch.svelte │ ├── utils │ │ ├── themes.ts │ │ ├── mail │ │ │ ├── types.ts │ │ │ └── templates │ │ │ │ ├── mail-theme.ts │ │ │ │ ├── welcome.svelte │ │ │ │ └── reset-password.svelte │ │ ├── helpers │ │ │ ├── name.ts │ │ │ ├── image.ts │ │ │ └── generate.ts │ │ ├── utils.ts │ │ └── messages.json │ ├── assets │ │ ├── logo.png │ │ ├── avatar.png │ │ └── meta_image.png │ ├── db │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── session.ts │ │ │ ├── verification.ts │ │ │ ├── account.ts │ │ │ └── user.ts │ │ ├── migrations │ │ │ ├── meta │ │ │ │ └── _journal.json │ │ │ └── 0000_tricky_angel.sql │ │ └── clear.ts │ ├── hooks │ │ ├── is-mobile.svelte.ts │ │ └── use-theme.ts │ ├── server │ │ ├── storage.ts │ │ └── database.ts │ └── validations │ │ └── files.ts ├── routes │ ├── +page.server.ts │ ├── (app) │ │ ├── +layout.server.ts │ │ ├── dashboard │ │ │ └── +page.svelte │ │ └── settings │ │ │ └── +layout.svelte │ ├── +layout.server.ts │ ├── +error.svelte │ ├── (auth) │ │ ├── login │ │ │ ├── google │ │ │ │ └── +server.ts │ │ │ └── +page.server.ts │ │ ├── +layout.svelte │ │ ├── logout │ │ │ └── +server.ts │ │ └── password │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── reset │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ ├── api │ │ └── upload │ │ │ └── +server.ts │ └── +layout.svelte ├── hooks.server.ts ├── app.d.ts └── app.html ├── tests ├── unit │ └── demo.spec.ts └── e2e │ └── homepage.test.ts ├── playwright.config.ts ├── tsconfig.json ├── components.json ├── drizzle.config.ts ├── .env.example ├── svelte.config.js ├── vite.config.ts ├── .prettierrc ├── .gitignore ├── .prettierignore ├── eslint.config.js └── .github └── workflows └── ci.yml /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /admin -------------------------------------------------------------------------------- /src/lib/components/ui/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Toaster } from './sonner.svelte'; 2 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/lib/utils/themes.ts: -------------------------------------------------------------------------------- 1 | export const themes = { default: 'dark', light: 'light', dark: 'dark' }; 2 | -------------------------------------------------------------------------------- /src/lib/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/src/lib/assets/logo.png -------------------------------------------------------------------------------- /src/lib/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/src/lib/assets/avatar.png -------------------------------------------------------------------------------- /src/lib/assets/meta_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/src/lib/assets/meta_image.png -------------------------------------------------------------------------------- /static/fonts/Sora-VariableFont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/static/fonts/Sora-VariableFont.ttf -------------------------------------------------------------------------------- /src/lib/utils/mail/types.ts: -------------------------------------------------------------------------------- 1 | export interface EmailTemplate { 2 | subject: string; 3 | html: string; 4 | text: string; 5 | } 6 | -------------------------------------------------------------------------------- /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/label/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './label.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Label 7 | }; 8 | -------------------------------------------------------------------------------- /static/fonts/Montserrat-VariableFont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/static/fonts/Montserrat-VariableFont.ttf -------------------------------------------------------------------------------- /src/lib/components/ui/slider/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './slider.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Slider 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/switch/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './switch.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Switch 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './checkbox.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Checkbox 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/progress/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './progress.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Progress 7 | }; 8 | -------------------------------------------------------------------------------- /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/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/textarea/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './textarea.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Textarea 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/db/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account'; 2 | export * from './session'; 3 | export * from './user'; 4 | export * from './verification'; 5 | -------------------------------------------------------------------------------- /src/lib/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Badge } from './badge.svelte'; 2 | export { badgeVariants, type BadgeVariant } from './badge.svelte'; 3 | -------------------------------------------------------------------------------- /src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirectIfLoggedIn } from '$lib/server/auth'; 2 | 3 | export async function load() { 4 | redirectIfLoggedIn(); 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/demo.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/routes/(app)/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { requireLogin } from '$lib/server/auth'; 2 | 3 | export async function load() { 4 | const { user, session } = requireLogin(); 5 | return { user, session }; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/components/ui/kbd/index.ts: -------------------------------------------------------------------------------- 1 | import Group from './kbd-group.svelte'; 2 | import Root from './kbd.svelte'; 3 | 4 | export { 5 | Root, 6 | Group, 7 | // 8 | Root as Kbd, 9 | Group as KbdGroup 10 | }; 11 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | webServer: { 5 | command: 'vite build && vite preview', 6 | port: 4173 7 | }, 8 | testDir: 'tests/e2e' 9 | }); 10 | -------------------------------------------------------------------------------- /src/lib/components/ui/radio-group/index.ts: -------------------------------------------------------------------------------- 1 | import Item from './radio-group-item.svelte'; 2 | import Root from './radio-group.svelte'; 3 | 4 | export { 5 | Root, 6 | Item, 7 | // 8 | Root as RadioGroup, 9 | Item as RadioGroupItem 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/components/ui/toggle-group/index.ts: -------------------------------------------------------------------------------- 1 | import Item from './toggle-group-item.svelte'; 2 | import Root from './toggle-group.svelte'; 3 | 4 | export { 5 | Root, 6 | Item, 7 | // 8 | Root as ToggleGroup, 9 | Item as ToggleGroupItem 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/components/ui/toggle/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './toggle.svelte'; 2 | 3 | export { toggleVariants, type ToggleSize, type ToggleVariant, type ToggleVariants } from './toggle.svelte'; 4 | 5 | export { 6 | Root, 7 | // 8 | Root as Toggle 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-button.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/db/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1754650179463, 9 | "tag": "0000_tricky_angel", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-group.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-close.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-close.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/hooks/is-mobile.svelte.ts: -------------------------------------------------------------------------------- 1 | import { MediaQuery } from 'svelte/reactivity'; 2 | 3 | const DEFAULT_MOBILE_BREAKPOINT = 768; 4 | 5 | export class IsMobile extends MediaQuery { 6 | constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { 7 | super(`max-width: ${breakpoint - 1}px`); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/tooltip-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { loadFlash } from 'sveltekit-flash-message/server'; 2 | 3 | import { auth } from '$lib/server/auth'; 4 | 5 | export const load = loadFlash(async ({ request }) => { 6 | const session = await auth.api.getSession(request); 7 | return { 8 | user: session?.user ?? null 9 | }; 10 | }); 11 | -------------------------------------------------------------------------------- /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/avatar/index.ts: -------------------------------------------------------------------------------- 1 | import Fallback from './avatar-fallback.svelte'; 2 | import Image from './avatar-image.svelte'; 3 | import Root from './avatar.svelte'; 4 | 5 | export { 6 | Root, 7 | Image, 8 | Fallback, 9 | // 10 | Root as Avatar, 11 | Image as AvatarImage, 12 | Fallback as AvatarFallback 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/accordion.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/accordion-root.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import type { ButtonProps, ButtonSize, ButtonVariant } from './button.svelte'; 2 | 3 | import Root, { buttonVariants } from './button.svelte'; 4 | 5 | export { 6 | Root, 7 | type ButtonProps as Props, 8 | // 9 | Root as Button, 10 | buttonVariants, 11 | type ButtonProps, 12 | type ButtonSize, 13 | type ButtonVariant 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | import Content from './tabs-content.svelte'; 2 | import List from './tabs-list.svelte'; 3 | import Trigger from './tabs-trigger.svelte'; 4 | import Root from './tabs.svelte'; 5 | 6 | export { 7 | Root, 8 | Content, 9 | List, 10 | Trigger, 11 | // 12 | Root as Tabs, 13 | Content as TabsContent, 14 | List as TabsList, 15 | Trigger as TabsTrigger 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/index.ts: -------------------------------------------------------------------------------- 1 | import Description from './alert-description.svelte'; 2 | import Title from './alert-title.svelte'; 3 | import Root from './alert.svelte'; 4 | 5 | export { alertVariants, type AlertVariant } from './alert.svelte'; 6 | 7 | export { 8 | Root, 9 | Description, 10 | Title, 11 | // 12 | Root as Alert, 13 | Description as AlertDescription, 14 | Title as AlertTitle 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/tabs-content.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/components/ui/popover/popover-trigger.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

7 | {page.status}: {page.error?.message} 8 |

9 |

10 | Go back home 11 |

12 |
13 | -------------------------------------------------------------------------------- /src/lib/components/ui/kbd/kbd-group.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {@render children?.()} 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/index.ts: -------------------------------------------------------------------------------- 1 | import Content from './accordion-content.svelte'; 2 | import Item from './accordion-item.svelte'; 3 | import Trigger from './accordion-trigger.svelte'; 4 | import Root from './accordion.svelte'; 5 | 6 | export { 7 | Root, 8 | Content, 9 | Item, 10 | Trigger, 11 | // 12 | Root as Accordion, 13 | Content as AccordionContent, 14 | Item as AccordionItem, 15 | Trigger as AccordionTrigger 16 | }; 17 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-svelte.com/schema.json", 3 | "tailwind": { 4 | "css": "src/styles/app.css", 5 | "baseColor": "stone" 6 | }, 7 | "aliases": { 8 | "components": "$components", 9 | "utils": "$lib/utils/utils", 10 | "ui": "$lib/components/ui", 11 | "hooks": "$lib/hooks", 12 | "lib": "$lib" 13 | }, 14 | "typescript": true, 15 | "registry": "https://shadcn-svelte.com/registry" 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/avatar/avatar-image.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-title.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/lib/server/storage.ts: -------------------------------------------------------------------------------- 1 | import { R2_ACCESS_KEY_ID, R2_ACCOUNT_ID, R2_SECRET_ACCESS_KEY } from '$env/static/private'; 2 | 3 | import { S3Client } from '@aws-sdk/client-s3'; 4 | 5 | const r2Endpoint = `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`; 6 | 7 | export const s3 = new S3Client({ 8 | endpoint: r2Endpoint, 9 | credentials: { 10 | accessKeyId: R2_ACCESS_KEY_ID, 11 | secretAccessKey: R2_SECRET_ACCESS_KEY 12 | }, 13 | region: 'auto' 14 | }); 15 | -------------------------------------------------------------------------------- /src/lib/utils/mail/templates/mail-theme.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from 'sailkit'; 2 | 3 | export const mailTheme = createTheme({ 4 | fonts: [ 5 | { 6 | name: 'Outfit', 7 | href: 'https://fonts.googleapis.com/css2?family=Outfit' 8 | } 9 | ], 10 | styles: { 11 | global: { 12 | fontFamily: 13 | 'Outfit, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Ubuntu, Helvetica, Arial, sans-serif' 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/tabs.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/accordion-item.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-description.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit'; 2 | 3 | import 'dotenv/config'; 4 | 5 | export default defineConfig({ 6 | dialect: 'turso', 7 | schema: './src/lib/db/models/index.ts', 8 | out: './src/lib/db/migrations', 9 | breakpoints: true, 10 | casing: 'snake_case', 11 | strict: true, 12 | dbCredentials: { 13 | url: process.env.DATABASE_URL ?? '', 14 | authToken: process.env.NODE_ENV === 'development' ? undefined : process.env.DATABASE_AUTH_TOKEN 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { building } from '$app/environment'; 2 | 3 | import { svelteKitHandler } from 'better-auth/svelte-kit'; 4 | 5 | import { auth } from '$lib/server/auth'; 6 | 7 | export async function handle({ event, resolve }) { 8 | const session = await auth.api.getSession(event.request); 9 | if (session) { 10 | event.locals.session = session.session; 11 | event.locals.user = session.user; 12 | } 13 | 14 | return svelteKitHandler({ event, resolve, auth, building }); 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/popover/index.ts: -------------------------------------------------------------------------------- 1 | import { Popover as PopoverPrimitive } from 'bits-ui'; 2 | 3 | import Content from './popover-content.svelte'; 4 | import Trigger from './popover-trigger.svelte'; 5 | 6 | const Root = PopoverPrimitive.Root; 7 | const Close = PopoverPrimitive.Close; 8 | 9 | export { 10 | Root, 11 | Content, 12 | Trigger, 13 | Close, 14 | // 15 | Root as Popover, 16 | Content as PopoverContent, 17 | Trigger as PopoverTrigger, 18 | Close as PopoverClose 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/utils/helpers/name.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extracts user initials for avatar placeholders and compact displays. 3 | * Provides a consistent way to represent users when profile images aren't available. 4 | * 5 | * @param firstName - User's first name 6 | * @param lastName - User's last name 7 | * @returns Two-character initials in uppercase 8 | */ 9 | export function getInitials(firstName: string, lastName: string): string { 10 | return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); 11 | } 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # General 2 | PUBLIC_BASE_URL=http://localhost:5173 3 | 4 | # Database 5 | DATABASE_URL=file:sveltekit_omakase_local.db 6 | 7 | # Better-Auth 8 | BETTER_AUTH_URL= 9 | BETTER_AUTH_SECRET= 10 | 11 | # OAuth 12 | GOOGLE_CLIENT_ID= 13 | GOOGLE_CLIENT_SECRET= 14 | 15 | # R2 16 | R2_ACCOUNT_ID= 17 | R2_ACCESS_KEY_ID= 18 | R2_SECRET_ACCESS_KEY= 19 | PUBLIC_R2_BUCKET_NAME= 20 | PUBLIC_R2_BUCKET_URL= # use R2.dev subdomain in development 21 | 22 | # Resend 23 | RESEND_API_KEY= 24 | EMAIL_SENDER= 25 | 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/avatar/avatar-fallback.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-legend.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/sonner/sonner.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/tabs-list.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/radio-group/radio-group.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-separator.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-separator.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-action.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-content.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | {@render children?.()} 17 |
18 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace App { 3 | interface Locals { 4 | user: import('$lib/server/auth').User; 5 | session: import('$lib/server/auth').Session; 6 | } 7 | 8 | interface PageData { 9 | user: User | null; 10 | session: Session | null; 11 | metadata: { 12 | title: string; 13 | description: string; 14 | image: string; 15 | url: string; 16 | breadcrumbs: { 17 | title: string; 18 | href: string; 19 | }[]; 20 | }; 21 | } 22 | } 23 | } 24 | 25 | export {}; 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/avatar/avatar.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-description.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /src/lib/server/database.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import type { Client } from '@libsql/client'; 4 | 5 | import { createClient } from '@libsql/client'; 6 | import { drizzle } from 'drizzle-orm/libsql'; 7 | 8 | import * as schema from '$lib/db/models/index'; 9 | 10 | export const client: Client = createClient({ 11 | url: process.env.DATABASE_URL || '', 12 | authToken: process.env.NODE_ENV === 'development' ? undefined : process.env.DATABASE_AUTH_TOKEN 13 | }); 14 | 15 | const db = drizzle(client, { 16 | schema, 17 | casing: 'snake_case' 18 | }); 19 | 20 | export default db; 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-item.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
  • 16 | {@render children?.()} 17 |
  • 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-title.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    16 | {@render children?.()} 17 |
    18 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-header.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    16 | {@render children?.()} 17 |
    18 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-header.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | {@render children?.()} 17 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip as TooltipPrimitive } from 'bits-ui'; 2 | 3 | import Content from './tooltip-content.svelte'; 4 | import Trigger from './tooltip-trigger.svelte'; 5 | 6 | const Root = TooltipPrimitive.Root; 7 | const Provider = TooltipPrimitive.Provider; 8 | const Portal = TooltipPrimitive.Portal; 9 | 10 | export { 11 | Root, 12 | Trigger, 13 | Content, 14 | Provider, 15 | Portal, 16 | // 17 | Root as Tooltip, 18 | Content as TooltipContent, 19 | Trigger as TooltipTrigger, 20 | Provider as TooltipProvider, 21 | Portal as TooltipPortal 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-footer.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    16 | {@render children?.()} 17 |
    18 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-description.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |

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

    18 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-body.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | {@render children?.()} 17 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-overlay.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/skeleton.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
    20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/lib/db/models/session.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | 3 | import { User } from './user'; 4 | 5 | export const Session = sqliteTable('session', { 6 | id: integer().primaryKey({ autoIncrement: true }), 7 | token: text().notNull().unique(), 8 | createdAt: integer({ mode: 'timestamp' }).notNull(), 9 | updatedAt: integer({ mode: 'timestamp' }).notNull(), 10 | expiresAt: integer({ mode: 'timestamp' }).notNull(), 11 | ipAddress: text(), 12 | userAgent: text(), 13 | userId: text() 14 | .notNull() 15 | .references(() => User.id, { onDelete: 'cascade' }) 16 | }); 17 | -------------------------------------------------------------------------------- /src/lib/components/placeholder-pattern.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-cell.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | {@render children?.()} 17 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/separator.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-input.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-footer.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    21 | {@render children?.()} 22 |
    23 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-list.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
      16 | {@render children?.()} 17 |
    18 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-caption.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | {@render children?.()} 22 | 23 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | kit: { 7 | adapter: adapter(), 8 | 9 | alias: { 10 | $components: 'src/lib/components', 11 | '$components/*': 'src/lib/components/*', 12 | 13 | $models: 'src/lib/db/models', 14 | '$models/*': 'src/lib/db/models/*', 15 | 16 | $queries: 'src/lib/db/queries', 17 | '$queries/*': 'src/lib/db/queries/*' 18 | } 19 | }, 20 | 21 | preprocess: vitePreprocess() 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    21 | {@render children?.()} 22 |
    23 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/label.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-label.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    21 | {@render children?.()} 22 |
    23 | -------------------------------------------------------------------------------- /src/lib/db/models/verification.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | 3 | export const Verification = sqliteTable('verification', { 4 | id: integer().primaryKey({ autoIncrement: true }), 5 | identifier: text().notNull(), 6 | value: text().notNull(), 7 | expiresAt: integer().notNull(), 8 | createdAt: integer({ mode: 'timestamp' }) 9 | .notNull() 10 | .$defaultFn(() => new Date()), 11 | updatedAt: integer({ mode: 'timestamp' }) 12 | .notNull() 13 | .$defaultFn(() => new Date()) 14 | .$onUpdateFn(() => new Date()) 15 | }); 16 | 17 | export type verification = typeof Verification.$inferSelect; 18 | -------------------------------------------------------------------------------- /src/routes/(auth)/login/google/+server.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '$lib/server/auth'; 2 | 3 | export async function GET(): Promise { 4 | try { 5 | const response = await auth.api.signInSocial({ 6 | body: { 7 | provider: 'google' 8 | } 9 | }); 10 | 11 | return new Response(null, { 12 | status: 302, 13 | headers: { 14 | Location: response.url?.toString() ?? '/login' 15 | } 16 | }); 17 | } catch (error) { 18 | console.error(error); 19 | return new Response(null, { 20 | status: 500, 21 | headers: { 22 | Location: '/login' 23 | } 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-footer.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    22 | {@render children?.()} 23 |
    24 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-header.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    22 | {@render children?.()} 23 |
    24 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/alert-title.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    21 | {@render children?.()} 22 |
    23 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-action.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    21 | {@render children?.()} 22 |
    23 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    21 | {@render children?.()} 22 |
    23 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    21 | {@render children?.()} 22 |
    23 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    21 | {@render children?.()} 22 |
    23 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-fieldset.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group-content.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    22 | {@render children?.()} 23 |
    24 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    22 | {@render children?.()} 23 |
    24 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-footer.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | tr]:last:border-b-0', className)} 19 | {...restProps} 20 | > 21 | {@render children?.()} 22 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    21 | {@render children?.()} 22 |
    23 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
  • 22 | {@render children?.()} 23 |
  • 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    16 | 17 | {@render children?.()} 18 |
    19 |
    20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | {@render children?.()} 22 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
  • 22 | {@render children?.()} 23 |
  • 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
      22 | {@render children?.()} 23 |
    24 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import tailwindcss from '@tailwindcss/vite'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | plugins: [tailwindcss(), sveltekit()], 7 | test: { 8 | expect: { requireAssertions: true }, 9 | projects: [ 10 | { 11 | extends: './vite.config.ts', 12 | test: { 13 | name: 'server', 14 | environment: 'node', 15 | include: ['src/**/*.{test,spec}.{js,ts}', 'tests/unit/**/*.{test,spec}.{js,ts}'], 16 | exclude: ['src/**/*.svelte.{test,spec}.{js,ts}', 'tests/e2e/**'] 17 | } 18 | } 19 | ] 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | import Action from './card-action.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 | import Root from './card.svelte'; 8 | 9 | export { 10 | Root, 11 | Content, 12 | Description, 13 | Footer, 14 | Header, 15 | Title, 16 | Action, 17 | // 18 | Root as Card, 19 | Content as CardContent, 20 | Description as CardDescription, 21 | Footer as CardFooter, 22 | Header as CardHeader, 23 | Title as CardTitle, 24 | Action as CardAction 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-head.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | {@render children?.()} 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | {@render children?.()} 23 | 24 | -------------------------------------------------------------------------------- /src/routes/(app)/dashboard/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 | {#each Array(3)} 7 |
    8 | 9 |
    10 | {/each} 11 | 14 |
    15 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 24 | {@render children?.()} 25 | 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/alert-description.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    24 | {@render children?.()} 25 |
    26 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/index.ts: -------------------------------------------------------------------------------- 1 | import Body from './table-body.svelte'; 2 | import Caption from './table-caption.svelte'; 3 | import Cell from './table-cell.svelte'; 4 | import Footer from './table-footer.svelte'; 5 | import Head from './table-head.svelte'; 6 | import Header from './table-header.svelte'; 7 | import Row from './table-row.svelte'; 8 | import Root from './table.svelte'; 9 | 10 | export { 11 | Root, 12 | Body, 13 | Caption, 14 | Cell, 15 | Footer, 16 | Head, 17 | Header, 18 | Row, 19 | // 20 | Root as Table, 21 | Body as TableBody, 22 | Caption as TableCaption, 23 | Cell as TableCell, 24 | Footer as TableFooter, 25 | Head as TableHead, 26 | Header as TableHeader, 27 | Row as TableRow 28 | }; 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-row.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | svelte-css-wrapper]:[&>th,td]:bg-muted/50', 20 | className 21 | )} 22 | {...restProps} 23 | > 24 | {@render children?.()} 25 | 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/index.ts: -------------------------------------------------------------------------------- 1 | import Ellipsis from './breadcrumb-ellipsis.svelte'; 2 | import Item from './breadcrumb-item.svelte'; 3 | import Link from './breadcrumb-link.svelte'; 4 | import List from './breadcrumb-list.svelte'; 5 | import Page from './breadcrumb-page.svelte'; 6 | import Separator from './breadcrumb-separator.svelte'; 7 | import Root from './breadcrumb.svelte'; 8 | 9 | export { 10 | Root, 11 | Ellipsis, 12 | Item, 13 | Separator, 14 | Link, 15 | List, 16 | Page, 17 | // 18 | Root as Breadcrumb, 19 | Ellipsis as BreadcrumbEllipsis, 20 | Item as BreadcrumbItem, 21 | Separator as BreadcrumbSeparator, 22 | Link as BreadcrumbLink, 23 | List as BreadcrumbList, 24 | Page as BreadcrumbPage 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | %sveltekit.head% 18 | 19 | 20 |
    %sveltekit.body%
    21 | 22 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
    25 | {@render children?.()} 26 |
    27 | -------------------------------------------------------------------------------- /src/lib/utils/helpers/image.ts: -------------------------------------------------------------------------------- 1 | import { PUBLIC_R2_BUCKET_URL } from '$env/static/public'; 2 | 3 | import avatarPlaceholder from '$lib/assets/avatar.png'; 4 | 5 | /** 6 | * Resolves avatar URLs with automatic fallback to placeholder. 7 | * Abstracts the complexity of handling both missing avatars and R2 storage paths, 8 | * ensuring consistent avatar display across the application. 9 | * 10 | * @param image - Avatar filename or null/undefined 11 | * @returns Complete avatar URL or placeholder path 12 | */ 13 | export function getAvatarUrl(image: string | null | undefined): string { 14 | if (!image) return avatarPlaceholder; 15 | 16 | // Construct the R2 URL for the local filename 17 | return `${PUBLIC_R2_BUCKET_URL}/images/avatars/${image}`; 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-content.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    25 | {@render children?.()} 26 |
    27 | -------------------------------------------------------------------------------- /tests/e2e/homepage.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('home page has expected h1', async ({ page }) => { 4 | await page.goto('/'); 5 | await expect(page.locator('h1')).toHaveText('SvelteKit Omakase'); 6 | }); 7 | 8 | test('home page has navigation links', async ({ page }) => { 9 | await page.goto('/'); 10 | 11 | // Check for Login button 12 | const loginButton = page.locator('a[href="/login"]'); 13 | await expect(loginButton).toBeVisible(); 14 | await expect(loginButton).toHaveText('Login'); 15 | 16 | // Check for Register button 17 | const registerButton = page.locator('a[href="/register"]'); 18 | await expect(registerButton).toBeVisible(); 19 | await expect(registerButton).toHaveText('Register'); 20 | }); 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-label.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | {#snippet child({ props })} 20 | 23 | {/snippet} 24 | 25 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-header.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    24 | {@render children?.()} 25 |
    26 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-scroll-up-button.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/lib/db/models/account.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | 3 | import { User } from './user'; 4 | 5 | export const Account = sqliteTable('account', { 6 | id: integer().primaryKey({ autoIncrement: true }), 7 | accountId: text().notNull(), 8 | providerId: text().notNull(), 9 | userId: text() 10 | .notNull() 11 | .references(() => User.id, { onDelete: 'cascade' }), 12 | accessToken: text(), 13 | refreshToken: text(), 14 | idToken: text(), 15 | accessTokenExpiresAt: integer(), 16 | refreshTokenExpiresAt: integer(), 17 | scope: text(), 18 | password: text(), 19 | createdAt: integer({ mode: 'timestamp' }).notNull(), 20 | updatedAt: integer({ mode: 'timestamp' }).notNull() 21 | }); 22 | 23 | export type Account = typeof Account.$inferSelect; 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/accordion-content.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 |
    23 | {@render children?.()} 24 |
    25 |
    26 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-sub.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
      26 | {@render children?.()} 27 |
    28 | -------------------------------------------------------------------------------- /src/lib/components/ui/kbd/kbd.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | {@render children?.()} 20 | 21 | -------------------------------------------------------------------------------- /src/lib/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from 'clsx'; 2 | 3 | import { clsx } from 'clsx'; 4 | import { twMerge } from 'tailwind-merge'; 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)); 8 | } 9 | 10 | export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | export type WithoutChild = T extends { child?: any } ? Omit : T; 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | export type WithoutChildren = T extends { children?: any } ? Omit : T; 16 | export type WithoutChildrenOrChild = WithoutChildren>; 17 | export type WithElementRef = T & { ref?: U | null }; 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-scroll-down-button.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/lib/utils/helpers/generate.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from 'nanoid'; 2 | 3 | /** Alphanumeric characters used for ID generation (lowercase, URL-safe) */ 4 | const ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; 5 | 6 | /** Standard length for generated IDs across the application */ 7 | const ID_LENGTH = 12; 8 | 9 | /** 10 | * Generates consistent, URL-safe IDs for database records and file names. 11 | * Uses a curated alphabet to ensure compatibility across systems while maintaining 12 | * sufficient entropy for uniqueness. 13 | * 14 | * @returns A 12-character alphanumeric ID 15 | * @example 16 | * ```ts 17 | * const userId = generateNanoId(); // "a1b2c3d4e5f6" 18 | * ``` 19 | */ 20 | export function generateNanoId() { 21 | const nanoid = customAlphabet(ID_ALPHABET, ID_LENGTH); 22 | return nanoid(); 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /src/routes/(auth)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 |
    19 |
    20 |
    21 | {@render children?.()} 22 |
    23 |
    24 |
    25 | 26 |
    27 | 28 |
    29 | -------------------------------------------------------------------------------- /src/routes/(auth)/logout/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@sveltejs/kit'; 2 | 3 | import { redirect } from 'sveltekit-flash-message/server'; 4 | 5 | import { auth, requireLogin } from '$lib/server/auth'; 6 | import * as m from '$lib/utils/messages.json'; 7 | 8 | export const POST: RequestHandler = async (event) => { 9 | requireLogin(); 10 | 11 | try { 12 | await auth.api.signOut({ 13 | headers: event.request.headers 14 | }); 15 | } catch (error) { 16 | console.log(error); 17 | redirect( 18 | '/', 19 | { 20 | status: 500, 21 | type: 'error', 22 | message: m.general.error 23 | }, 24 | event 25 | ); 26 | } 27 | redirect( 28 | '/', 29 | { 30 | status: 303, 31 | type: 'success', 32 | message: m.auth.logout.success 33 | }, 34 | event 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/lib/components/app-sidebar-header.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | {#snippet child({ props })} 11 | 12 |
    15 | SvelteKit Omakase 16 |
    17 |
    18 | SvelteKit Omakase 19 |
    20 |
    21 | {/snippet} 22 |
    23 |
    24 |
    25 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-inset.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    25 | {@render children?.()} 26 |
    27 | -------------------------------------------------------------------------------- /src/lib/components/ui/progress/progress.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 |
    30 |
    31 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-link.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | {#if child} 28 | {@render child({ props: attrs })} 29 | {:else} 30 | 31 | {@render children?.()} 32 | 33 | {/if} 34 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 120, 6 | "plugins": ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "importOrder": [ 8 | "^\\$env/(.*)$", 9 | "", 10 | "", 11 | "^@lucide/svelte$", 12 | "", 13 | "^\\$app/(.*)$", 14 | "", 15 | "", 16 | "", 17 | "", 18 | "^\\$lib/(?!assets/)(.*)$", 19 | "", 20 | "^\\$components/(.*)$", 21 | "", 22 | "^@lucide/svelte$", 23 | "^\\$lib/assets/(.*)$", 24 | "", 25 | "^[./]" 26 | ], 27 | "importOrderParserPlugins": ["typescript", "decorators-legacy"], 28 | "importOrderTypeScriptVersion": "5.0.0", 29 | "overrides": [ 30 | { 31 | "files": "*.svelte", 32 | "options": { "parser": "svelte" } 33 | } 34 | ], 35 | "tailwindStylesheet": "./src/styles/app.css" 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-field-errors.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | {#snippet children({ errors, errorProps })} 21 | {#if childrenProp} 22 | {@render childrenProp({ errors, errorProps })} 23 | {:else} 24 | {#each errors as error (error)} 25 |
    {error}
    26 | {/each} 27 | {/if} 28 | {/snippet} 29 |
    30 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/index.ts: -------------------------------------------------------------------------------- 1 | import * as FormPrimitive from 'formsnap'; 2 | 3 | import Button from './form-button.svelte'; 4 | import Description from './form-description.svelte'; 5 | import ElementField from './form-element-field.svelte'; 6 | import FieldErrors from './form-field-errors.svelte'; 7 | import Field from './form-field.svelte'; 8 | import Fieldset from './form-fieldset.svelte'; 9 | import Label from './form-label.svelte'; 10 | import Legend from './form-legend.svelte'; 11 | 12 | const Control = FormPrimitive.Control; 13 | 14 | export { 15 | Field, 16 | Control, 17 | Label, 18 | Button, 19 | FieldErrors, 20 | Description, 21 | Fieldset, 22 | Legend, 23 | ElementField, 24 | // 25 | Field as FormField, 26 | Control as FormControl, 27 | Description as FormDescription, 28 | Label as FormLabel, 29 | FieldErrors as FormFieldErrors, 30 | Fieldset as FormFieldset, 31 | Legend as FormLegend, 32 | ElementField as FormElementField, 33 | Button as FormButton 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/db/models/user.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'; 2 | 3 | import { generateNanoId } from '../../utils/helpers/generate'; 4 | 5 | export const User = sqliteTable( 6 | 'user', 7 | { 8 | id: integer().primaryKey({ autoIncrement: true }), 9 | publicId: text() 10 | .$defaultFn(() => generateNanoId()) 11 | .unique(), 12 | email: text().notNull().unique(), 13 | emailVerified: integer({ mode: 'boolean' }).notNull().default(false), 14 | firstName: text().notNull(), 15 | lastName: text().notNull(), 16 | name: text().notNull(), 17 | avatar: text(), 18 | image: text(), 19 | createdAt: integer({ mode: 'timestamp' }) 20 | .notNull() 21 | .$defaultFn(() => new Date()), 22 | updatedAt: integer({ mode: 'timestamp' }) 23 | .notNull() 24 | .$defaultFn(() => new Date()) 25 | .$onUpdateFn(() => new Date()) 26 | }, 27 | (User) => [uniqueIndex('public_id_index').on(User.publicId)] 28 | ); 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-trigger.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 40 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as SheetPrimitive } from 'bits-ui'; 2 | 3 | import Close from './sheet-close.svelte'; 4 | import Content from './sheet-content.svelte'; 5 | import Description from './sheet-description.svelte'; 6 | import Footer from './sheet-footer.svelte'; 7 | import Header from './sheet-header.svelte'; 8 | import Overlay from './sheet-overlay.svelte'; 9 | import Title from './sheet-title.svelte'; 10 | import Trigger from './sheet-trigger.svelte'; 11 | 12 | const Root = SheetPrimitive.Root; 13 | const Portal = SheetPrimitive.Portal; 14 | 15 | export { 16 | Root, 17 | Close, 18 | Trigger, 19 | Portal, 20 | Overlay, 21 | Content, 22 | Header, 23 | Footer, 24 | Title, 25 | Description, 26 | // 27 | Root as Sheet, 28 | Close as SheetClose, 29 | Trigger as SheetTrigger, 30 | Portal as SheetPortal, 31 | Overlay as SheetOverlay, 32 | Content as SheetContent, 33 | Header as SheetHeader, 34 | Footer as SheetFooter, 35 | Title as SheetTitle, 36 | Description as SheetDescription 37 | }; 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ------------------------- 2 | # OS-specific files 3 | # ------------------------- 4 | .DS_Store 5 | Thumbs.db 6 | 7 | # ------------------------- 8 | # Node.js dependencies 9 | # ------------------------- 10 | node_modules/ 11 | 12 | # ------------------------- 13 | # Package manager lockfiles 14 | # ------------------------- 15 | # package-lock.json 16 | # pnpm-lock.yaml 17 | # yarn.lock 18 | # bun.lockb 19 | # bun.lock 20 | 21 | # ------------------------- 22 | # Build output 23 | # ------------------------- 24 | /build 25 | /.svelte-kit 26 | /package 27 | /dist 28 | 29 | # ------------------------- 30 | # Environment & secrets 31 | # ------------------------- 32 | .env 33 | .env.* 34 | !.env.example 35 | 36 | # ------------------------- 37 | # Databases & local data 38 | # ------------------------- 39 | dev.db 40 | *.db 41 | 42 | # ------------------------- 43 | # Test results 44 | # ------------------------- 45 | /test-results 46 | 47 | # ------------------------- 48 | # Editor-specific files 49 | # ------------------------- 50 | .vscode/ 51 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as DialogPrimitive } from 'bits-ui'; 2 | 3 | import Close from './dialog-close.svelte'; 4 | import Content from './dialog-content.svelte'; 5 | import Description from './dialog-description.svelte'; 6 | import Footer from './dialog-footer.svelte'; 7 | import Header from './dialog-header.svelte'; 8 | import Overlay from './dialog-overlay.svelte'; 9 | import Title from './dialog-title.svelte'; 10 | import Trigger from './dialog-trigger.svelte'; 11 | 12 | const Root = DialogPrimitive.Root; 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/form/form-field.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | {#snippet children({ constraints, errors, tainted, value })} 22 |
    23 | {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })} 24 |
    25 | {/snippet} 26 |
    27 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-element-field.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | {#snippet children({ constraints, errors, tainted, value })} 22 |
    23 | {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })} 24 |
    25 | {/snippet} 26 |
    27 | -------------------------------------------------------------------------------- /src/lib/components/ui/textarea/textarea.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/tabs-trigger.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/lib/utils/mail/templates/welcome.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 19 | 20 |
    21 | 22 | 23 |

    Welcome to SvelteKit Omakase

    24 |
    25 | 26 |

    Hey, {userFirstName}!

    27 |

    We're thrilled to have you onboard. ⭐

    28 |
    29 | 30 | 31 |

    32 | Need help? we are just an email away. 33 |

    34 |
    35 |
    36 |
    37 | 38 | 39 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-badge.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    30 | {@render children?.()} 31 |
    32 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | import { Select as SelectPrimitive } from 'bits-ui'; 2 | 3 | import Content from './select-content.svelte'; 4 | import GroupHeading from './select-group-heading.svelte'; 5 | import Group from './select-group.svelte'; 6 | import Item from './select-item.svelte'; 7 | import Label from './select-label.svelte'; 8 | import ScrollDownButton from './select-scroll-down-button.svelte'; 9 | import ScrollUpButton from './select-scroll-up-button.svelte'; 10 | import Separator from './select-separator.svelte'; 11 | import Trigger from './select-trigger.svelte'; 12 | 13 | const Root = SelectPrimitive.Root; 14 | 15 | export { 16 | Root, 17 | Group, 18 | Label, 19 | Item, 20 | Content, 21 | Trigger, 22 | Separator, 23 | ScrollDownButton, 24 | ScrollUpButton, 25 | GroupHeading, 26 | // 27 | Root as Select, 28 | Group as SelectGroup, 29 | Label as SelectLabel, 30 | Item as SelectItem, 31 | Content as SelectContent, 32 | Trigger as SelectTrigger, 33 | Separator as SelectSeparator, 34 | ScrollDownButton as SelectScrollDownButton, 35 | ScrollUpButton as SelectScrollUpButton, 36 | GroupHeading as SelectGroupHeading 37 | }; 38 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
    30 | {#if showIcon} 31 | 32 | {/if} 33 | 38 | {@render children?.()} 39 |
    40 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # ------------------------- 2 | # OS-specific files 3 | # ------------------------- 4 | .DS_Store 5 | Thumbs.db 6 | 7 | # ------------------------- 8 | # Node.js dependencies 9 | # ------------------------- 10 | node_modules/ 11 | 12 | # ------------------------- 13 | # Build output 14 | # ------------------------- 15 | /build 16 | /.svelte-kit 17 | /package 18 | /dist 19 | 20 | # ------------------------- 21 | # Environment & secrets 22 | # ------------------------- 23 | .env 24 | .env.* 25 | !.env.example 26 | 27 | # ------------------------- 28 | # Databases & local data 29 | # ------------------------- 30 | dev.db 31 | *.db 32 | /src/lib/db/migrations 33 | 34 | # ------------------------- 35 | # Test results 36 | # ------------------------- 37 | /test-results 38 | 39 | # ------------------------- 40 | # Editor-specific files 41 | # ------------------------- 42 | .vscode/ 43 | 44 | # ------------------------- 45 | # Lockfiles 46 | # ------------------------- 47 | package-lock.json 48 | pnpm-lock.yaml 49 | yarn.lock 50 | bun.lock 51 | bun.lockb 52 | 53 | # ------------------------- 54 | # Miscellaneous 55 | # ------------------------- 56 | .gitignore 57 | .prettierrc 58 | .prettierignore 59 | .husky/ 60 | .npmrc 61 | /static/ -------------------------------------------------------------------------------- /src/lib/components/ui/popover/popover-content.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /src/lib/components/app-sidebar.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/lib/components/ui/toggle-group/toggle-group-item.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 40 | -------------------------------------------------------------------------------- /src/routes/(app)/settings/+layout.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
    24 |
    25 |

    Settings

    26 |

    Manage your account settings and preferences.

    27 |
    28 | 29 |
    30 | 33 | 34 | 35 | 36 |
    37 |
    38 | {@render children?.()} 39 |
    40 |
    41 |
    42 |
    43 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 | {@render children?.()} 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group-label.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | {#if child} 31 | {@render child({ props: mergedProps })} 32 | {:else} 33 |
    34 | {@render children?.()} 35 |
    36 | {/if} 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; 2 | 3 | import Action from './alert-dialog-action.svelte'; 4 | import Cancel from './alert-dialog-cancel.svelte'; 5 | import Content from './alert-dialog-content.svelte'; 6 | import Description from './alert-dialog-description.svelte'; 7 | import Footer from './alert-dialog-footer.svelte'; 8 | import Header from './alert-dialog-header.svelte'; 9 | import Overlay from './alert-dialog-overlay.svelte'; 10 | import Title from './alert-dialog-title.svelte'; 11 | import Trigger from './alert-dialog-trigger.svelte'; 12 | 13 | const Root = AlertDialogPrimitive.Root; 14 | const Portal = AlertDialogPrimitive.Portal; 15 | 16 | export { 17 | Root, 18 | Title, 19 | Action, 20 | Cancel, 21 | Portal, 22 | Footer, 23 | Header, 24 | Trigger, 25 | Overlay, 26 | Content, 27 | Description, 28 | // 29 | Root as AlertDialog, 30 | Title as AlertDialogTitle, 31 | Action as AlertDialogAction, 32 | Cancel as AlertDialogCancel, 33 | Portal as AlertDialogPortal, 34 | Footer as AlertDialogFooter, 35 | Header as AlertDialogHeader, 36 | Trigger as AlertDialogTrigger, 37 | Overlay as AlertDialogOverlay, 38 | Content as AlertDialogContent, 39 | Description as AlertDialogDescription 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 31 | 32 | -------------------------------------------------------------------------------- /src/routes/api/upload/+server.ts: -------------------------------------------------------------------------------- 1 | import { PUBLIC_R2_BUCKET_NAME } from '$env/static/public'; 2 | 3 | import type { RequestHandler } from '@sveltejs/kit'; 4 | 5 | import crypto from 'crypto'; 6 | import { PutObjectCommand } from '@aws-sdk/client-s3'; 7 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 8 | import { error, json } from '@sveltejs/kit'; 9 | 10 | import { auth } from '$lib/server/auth'; 11 | import { s3 } from '$lib/server/storage'; 12 | 13 | export const POST: RequestHandler = async ({ request }) => { 14 | const session = await auth.api.getSession(request); 15 | 16 | if (!session?.user) { 17 | error(401, 'Unauthorized'); 18 | } 19 | 20 | try { 21 | const { fileType, destinationDirectory } = await request.json(); 22 | 23 | const fileName = crypto.randomBytes(16).toString('hex'); 24 | 25 | const file = { 26 | Bucket: PUBLIC_R2_BUCKET_NAME, 27 | Key: `${destinationDirectory}/${fileName}`, 28 | ContentType: fileType 29 | }; 30 | 31 | const command = new PutObjectCommand(file); 32 | const url = await getSignedUrl(s3, command, { expiresIn: 60000 }); 33 | 34 | return json({ 35 | presignedUrl: url, 36 | fileName 37 | }); 38 | } catch (err) { 39 | console.log(err); 40 | } 41 | 42 | error(500, 'Something went wrong'); 43 | }; 44 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | {#snippet children({ checked })} 27 | 28 | {#if checked} 29 | 30 | {/if} 31 | 32 | {@render childrenProp?.({ checked })} 33 | {/snippet} 34 | 35 | -------------------------------------------------------------------------------- /src/lib/utils/mail/templates/reset-password.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 |
    18 | 19 | 20 |

    🔒 Reset Your Password

    21 |
    22 | 23 |

    Hey, {userFirstName}!

    24 |

    It looks like you requested a password reset. No worries, we've got you covered!

    25 |

    Click the link below to reset your password:

    26 |

    27 | Reset Your Password 28 |

    29 |
    30 | 31 | 32 |

    33 | If you didn't request this, please ignore this email or 34 | contact us. 35 |

    36 |

    Stay secure 🛟

    37 |
    38 |
    39 |
    40 | 41 | 42 | -------------------------------------------------------------------------------- /src/lib/components/ui/switch/switch.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | 32 | 33 | -------------------------------------------------------------------------------- /src/lib/components/ui/radio-group/radio-group-item.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | {#snippet children({ checked })} 26 |
    27 | {#if checked} 28 | 29 | {/if} 30 |
    31 | {/snippet} 32 |
    33 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/accordion-trigger.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | svg]:rotate-180', 26 | className 27 | )} 28 | {...restProps} 29 | > 30 | {@render children?.()} 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/lib/components/settings-navbar.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 47 | -------------------------------------------------------------------------------- /src/lib/components/ui/toggle-group/toggle-group.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 34 | 35 | 39 | 48 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-group-action.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | {#if child} 33 | {@render child({ props: mergedProps })} 34 | {:else} 35 | 38 | {/if} 39 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/alert.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /src/lib/hooks/use-theme.ts: -------------------------------------------------------------------------------- 1 | import { resetMode, setMode, userPrefersMode } from 'mode-watcher'; 2 | 3 | /** 4 | * Cycles through theme modes: system → light → dark → system 5 | */ 6 | export function cycleThemeMode(): void { 7 | const currentUserMode = userPrefersMode.current; 8 | if (currentUserMode === 'system') { 9 | setMode('light'); 10 | } else if (currentUserMode === 'light') { 11 | setMode('dark'); 12 | } else { 13 | resetMode(); 14 | } 15 | } 16 | 17 | /** 18 | * Sets up a keyboard listener for theme cycling. 19 | * Listens for 't' key and cycles through themes. 20 | * Ignores key presses when typing in input fields. 21 | * 22 | * @returns Cleanup function to remove the event listener 23 | */ 24 | export function setupThemeCyclingKeyListener(): () => void { 25 | const keyListener = (e: KeyboardEvent) => { 26 | // Ignore key presses when typing in input fields 27 | if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { 28 | return; 29 | } 30 | 31 | if (e.key === 't') { 32 | cycleThemeMode(); 33 | } 34 | }; 35 | 36 | window.addEventListener('keydown', keyListener); 37 | 38 | // Return cleanup function 39 | return () => { 40 | window.removeEventListener('keydown', keyListener); 41 | }; 42 | } 43 | 44 | /** 45 | * Custom hook for theme cycling with keyboard support. 46 | */ 47 | export function useTheme() { 48 | return { 49 | cycleMode: cycleThemeMode, 50 | setupKeyListener: setupThemeCyclingKeyListener 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-item.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | {#snippet children({ selected, highlighted })} 30 | 31 | {#if selected} 32 | 33 | {/if} 34 | 35 | {#if childrenProp} 36 | {@render childrenProp({ selected, highlighted })} 37 | {:else} 38 | {label || value} 39 | {/if} 40 | {/snippet} 41 | 42 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
    61 | {@render children?.()} 62 |
    63 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-rail.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 40 | -------------------------------------------------------------------------------- /src/lib/validations/files.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const MAX_AVATAR_SIZE = 2000000; // 2MB 4 | const MAX_IMAGE_SIZE = 4000000; // 4MB 5 | const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; 6 | 7 | const imageFileSchema = z 8 | .instanceof(File) 9 | .refine((file) => file.size <= MAX_IMAGE_SIZE, { 10 | error: 'Image size must be less than 4MB.' 11 | }) 12 | .refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), { 13 | error: 'Only .jpg, .jpeg, .png and .webp formats are supported.' 14 | }); 15 | 16 | export const avatarFileSchema = z 17 | .instanceof(File) 18 | .refine((file) => file.size <= MAX_AVATAR_SIZE, { 19 | error: 'Avatar size must be less than 2MB.' 20 | }) 21 | .refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), { 22 | error: 'Only .jpg, .jpeg, .png and .webp formats are supported.' 23 | }); 24 | 25 | export function validateImageFile( 26 | imageFile: File, 27 | type: string 28 | ): { 29 | valid: boolean; 30 | errors: string[]; 31 | } { 32 | const schema = type === 'avatar' ? avatarFileSchema : imageFileSchema; 33 | const result = schema.safeParse(imageFile); 34 | 35 | return { 36 | valid: result.success, 37 | errors: result.success ? [] : result.error.issues.map((e) => e.message) 38 | }; 39 | } 40 | 41 | // Avatar file validations 42 | export function validateAvatarFile(avatarFile: File): { 43 | valid: boolean; 44 | errors: string[]; 45 | } { 46 | const result = avatarFileSchema.safeParse(avatarFile); 47 | 48 | return { 49 | valid: result.success, 50 | errors: result.success ? [] : result.error.issues.map((e) => e.message) 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/components/app-head.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 25 | {displayTitle} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-trigger.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | {@render children?.()} 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/lib/components/ui/checkbox/checkbox.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | {#snippet children({ checked, indeterminate })} 31 |
    32 | {#if checked} 33 | 34 | {:else if indeterminate} 35 | 36 | {/if} 37 |
    38 | {/snippet} 39 |
    40 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-provider.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 39 | 40 |
    47 | {@render children?.()} 48 |
    49 |
    50 | -------------------------------------------------------------------------------- /src/lib/components/app-sidebar-content.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | Platform 23 | 24 | {#each items as item (item.title)} 25 | {@const isActive = page.url.pathname === item.url} 26 | 27 | 28 | {#snippet child({ props })} 29 | 30 | {#if item.icon} 31 | {@const Icon = item.icon} 32 | 33 | {/if} 34 | {item.title} 35 | 36 | {/snippet} 37 | 38 | {/each} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {#snippet child({ props })} 48 | 49 | 50 | GitHub 51 | 52 | {/snippet} 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | {#snippet children({ checked, indeterminate })} 35 | 36 | {#if indeterminate} 37 | 38 | {:else} 39 | 40 | {/if} 41 | 42 | {@render childrenProp?.()} 43 | {/snippet} 44 | 45 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import { includeIgnoreFile } from '@eslint/compat'; 3 | import js from '@eslint/js'; 4 | import prettier from 'eslint-config-prettier'; 5 | import svelte from 'eslint-plugin-svelte'; 6 | import unicorn from 'eslint-plugin-unicorn'; 7 | import { defineConfig } from 'eslint/config'; 8 | import globals from 'globals'; 9 | import ts from 'typescript-eslint'; 10 | 11 | import svelteConfig from './svelte.config.js'; 12 | 13 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 14 | 15 | export default defineConfig([ 16 | includeIgnoreFile(gitignorePath), 17 | js.configs.recommended, 18 | ...ts.configs.recommended, 19 | ...svelte.configs.recommended, 20 | prettier, 21 | ...svelte.configs.prettier, 22 | { 23 | plugins: { 24 | unicorn 25 | }, 26 | languageOptions: { 27 | globals: { ...globals.browser, ...globals.node } 28 | }, 29 | rules: { 30 | 'no-undef': 'off', 31 | 'unicorn/filename-case': [ 32 | 'error', 33 | { 34 | case: 'kebabCase', 35 | ignore: ['^\\.[a-z]+rc\\.(js|ts|json)$', '^[A-Z]+\\.(md|txt)$'] 36 | } 37 | ] 38 | } 39 | }, 40 | { 41 | files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 42 | languageOptions: { 43 | parserOptions: { 44 | projectService: true, 45 | extraFileExtensions: ['.svelte'], 46 | parser: ts.parser, 47 | svelteConfig 48 | } 49 | }, 50 | rules: { 51 | 'svelte/no-navigation-without-resolve': [ 52 | 'error', 53 | { 54 | ignoreLinks: true 55 | } 56 | ] 57 | } 58 | } 59 | // Disable rules for shadcn-svelte UI components (OPTIONAL) 60 | // { 61 | // ignores: ['src/lib/components/ui/**'] 62 | // } 63 | ]); 64 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; 2 | 3 | import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; 4 | import Content from './dropdown-menu-content.svelte'; 5 | import GroupHeading from './dropdown-menu-group-heading.svelte'; 6 | import Group from './dropdown-menu-group.svelte'; 7 | import Item from './dropdown-menu-item.svelte'; 8 | import Label from './dropdown-menu-label.svelte'; 9 | import RadioGroup from './dropdown-menu-radio-group.svelte'; 10 | import RadioItem from './dropdown-menu-radio-item.svelte'; 11 | import Separator from './dropdown-menu-separator.svelte'; 12 | import Shortcut from './dropdown-menu-shortcut.svelte'; 13 | import SubContent from './dropdown-menu-sub-content.svelte'; 14 | import SubTrigger from './dropdown-menu-sub-trigger.svelte'; 15 | import Trigger from './dropdown-menu-trigger.svelte'; 16 | 17 | const Sub = DropdownMenuPrimitive.Sub; 18 | const Root = DropdownMenuPrimitive.Root; 19 | 20 | export { 21 | CheckboxItem, 22 | Content, 23 | Root as DropdownMenu, 24 | CheckboxItem as DropdownMenuCheckboxItem, 25 | Content as DropdownMenuContent, 26 | Group as DropdownMenuGroup, 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 | GroupHeading as DropdownMenuGroupHeading, 38 | Group, 39 | GroupHeading, 40 | Item, 41 | Label, 42 | RadioGroup, 43 | RadioItem, 44 | Root, 45 | Separator, 46 | Shortcut, 47 | Sub, 48 | SubContent, 49 | SubTrigger, 50 | Trigger 51 | }; 52 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 | {#if child} 40 | {@render child({ props: mergedProps })} 41 | {:else} 42 | 43 | {@render children?.()} 44 | 45 | {/if} 46 | -------------------------------------------------------------------------------- /src/routes/(auth)/password/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { Action, Actions } from './$types'; 2 | 3 | import { redirect } from 'sveltekit-flash-message/server'; 4 | import { zod4 } from 'sveltekit-superforms/adapters'; 5 | import { superValidate } from 'sveltekit-superforms/server'; 6 | 7 | import { auth, redirectIfLoggedIn } from '$lib/server/auth'; 8 | import { isRateLimited, setFormError, setFormFail } from '$lib/utils/helpers/forms'; 9 | import * as m from '$lib/utils/messages.json'; 10 | import { requestPasswordResetSchema } from '$lib/validations/auth'; 11 | 12 | export async function load() { 13 | redirectIfLoggedIn(); 14 | 15 | const form = await superValidate(zod4(requestPasswordResetSchema)); 16 | 17 | return { 18 | metadata: { 19 | title: 'Request Password Reset' 20 | }, 21 | form 22 | }; 23 | } 24 | 25 | const requestPasswordReset: Action = async (event) => { 26 | const form = await superValidate(event.request, zod4(requestPasswordResetSchema)); 27 | 28 | await isRateLimited(form, event, { field: 'email' }); 29 | 30 | if (!form.valid) { 31 | return setFormFail(form); 32 | } 33 | 34 | const { email } = form.data; 35 | 36 | try { 37 | await auth.api.requestPasswordReset({ 38 | body: { 39 | email, 40 | redirectTo: `/password/reset?email=${email}` 41 | } 42 | }); 43 | } catch (error) { 44 | console.log(error); 45 | return setFormError(form, m.general.error, { 46 | status: 500 47 | }); 48 | } 49 | 50 | // we send a success message even if the user doesn't exist to prevent email enumeration 51 | redirect( 52 | '/', 53 | { 54 | type: 'success', 55 | message: m.auth.requestResetPassword.success 56 | }, 57 | event 58 | ); 59 | }; 60 | 61 | export const actions = { 62 | default: requestPasswordReset 63 | } satisfies Actions; 64 | -------------------------------------------------------------------------------- /src/lib/db/clear.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | import db from '$lib/server/database'; 5 | 6 | async function clearDb() { 7 | config(); 8 | 9 | if (process.env.NODE_ENV === 'production') { 10 | console.error('Clear script should not be run in production!'); 11 | process.exit(1); 12 | } 13 | 14 | const tableSchema = db._.schema; 15 | if (!tableSchema) { 16 | throw new Error('No table schema found'); 17 | } 18 | 19 | console.log('🗑️ Emptying the entire database'); 20 | 21 | const dropForeignKeys = sql.raw('PRAGMA foreign_keys = OFF;'); 22 | const enableForeignKeys = sql.raw('PRAGMA foreign_keys = ON;'); 23 | 24 | const queries = Object.values(tableSchema).map((table) => { 25 | console.log(`🧨 Preparing DELETE query for table: ${table.dbName}`); 26 | return { 27 | query: sql.raw(`DELETE FROM ${table.dbName}; DELETE FROM sqlite_sequence WHERE name='${table.dbName}';`), 28 | queryString: `DELETE FROM ${table.dbName}; DELETE FROM sqlite_sequence WHERE name='${table.dbName}';` 29 | }; 30 | }); 31 | 32 | try { 33 | console.log('🔒 Disabling foreign keys...'); 34 | await db.run(dropForeignKeys); 35 | 36 | await db.transaction(async (tx) => { 37 | console.log('📨 Sending queries...'); 38 | for (const { query, queryString } of queries) { 39 | console.log(`💽 Executing query: ${queryString}`); 40 | await tx.run(query); 41 | } 42 | }); 43 | 44 | console.log('🔓 Enabling foreign keys...'); 45 | await db.run(enableForeignKeys); 46 | 47 | console.log('✅ Database emptied'); 48 | } catch (error) { 49 | console.error('❌ Error occurred while clearing the database:', error); 50 | process.exit(1); 51 | } 52 | } 53 | 54 | clearDb().catch((e) => { 55 | console.error(e); 56 | process.exit(1); 57 | }); 58 | -------------------------------------------------------------------------------- /src/lib/components/ui/sidebar/sidebar-menu-action.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 | {#if child} 40 | {@render child({ props: mergedProps })} 41 | {:else} 42 | 45 | {/if} 46 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/tooltip-content.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 31 | {@render children?.()} 32 | 33 | {#snippet child({ props })} 34 |
    45 | {/snippet} 46 |
    47 |
    48 |
    49 | -------------------------------------------------------------------------------- /src/routes/(auth)/password/+page.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | Request Password Reset 25 | Enter your email below to receive a password reset link 26 | 27 | 28 |
    29 | 30 | {#snippet children({ constraints })} 31 | 32 | {#snippet children({ props })} 33 | Email 34 | 42 | 43 | {/snippet} 44 | 45 | {/snippet} 46 | 47 | 48 | 49 | {#if $delayed} 50 | 51 | {/if} 52 | Send 53 | 54 |
    55 |
    56 |
    57 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 28 | 37 | {@render children?.()} 38 | {#if showCloseButton} 39 | 42 | 43 | Close 44 | 45 | {/if} 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-content.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 34 | 35 | 38 | {@render children?.()} 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/lib/db/migrations/0000_tricky_angel.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `account` ( 2 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 3 | `account_id` text NOT NULL, 4 | `provider_id` text NOT NULL, 5 | `user_id` text NOT NULL, 6 | `access_token` text, 7 | `refresh_token` text, 8 | `id_token` text, 9 | `access_token_expires_at` integer, 10 | `refresh_token_expires_at` integer, 11 | `scope` text, 12 | `password` text, 13 | `created_at` integer NOT NULL, 14 | `updated_at` integer NOT NULL, 15 | FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade 16 | ); 17 | --> statement-breakpoint 18 | CREATE TABLE `session` ( 19 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 20 | `token` text NOT NULL, 21 | `created_at` integer NOT NULL, 22 | `updated_at` integer NOT NULL, 23 | `expires_at` integer NOT NULL, 24 | `ip_address` text, 25 | `user_agent` text, 26 | `user_id` text NOT NULL, 27 | FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade 28 | ); 29 | --> statement-breakpoint 30 | CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint 31 | CREATE TABLE `user` ( 32 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 33 | `public_id` text, 34 | `email` text NOT NULL, 35 | `email_verified` integer DEFAULT false NOT NULL, 36 | `first_name` text NOT NULL, 37 | `last_name` text NOT NULL, 38 | `name` text NOT NULL, 39 | `avatar` text, 40 | `image` text, 41 | `created_at` integer NOT NULL, 42 | `updated_at` integer NOT NULL 43 | ); 44 | --> statement-breakpoint 45 | CREATE UNIQUE INDEX `user_publicId_unique` ON `user` (`public_id`);--> statement-breakpoint 46 | CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint 47 | CREATE UNIQUE INDEX `public_id_index` ON `user` (`public_id`);--> statement-breakpoint 48 | CREATE TABLE `verification` ( 49 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 50 | `identifier` text NOT NULL, 51 | `value` text NOT NULL, 52 | `expires_at` integer NOT NULL, 53 | `created_at` integer NOT NULL, 54 | `updated_at` integer NOT NULL 55 | ); 56 | -------------------------------------------------------------------------------- /src/lib/components/ui/badge/badge.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 42 | 43 | 51 | {@render children?.()} 52 | 53 | -------------------------------------------------------------------------------- /src/lib/components/ui/slider/slider.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 32 | {#snippet children({ thumbs })} 33 | 40 | 44 | 45 | {#each thumbs as thumb (thumb)} 46 | 51 | {/each} 52 | {/snippet} 53 | 54 | -------------------------------------------------------------------------------- /src/lib/components/ui/toggle/toggle.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | 47 | 48 | 55 | -------------------------------------------------------------------------------- /src/routes/(auth)/login/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { Action, Actions } from './$types'; 2 | 3 | import { APIError as BetterAuthAPIError } from 'better-auth/api'; 4 | import { redirect } from 'sveltekit-flash-message/server'; 5 | import { zod4 } from 'sveltekit-superforms/adapters'; 6 | import { superValidate } from 'sveltekit-superforms/server'; 7 | 8 | import { auth, redirectIfLoggedIn } from '$lib/server/auth'; 9 | import { isRateLimited, setFormError, setFormFail } from '$lib/utils/helpers/forms'; 10 | import * as m from '$lib/utils/messages.json'; 11 | import { loginSchema } from '$lib/validations/auth'; 12 | 13 | export async function load() { 14 | redirectIfLoggedIn(); 15 | 16 | const form = await superValidate(zod4(loginSchema)); 17 | 18 | return { 19 | metadata: { 20 | title: 'Login' 21 | }, 22 | form 23 | }; 24 | } 25 | 26 | const login: Action = async (event) => { 27 | const form = await superValidate(event.request, zod4(loginSchema)); 28 | 29 | await isRateLimited(form, event, { field: 'email', removeSensitiveData: ['password'] }); 30 | 31 | if (!form.valid) { 32 | return setFormFail(form, { removeSensitiveData: ['password'] }); 33 | } 34 | 35 | const { email, password } = form.data; 36 | 37 | try { 38 | await auth.api.signInEmail({ 39 | body: { 40 | email, 41 | password 42 | }, 43 | headers: event.request.headers 44 | }); 45 | } catch (error) { 46 | if (error instanceof BetterAuthAPIError) { 47 | console.log(error); 48 | if (error.body?.code === 'INVALID_EMAIL_OR_PASSWORD') { 49 | return setFormError( 50 | form, 51 | m.auth.login.error, 52 | { 53 | field: 'email', 54 | removeSensitiveData: ['password', 'passwordConfirmation'] 55 | }, 56 | event 57 | ); 58 | } 59 | } 60 | return setFormFail(form, { 61 | removeSensitiveData: ['password'] 62 | }); 63 | } 64 | 65 | redirect( 66 | '/', 67 | { 68 | status: 303, 69 | type: 'success', 70 | message: m.auth.login.success 71 | }, 72 | event 73 | ); 74 | }; 75 | 76 | export const actions = { 77 | default: login 78 | } satisfies Actions; 79 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | {#if type === 'file'} 24 | 38 | {:else} 39 | 52 | {/if} 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | BUN_VERSION: 'latest' 11 | 12 | jobs: 13 | quality: 14 | name: Code Quality 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 15 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Bun 23 | uses: oven-sh/setup-bun@v2 24 | with: 25 | bun-version: ${{ env.BUN_VERSION }} 26 | 27 | - name: Cache Bun dependencies 28 | uses: actions/cache@v4 29 | with: 30 | path: | 31 | ~/.bun/install/cache 32 | ./.bun 33 | key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} 34 | restore-keys: ${{ runner.os }}-bun- 35 | 36 | - name: Install dependencies 37 | run: | 38 | if [ -f bun.lock ]; then 39 | bun install --frozen-lockfile 40 | else 41 | echo "⚠️ No bun.lock found — installing without frozen lockfile" 42 | bun install 43 | fi 44 | 45 | - name: Run Prettier 46 | run: bun run prettier --check . 47 | 48 | - name: Run ESLint 49 | run: bun run eslint . 50 | 51 | tests: 52 | name: Tests 53 | runs-on: ubuntu-latest 54 | timeout-minutes: 15 55 | 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v4 59 | 60 | - name: Setup Bun 61 | uses: oven-sh/setup-bun@v2 62 | with: 63 | bun-version: ${{ env.BUN_VERSION }} 64 | 65 | - name: Cache Bun dependencies 66 | uses: actions/cache@v4 67 | with: 68 | path: | 69 | ~/.bun/install/cache 70 | ./.bun 71 | key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} 72 | restore-keys: ${{ runner.os }}-bun- 73 | 74 | - name: Install dependencies 75 | run: | 76 | if [ -f bun.lock ]; then 77 | bun install --frozen-lockfile 78 | else 79 | echo "⚠️ No bun.lock found — installing without frozen lockfile" 80 | bun install 81 | fi 82 | 83 | - name: Run unit tests 84 | run: bun run test:unit 85 | -------------------------------------------------------------------------------- /src/lib/utils/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "error": "Something went wrong. Please try again." 4 | }, 5 | "pages": { 6 | "notFound": { 7 | "title": "404 - Page Not Found", 8 | "message": "Oops! We can't seem to find the page you're looking for." 9 | } 10 | }, 11 | "auth": { 12 | "unauthorized": "Access denied. You do not have permission to view this page.", 13 | "login": { 14 | "success": "You're now logged in.", 15 | "registeredWithDifferentMethod": "This account was created with a different method. Please log in with the same method you used to register.", 16 | "error": "Login failed. Please check your email and password and try again." 17 | }, 18 | "logout": { 19 | "success": "You've been logged out successfully." 20 | }, 21 | "register": { 22 | "success": "Awesome! You're all set to go.", 23 | "emailIsTaken": "This email is already in use. Try another?", 24 | "passwordsMismatch": "Oops! The passwords don't match. Try again?" 25 | }, 26 | "requestResetPassword": { 27 | "success": "Check your inbox! We've sent you a link to reset your password." 28 | }, 29 | "resetPassword": { 30 | "success": "All set! Your password has been reset successfully. You can now log in with your new password.", 31 | "invalidToken": "Your password reset link is invalid or has expired. Please request a new one.", 32 | "passwordsMismatch": "The passwords don't match. Please try again." 33 | } 34 | }, 35 | "settings": { 36 | "userProfile": { 37 | "edit": { 38 | "success": "Your profile has been updated successfully.", 39 | "noChanges": "Looks like there were no changes to save." 40 | }, 41 | "delete": { 42 | "success": "Your user account has been permanently deleted.", 43 | "destructiveOperation": "Are you sure you want to delete your user account? This cannot be undone." 44 | } 45 | }, 46 | "password": { 47 | "edit": { 48 | "success": { 49 | "set": "Password set successfully! You can now log in using your email and password in addition to your existing login method.", 50 | "update": "Your password has been updated successfully." 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/components/theme-switch.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | {#if mode.current} 32 | 33 | 34 | 35 | {#snippet child({ props })} 36 | 58 | {/snippet} 59 | 60 | 61 | 62 |

    switch to {nextMode} mode

    63 | T 64 |
    65 |
    66 |
    67 |
    68 | {/if} 69 | -------------------------------------------------------------------------------- /src/routes/(auth)/password/reset/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { Action, Actions } from './$types'; 2 | 3 | import { redirect } from 'sveltekit-flash-message/server'; 4 | import { zod4 } from 'sveltekit-superforms/adapters'; 5 | import { superValidate } from 'sveltekit-superforms/server'; 6 | 7 | import { auth, redirectIfLoggedIn } from '$lib/server/auth'; 8 | import { setFormError, setFormFail } from '$lib/utils/helpers/forms'; 9 | import * as m from '$lib/utils/messages.json'; 10 | import { passwordResetSchema } from '$lib/validations/auth'; 11 | 12 | export async function load(event) { 13 | redirectIfLoggedIn(); 14 | 15 | const token = event.url.searchParams.get('token'); 16 | const email = event.url.searchParams.get('email'); 17 | const error = event.url.searchParams.get('error'); 18 | 19 | if (error || !token || !email) { 20 | if (error) console.log(`Password Reset Error: ${error}`); 21 | 22 | redirect( 23 | '/password', 24 | { 25 | type: 'error', 26 | message: m.auth.resetPassword.invalidToken 27 | }, 28 | event 29 | ); 30 | } else { 31 | const form = await superValidate({ email, token }, zod4(passwordResetSchema), { 32 | errors: false 33 | }); 34 | 35 | return { 36 | metadata: { 37 | title: 'Password Reset' 38 | }, 39 | form 40 | }; 41 | } 42 | } 43 | 44 | const resetPassword: Action = async (event) => { 45 | const form = await superValidate(event.request, zod4(passwordResetSchema)); 46 | 47 | if (!form.valid) { 48 | return setFormFail(form); 49 | } 50 | 51 | const { token, password, passwordConfirmation } = form.data; 52 | 53 | if (password !== passwordConfirmation) { 54 | return setFormError( 55 | form, 56 | m.auth.register.passwordsMismatch, 57 | { 58 | field: 'passwordConfirmation', 59 | removeSensitiveData: ['password', 'passwordConfirmation'] 60 | }, 61 | event 62 | ); 63 | } 64 | 65 | try { 66 | await auth.api.resetPassword({ body: { newPassword: password, token } }); 67 | } catch (error) { 68 | console.log(error); 69 | return setFormError(form, m.general.error, { 70 | status: 500 71 | }); 72 | } 73 | 74 | redirect( 75 | '/login', 76 | { 77 | type: 'success', 78 | message: m.auth.resetPassword.success 79 | }, 80 | event 81 | ); 82 | }; 83 | 84 | export const actions = { 85 | default: resetPassword 86 | } satisfies Actions; 87 | -------------------------------------------------------------------------------- /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/routes/(auth)/password/reset/+page.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | Reset Your Password 25 | Enter a new password for {$formData.email} 26 | 27 | 28 |
    29 | 30 | 31 | 32 | {#snippet children({ constraints })} 33 | 34 | {#snippet children({ props })} 35 | Password 36 | 43 | 44 | {/snippet} 45 | 46 | {/snippet} 47 | 48 | 49 | 50 | {#snippet children({ constraints })} 51 | 52 | {#snippet children({ props })} 53 | Password Confirmation 54 | 61 | 62 | {/snippet} 63 | 64 | {/snippet} 65 | 66 | 67 | 68 | {#if $delayed} 69 | 70 | {/if} 71 | Reset 72 | 73 |
    74 |
    75 |
    76 | -------------------------------------------------------------------------------- /src/lib/components/ui/sheet/sheet-content.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 50 | 51 | 52 | 53 | 59 | {@render children?.()} 60 | 63 | 64 | Close 65 | 66 | 67 | 68 | --------------------------------------------------------------------------------