mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Adding paraglide for i18n support and dropdown to header. Formatting code based on a new SvelteKit Svelte5 app.
This commit is contained in:
parent
b5dae43ba4
commit
3204b0b28b
72 changed files with 2904 additions and 952 deletions
|
|
@ -1,13 +0,0 @@
|
||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
/build
|
|
||||||
/.svelte-kit
|
|
||||||
/package
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
/** @type { import("eslint").Linter.Config } */
|
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:svelte/recommended',
|
|
||||||
'prettier'
|
|
||||||
],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['@typescript-eslint'],
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
extraFileExtensions: ['.svelte']
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2017: true,
|
|
||||||
node: true
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['*.svelte'],
|
|
||||||
parser: 'svelte-eslint-parser',
|
|
||||||
parserOptions: {
|
|
||||||
parser: '@typescript-eslint/parser'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
/build
|
|
||||||
/.svelte-kit
|
|
||||||
/package
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
11
.prettierrc
11
.prettierrc
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"useTabs": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"printWidth": 100,
|
|
||||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
|
||||||
"pluginSearchDirs": ["."],
|
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
boredgame.localhost {
|
|
||||||
reverse_proxy / localhost:4173
|
|
||||||
}
|
|
||||||
116
biome.json
116
biome.json
|
|
@ -1,55 +1,65 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||||
"formatter": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"formatWithErrors": false,
|
"clientKind": "git",
|
||||||
"indentStyle": "tab",
|
"useIgnoreFile": false
|
||||||
"indentWidth": 2,
|
},
|
||||||
"lineEnding": "lf",
|
"files": { "ignoreUnknown": false, "ignore": [] },
|
||||||
"lineWidth": 150,
|
"formatter": {
|
||||||
"attributePosition": "auto",
|
"enabled": true,
|
||||||
"ignore": [
|
"formatWithErrors": false,
|
||||||
"**/.DS_Store",
|
"indentStyle": "space",
|
||||||
"**/node_modules",
|
"indentWidth": 2,
|
||||||
"./build",
|
"lineEnding": "lf",
|
||||||
"./.svelte-kit",
|
"lineWidth": 150,
|
||||||
"./package",
|
"attributePosition": "auto",
|
||||||
"**/.env",
|
"ignore": [
|
||||||
"**/.env.*",
|
"**/.DS_Store",
|
||||||
"**/pnpm-lock.yaml",
|
"**/node_modules",
|
||||||
"**/package-lock.json",
|
"./build",
|
||||||
"**/yarn.lock"
|
"./.svelte-kit",
|
||||||
]
|
"./package",
|
||||||
},
|
"**/.env",
|
||||||
"organizeImports": { "enabled": true },
|
"**/.env.*",
|
||||||
"linter": { "enabled": true, "rules": { "recommended": true } },
|
"**/pnpm-lock.yaml",
|
||||||
"javascript": {
|
"**/package-lock.json",
|
||||||
"formatter": {
|
"**/yarn.lock",
|
||||||
"jsxQuoteStyle": "double",
|
"**/paraglide/**"
|
||||||
"quoteProperties": "asNeeded",
|
]
|
||||||
"trailingCommas": "all",
|
},
|
||||||
"semicolons": "always",
|
"organizeImports": { "enabled": true },
|
||||||
"arrowParentheses": "always",
|
"linter": { "enabled": true, "rules": { "recommended": true } },
|
||||||
"bracketSpacing": true,
|
"javascript": {
|
||||||
"bracketSameLine": false,
|
"formatter": {
|
||||||
"quoteStyle": "single",
|
"jsxQuoteStyle": "double",
|
||||||
"attributePosition": "auto"
|
"quoteProperties": "asNeeded",
|
||||||
},
|
"trailingCommas": "all",
|
||||||
"parser": {
|
"indentStyle": "space",
|
||||||
"unsafeParameterDecoratorsEnabled": true
|
"lineEnding": "lf",
|
||||||
}
|
"lineWidth": 150,
|
||||||
},
|
"semicolons": "always",
|
||||||
"overrides": [
|
"arrowParentheses": "always",
|
||||||
{
|
"bracketSpacing": true,
|
||||||
"include": ["*.svelte"],
|
"bracketSameLine": false,
|
||||||
"linter": {
|
"quoteStyle": "single",
|
||||||
"rules": {
|
"attributePosition": "auto"
|
||||||
"style": {
|
},
|
||||||
"useConst": "off",
|
"parser": {
|
||||||
"useImportType": "off"
|
"unsafeParameterDecoratorsEnabled": true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"overrides": [
|
||||||
}
|
{
|
||||||
]
|
"include": ["*.svelte"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"style": {
|
||||||
|
"useConst": "off",
|
||||||
|
"useImportType": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
messages/en.json
Normal file
4
messages/en.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"hello_world": "Hello, {name} from en!"
|
||||||
|
}
|
||||||
4
messages/es.json
Normal file
4
messages/es.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"hello_world": "Hello, {name} from es!"
|
||||||
|
}
|
||||||
22
package.json
22
package.json
|
|
@ -12,7 +12,7 @@
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"package": "svelte-kit package",
|
"package": "svelte-kit package",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:ui": "svelte-kit sync && playwright test --ui",
|
"test:ui": "svelte-kit sync && playwright test --ui",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
|
@ -23,14 +23,14 @@
|
||||||
"test:unit": "vitest"
|
"test:unit": "vitest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.8.3",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@melt-ui/pp": "^0.3.2",
|
"@melt-ui/pp": "^0.3.2",
|
||||||
"@melt-ui/svelte": "^0.83.0",
|
"@melt-ui/svelte": "^0.83.0",
|
||||||
"@playwright/test": "^1.48.2",
|
"@playwright/test": "^1.48.2",
|
||||||
"@sveltejs/adapter-auto": "^3.3.1",
|
"@sveltejs/adapter-auto": "^3.3.1",
|
||||||
"@sveltejs/enhanced-img": "^0.3.10",
|
"@sveltejs/enhanced-img": "^0.3.10",
|
||||||
"@sveltejs/kit": "^2.8.0",
|
"@sveltejs/kit": "^2.8.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
|
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/node": "^20.17.6",
|
"@types/node": "^20.17.6",
|
||||||
|
|
@ -42,9 +42,6 @@
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"bits-ui": "^0.21.16",
|
"bits-ui": "^0.21.16",
|
||||||
"drizzle-kit": "^0.27.2",
|
"drizzle-kit": "^0.27.2",
|
||||||
"eslint": "^8.57.1",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-svelte": "2.36.0-next.13",
|
|
||||||
"formsnap": "^1.0.1",
|
"formsnap": "^1.0.1",
|
||||||
"just-clone": "^6.2.0",
|
"just-clone": "^6.2.0",
|
||||||
"just-debounce-it": "^3.2.0",
|
"just-debounce-it": "^3.2.0",
|
||||||
|
|
@ -52,12 +49,12 @@
|
||||||
"lucide-svelte": "^0.408.0",
|
"lucide-svelte": "^0.408.0",
|
||||||
"mode-watcher": "^0.4.1",
|
"mode-watcher": "^0.4.1",
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.49",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"postcss-load-config": "^5.1.0",
|
"postcss-load-config": "^5.1.0",
|
||||||
"postcss-preset-env": "^9.6.0",
|
"postcss-preset-env": "^9.6.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-svelte": "^3.2.7",
|
"prettier-plugin-svelte": "^3.2.8",
|
||||||
"svelte": "5.0.0-next.175",
|
"svelte": "5.0.0-next.175",
|
||||||
"svelte-check": "^3.8.6",
|
"svelte-check": "^3.8.6",
|
||||||
"svelte-headless-table": "^0.18.3",
|
"svelte-headless-table": "^0.18.3",
|
||||||
|
|
@ -72,7 +69,7 @@
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.11",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
|
|
@ -84,6 +81,7 @@
|
||||||
"@hono/zod-validator": "^0.2.2",
|
"@hono/zod-validator": "^0.2.2",
|
||||||
"@iconify-icons/line-md": "^1.2.30",
|
"@iconify-icons/line-md": "^1.2.30",
|
||||||
"@iconify-icons/mdi": "^1.2.48",
|
"@iconify-icons/mdi": "^1.2.48",
|
||||||
|
"@inlang/paraglide-sveltekit": "^0.11.1",
|
||||||
"@internationalized/date": "^3.5.6",
|
"@internationalized/date": "^3.5.6",
|
||||||
"@lucia-auth/adapter-drizzle": "^1.1.0",
|
"@lucia-auth/adapter-drizzle": "^1.1.0",
|
||||||
"@lukeed/uuid": "^2.0.1",
|
"@lukeed/uuid": "^2.0.1",
|
||||||
|
|
@ -102,17 +100,17 @@
|
||||||
"@sveltejs/adapter-vercel": "^5.4.7",
|
"@sveltejs/adapter-vercel": "^5.4.7",
|
||||||
"@types/feather-icons": "^4.29.4",
|
"@types/feather-icons": "^4.29.4",
|
||||||
"boardgamegeekclient": "^1.9.1",
|
"boardgamegeekclient": "^1.9.1",
|
||||||
"bullmq": "^5.25.4",
|
"bullmq": "^5.25.6",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"dotenv-expand": "^11.0.6",
|
"dotenv-expand": "^11.0.7",
|
||||||
"drizzle-orm": "^0.36.1",
|
"drizzle-orm": "^0.36.1",
|
||||||
"drizzle-zod": "^0.5.1",
|
"drizzle-zod": "^0.5.1",
|
||||||
"feather-icons": "^4.29.2",
|
"feather-icons": "^4.29.2",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"hono": "^4.6.9",
|
"hono": "^4.6.10",
|
||||||
"hono-pino": "^0.3.0",
|
"hono-pino": "^0.3.0",
|
||||||
"hono-rate-limiter": "^0.4.0",
|
"hono-rate-limiter": "^0.4.0",
|
||||||
"hono-zod-openapi": "^0.4.2",
|
"hono-zod-openapi": "^0.4.2",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
const config: PlaywrightTestConfig = {
|
export default defineConfig({
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run build && npm run preview',
|
command: 'npm run build && npm run preview',
|
||||||
port: 4173
|
port: 4173
|
||||||
},
|
},
|
||||||
testDir: 'tests',
|
testDir: 'e2e',
|
||||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
|
||||||
};
|
});
|
||||||
|
|
||||||
export default config;
|
|
||||||
|
|
|
||||||
1790
pnpm-lock.yaml
1790
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,5 @@
|
||||||
const tailwindcss = require("tailwindcss");
|
const tailwindcss = require("tailwindcss");
|
||||||
const tailwindNesting = require('tailwindcss/nesting');
|
const tailwindNesting = require('tailwindcss/nesting');
|
||||||
const autoprefixer = require('autoprefixer');
|
|
||||||
const postcssPresetEnv = require('postcss-preset-env');
|
const postcssPresetEnv = require('postcss-preset-env');
|
||||||
const atImport = require('postcss-import');
|
const atImport = require('postcss-import');
|
||||||
|
|
||||||
|
|
|
||||||
1
project.inlang/.gitignore
vendored
Normal file
1
project.inlang/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
cache
|
||||||
1
project.inlang/project_id
Normal file
1
project.inlang/project_id
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
927922ef9fe834ca9a0dea407aa01519bbcb60dadb84c6bd9cafd324f6108c44
|
||||||
20
project.inlang/settings.json
Normal file
20
project.inlang/settings.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/project-settings",
|
||||||
|
"modules": [
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@1/dist/index.js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-identical-pattern@1/dist/index.js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@1/dist/index.js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@1/dist/index.js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@1/dist/index.js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@2/dist/index.js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@0/dist/index.js"
|
||||||
|
],
|
||||||
|
"plugin.inlang.messageFormat": {
|
||||||
|
"pathPattern": "./messages/{languageTag}.json"
|
||||||
|
},
|
||||||
|
"sourceLanguageTag": "en",
|
||||||
|
"languageTags": [
|
||||||
|
"en",
|
||||||
|
"es"
|
||||||
|
]
|
||||||
|
}
|
||||||
67
src/app.d.ts
vendored
67
src/app.d.ts
vendored
|
|
@ -1,44 +1,41 @@
|
||||||
import type { ApiClient } from '$lib/server/api';
|
import type { ApiClient } from '$lib/server/api';
|
||||||
|
import type { Users } from '$lib/server/api/databases/postgres/tables';
|
||||||
import type { parseApiResponse } from '$lib/utils/api';
|
import type { parseApiResponse } from '$lib/utils/api';
|
||||||
import type { User } from 'lucia';
|
|
||||||
|
|
||||||
// See https://kit.svelte.dev/docs/types#app
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
// and what to do when importing types
|
|
||||||
|
|
||||||
// src/app.d.ts
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
interface PageData {
|
interface PageData {
|
||||||
flash?: {
|
flash?: {
|
||||||
type: 'success' | 'error' | 'info';
|
type: 'success' | 'error' | 'info';
|
||||||
message: string;
|
message: string;
|
||||||
data?: Record<string, unknown>;
|
data?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
api: ApiClient['api'];
|
api: ApiClient['api'];
|
||||||
parseApiResponse: typeof parseApiResponse;
|
parseApiResponse: typeof parseApiResponse;
|
||||||
getAuthedUser: () => Promise<Returned<User> | null>;
|
getAuthedUser: () => Promise<Returned<Users> | null>;
|
||||||
getAuthedUserOrThrow: () => Promise<Returned<User>>;
|
getAuthedUserOrThrow: () => Promise<Returned<User>>;
|
||||||
}
|
}
|
||||||
namespace Superforms {
|
namespace Superforms {
|
||||||
type Message = {
|
type Message = {
|
||||||
type: 'error' | 'success' | 'info';
|
type: 'error' | 'success' | 'info';
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
interface Error {
|
interface Error {
|
||||||
code?: string;
|
code?: string;
|
||||||
errorId?: string;
|
errorId?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
startViewTransition: (callback: never) => void; // Add your custom property/method here
|
startViewTransition: (callback: never) => void; // Add your custom property/method here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// THIS IS IMPORTANT!!!
|
// THIS IS IMPORTANT!!!
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="%paraglide.lang%" dir="%paraglide.textDirection%">
|
||||||
<head>
|
<head>
|
||||||
<meta name="robots" content="noindex, nofollow"/>
|
<meta name="robots" content="noindex, nofollow"/>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="description" content="Bored? Find a game! Bored Game!"/>
|
<meta name="description" content="Bored? Find a game! Bored Game!"/>
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon-bored-game.svg"/>
|
<link rel="icon" href="%sveltekit.assets%/favicon-bored-game.svg"/>
|
||||||
<meta name="viewport" content="width=device-width"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div id="svelte">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
import 'reflect-metadata';
|
import "reflect-metadata";
|
||||||
import { StatusCodes } from '$lib/constants/status-codes';
|
import { StatusCodes } from "$lib/constants/status-codes";
|
||||||
import type { ApiRoutes } from '$lib/server/api';
|
import type { ApiRoutes } from "$lib/server/api";
|
||||||
import { parseApiResponse } from '$lib/utils/api';
|
import { parseApiResponse } from "$lib/utils/api";
|
||||||
import { type Handle, redirect } from '@sveltejs/kit';
|
import { type Handle, redirect } from "@sveltejs/kit";
|
||||||
import { sequence } from '@sveltejs/kit/hooks';
|
import { sequence } from "@sveltejs/kit/hooks";
|
||||||
import { hc } from 'hono/client';
|
import { hc } from "hono/client";
|
||||||
|
import { i18n } from "$lib/i18n";
|
||||||
|
|
||||||
|
const handleParaglide: Handle = i18n.handle();
|
||||||
|
|
||||||
const apiClient: Handle = async ({ event, resolve }) => {
|
const apiClient: Handle = async ({ event, resolve }) => {
|
||||||
/* ------------------------------ Register api ------------------------------ */
|
/* ------------------------------ Register api ------------------------------ */
|
||||||
const { api } = hc<ApiRoutes>('/', {
|
const { api } = hc<ApiRoutes>("/", {
|
||||||
fetch: event.fetch,
|
fetch: event.fetch,
|
||||||
headers: {
|
headers: {
|
||||||
'x-forwarded-for': event.url.host.includes('sveltekit-prerender') ? '127.0.0.1' : event.getClientAddress(),
|
"x-forwarded-for": event.url.host.includes("sveltekit-prerender") ? "127.0.0.1" : event.getClientAddress(),
|
||||||
host: event.request.headers.get('host') || '',
|
host: event.request.headers.get("host") || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -25,7 +28,7 @@ const apiClient: Handle = async ({ event, resolve }) => {
|
||||||
async function getAuthedUserOrThrow() {
|
async function getAuthedUserOrThrow() {
|
||||||
const { data } = await api.user.$get().then(parseApiResponse);
|
const { data } = await api.user.$get().then(parseApiResponse);
|
||||||
if (!data || !data.user) {
|
if (!data || !data.user) {
|
||||||
throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/');
|
throw redirect(StatusCodes.TEMPORARY_REDIRECT, "/");
|
||||||
}
|
}
|
||||||
return data?.user;
|
return data?.user;
|
||||||
}
|
}
|
||||||
|
|
@ -40,4 +43,4 @@ const apiClient: Handle = async ({ event, resolve }) => {
|
||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handle: Handle = sequence(apiClient);
|
export const handle: Handle = sequence(apiClient, handleParaglide);
|
||||||
|
|
|
||||||
2
src/hooks.ts
Normal file
2
src/hooks.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
import { i18n } from '$lib/i18n';
|
||||||
|
export const reroute = i18n.reroute();
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { toastMessage } from '$lib/utils/superforms'; // Adjust the path if necessary
|
import { toastMessage } from '$lib/utils/superforms'; // Adjust the path if necessary
|
||||||
|
|
||||||
const { codeContent, language }: { codeContent: string; language: string } = $props()
|
const { codeContent, language }: { codeContent: string; language: string } = $props();
|
||||||
|
|
||||||
// Function to copy code to clipboard
|
// Function to copy code to clipboard
|
||||||
const copyToClipboard = () => {
|
const copyToClipboard = () => {
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
.writeText(codeContent)
|
.writeText(codeContent)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toastMessage({ text: 'Copied to clipboard!', type: 'success' })
|
toastMessage({ text: 'Copied to clipboard!', type: 'success' });
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('Failed to copy: ', err)
|
console.error('Failed to copy: ', err);
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if codeContent}
|
{#if codeContent}
|
||||||
|
|
@ -24,7 +24,7 @@ const copyToClipboard = () => {
|
||||||
{codeContent}
|
{codeContent}
|
||||||
</span>
|
</span>
|
||||||
</code>
|
</code>
|
||||||
<Button class="copy-button" on:click={copyToClipboard}>Copy</Button>
|
<Button class="copy-button" onclick={copyToClipboard}>Copy</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,32 @@ import Logo from '$components/logo.svelte';
|
||||||
import * as Avatar from '$lib/components/ui/avatar';
|
import * as Avatar from '$lib/components/ui/avatar';
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte';
|
import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte';
|
||||||
|
import { type AvailableLanguageTag, languageTag } from '$lib/paraglide/runtime';
|
||||||
|
import { i18n } from '$lib/i18n';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import type { Users } from '$lib/server/api/databases/postgres/tables';
|
||||||
|
|
||||||
let { user = null } = $props();
|
let { user = null }: { user: Users | null } = $props();
|
||||||
|
|
||||||
let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)');
|
let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)');
|
||||||
|
|
||||||
|
let language = $derived.by(() => {
|
||||||
|
switch (languageTag()) {
|
||||||
|
case 'en':
|
||||||
|
return '🇺🇸';
|
||||||
|
case 'es':
|
||||||
|
return '🇲🇽';
|
||||||
|
default:
|
||||||
|
return '🇺🇸';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function switchToLanguage(newLanguage: AvailableLanguageTag) {
|
||||||
|
const canonicalPath = i18n.route($page.url.pathname);
|
||||||
|
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage);
|
||||||
|
goto(localisedPath);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
|
|
@ -25,9 +47,23 @@ let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
|
||||||
<a href="/login"> <span class="flex-auto">Login</span></a>
|
<a href="/login"> <span class="flex-auto">Login</span></a>
|
||||||
<a href="/signup"> <span class="flex-auto">Sign Up</span></a>
|
<a href="/signup"> <span class="flex-auto">Sign Up</span></a>
|
||||||
{/if}
|
{/if}
|
||||||
|
{@render languageDropdown()}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{#snippet languageDropdown()}
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<span class="flex-auto">{language}</span>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<button onclick={() => switchToLanguage('en')}><DropdownMenu.Item><span>🇺🇸 English</span></DropdownMenu.Item></button>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<button onclick={() => switchToLanguage('es')}><DropdownMenu.Item><span>🇲🇽 Spanish</span></DropdownMenu.Item></button>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
{#snippet userDropdown()}
|
{#snippet userDropdown()}
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
<!-- Taken from carbon design system svelte -->
|
<!-- Taken from carbon design system svelte -->
|
||||||
<!-- https://github.com/carbon-design-system/carbon-components-svelte/blob/master/src/SkeletonPlaceholder/SkeletonPlaceholder.svelte -->
|
<!-- https://github.com/carbon-design-system/carbon-components-svelte/blob/master/src/SkeletonPlaceholder/SkeletonPlaceholder.svelte -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let style: string
|
let style: string = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
{style}
|
{style}
|
||||||
class:bx--skeleton__placeholder={true}
|
class:bx--skeleton__placeholder={true}
|
||||||
{...$$restProps}
|
<!-- {...$$restProps}-->
|
||||||
on:click
|
<!-- click-->
|
||||||
on:mouseover
|
<!-- mouseover-->
|
||||||
on:mouseenter
|
<!-- mouseenter-->
|
||||||
on:mouseleave
|
<!-- mouseleave-->
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.GroupHeadingProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.GroupHeading
|
||||||
|
bind:ref
|
||||||
|
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
15
src/lib/components/ui/input-otp/index.ts
Normal file
15
src/lib/components/ui/input-otp/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import Root from "./input-otp.svelte";
|
||||||
|
import Group from "./input-otp-group.svelte";
|
||||||
|
import Slot from "./input-otp-slot.svelte";
|
||||||
|
import Separator from "./input-otp-separator.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Group,
|
||||||
|
Slot,
|
||||||
|
Separator,
|
||||||
|
Root as InputOTP,
|
||||||
|
Group as InputOTPGroup,
|
||||||
|
Slot as InputOTPSlot,
|
||||||
|
Separator as InputOTPSeparator,
|
||||||
|
};
|
||||||
16
src/lib/components/ui/input-otp/input-otp-group.svelte
Normal file
16
src/lib/components/ui/input-otp/input-otp-group.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} class={cn("flex items-center", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
19
src/lib/components/ui/input-otp/input-otp-separator.svelte
Normal file
19
src/lib/components/ui/input-otp/input-otp-separator.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import Dot from "lucide-svelte/icons/dot";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} role="separator" {...restProps}>
|
||||||
|
{#if children}
|
||||||
|
{@render children?.()}
|
||||||
|
{:else}
|
||||||
|
<Dot />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
30
src/lib/components/ui/input-otp/input-otp-slot.svelte
Normal file
30
src/lib/components/ui/input-otp/input-otp-slot.svelte
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { PinInput as InputOTPPrimitive } from "bits-ui";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
cell,
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof InputOTPPrimitive.Cell> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<InputOTPPrimitive.Cell
|
||||||
|
{cell}
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
"border-input relative flex h-10 w-10 items-center justify-center border-y border-r text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||||
|
cell.isActive && "ring-ring ring-offset-background z-10 ring-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{cell.char}
|
||||||
|
{#if cell.hasFakeCaret}
|
||||||
|
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
|
<div class="animate-caret-blink bg-foreground h-4 w-px duration-1000"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</InputOTPPrimitive.Cell>
|
||||||
22
src/lib/components/ui/input-otp/input-otp.svelte
Normal file
22
src/lib/components/ui/input-otp/input-otp.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { PinInput as InputOTPPrimitive } from "bits-ui";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value = $bindable(""),
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof InputOTPPrimitive.Root> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<InputOTPPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
bind:value
|
||||||
|
class={cn(
|
||||||
|
"flex items-center gap-2 has-[:disabled]:opacity-50 [&_input]:disabled:cursor-not-allowed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
16
src/lib/components/ui/select/select-group-heading.svelte
Normal file
16
src/lib/components/ui/select/select-group-heading.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: SelectPrimitive.GroupHeadingProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.GroupHeading
|
||||||
|
bind:ref
|
||||||
|
class={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||||
|
import { Select as SelectPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
bind:ref
|
||||||
|
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ChevronDown class="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
19
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
19
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import ChevronUp from "lucide-svelte/icons/chevron-up";
|
||||||
|
import { Select as SelectPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
bind:ref
|
||||||
|
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ChevronUp class="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
6
src/lib/components/ui/sidebar/constants.ts
Normal file
6
src/lib/components/ui/sidebar/constants.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const SIDEBAR_COOKIE_NAME = "sidebar:state";
|
||||||
|
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||||
|
export const SIDEBAR_WIDTH = "16rem";
|
||||||
|
export const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||||
|
export const SIDEBAR_WIDTH_ICON = "3rem";
|
||||||
|
export const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||||
81
src/lib/components/ui/sidebar/context.svelte.ts
Normal file
81
src/lib/components/ui/sidebar/context.svelte.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { IsMobile } from "$lib/hooks/is-mobile.svelte.js";
|
||||||
|
import { getContext, setContext } from "svelte";
|
||||||
|
import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js";
|
||||||
|
|
||||||
|
type Getter<T> = () => T;
|
||||||
|
|
||||||
|
export type SidebarStateProps = {
|
||||||
|
/**
|
||||||
|
* A getter function that returns the current open state of the sidebar.
|
||||||
|
* We use a getter function here to support `bind:open` on the `Sidebar.Provider`
|
||||||
|
* component.
|
||||||
|
*/
|
||||||
|
open: Getter<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that sets the open state of the sidebar. To support `bind:open`, we need
|
||||||
|
* a source of truth for changing the open state to ensure it will be synced throughout
|
||||||
|
* the sub-components and any `bind:` references.
|
||||||
|
*/
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SidebarState {
|
||||||
|
readonly props: SidebarStateProps;
|
||||||
|
open = $derived.by(() => this.props.open());
|
||||||
|
openMobile = $state(false);
|
||||||
|
setOpen: SidebarStateProps["setOpen"];
|
||||||
|
#isMobile: IsMobile;
|
||||||
|
state = $derived.by(() => (this.open ? "expanded" : "collapsed"));
|
||||||
|
|
||||||
|
constructor(props: SidebarStateProps) {
|
||||||
|
this.setOpen = props.setOpen;
|
||||||
|
this.#isMobile = new IsMobile();
|
||||||
|
this.props = props;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience getter for checking if the sidebar is mobile
|
||||||
|
// without this, we would need to use `sidebar.isMobile.current` everywhere
|
||||||
|
get isMobile() {
|
||||||
|
return this.#isMobile.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handler to apply to the `<svelte:window>`
|
||||||
|
handleShortcutKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setOpenMobile = (value: boolean) => {
|
||||||
|
this.openMobile = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
toggle = () => {
|
||||||
|
return this.#isMobile.current
|
||||||
|
? (this.openMobile = !this.openMobile)
|
||||||
|
: this.setOpen(!this.open);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const SYMBOL_KEY = "scn-sidebar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new `SidebarState` instance and sets it in the context.
|
||||||
|
*
|
||||||
|
* @param props The constructor props for the `SidebarState` class.
|
||||||
|
* @returns The `SidebarState` instance.
|
||||||
|
*/
|
||||||
|
export function setSidebar(props: SidebarStateProps): SidebarState {
|
||||||
|
return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the `SidebarState` instance from the context. This is a class instance,
|
||||||
|
* so you cannot destructure it.
|
||||||
|
* @returns The `SidebarState` instance.
|
||||||
|
*/
|
||||||
|
export function useSidebar(): SidebarState {
|
||||||
|
return getContext(Symbol.for(SYMBOL_KEY));
|
||||||
|
}
|
||||||
75
src/lib/components/ui/sidebar/index.ts
Normal file
75
src/lib/components/ui/sidebar/index.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { useSidebar } from "./context.svelte.js";
|
||||||
|
import Content from "./sidebar-content.svelte";
|
||||||
|
import Footer from "./sidebar-footer.svelte";
|
||||||
|
import GroupAction from "./sidebar-group-action.svelte";
|
||||||
|
import GroupContent from "./sidebar-group-content.svelte";
|
||||||
|
import GroupLabel from "./sidebar-group-label.svelte";
|
||||||
|
import Group from "./sidebar-group.svelte";
|
||||||
|
import Header from "./sidebar-header.svelte";
|
||||||
|
import Input from "./sidebar-input.svelte";
|
||||||
|
import Inset from "./sidebar-inset.svelte";
|
||||||
|
import MenuAction from "./sidebar-menu-action.svelte";
|
||||||
|
import MenuBadge from "./sidebar-menu-badge.svelte";
|
||||||
|
import MenuButton from "./sidebar-menu-button.svelte";
|
||||||
|
import MenuItem from "./sidebar-menu-item.svelte";
|
||||||
|
import MenuSkeleton from "./sidebar-menu-skeleton.svelte";
|
||||||
|
import MenuSubButton from "./sidebar-menu-sub-button.svelte";
|
||||||
|
import MenuSubItem from "./sidebar-menu-sub-item.svelte";
|
||||||
|
import MenuSub from "./sidebar-menu-sub.svelte";
|
||||||
|
import Menu from "./sidebar-menu.svelte";
|
||||||
|
import Provider from "./sidebar-provider.svelte";
|
||||||
|
import Rail from "./sidebar-rail.svelte";
|
||||||
|
import Separator from "./sidebar-separator.svelte";
|
||||||
|
import Trigger from "./sidebar-trigger.svelte";
|
||||||
|
import Root from "./sidebar.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Content,
|
||||||
|
Footer,
|
||||||
|
Group,
|
||||||
|
GroupAction,
|
||||||
|
GroupContent,
|
||||||
|
GroupLabel,
|
||||||
|
Header,
|
||||||
|
Input,
|
||||||
|
Inset,
|
||||||
|
Menu,
|
||||||
|
MenuAction,
|
||||||
|
MenuBadge,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuSkeleton,
|
||||||
|
MenuSub,
|
||||||
|
MenuSubButton,
|
||||||
|
MenuSubItem,
|
||||||
|
Provider,
|
||||||
|
Rail,
|
||||||
|
Root,
|
||||||
|
Separator,
|
||||||
|
//
|
||||||
|
Root as Sidebar,
|
||||||
|
Content as SidebarContent,
|
||||||
|
Footer as SidebarFooter,
|
||||||
|
Group as SidebarGroup,
|
||||||
|
GroupAction as SidebarGroupAction,
|
||||||
|
GroupContent as SidebarGroupContent,
|
||||||
|
GroupLabel as SidebarGroupLabel,
|
||||||
|
Header as SidebarHeader,
|
||||||
|
Input as SidebarInput,
|
||||||
|
Inset as SidebarInset,
|
||||||
|
Menu as SidebarMenu,
|
||||||
|
MenuAction as SidebarMenuAction,
|
||||||
|
MenuBadge as SidebarMenuBadge,
|
||||||
|
MenuButton as SidebarMenuButton,
|
||||||
|
MenuItem as SidebarMenuItem,
|
||||||
|
MenuSkeleton as SidebarMenuSkeleton,
|
||||||
|
MenuSub as SidebarMenuSub,
|
||||||
|
MenuSubButton as SidebarMenuSubButton,
|
||||||
|
MenuSubItem as SidebarMenuSubItem,
|
||||||
|
Provider as SidebarProvider,
|
||||||
|
Rail as SidebarRail,
|
||||||
|
Separator as SidebarSeparator,
|
||||||
|
Trigger as SidebarTrigger,
|
||||||
|
Trigger,
|
||||||
|
useSidebar,
|
||||||
|
};
|
||||||
24
src/lib/components/ui/sidebar/sidebar-content.svelte
Normal file
24
src/lib/components/ui/sidebar/sidebar-content.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-sidebar="content"
|
||||||
|
class={cn(
|
||||||
|
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
21
src/lib/components/ui/sidebar/sidebar-footer.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-footer.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-sidebar="footer"
|
||||||
|
class={cn("flex flex-col gap-2 p-2", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
36
src/lib/components/ui/sidebar/sidebar-group-action.svelte
Normal file
36
src/lib/components/ui/sidebar/sidebar-group-action.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import type { HTMLButtonAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
child,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLButtonAttributes> & {
|
||||||
|
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const propObj = $derived({
|
||||||
|
class: cn(
|
||||||
|
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
"after:absolute after:-inset-2 after:md:hidden",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className
|
||||||
|
),
|
||||||
|
"data-sidebar": "group-action",
|
||||||
|
...restProps,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if child}
|
||||||
|
{@render child({ props: propObj })}
|
||||||
|
{:else}
|
||||||
|
<button bind:this={ref} {...propObj}>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
21
src/lib/components/ui/sidebar/sidebar-group-content.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-group-content.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-sidebar="group-content"
|
||||||
|
class={cn("w-full text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
34
src/lib/components/ui/sidebar/sidebar-group-label.svelte
Normal file
34
src/lib/components/ui/sidebar/sidebar-group-label.svelte
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
children,
|
||||||
|
child,
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
||||||
|
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const mergedProps = $derived({
|
||||||
|
class: cn(
|
||||||
|
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||||
|
className
|
||||||
|
),
|
||||||
|
"data-sidebar": "group-label",
|
||||||
|
...restProps,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if child}
|
||||||
|
{@render child({ props: mergedProps })}
|
||||||
|
{:else}
|
||||||
|
<div bind:this={ref} {...mergedProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
21
src/lib/components/ui/sidebar/sidebar-group.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-group.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-sidebar="group"
|
||||||
|
class={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
21
src/lib/components/ui/sidebar/sidebar-header.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-header.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-sidebar="header"
|
||||||
|
class={cn("flex flex-col gap-2 p-2", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
23
src/lib/components/ui/sidebar/sidebar-input.svelte
Normal file
23
src/lib/components/ui/sidebar/sidebar-input.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(""),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof Input> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
bind:ref
|
||||||
|
bind:value
|
||||||
|
data-sidebar="input"
|
||||||
|
class={cn(
|
||||||
|
"bg-background focus-visible:ring-sidebar-ring h-8 w-full shadow-none focus-visible:ring-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
24
src/lib/components/ui/sidebar/sidebar-inset.svelte
Normal file
24
src/lib/components/ui/sidebar/sidebar-inset.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn(
|
||||||
|
"bg-background relative flex min-h-svh flex-1 flex-col",
|
||||||
|
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</main>
|
||||||
43
src/lib/components/ui/sidebar/sidebar-menu-action.svelte
Normal file
43
src/lib/components/ui/sidebar/sidebar-menu-action.svelte
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import type { HTMLButtonAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
showOnHover = false,
|
||||||
|
children,
|
||||||
|
child,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLButtonAttributes> & {
|
||||||
|
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||||
|
showOnHover?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const mergedProps = $derived({
|
||||||
|
class: cn(
|
||||||
|
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
"after:absolute after:-inset-2 after:md:hidden",
|
||||||
|
"peer-data-[size=sm]/menu-button:top-1",
|
||||||
|
"peer-data-[size=default]/menu-button:top-1.5",
|
||||||
|
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
showOnHover &&
|
||||||
|
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||||
|
className
|
||||||
|
),
|
||||||
|
"data-sidebar": "menu-action",
|
||||||
|
...restProps,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if child}
|
||||||
|
{@render child({ props: mergedProps })}
|
||||||
|
{:else}
|
||||||
|
<button bind:this={ref} {...mergedProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
29
src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
Normal file
29
src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-sidebar="menu-badge"
|
||||||
|
class={cn(
|
||||||
|
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums",
|
||||||
|
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||||
|
"peer-data-[size=sm]/menu-button:top-1",
|
||||||
|
"peer-data-[size=default]/menu-button:top-1.5",
|
||||||
|
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
97
src/lib/components/ui/sidebar/sidebar-menu-button.svelte
Normal file
97
src/lib/components/ui/sidebar/sidebar-menu-button.svelte
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<script lang="ts" module>
|
||||||
|
import { tv, type VariantProps } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const sidebarMenuButtonVariants = tv({
|
||||||
|
base: "peer/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-[width,height,padding] focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||||
|
outline:
|
||||||
|
"bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-8 text-sm",
|
||||||
|
sm: "h-7 text-xs",
|
||||||
|
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SidebarMenuButtonVariant = VariantProps<
|
||||||
|
typeof sidebarMenuButtonVariants
|
||||||
|
>["variant"];
|
||||||
|
export type SidebarMenuButtonSize = VariantProps<typeof sidebarMenuButtonVariants>["size"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import { mergeProps, type WithElementRef, type WithoutChildrenOrChild } from "bits-ui";
|
||||||
|
import type { ComponentProps, Snippet } from "svelte";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { useSidebar } from "./context.svelte.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
child,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
isActive = false,
|
||||||
|
tooltipContent,
|
||||||
|
tooltipContentProps,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
|
||||||
|
isActive?: boolean;
|
||||||
|
variant?: SidebarMenuButtonVariant;
|
||||||
|
size?: SidebarMenuButtonSize;
|
||||||
|
tooltipContent?: Snippet;
|
||||||
|
tooltipContentProps?: WithoutChildrenOrChild<ComponentProps<typeof Tooltip.Content>>;
|
||||||
|
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
|
||||||
|
const buttonProps = $derived({
|
||||||
|
class: cn(sidebarMenuButtonVariants({ variant, size }), className),
|
||||||
|
"data-sidebar": "menu-button",
|
||||||
|
"data-size": size,
|
||||||
|
"data-active": isActive,
|
||||||
|
...restProps,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet Button({ props }: { props?: Record<string, unknown> })}
|
||||||
|
{@const mergedProps = mergeProps(buttonProps, props)}
|
||||||
|
{#if child}
|
||||||
|
{@render child({ props: mergedProps })}
|
||||||
|
{:else}
|
||||||
|
<button bind:this={ref} {...mergedProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#if !tooltipContent}
|
||||||
|
{@render Button({})}
|
||||||
|
{:else}
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
{@render Button({ props })}
|
||||||
|
{/snippet}
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content
|
||||||
|
side="right"
|
||||||
|
align="center"
|
||||||
|
hidden={sidebar.state !== "collapsed" || sidebar.isMobile}
|
||||||
|
children={tooltipContent}
|
||||||
|
{...tooltipContentProps}
|
||||||
|
/>
|
||||||
|
</Tooltip.Root>
|
||||||
|
{/if}
|
||||||
21
src/lib/components/ui/sidebar/sidebar-menu-item.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-menu-item.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLLIElement>, HTMLLIElement> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li
|
||||||
|
bind:this={ref}
|
||||||
|
data-sidebar="menu-item"
|
||||||
|
class={cn("group/menu-item relative", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</li>
|
||||||
36
src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
Normal file
36
src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
showIcon = false,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
||||||
|
showIcon?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
// Random width between 50% and 90%
|
||||||
|
const width = `${Math.floor(Math.random() * 40) + 50}%`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-sidebar="menu-skeleton"
|
||||||
|
class={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#if showIcon}
|
||||||
|
<Skeleton class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
|
||||||
|
{/if}
|
||||||
|
<Skeleton
|
||||||
|
class="h-4 max-w-[--skeleton-width] flex-1"
|
||||||
|
data-sidebar="menu-skeleton-text"
|
||||||
|
style="--skeleton-width: {width};"
|
||||||
|
/>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
43
src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
Normal file
43
src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
children,
|
||||||
|
child,
|
||||||
|
class: className,
|
||||||
|
size = "md",
|
||||||
|
isActive,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||||
|
size?: "sm" | "md";
|
||||||
|
isActive?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const mergedProps = $derived({
|
||||||
|
class: cn(
|
||||||
|
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||||
|
size === "sm" && "text-xs",
|
||||||
|
size === "md" && "text-sm",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className
|
||||||
|
),
|
||||||
|
"data-sidebar": "menu-sub-button",
|
||||||
|
"data-size": size,
|
||||||
|
"data-active": isActive,
|
||||||
|
...restProps,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if child}
|
||||||
|
{@render child({ props: mergedProps })}
|
||||||
|
{:else}
|
||||||
|
<a bind:this={ref} {...mergedProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
14
src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
Normal file
14
src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLLIElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li bind:this={ref} data-sidebar="menu-sub-item" {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</li>
|
||||||
25
src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
Normal file
25
src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
bind:this={ref}
|
||||||
|
data-sidebar="menu-sub"
|
||||||
|
class={cn(
|
||||||
|
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</ul>
|
||||||
21
src/lib/components/ui/sidebar/sidebar-menu.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-menu.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLUListElement>, HTMLUListElement> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
bind:this={ref}
|
||||||
|
data-sidebar="menu"
|
||||||
|
class={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</ul>
|
||||||
59
src/lib/components/ui/sidebar/sidebar-provider.svelte
Normal file
59
src/lib/components/ui/sidebar/sidebar-provider.svelte
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import {
|
||||||
|
SIDEBAR_COOKIE_MAX_AGE,
|
||||||
|
SIDEBAR_COOKIE_NAME,
|
||||||
|
SIDEBAR_WIDTH,
|
||||||
|
SIDEBAR_WIDTH_ICON,
|
||||||
|
} from "./constants.js";
|
||||||
|
import { setSidebar } from "./context.svelte.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
open = $bindable(true),
|
||||||
|
onOpenChange = () => {},
|
||||||
|
controlledOpen = false,
|
||||||
|
class: className,
|
||||||
|
style,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
controlledOpen?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const sidebar = setSidebar({
|
||||||
|
open: () => open,
|
||||||
|
setOpen: (value: boolean) => {
|
||||||
|
if (controlledOpen) {
|
||||||
|
onOpenChange(value);
|
||||||
|
} else {
|
||||||
|
open = value;
|
||||||
|
onOpenChange(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This sets the cookie to keep the sidebar state.
|
||||||
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={sidebar.handleShortcutKeydown} />
|
||||||
|
|
||||||
|
<Tooltip.Provider delayDuration={0}>
|
||||||
|
<div
|
||||||
|
style="--sidebar-width: {SIDEBAR_WIDTH}; --sidebar-width-icon: {SIDEBAR_WIDTH_ICON}; {style}"
|
||||||
|
class={cn(
|
||||||
|
"group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
bind:this={ref}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</Tooltip.Provider>
|
||||||
36
src/lib/components/ui/sidebar/sidebar-rail.svelte
Normal file
36
src/lib/components/ui/sidebar/sidebar-rail.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { useSidebar } from "./context.svelte.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> = $props();
|
||||||
|
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-sidebar="rail"
|
||||||
|
aria-label="Toggle Sidebar"
|
||||||
|
tabIndex={-1}
|
||||||
|
onclick={() => sidebar.toggle()}
|
||||||
|
title="Toggle Sidebar"
|
||||||
|
class={cn(
|
||||||
|
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
|
||||||
|
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
||||||
|
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||||
|
"group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||||
|
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||||
|
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
18
src/lib/components/ui/sidebar/sidebar-separator.svelte
Normal file
18
src/lib/components/ui/sidebar/sidebar-separator.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof Separator> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Separator
|
||||||
|
bind:ref
|
||||||
|
data-sidebar="separator"
|
||||||
|
class={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
34
src/lib/components/ui/sidebar/sidebar-trigger.svelte
Normal file
34
src/lib/components/ui/sidebar/sidebar-trigger.svelte
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import PanelLeft from "lucide-svelte/icons/panel-left";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import { useSidebar } from "./context.svelte.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
onclick,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof Button> & {
|
||||||
|
onclick?: (e: MouseEvent) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onclick={(e) => {
|
||||||
|
onclick?.(e);
|
||||||
|
sidebar.toggle();
|
||||||
|
}}
|
||||||
|
data-sidebar="trigger"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class={cn("h-7 w-7", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<PanelLeft />
|
||||||
|
<span class="sr-only">Toggle Sidebar</span>
|
||||||
|
</Button>
|
||||||
98
src/lib/components/ui/sidebar/sidebar.svelte
Normal file
98
src/lib/components/ui/sidebar/sidebar.svelte
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Sheet from "$lib/components/ui/sheet/index.js";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { SIDEBAR_WIDTH_MOBILE } from "./constants.js";
|
||||||
|
import { useSidebar } from "./context.svelte.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
side = "left",
|
||||||
|
variant = "sidebar",
|
||||||
|
collapsible = "offcanvas",
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
side?: "left" | "right";
|
||||||
|
variant?: "sidebar" | "floating" | "inset";
|
||||||
|
collapsible?: "offcanvas" | "icon" | "none";
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if collapsible === "none"}
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
"bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
bind:this={ref}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
{:else if sidebar.isMobile}
|
||||||
|
<Sheet.Root
|
||||||
|
controlledOpen
|
||||||
|
open={sidebar.openMobile}
|
||||||
|
onOpenChange={sidebar.setOpenMobile}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<Sheet.Content
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
data-mobile="true"
|
||||||
|
class="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
|
||||||
|
style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};"
|
||||||
|
{side}
|
||||||
|
>
|
||||||
|
<div class="flex h-full w-full flex-col">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</Sheet.Content>
|
||||||
|
</Sheet.Root>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
class="text-sidebar-foreground group peer hidden md:block"
|
||||||
|
data-state={sidebar.state}
|
||||||
|
data-collapsible={sidebar.state === "collapsed" ? collapsible : ""}
|
||||||
|
data-variant={variant}
|
||||||
|
data-side={side}
|
||||||
|
>
|
||||||
|
<!-- This is what handles the sidebar gap on desktop -->
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
|
||||||
|
"group-data-[collapsible=offcanvas]:w-0",
|
||||||
|
"group-data-[side=right]:rotate-180",
|
||||||
|
variant === "floating" || variant === "inset"
|
||||||
|
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||||
|
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
|
||||||
|
)}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||||
|
side === "left"
|
||||||
|
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||||
|
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||||
|
// Adjust the padding for floating and inset variants.
|
||||||
|
variant === "floating" || variant === "inset"
|
||||||
|
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||||
|
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow"
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
7
src/lib/components/ui/skeleton/index.ts
Normal file
7
src/lib/components/ui/skeleton/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import Root from "./skeleton.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Skeleton,
|
||||||
|
};
|
||||||
17
src/lib/components/ui/skeleton/skeleton.svelte
Normal file
17
src/lib/components/ui/skeleton/skeleton.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef, WithoutChildren } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn("bg-muted animate-pulse rounded-md", className)}
|
||||||
|
{...restProps}
|
||||||
|
></div>
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
import {z} from "zod";
|
import { refinePasswords } from '$lib/validations/account';
|
||||||
import {refinePasswords} from "$lib/validations/account";
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const signupUsernameEmailDto = z.object({
|
export const signupUsernameEmailDto = z
|
||||||
firstName: z.string().trim().optional(),
|
.object({
|
||||||
lastName: z.string().trim().optional(),
|
firstName: z.string().trim().optional(),
|
||||||
email: z.string()
|
lastName: z.string().trim().optional(),
|
||||||
.trim()
|
email: z
|
||||||
.max(64, {message: 'Email must be less than 64 characters'})
|
|
||||||
.email({message: 'Please enter a valid email'})
|
|
||||||
.optional(),
|
|
||||||
username: z
|
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(3, {message: 'Must be at least 3 characters'})
|
.max(64, { message: 'Email must be less than 64 characters' })
|
||||||
.max(50, {message: 'Must be less than 50 characters'}),
|
.refine((value) => !value || z.string().email().safeParse(value).success, {
|
||||||
password: z.string({required_error: 'Password is required'}).trim(),
|
message: 'Please enter a valid email',
|
||||||
confirm_password: z.string({required_error: 'Confirm Password is required'}).trim()
|
})
|
||||||
|
.optional(),
|
||||||
|
username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
|
||||||
|
password: z.string({ required_error: 'Password is required' }).trim(),
|
||||||
|
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
|
||||||
})
|
})
|
||||||
.superRefine(({ confirm_password, password }, ctx) => {
|
.superRefine(({ confirm_password, password }, ctx) => {
|
||||||
return refinePasswords(confirm_password, password, ctx);
|
return refinePasswords(confirm_password, password, ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>
|
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>;
|
||||||
|
|
|
||||||
27
src/lib/hooks/is-mobile.svelte.ts
Normal file
27
src/lib/hooks/is-mobile.svelte.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { untrack } from "svelte";
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
|
export class IsMobile {
|
||||||
|
#current = $state<boolean>(false);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
$effect(() => {
|
||||||
|
return untrack(() => {
|
||||||
|
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||||
|
const onChange = () => {
|
||||||
|
this.#current = window.innerWidth < MOBILE_BREAKPOINT;
|
||||||
|
};
|
||||||
|
mql.addEventListener("change", onChange);
|
||||||
|
onChange();
|
||||||
|
return () => {
|
||||||
|
mql.removeEventListener("change", onChange);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get current() {
|
||||||
|
return this.#current;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/lib/i18n.ts
Normal file
3
src/lib/i18n.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import * as runtime from '$lib/paraglide/runtime';
|
||||||
|
import { createI18n } from '@inlang/paraglide-sveltekit';
|
||||||
|
export const i18n = createI18n(runtime);
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
import {z} from "zod";
|
import { refinePasswords } from '$lib/validations/account';
|
||||||
import {refinePasswords} from "$lib/validations/account";
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const signupUsernameEmailDto = z.object({
|
export const signupUsernameEmailDto = z
|
||||||
firstName: z.string().trim().optional(),
|
.object({
|
||||||
lastName: z.string().trim().optional(),
|
firstName: z.string().trim().optional(),
|
||||||
email: z.string()
|
lastName: z.string().trim().optional(),
|
||||||
.trim()
|
email: z
|
||||||
.max(64, {message: 'Email must be less than 64 characters'})
|
|
||||||
.email({message: 'Please enter a valid email'})
|
|
||||||
.optional(),
|
|
||||||
username: z
|
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(3, {message: 'Must be at least 3 characters'})
|
.max(64, { message: 'Email must be less than 64 characters' })
|
||||||
.max(50, {message: 'Must be less than 50 characters'}),
|
.refine((value) => !value || z.string().email().safeParse(value).success, {
|
||||||
password: z.string({required_error: 'Password is required'}).trim(),
|
message: 'Please enter a valid email',
|
||||||
confirm_password: z.string({required_error: 'Confirm Password is required'}).trim()
|
})
|
||||||
|
.optional(),
|
||||||
|
username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
|
||||||
|
password: z.string({ required_error: 'Password is required' }).trim(),
|
||||||
|
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
|
||||||
})
|
})
|
||||||
.superRefine(({ confirm_password, password }, ctx) => {
|
.superRefine(({ confirm_password, password }, ctx) => {
|
||||||
return refinePasswords(confirm_password, password, ctx);
|
return refinePasswords(confirm_password, password, ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>
|
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>;
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,43 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$components/ui/button';
|
import { Button } from "$components/ui/button";
|
||||||
import * as Card from '$components/ui/card';
|
import * as Card from "$components/ui/card";
|
||||||
import * as Form from '$components/ui/form';
|
import * as Form from "$components/ui/form";
|
||||||
import { Input } from '$components/ui/input';
|
import { Input } from "$components/ui/input";
|
||||||
import { boredState } from '$lib/stores/boredState.js';
|
import { boredState } from "$lib/stores/boredState.js";
|
||||||
import { receive, send } from '$lib/utils/pageCrossfade';
|
import { receive, send } from "$lib/utils/pageCrossfade";
|
||||||
import * as flashModule from 'sveltekit-flash-message/client';
|
import * as flashModule from "sveltekit-flash-message/client";
|
||||||
import { superForm } from 'sveltekit-superforms/client';
|
import { superForm } from "sveltekit-superforms/client";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
const superLoginForm = superForm(data.form, {
|
const superLoginForm = superForm(data.form, {
|
||||||
onSubmit: () => boredState.update((n) => ({ ...n, loading: true })),
|
onSubmit: () => boredState.update((n) => ({ ...n, loading: true })),
|
||||||
onResult: () => boredState.update((n) => ({ ...n, loading: false })),
|
onResult: () => boredState.update((n) => ({ ...n, loading: false })),
|
||||||
flashMessage: {
|
flashMessage: {
|
||||||
module: flashModule,
|
module: flashModule,
|
||||||
onError: ({ result, flashMessage }) => {
|
onError: ({ result, flashMessage }) => {
|
||||||
// Error handling for the flash message:
|
// Error handling for the flash message:
|
||||||
// - result is the ActionResult
|
// - result is the ActionResult
|
||||||
// - message is the flash store (not the status message store)
|
// - message is the flash store (not the status message store)
|
||||||
const errorMessage = result.error.message;
|
const errorMessage = result.error.message;
|
||||||
flashMessage.set({ type: 'error', message: errorMessage });
|
flashMessage.set({ type: "error", message: errorMessage });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
syncFlashMessage: false,
|
||||||
syncFlashMessage: false,
|
taintedMessage: null,
|
||||||
taintedMessage: null,
|
// validators: zodClient(signInSchema),
|
||||||
// validators: zodClient(signInSchema),
|
// validationMethod: 'oninput',
|
||||||
// validationMethod: 'oninput',
|
delayMs: 0,
|
||||||
delayMs: 0,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const { form: loginForm, enhance } = superLoginForm;
|
const { form: loginForm, enhance } = superLoginForm;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Bored Game | Login</title>
|
<title>Bored Game | Login</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div in:receive={{ key: 'auth-card' }} out:send={{ key: 'auth-card' }}>
|
<div in:receive={{ key: "auth-card" }} out:send={{ key: "auth-card" }}>
|
||||||
<Card.Root class="mx-auto mt-24 max-w-sm">
|
<Card.Root class="mx-auto mt-24 max-w-sm">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title class="text-2xl">Log into your account</Card.Title>
|
<Card.Title class="text-2xl">Log into your account</Card.Title>
|
||||||
|
|
@ -48,13 +48,9 @@ const { form: loginForm, enhance } = superLoginForm;
|
||||||
{@render oAuthButtons()}
|
{@render oAuthButtons()}
|
||||||
<p class="px-8 py-4 text-center text-sm text-muted-foreground">
|
<p class="px-8 py-4 text-center text-sm text-muted-foreground">
|
||||||
By clicking continue, you agree to our
|
By clicking continue, you agree to our
|
||||||
<a href="/terms" class="underline underline-offset-4 hover:text-primary">
|
<a href="/terms" class="underline underline-offset-4 hover:text-primary"> Terms of Use </a>
|
||||||
Terms of Use
|
|
||||||
</a>
|
|
||||||
and
|
and
|
||||||
<a href="/privacy" class="underline underline-offset-4 hover:text-primary">
|
<a href="/privacy" class="underline underline-offset-4 hover:text-primary"> Privacy Policy </a>.
|
||||||
Privacy Policy
|
|
||||||
</a>.
|
|
||||||
</p>
|
</p>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
@ -85,8 +81,24 @@ const { form: loginForm, enhance } = superLoginForm;
|
||||||
|
|
||||||
{#snippet oAuthButtons()}
|
{#snippet oAuthButtons()}
|
||||||
<div class="grid gap-4">
|
<div class="grid gap-4">
|
||||||
<Button href="/login/google" variant="outline" class="w-full flex items-center gap-2"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg> Google</Button>
|
<Button href="/login/google" variant="outline" class="w-full flex items-center gap-2">
|
||||||
<Button href="/login/github" variant="outline" class="w-full flex items-center gap-2"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg> GitHub</Button>
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><title>Google</title>
|
||||||
|
<path
|
||||||
|
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Google
|
||||||
|
</Button>
|
||||||
|
<Button href="/login/github" variant="outline" class="w-full flex items-center gap-2">
|
||||||
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><title>GitHub</title>
|
||||||
|
<path
|
||||||
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
GitHub
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
|
@ -95,4 +107,4 @@ const { form: loginForm, enhance } = superLoginForm;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export const load: PageServerLoad = async (event) => {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
form: await superValidate(zod(signupUsernameEmailDto), {
|
signupForm: await superValidate(zod(signupUsernameEmailDto), {
|
||||||
defaults: signUpDefaults,
|
defaults: signUpDefaults,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,33 +6,23 @@ import * as Alert from '$lib/components/ui/alert';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||||
import { signupUsernameEmailDto } from '$lib/dtos/signup-username-email.dto';
|
import { signupUsernameEmailDto } from '$lib/dtos/signup-username-email.dto';
|
||||||
import { boredState } from '$lib/stores/boredState.js';
|
|
||||||
import { receive, send } from '$lib/utils/pageCrossfade';
|
import { receive, send } from '$lib/utils/pageCrossfade';
|
||||||
import { ChevronsUpDown } from 'lucide-svelte';
|
import { ChevronsUpDown } from 'lucide-svelte';
|
||||||
import { quintIn } from 'svelte/easing';
|
import { quintIn } from 'svelte/easing';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import * as flashModule from 'sveltekit-flash-message/client';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zodClient } from 'sveltekit-superforms/adapters';
|
import { zodClient } from 'sveltekit-superforms/adapters';
|
||||||
import { superForm } from 'sveltekit-superforms/client';
|
|
||||||
|
|
||||||
export let data;
|
const { data } = $props();
|
||||||
|
|
||||||
const { form, errors, enhance } = superForm(data.form, {
|
const signupForm = superForm(data.signupForm, {
|
||||||
onSubmit: () => boredState.update((n) => ({ ...n, loading: true })),
|
|
||||||
onResult: () => boredState.update((n) => ({ ...n, loading: false })),
|
|
||||||
flashMessage: {
|
|
||||||
module: flashModule,
|
|
||||||
onError: ({ result, flashMessage }) => {
|
|
||||||
const errorMessage = result.error.message;
|
|
||||||
flashMessage.set({ type: 'error', message: errorMessage });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
taintedMessage: null,
|
|
||||||
validators: zodClient(signupUsernameEmailDto),
|
validators: zodClient(signupUsernameEmailDto),
|
||||||
delayMs: 0,
|
resetForm: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let collapsibleOpen = false;
|
const { form: signupFormData, errors: signupErrors, enhance: signupEnhance } = signupForm;
|
||||||
|
|
||||||
|
let collapsibleOpen = $state(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -45,26 +35,26 @@ let collapsibleOpen = false;
|
||||||
<Card.Title class="text-2xl">Signup for an account</Card.Title>
|
<Card.Title class="text-2xl">Signup for an account</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<form method="POST" action="/signup" use:enhance class="grid gap-2 mt-4">
|
<form method="POST" action="/signup" use:signupEnhance class="grid gap-2 mt-4">
|
||||||
<Label for="username">Username <small>(required)</small></Label>
|
<Label for="username">Username <small>(required)</small></Label>
|
||||||
<Input type="text" id="username" class={$errors.username && "outline outline-destructive"} name="username"
|
<Input type="text" id="username" class={$signupErrors.username && "outline outline-destructive"} name="username"
|
||||||
placeholder="Username" autocomplete="username" data-invalid={$errors.username} bind:value={$form.username} />
|
placeholder="Username" autocomplete="username" data-invalid={$signupErrors.username} bind:value={$signupFormData.username} />
|
||||||
{#if $errors.username}
|
{#if $signupErrors.username}
|
||||||
<p class="text-sm text-destructive">{$errors.username}</p>
|
<p class="text-sm text-destructive">{$signupErrors.username}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<Label for="password">Password <small>(required)</small></Label>
|
<Label for="password">Password <small>(required)</small></Label>
|
||||||
<Input type="password" id="password" class={$errors.password && "outline outline-destructive"} name="password"
|
<Input type="password" id="password" class={$signupErrors.password && "outline outline-destructive"} name="password"
|
||||||
placeholder="Password" autocomplete="new-password" data-invalid={$errors.password}
|
placeholder="Password" autocomplete="new-password" data-invalid={$signupErrors.password}
|
||||||
bind:value={$form.password} />
|
bind:value={$signupFormData.password} />
|
||||||
{#if $errors.password}
|
{#if $signupErrors.password}
|
||||||
<p class="text-sm text-destructive">{$errors.password}</p>
|
<p class="text-sm text-destructive">{$signupErrors.password}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<Label for="confirm_password">Confirm Password <small>(required)</small></Label>
|
<Label for="confirm_password">Confirm Password <small>(required)</small></Label>
|
||||||
<Input type="password" id="confirm_password" class={$errors.confirm_password && "outline outline-destructive"}
|
<Input type="password" id="confirm_password" class={$signupErrors.confirm_password && "outline outline-destructive"}
|
||||||
name="confirm_password" placeholder="Confirm Password" autocomplete="new-password"
|
name="confirm_password" placeholder="Confirm Password" autocomplete="new-password"
|
||||||
data-invalid={$errors.confirm_password} bind:value={$form.confirm_password} />
|
data-invalid={$signupErrors.confirm_password} bind:value={$signupFormData.confirm_password} />
|
||||||
{#if $errors.confirm_password}
|
{#if $signupErrors.confirm_password}
|
||||||
<p class="text-sm text-destructive">{$errors.confirm_password}</p>
|
<p class="text-sm text-destructive">{$signupErrors.confirm_password}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<Collapsible.Root bind:open={collapsibleOpen} class="grid w-full max-w-sm items-center gap-2.5">
|
<Collapsible.Root bind:open={collapsibleOpen} class="grid w-full max-w-sm items-center gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -79,32 +69,32 @@ let collapsibleOpen = false;
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||||
<Label for="email">Email</Label>
|
<Label for="email">Email</Label>
|
||||||
<Input type="email" id="email" class={$errors.email && "outline outline-destructive"} name="email"
|
<Input type="email" id="email" class={$signupErrors.email && "outline outline-destructive"} name="email"
|
||||||
placeholder="Email" autocomplete="email" data-invalid={$errors.email} bind:value={$form.email} />
|
placeholder="Email" autocomplete="email" data-invalid={$signupErrors.email} bind:value={$signupFormData.email} />
|
||||||
{#if $errors.email}
|
{#if $signupErrors.email}
|
||||||
<p class="text-sm text-destructive">{$errors.email}</p>
|
<p class="text-sm text-destructive">{$signupErrors.email}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||||
<Label for="firstName">First Name</Label>
|
<Label for="firstName">First Name</Label>
|
||||||
<Input type="text" id="firstName" class={$errors.firstName && "outline outline-destructive"} name="firstName"
|
<Input type="text" id="firstName" class={$signupErrors.firstName && "outline outline-destructive"} name="firstName"
|
||||||
placeholder="First Name" autocomplete="given-name" data-invalid={$errors.firstName}
|
placeholder="First Name" autocomplete="given-name" data-invalid={$signupErrors.firstName}
|
||||||
bind:value={$form.firstName} />
|
bind:value={$signupFormData.firstName} />
|
||||||
{#if $errors.firstName}
|
{#if $signupErrors.firstName}
|
||||||
<p class="text-sm text-destructive">{$errors.firstName}</p>
|
<p class="text-sm text-destructive">{$signupErrors.firstName}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||||
<Label for="firstName">Last Name</Label>
|
<Label for="firstName">Last Name</Label>
|
||||||
<Input type="text" id="lastName" class={$errors.firstName && "outline outline-destructive"} name="lastName"
|
<Input type="text" id="lastName" class={$signupErrors.firstName && "outline outline-destructive"} name="lastName"
|
||||||
placeholder="Last Name" autocomplete="family-name" data-invalid={$errors.lastName}
|
placeholder="Last Name" autocomplete="family-name" data-invalid={$signupErrors.lastName}
|
||||||
bind:value={$form.lastName} />
|
bind:value={$signupFormData.lastName} />
|
||||||
{#if $errors.lastName}
|
{#if $signupErrors.lastName}
|
||||||
<p class="text-sm text-destructive">{$errors.lastName}</p>
|
<p class="text-sm text-destructive">{$signupErrors.lastName}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
|
|
@ -113,7 +103,7 @@ let collapsibleOpen = false;
|
||||||
<Button type="submit">Signup</Button>
|
<Button type="submit">Signup</Button>
|
||||||
<Button variant="link" class="text-secondary-foreground" href="/">or Cancel</Button>
|
<Button variant="link" class="text-secondary-foreground" href="/">or Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
{#if !$form.email}
|
{#if !$signupFormData.email}
|
||||||
<Alert.Root>
|
<Alert.Root>
|
||||||
<Alert.Title level="h3">Heads up!</Alert.Title>
|
<Alert.Title level="h3">Heads up!</Alert.Title>
|
||||||
<Alert.Description>
|
<Alert.Description>
|
||||||
|
|
@ -128,41 +118,4 @@ let collapsibleOpen = false;
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.sign-up {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
margin-right: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 24rem;
|
|
||||||
|
|
||||||
h2:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
font-size: 1.875rem;
|
|
||||||
line-height: 2.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 300ms;
|
|
||||||
scroll-margin: 5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,67 +1,69 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$lib/styles/app.pcss';
|
import '$lib/styles/app.pcss';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { MetaTags } from 'svelte-meta-tags';
|
import { MetaTags } from 'svelte-meta-tags';
|
||||||
import { getFlash } from 'sveltekit-flash-message/client';
|
import { getFlash } from 'sveltekit-flash-message/client';
|
||||||
import 'iconify-icon';
|
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
|
||||||
import { onNavigate } from '$app/navigation';
|
import 'iconify-icon';
|
||||||
import { page } from '$app/stores';
|
import { onNavigate } from '$app/navigation';
|
||||||
import Analytics from '$components/Analytics.svelte';
|
import { page } from '$app/stores';
|
||||||
import { Toaster } from '$lib/components/ui/sonner';
|
import Analytics from '$components/Analytics.svelte';
|
||||||
import PageLoadingIndicator from '$lib/page_loading_indicator.svelte';
|
import { Toaster } from '$lib/components/ui/sonner';
|
||||||
import { toastMessage } from '$lib/utils/superforms.js';
|
import PageLoadingIndicator from '$lib/page_loading_indicator.svelte';
|
||||||
import { theme } from '$state/theme';
|
import { toastMessage } from '$lib/utils/superforms.js';
|
||||||
// import { ModeWatcher } from 'mode-watcher'
|
import { theme } from '$state/theme';
|
||||||
|
import { i18n } from '$lib/i18n';
|
||||||
|
// import { ModeWatcher } from 'mode-watcher'
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV !== 'production';
|
const dev = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
const { data, children } = $props();
|
const { data, children } = $props();
|
||||||
const { user } = data;
|
const { user } = data;
|
||||||
|
|
||||||
const metaTags = $derived({
|
const metaTags = $derived({
|
||||||
titleTemplate: '%s | Bored Game',
|
|
||||||
description: 'Bored Game, keep track of your games.',
|
|
||||||
openGraph: {
|
|
||||||
type: 'website',
|
|
||||||
titleTemplate: '%s | Bored Game',
|
titleTemplate: '%s | Bored Game',
|
||||||
locale: 'en_US',
|
description: 'Bored Game, keep track of your games.',
|
||||||
description: 'Bored Game, keep track of your games',
|
openGraph: {
|
||||||
},
|
type: 'website',
|
||||||
...$page.data.metaTagsChild,
|
titleTemplate: '%s | Bored Game',
|
||||||
});
|
locale: 'en_US',
|
||||||
|
description: 'Bored Game, keep track of your games',
|
||||||
|
},
|
||||||
|
...$page.data.metaTagsChild,
|
||||||
|
});
|
||||||
|
|
||||||
const flash = getFlash(page, {
|
const flash = getFlash(page, {
|
||||||
clearOnNavigate: true,
|
clearOnNavigate: true,
|
||||||
clearAfterMs: 3000,
|
clearAfterMs: 3000,
|
||||||
clearArray: true,
|
clearArray: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// set the theme to the user's active theme
|
// set the theme to the user's active theme
|
||||||
$theme = user?.theme || 'system';
|
$theme = user?.theme || 'system';
|
||||||
document.querySelector('html')?.setAttribute('data-theme', $theme);
|
document.querySelector('html')?.setAttribute('data-theme', $theme);
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
console.log('flash', $flash);
|
console.log('flash', $flash);
|
||||||
if ($flash) {
|
if ($flash) {
|
||||||
toastMessage({ type: $flash.type, text: $flash.message });
|
toastMessage({ type: $flash.type, text: $flash.message });
|
||||||
// Clearing the flash message could sometimes
|
// Clearing the flash message could sometimes
|
||||||
// be required here to avoid double-toasting.
|
// be required here to avoid double-toasting.
|
||||||
flash.set(undefined);
|
flash.set(undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onNavigate(async (navigation) => {
|
onNavigate(async (navigation) => {
|
||||||
if (!document.startViewTransition) return;
|
if (!document.startViewTransition) return;
|
||||||
|
|
||||||
return new Promise((oldStateCaptureResolve) => {
|
return new Promise((oldStateCaptureResolve) => {
|
||||||
document.startViewTransition(async () => {
|
document.startViewTransition(async () => {
|
||||||
oldStateCaptureResolve();
|
oldStateCaptureResolve();
|
||||||
await navigation.complete;
|
await navigation.complete;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !dev}
|
{#if !dev}
|
||||||
|
|
@ -72,4 +74,7 @@ onNavigate(async (navigation) => {
|
||||||
<PageLoadingIndicator />
|
<PageLoadingIndicator />
|
||||||
<!-- <ModeWatcher /> -->
|
<!-- <ModeWatcher /> -->
|
||||||
<Toaster />
|
<Toaster />
|
||||||
{@render children()}
|
|
||||||
|
<ParaglideJS {i18n}>
|
||||||
|
{@render children()}
|
||||||
|
</ParaglideJS>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import 'reflect-metadata'
|
|
||||||
import { preprocessMeltUI } from '@melt-ui/pp'
|
import { preprocessMeltUI } from '@melt-ui/pp'
|
||||||
import adapter from '@sveltejs/adapter-node'
|
import adapter from '@sveltejs/adapter-node'
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||||
|
import type { Config } from "tailwindcss";
|
||||||
import tailwindcssAnimate from "tailwindcss-animate";
|
import tailwindcssAnimate from "tailwindcss-animate";
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
const config: Config = {
|
||||||
const config = {
|
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||||
|
safelist: ["dark"],
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
|
|
@ -47,16 +48,46 @@ const config = {
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: "hsl(var(--card))",
|
||||||
foreground: "hsl(var(--card-foreground))"
|
foreground: "hsl(var(--card-foreground))"
|
||||||
}
|
},
|
||||||
|
sidebar: {
|
||||||
|
DEFAULT: "hsl(var(--sidebar-background))",
|
||||||
|
foreground: "hsl(var(--sidebar-foreground))",
|
||||||
|
primary: "hsl(var(--sidebar-primary))",
|
||||||
|
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
||||||
|
accent: "hsl(var(--sidebar-accent))",
|
||||||
|
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||||
|
border: "hsl(var(--sidebar-border))",
|
||||||
|
ring: "hsl(var(--sidebar-ring))",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
|
xl: "calc(var(--radius) + 4px)",
|
||||||
lg: "var(--radius)",
|
lg: "var(--radius)",
|
||||||
md: "calc(var(--radius) - 2px)",
|
md: "calc(var(--radius) - 2px)",
|
||||||
sm: "calc(var(--radius) - 4px)"
|
sm: "calc(var(--radius) - 4px)"
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: [...fontFamily.sans]
|
sans: [...fontFamily.sans]
|
||||||
}
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: "0" },
|
||||||
|
to: { height: "var(--bits-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--bits-accordion-content-height)" },
|
||||||
|
to: { height: "0" },
|
||||||
|
},
|
||||||
|
"caret-blink": {
|
||||||
|
"0%,70%,100%": { opacity: "1" },
|
||||||
|
"20%,50%": { opacity: "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [tailwindcssAnimate]
|
plugins: [tailwindcssAnimate]
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
//
|
//
|
||||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,13 @@
|
||||||
// import { sentrySvelteKit } from "@sentry/sveltekit";
|
import { paraglide } from "@inlang/paraglide-sveltekit/vite";
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
// TODO: Fix Sentry
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
// sentrySvelteKit({
|
sveltekit(), paraglide({
|
||||||
// sourceMapsUploadOptions: {
|
project: "./project.inlang",
|
||||||
// org: process.env.SENTRY_ORG,
|
outdir: "./src/lib/paraglide"
|
||||||
// project: process.env.SENTRY_PROJECT,
|
}),
|
||||||
// authToken: process.env.SENTRY_AUTH_TOKEN,
|
|
||||||
// cleanArtifacts: true,
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
sveltekit(),
|
|
||||||
],
|
],
|
||||||
esbuild: {
|
esbuild: {
|
||||||
target: 'es2022',
|
target: 'es2022',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue