feat: move structure
1
web/.env.example
Normal file
|
@ -0,0 +1 @@
|
|||
PUBLIC_CLIENT_PB="http://127.0.0.1:8090"
|
13
web/.eslintignore
Normal file
|
@ -0,0 +1,13 @@
|
|||
.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
|
37
web/.eslintrc.cjs
Normal file
|
@ -0,0 +1,37 @@
|
|||
/** @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'
|
||||
}
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ varsIgnorePattern: '^\\$\\$(Props|Events|Slots)$' }
|
||||
]
|
||||
}
|
||||
};
|
10
web/.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
1
web/.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
4
web/.prettierignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
15
web/.prettierrc
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
38
web/README.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
13
web/components.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "new-york",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app.pcss",
|
||||
"baseColor": "neutral"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils"
|
||||
}
|
||||
}
|
54
web/package.json
Normal file
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"name": "web",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/eslint": "8.56.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"postcss": "^8.4.32",
|
||||
"postcss-load-config": "^5.0.2",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"prettier-plugin-tailwindcss": "^0.5.9",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"sveltekit-superforms": "^1.13.4",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.3",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"bits-ui": "^0.15.1",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk-sv": "^0.0.13",
|
||||
"formsnap": "^0.4.3",
|
||||
"lucide-svelte": "^0.316.0",
|
||||
"mode-watcher": "^0.1.2",
|
||||
"pocketbase": "^0.21.0",
|
||||
"radix-icons-svelte": "^1.2.1",
|
||||
"svelte-headless-table": "^0.18.1",
|
||||
"svelte-sonner": "^0.3.17",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwind-variants": "^0.1.20"
|
||||
}
|
||||
}
|
2709
web/pnpm-lock.yaml
generated
Normal file
13
web/postcss.config.cjs
Normal file
|
@ -0,0 +1,13 @@
|
|||
const tailwindcss = require('tailwindcss');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
|
||||
const config = {
|
||||
plugins: [
|
||||
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||
tailwindcss(),
|
||||
//But others, like autoprefixer, need to run after,
|
||||
autoprefixer
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
17
web/src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
pocketBase: PocketBase;
|
||||
id: string;
|
||||
email: string;
|
||||
}
|
||||
// interface Error {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
24
web/src/app.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%sveltekit.assets%/favicon-16x16.png" />
|
||||
<link rel="manifest" href="%sveltekit.assets%/site.webmanifest" />
|
||||
<link rel="mask-icon" href="%sveltekit.assets%/safari-pinned-tab.svg" color="#222222" />
|
||||
<meta name="msapplication-TileColor" content="#222222" />
|
||||
<meta name="theme-color" content="#222222" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body
|
||||
data-sveltekit-preload-data="hover"
|
||||
class="min-h-screen bg-background font-sans antialiased"
|
||||
>
|
||||
<div style="display: contents" class="relative flex min-h-screen flex-col">
|
||||
%sveltekit.body%
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
58
web/src/app.pcss
Normal file
|
@ -0,0 +1,58 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 72.2% 50.6%;
|
||||
--primary-foreground: 0 85.7% 97.3%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 72.22% 50.59%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 72.2% 50.6%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 72.2% 50.6%;
|
||||
--primary-foreground: 0 85.7% 97.3%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 72.2% 50.6%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
36
web/src/hooks.server.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { type Handle } from '@sveltejs/kit';
|
||||
import PocketBase from 'pocketbase';
|
||||
import { building, dev } from '$app/environment';
|
||||
import { SERVER_PB } from '$env/static/private';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
event.locals.id = '';
|
||||
event.locals.email = '';
|
||||
event.locals.pocketBase = new PocketBase(SERVER_PB);
|
||||
|
||||
const isAuth: boolean = event.url.pathname === '/auth';
|
||||
if (isAuth || building) {
|
||||
event.cookies.set('pb_auth', '', { path: '/' });
|
||||
return await resolve(event);
|
||||
}
|
||||
|
||||
const pb_auth = event.request.headers.get('cookie') ?? '';
|
||||
event.locals.pocketBase.authStore.loadFromCookie(pb_auth);
|
||||
try {
|
||||
const auth = await event.locals.pocketBase
|
||||
.collection('users')
|
||||
.authRefresh<{ id: string; email: string }>();
|
||||
event.locals.id = auth.record.id;
|
||||
event.locals.email = auth.record.email;
|
||||
} catch (_) {
|
||||
event.locals.pocketBase.authStore.clear();
|
||||
}
|
||||
|
||||
const response = await resolve(event);
|
||||
const cookie = event.locals.pocketBase.authStore.exportToCookie({
|
||||
secure: !dev,
|
||||
sameSite: 'lax'
|
||||
});
|
||||
response.headers.append('set-cookie', cookie);
|
||||
return response;
|
||||
};
|
18
web/src/lib/components/site/data-indicator.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
|
||||
|
||||
export let data: object;
|
||||
</script>
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild let:builder>
|
||||
<Button builders={[builder]} variant="ghost" size="icon" class="block">
|
||||
{'{}'}
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-auto">
|
||||
<SuperDebug label="$layout data" status={false} {data} />
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
13
web/src/lib/components/site/icons/apple.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 384 512"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...$$restProps}
|
||||
><path
|
||||
d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"
|
||||
></path></svg
|
||||
>
|
After Width: | Height: | Size: 638 B |
13
web/src/lib/components/site/icons/bitbucket.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 512 512"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...$$restProps}
|
||||
><path
|
||||
d="M22.2 32A16 16 0 0 0 6 47.8a26.35 26.35 0 0 0 .2 2.8l67.9 412.1a21.77 21.77 0 0 0 21.3 18.2h325.7a16 16 0 0 0 16-13.4L505 50.7a16 16 0 0 0-13.2-18.3 24.58 24.58 0 0 0-2.8-.2L22.2 32zm285.9 297.8h-104l-28.1-147h157.3l-25.2 147z"
|
||||
></path></svg
|
||||
>
|
After Width: | Height: | Size: 429 B |
13
web/src/lib/components/site/icons/discord.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 640 512"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...$$restProps}
|
||||
><path
|
||||
d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"
|
||||
></path></svg
|
||||
>
|
After Width: | Height: | Size: 1.5 KiB |
13
web/src/lib/components/site/icons/facebook.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 448 512"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...$$restProps}
|
||||
><path
|
||||
d="M400 32H48A48 48 0 0 0 0 80v352a48 48 0 0 0 48 48h137.25V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.27c-30.81 0-40.42 19.12-40.42 38.73V256h68.78l-11 71.69h-57.78V480H400a48 48 0 0 0 48-48V80a48 48 0 0 0-48-48z"
|
||||
></path></svg
|
||||
>
|
After Width: | Height: | Size: 461 B |
4
web/src/lib/components/site/icons/github.svelte
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg viewBox="0 0 24 24" {...$$restProps}>
|
||||
<path d="M0 0h24v24H0z" />
|
||||
<path d="M3 19h18l-9 -15z" />
|
||||
</svg>
|
After Width: | Height: | Size: 109 B |
13
web/src/lib/components/site/icons/gitlab.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 512 512"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...$$restProps}
|
||||
><path
|
||||
d="M503.5 204.6L502.8 202.8L433.1 21.02C431.7 17.45 429.2 14.43 425.9 12.38C423.5 10.83 420.8 9.865 417.9 9.57C415 9.275 412.2 9.653 409.5 10.68C406.8 11.7 404.4 13.34 402.4 15.46C400.5 17.58 399.1 20.13 398.3 22.9L351.3 166.9H160.8L113.7 22.9C112.9 20.13 111.5 17.59 109.6 15.47C107.6 13.35 105.2 11.72 102.5 10.7C99.86 9.675 96.98 9.295 94.12 9.587C91.26 9.878 88.51 10.83 86.08 12.38C82.84 14.43 80.33 17.45 78.92 21.02L9.267 202.8L8.543 204.6C-1.484 230.8-2.72 259.6 5.023 286.6C12.77 313.5 29.07 337.3 51.47 354.2L51.74 354.4L52.33 354.8L158.3 434.3L210.9 474L242.9 498.2C246.6 500.1 251.2 502.5 255.9 502.5C260.6 502.5 265.2 500.1 268.9 498.2L300.9 474L353.5 434.3L460.2 354.4L460.5 354.1C482.9 337.2 499.2 313.5 506.1 286.6C514.7 259.6 513.5 230.8 503.5 204.6z"
|
||||
></path></svg
|
||||
>
|
After Width: | Height: | Size: 967 B |
13
web/src/lib/components/site/icons/google.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 488 512"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...$$restProps}
|
||||
><path
|
||||
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
|
||||
></path></svg
|
||||
>
|
After Width: | Height: | Size: 450 B |
35
web/src/lib/components/site/icons/index.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import type { Icon as LucideIcon } from 'lucide-svelte';
|
||||
import { ArrowRight, Loader2 } from 'lucide-svelte';
|
||||
import { GithubLogo, VercelLogo, LinkedinLogo } from 'radix-icons-svelte';
|
||||
import Logo from './logo.svelte';
|
||||
import Svelte from './svelte.svelte';
|
||||
import MicrosoftLogo from './microsoft.svelte';
|
||||
import AppleLogo from './apple.svelte';
|
||||
import GitLabLogo from './gitlab.svelte';
|
||||
import BitBucketLogo from './bitbucket.svelte';
|
||||
import DiscordLogo from './discord.svelte';
|
||||
import FacebookLogo from './facebook.svelte';
|
||||
import GoogleLogo from './google.svelte';
|
||||
import InstagramLogo from './instagram.svelte';
|
||||
import TwitterLogo from './twitter.svelte';
|
||||
|
||||
export type Icon = LucideIcon;
|
||||
|
||||
export const Icons = {
|
||||
logo: Logo,
|
||||
gitHub: GithubLogo,
|
||||
microsoft: MicrosoftLogo,
|
||||
svelte: Svelte,
|
||||
vercel: VercelLogo,
|
||||
linkedIn: LinkedinLogo,
|
||||
spinner: Loader2,
|
||||
arrowRight: ArrowRight,
|
||||
apple: AppleLogo,
|
||||
bitBucket: BitBucketLogo,
|
||||
gitLab: GitLabLogo,
|
||||
discord: DiscordLogo,
|
||||
facebook: FacebookLogo,
|
||||
google: GoogleLogo,
|
||||
instagram: InstagramLogo,
|
||||
twitter: TwitterLogo
|
||||
};
|
13
web/src/lib/components/site/icons/instagram.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 448 512"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...$$restProps}
|
||||
><path
|
||||
d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"
|
||||
></path></svg
|
||||
>
|
After Width: | Height: | Size: 1.1 KiB |
24
web/src/lib/components/site/icons/logo.svelte
Normal file
|
@ -0,0 +1,24 @@
|
|||
<script>
|
||||
export let colours = {
|
||||
topLeft: '#FF6C22',
|
||||
topRight: '#FF9209',
|
||||
bottomLeft: '#FFD099',
|
||||
bottomRight: '#3B48D3'
|
||||
};
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="7" height="9" x="3" y="3" rx="1" stroke={colours.topLeft} />
|
||||
<rect width="7" height="5" x="14" y="3" rx="1" stroke={colours.topRight} />
|
||||
<rect width="7" height="5" x="3" y="16" rx="1" stroke={colours.bottomLeft} />
|
||||
<rect width="7" height="9" x="14" y="12" rx="1" stroke={colours.bottomRight} />
|
||||
</svg>
|
12
web/src/lib/components/site/icons/microsoft.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
version="1.2"
|
||||
baseProfile="tiny"
|
||||
viewBox="0 0 24 24"
|
||||
{...$$restProps}
|
||||
><path
|
||||
d="M10 12.5c0-.3-.2-.5-.5-.5h-6c-.3 0-.5.2-.5.5v5c0 .3.2.5.5.6l6 .7c.3 0 .5-.2.5-.4v-5.9zM11.5 12c-.3 0-.5.2-.5.5v5.9c0 .3.2.5.5.6l9 1c.3 0 .5-.2.5-.4v-7c0-.3-.2-.5-.5-.5l-9-.1zM10 4.7c0-.3-.2-.5-.5-.4l-6 .7c-.3 0-.5.2-.5.5v5c0 .3.2.5.5.5h6c.3 0 .5-.2.5-.5v-5.8zM11.5 4.1c-.3 0-.5.3-.5.6v5.9c0 .3.2.5.5.5h9c.3 0 .5-.2.5-.5v-7c0-.3-.2-.5-.5-.4l-9 .9z"
|
||||
></path></svg
|
||||
>
|
After Width: | Height: | Size: 519 B |
15
web/src/lib/components/site/icons/svelte.svelte
Normal file
|
@ -0,0 +1,15 @@
|
|||
<svg
|
||||
class="inline-svg"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...$$restProps}
|
||||
><path
|
||||
d="M10.354 21.125a4.44 4.44 0 0 1-4.765-1.767 4.109 4.109 0 0 1-.703-3.107 3.898 3.898 0 0 1 .134-.522l.105-.321.287.21a7.21 7.21 0 0 0 2.186 1.092l.208.063-.02.208a1.253 1.253 0 0 0 .226.83 1.337 1.337 0 0 0 1.435.533 1.231 1.231 0 0 0 .343-.15l5.59-3.562a1.164 1.164 0 0 0 .524-.778 1.242 1.242 0 0 0-.211-.937 1.338 1.338 0 0 0-1.435-.533 1.23 1.23 0 0 0-.343.15l-2.133 1.36a4.078 4.078 0 0 1-1.135.499 4.44 4.44 0 0 1-4.765-1.766 4.108 4.108 0 0 1-.702-3.108 3.855 3.855 0 0 1 1.742-2.582l5.589-3.563a4.072 4.072 0 0 1 1.135-.499 4.44 4.44 0 0 1 4.765 1.767 4.109 4.109 0 0 1 .703 3.107 3.943 3.943 0 0 1-.134.522l-.105.321-.286-.21a7.204 7.204 0 0 0-2.187-1.093l-.208-.063.02-.207a1.255 1.255 0 0 0-.226-.831 1.337 1.337 0 0 0-1.435-.532 1.231 1.231 0 0 0-.343.15L8.62 9.368a1.162 1.162 0 0 0-.524.778 1.24 1.24 0 0 0 .211.937 1.338 1.338 0 0 0 1.435.533 1.235 1.235 0 0 0 .344-.151l2.132-1.36a4.067 4.067 0 0 1 1.135-.498 4.44 4.44 0 0 1 4.765 1.766 4.108 4.108 0 0 1 .702 3.108 3.857 3.857 0 0 1-1.742 2.583l-5.589 3.562a4.072 4.072 0 0 1-1.135.499m10.358-17.95C18.484-.015 14.082-.96 10.9 1.068L5.31 4.63a6.412 6.412 0 0 0-2.896 4.295 6.753 6.753 0 0 0 .666 4.336 6.43 6.43 0 0 0-.96 2.396 6.833 6.833 0 0 0 1.168 5.167c2.229 3.19 6.63 4.135 9.812 2.108l5.59-3.562a6.41 6.41 0 0 0 2.896-4.295 6.756 6.756 0 0 0-.665-4.336 6.429 6.429 0 0 0 .958-2.396 6.831 6.831 0 0 0-1.167-5.168Z"
|
||||
/></svg
|
||||
>
|
After Width: | Height: | Size: 1.6 KiB |
13
web/src/lib/components/site/icons/twitter.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 512 512"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...$$restProps}
|
||||
><path
|
||||
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
|
||||
></path></svg
|
||||
>
|
After Width: | Height: | Size: 994 B |
4
web/src/lib/components/site/icons/vercel.svelte
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg viewBox="0 0 24 24" {...$$restProps}>
|
||||
<path stroke="none" d="M0 0h24v24H0z" />
|
||||
<path d="M3 19h18l-9 -15z" />
|
||||
</svg>
|
After Width: | Height: | Size: 123 B |
9
web/src/lib/components/site/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export { default as Metadata } from './metadata.svelte';
|
||||
export { default as SiteFooter } from './site-footer.svelte';
|
||||
export { default as SiteNavBar } from './site-navbar.svelte';
|
||||
export { default as TailwindIndicator } from './tailwind-indicator.svelte';
|
||||
export { default as ModeToggle } from './mode-toggle.svelte';
|
||||
export { default as Particles } from './particles.svelte';
|
||||
|
||||
export * from './icons';
|
||||
export * from './nav';
|
36
web/src/lib/components/site/metadata.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { siteConfig } from '$lib/config/site';
|
||||
|
||||
export let title: string = siteConfig.name;
|
||||
|
||||
$: title = $page.data?.name ? `${$page.data.name} — ${siteConfig.name}` : siteConfig.name;
|
||||
$: description = $page.data?.subTitle ?? siteConfig.description;
|
||||
$: ogImage = encodeURI(
|
||||
`${siteConfig.ogImage}?title=${$page.data.title}&subTitle=${$page.data.subTitle}`
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="keywords" content={siteConfig.keywords} />
|
||||
<meta name="author" content="Bart van der Braak" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content={siteConfig.url} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
<meta name="twitter:image:alt" content={siteConfig.name} />
|
||||
<meta name="twitter:creator" content="Bart van der Braak" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content={siteConfig.url + $page.url.pathname} />
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:alt" content={siteConfig.name} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:site_name" content={siteConfig.name} />
|
||||
<meta property="og:locale" content="EN_US" />
|
||||
</svelte:head>
|
25
web/src/lib/components/site/mode-toggle.svelte
Normal file
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { Moon, Sun } from 'lucide-svelte';
|
||||
import { Button } from '../ui/button';
|
||||
import * as DropdownMenu from '../ui/dropdown-menu';
|
||||
import { resetMode, setMode } from 'mode-watcher';
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
<Button builders={[builder]} variant="ghost" class="h-9 w-9">
|
||||
<Sun
|
||||
class="absolute h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
||||
/>
|
||||
<Moon
|
||||
class="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
|
||||
/>
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item on:click={() => setMode('light')}>Light</DropdownMenu.Item>
|
||||
<DropdownMenu.Item on:click={() => setMode('dark')}>Dark</DropdownMenu.Item>
|
||||
<DropdownMenu.Item on:click={() => resetMode()}>System</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
2
web/src/lib/components/site/nav/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as MainNav } from './main-nav.svelte';
|
||||
export { default as MobileNav } from './mobile-nav.svelte';
|
25
web/src/lib/components/site/nav/main-nav.svelte
Normal file
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { cn } from '$lib/utils';
|
||||
import { navConfig } from '$lib/config/nav';
|
||||
|
||||
export let authenticated = false;
|
||||
</script>
|
||||
|
||||
<div class="mr-4 hidden md:flex">
|
||||
<nav class="flex items-center space-x-6 text-sm font-medium">
|
||||
{#each navConfig.mainNav as navItem, index (navItem + index.toString())}
|
||||
{#if navItem.href && (navItem.auth == authenticated || navItem.always)}
|
||||
<a
|
||||
href={navItem.href}
|
||||
class={cn(
|
||||
'transition-colors hover:text-foreground/80',
|
||||
$page.url.pathname === navItem.href ? 'text-foreground' : 'text-foreground/60'
|
||||
)}
|
||||
>
|
||||
{navItem.title}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
23
web/src/lib/components/site/nav/mobile-link.svelte
Normal file
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
export let href: string;
|
||||
export let open: boolean;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<a
|
||||
{href}
|
||||
on:click={() => (open = false)}
|
||||
class={cn(
|
||||
$page.url.pathname === href ? 'text-foreground' : 'text-foreground/60',
|
||||
'hover:text-foreground',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</a>
|
68
web/src/lib/components/site/nav/mobile-nav.svelte
Normal file
|
@ -0,0 +1,68 @@
|
|||
<script lang="ts">
|
||||
import * as Sheet from '$lib/components/ui/sheet/';
|
||||
import { HamburgerMenu } from 'radix-icons-svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { navConfig } from '$lib/config/nav';
|
||||
import { siteConfig } from '$lib/config/site';
|
||||
import { Icons } from '../icons';
|
||||
import MobileLink from './mobile-link.svelte';
|
||||
|
||||
let open = false;
|
||||
export let authenticated = false;
|
||||
</script>
|
||||
|
||||
<Sheet.Root bind:open>
|
||||
<Sheet.Trigger asChild let:builder>
|
||||
<Button
|
||||
builders={[builder]}
|
||||
variant="ghost"
|
||||
class="mr-2 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden"
|
||||
>
|
||||
<HamburgerMenu class="h-5 w-5" />
|
||||
<span class="sr-only">Toggle Menu</span>
|
||||
</Button>
|
||||
</Sheet.Trigger>
|
||||
<Sheet.Content side="right" class="pr-0">
|
||||
<MobileLink href="/" class="flex items-center" bind:open>
|
||||
<span class="sr-only">Logo icon (return home)</span>
|
||||
<div class="mr-4 rounded-sm bg-gray-950 p-0.5 dark:bg-transparent">
|
||||
<Icons.logo />
|
||||
</div>
|
||||
<span class="font-mono font-bold tracking-tighter">{siteConfig.name}</span>
|
||||
</MobileLink>
|
||||
<div class="my-4 h-[calc(100vh-8rem)] overflow-auto pl-1 pt-10">
|
||||
<div class="flex flex-col space-y-3">
|
||||
{#each navConfig.mainNav as navItem, index (navItem + index.toString())}
|
||||
{#if navItem.href && (navItem.auth == authenticated || navItem.always)}
|
||||
<MobileLink href={navItem.href} bind:open class="pt-2 text-5xl font-bold">
|
||||
{navItem.title}
|
||||
</MobileLink>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
{#each navConfig.sidebarNav as navItem, index (index)}
|
||||
<div class="flex flex-col space-y-3 pt-6">
|
||||
<h4 class="font-medium">{navItem.title}</h4>
|
||||
{#if navItem?.items?.length}
|
||||
{#each navItem.items as item}
|
||||
{#if !item.disabled && item.href}
|
||||
<MobileLink href={item.href} bind:open class="text-muted-foreground">
|
||||
{item.title}
|
||||
{#if item.label}
|
||||
<span
|
||||
class="ml-2 rounded-md bg-[#adfa1d] px-1.5 py-0.5 text-xs leading-none text-[#000000]"
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
{/if}
|
||||
</MobileLink>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
49
web/src/lib/components/site/nav/user-nav.svelte
Normal file
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts">
|
||||
import * as Avatar from '$lib/components/ui/avatar';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import type { BaseAuthStore } from 'pocketbase';
|
||||
|
||||
export let authenticated = false;
|
||||
export let user: BaseAuthStore['model'];
|
||||
</script>
|
||||
|
||||
{#if authenticated}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
<Button variant="ghost" builders={[builder]} class="relative h-8 w-8 rounded-full">
|
||||
<Avatar.Root class="h-9 w-9">
|
||||
<Avatar.Image src={user?.avatarUrl} alt={user?.name} />
|
||||
<Avatar.Fallback>{user?.initials}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="w-56" align="end">
|
||||
<DropdownMenu.Label class="font-normal">
|
||||
<div class="flex flex-col space-y-1">
|
||||
<p class="text-sm font-medium leading-none">{user?.name || user?.username}</p>
|
||||
<p class="text-xs leading-none text-muted-foreground">{user?.email}</p>
|
||||
</div>
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item>Dashboards</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>Connectors</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Label class="text-xs leading-none text-muted-foreground">
|
||||
Settings
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item href="/settings">Profile</DropdownMenu.Item>
|
||||
<DropdownMenu.Item href="/settings/appearance">Appearance</DropdownMenu.Item>
|
||||
<DropdownMenu.Item href="/settings/notifications">Notifications</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item href="/logout">
|
||||
Log out
|
||||
<DropdownMenu.Shortcut>⇧⌘Q</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{:else}
|
||||
<Button href="/auth">Login</Button>
|
||||
{/if}
|
248
web/src/lib/components/site/particles.svelte
Normal file
|
@ -0,0 +1,248 @@
|
|||
<script>
|
||||
import { mode } from 'mode-watcher';
|
||||
import { onMount, beforeUpdate, onDestroy } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const mousePositionStore = writable({ x: 0, y: 0 });
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
const handleMouseMove = (/** @type {{ clientX: number; clientY: number; }} */ event) => {
|
||||
x = event.clientX;
|
||||
y = event.clientY;
|
||||
mousePositionStore.set({ x, y });
|
||||
};
|
||||
|
||||
export let className = 'h-full';
|
||||
export let quantity = 30;
|
||||
export let staticity = 50;
|
||||
export let ease = 50;
|
||||
export let vx = 0;
|
||||
export let vy = 0;
|
||||
|
||||
let color = '#ffffff';
|
||||
let rgb = hexToRgb(color);
|
||||
|
||||
/**
|
||||
* @type {HTMLCanvasElement}
|
||||
*/
|
||||
let canvasRef;
|
||||
/**
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
let canvasContainerRef;
|
||||
/**
|
||||
* @type {CanvasRenderingContext2D | null}
|
||||
*/
|
||||
let context;
|
||||
/**
|
||||
* @type {any[]}
|
||||
*/
|
||||
let circles = [];
|
||||
let mousePosition = mousePositionStore;
|
||||
let mouse = { x: 0, y: 0 };
|
||||
let canvasSize = { w: 0, h: 0 };
|
||||
let dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
|
||||
|
||||
/**
|
||||
* @param {string} hex
|
||||
*/
|
||||
function hexToRgb(hex) {
|
||||
hex = hex.replace('#', '');
|
||||
const hexInt = parseInt(hex, 16);
|
||||
const red = (hexInt >> 16) & 255;
|
||||
const green = (hexInt >> 8) & 255;
|
||||
const blue = hexInt & 255;
|
||||
return [red, green, blue];
|
||||
}
|
||||
|
||||
mode.subscribe((value) => {
|
||||
color = value === 'dark' ? '#ffffff' : '#000000';
|
||||
rgb = hexToRgb(color);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {{ x: any; y: any; translateX: any; translateY: any; size: any; alpha: any; targetAlpha?: number; dx?: number; dy?: number; magnetism?: number; }} circle
|
||||
*/
|
||||
function drawCircle(circle, update = false) {
|
||||
if (context) {
|
||||
const { x, y, translateX, translateY, size, alpha } = circle;
|
||||
context.translate(translateX, translateY);
|
||||
context.beginPath();
|
||||
context.arc(x, y, size, 0, 2 * Math.PI);
|
||||
context.fillStyle = `rgba(${rgb.join(', ')}, ${alpha})`;
|
||||
context.fill();
|
||||
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
if (!update) {
|
||||
circles.push(circle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
resizeCanvas();
|
||||
drawParticles();
|
||||
}
|
||||
|
||||
function onMouseMove() {
|
||||
if (canvasRef) {
|
||||
const rect = canvasRef.getBoundingClientRect();
|
||||
const { w, h } = canvasSize;
|
||||
const x = $mousePosition.x - rect.left - w / 2;
|
||||
const y = $mousePosition.y - rect.top - h / 2;
|
||||
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
|
||||
if (inside) {
|
||||
mouse.x = x;
|
||||
mouse.y = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resizeCanvas() {
|
||||
if (canvasContainerRef && canvasRef && context) {
|
||||
circles = [];
|
||||
canvasSize.w = canvasContainerRef.offsetWidth;
|
||||
canvasSize.h = canvasContainerRef.offsetHeight;
|
||||
canvasRef.width = canvasSize.w * dpr;
|
||||
canvasRef.height = canvasSize.h * dpr;
|
||||
canvasRef.style.width = `${canvasSize.w}px`;
|
||||
canvasRef.style.height = `${canvasSize.h}px`;
|
||||
context.scale(dpr, dpr);
|
||||
}
|
||||
}
|
||||
|
||||
function circleParams() {
|
||||
const x = Math.floor(Math.random() * canvasSize.w);
|
||||
const y = Math.floor(Math.random() * canvasSize.h);
|
||||
const translateX = 0;
|
||||
const translateY = 0;
|
||||
const size = Math.floor(Math.random() * 2) + 1;
|
||||
const alpha = 0;
|
||||
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
|
||||
const dx = (Math.random() - 0.5) * 0.2;
|
||||
const dy = (Math.random() - 0.5) * 0.2;
|
||||
const magnetism = 0.1 + Math.random() * 4;
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
translateX,
|
||||
translateY,
|
||||
size,
|
||||
alpha,
|
||||
targetAlpha,
|
||||
dx,
|
||||
dy,
|
||||
magnetism
|
||||
};
|
||||
}
|
||||
|
||||
function clearContext() {
|
||||
if (context) {
|
||||
context.clearRect(0, 0, canvasSize.w, canvasSize.h);
|
||||
}
|
||||
}
|
||||
|
||||
function drawParticles() {
|
||||
clearContext();
|
||||
const particleCount = quantity;
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const circle = circleParams();
|
||||
drawCircle(circle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @param {number} start1
|
||||
* @param {number} end1
|
||||
* @param {number} start2
|
||||
* @param {number} end2
|
||||
*/
|
||||
function remapValue(value, start1, end1, start2, end2) {
|
||||
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||
return remapped > 0 ? remapped : 0;
|
||||
}
|
||||
|
||||
function animate() {
|
||||
clearContext();
|
||||
circles.forEach((circle, i) => {
|
||||
const edge = [
|
||||
circle.x + circle.translateX - circle.size,
|
||||
canvasSize.w - circle.x - circle.translateX - circle.size,
|
||||
circle.y + circle.translateY - circle.size,
|
||||
canvasSize.h - circle.y - circle.translateY - circle.size
|
||||
];
|
||||
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
|
||||
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));
|
||||
if (remapClosestEdge > 1) {
|
||||
circle.alpha += 0.02;
|
||||
if (circle.alpha > circle.targetAlpha) {
|
||||
circle.alpha = circle.targetAlpha;
|
||||
}
|
||||
} else {
|
||||
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
||||
}
|
||||
circle.x += circle.dx + vx;
|
||||
circle.y += circle.dy + vy;
|
||||
circle.translateX += (mouse.x / (staticity / circle.magnetism) - circle.translateX) / ease;
|
||||
circle.translateY += (mouse.y / (staticity / circle.magnetism) - circle.translateY) / ease;
|
||||
if (
|
||||
circle.x < -circle.size ||
|
||||
circle.x > canvasSize.w + circle.size ||
|
||||
circle.y < -circle.size ||
|
||||
circle.y > canvasSize.h + circle.size
|
||||
) {
|
||||
circles.splice(i, 1);
|
||||
const newCircle = circleParams();
|
||||
drawCircle(newCircle);
|
||||
} else {
|
||||
drawCircle(
|
||||
{
|
||||
...circle,
|
||||
x: circle.x,
|
||||
y: circle.y,
|
||||
translateX: circle.translateX,
|
||||
translateY: circle.translateY,
|
||||
alpha: circle.alpha
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(animate);
|
||||
}, 1000 / 60); // Limit the frame rate to 60 FPS
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
if (canvasRef) {
|
||||
context = canvasRef?.getContext('2d');
|
||||
}
|
||||
initCanvas();
|
||||
animate();
|
||||
window.addEventListener('resize', initCanvas);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', initCanvas);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
});
|
||||
|
||||
beforeUpdate(() => {
|
||||
onMouseMove();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (canvasRef) {
|
||||
window.removeEventListener('resize', initCanvas);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class={className} bind:this={canvasContainerRef} aria-hidden="true">
|
||||
<canvas bind:this={canvasRef}></canvas>
|
||||
</div>
|
12
web/src/lib/components/site/site-footer.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { siteConfig } from '$lib/config/site';
|
||||
</script>
|
||||
|
||||
<footer class="container py-6">
|
||||
<div class="space-y-1">
|
||||
<p class="text-center text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} — {siteConfig.name} by {siteConfig.author}. Licensed
|
||||
under GPL-3.0.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
28
web/src/lib/components/site/site-navbar.svelte
Normal file
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { Icons, ModeToggle, MainNav, MobileNav } from '$lib/components/site';
|
||||
import { siteConfig } from '$lib/config/site';
|
||||
import UserNav from './nav/user-nav.svelte';
|
||||
|
||||
export let authenticated = false;
|
||||
export let user: object | null = null;
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="sticky top-0 z-50 w-full border-b bg-background/95 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
||||
>
|
||||
<div class="container flex h-14 items-center">
|
||||
<a href="/" class="mr-6 flex items-center space-x-2">
|
||||
<span class="sr-only">Logo (return home)</span>
|
||||
<div class="rounded-sm bg-gray-950 p-0.5 dark:bg-transparent">
|
||||
<Icons.logo />
|
||||
</div>
|
||||
<span class="text-xl font-bold tracking-tight">{siteConfig.name}</span>
|
||||
</a>
|
||||
<MainNav {authenticated} />
|
||||
<div class="flex flex-1 items-center justify-end space-x-2 sm:space-x-4">
|
||||
<ModeToggle />
|
||||
<UserNav {authenticated} {user} />
|
||||
<MobileNav {authenticated} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
14
web/src/lib/components/site/tailwind-indicator.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script>
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
</script>
|
||||
|
||||
<Button variant="ghost" size="icon" class="block sm:hidden">xs</Button>
|
||||
<Button variant="ghost" size="icon" class="hidden sm:block md:hidden lg:hidden xl:hidden 2xl:hidden"
|
||||
>sm</Button
|
||||
>
|
||||
<Button variant="ghost" size="icon" class="hidden md:block lg:hidden xl:hidden 2xl:hidden"
|
||||
>md</Button
|
||||
>
|
||||
<Button variant="ghost" size="icon" class="hidden lg:block xl:hidden 2xl:hidden">lg</Button>
|
||||
<Button variant="ghost" size="icon" class="hidden xl:block 2xl:hidden">xl</Button>
|
||||
<Button variant="ghost" size="icon" class="hidden 2xl:block">2xl</Button>
|
13
web/src/lib/components/ui/alert/alert-description.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn('text-sm [&_p]:leading-relaxed', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
21
web/src/lib/components/ui/alert/alert-title.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { HeadingLevel } from '.';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
||||
level?: HeadingLevel;
|
||||
};
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let level: $$Props['level'] = 'h5';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={level}
|
||||
class={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</svelte:element>
|
17
web/src/lib/components/ui/alert/alert.svelte
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { alertVariants, type Variant } from '.';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement> & {
|
||||
variant?: Variant;
|
||||
};
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let variant: $$Props['variant'] = 'default';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
|
||||
<slot />
|
||||
</div>
|
32
web/src/lib/components/ui/alert/index.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
import Root from './alert.svelte';
|
||||
import Description from './alert-description.svelte';
|
||||
import Title from './alert-title.svelte';
|
||||
|
||||
export const alertVariants = tv({
|
||||
base: 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type Variant = VariantProps<typeof alertVariants>['variant'];
|
||||
export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Description,
|
||||
Title,
|
||||
//
|
||||
Root as Alert,
|
||||
Description as AlertDescription,
|
||||
Title as AlertTitle
|
||||
};
|
16
web/src/lib/components/ui/avatar/avatar-fallback.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = AvatarPrimitive.FallbackProps;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Fallback
|
||||
class={cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</AvatarPrimitive.Fallback>
|
18
web/src/lib/components/ui/avatar/avatar-image.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = AvatarPrimitive.ImageProps;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let src: $$Props['src'] = undefined;
|
||||
export let alt: $$Props['alt'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Image
|
||||
{src}
|
||||
{alt}
|
||||
class={cn('aspect-square h-full w-full', className)}
|
||||
{...$$restProps}
|
||||
/>
|
18
web/src/lib/components/ui/avatar/avatar.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = AvatarPrimitive.Props;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let delayMs: $$Props['delayMs'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Root
|
||||
{delayMs}
|
||||
class={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</AvatarPrimitive.Root>
|
13
web/src/lib/components/ui/avatar/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import Root from './avatar.svelte';
|
||||
import Image from './avatar-image.svelte';
|
||||
import Fallback from './avatar-fallback.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Image,
|
||||
Fallback,
|
||||
//
|
||||
Root as Avatar,
|
||||
Image as AvatarImage,
|
||||
Fallback as AvatarFallback
|
||||
};
|
18
web/src/lib/components/ui/badge/badge.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { badgeVariants, type Variant } from '.';
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export let href: string | undefined = undefined;
|
||||
export let variant: Variant = 'default';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? 'a' : 'span'}
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant, className }))}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</svelte:element>
|
20
web/src/lib/components/ui/badge/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export { default as Badge } from './badge.svelte';
|
||||
export const badgeVariants = tv({
|
||||
base: 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 select-none',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
|
||||
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
|
||||
outline: 'text-foreground'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type Variant = VariantProps<typeof badgeVariants>['variant'];
|
25
web/src/lib/components/ui/button/button.svelte
Normal file
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { Button as ButtonPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
import { buttonVariants, type Props, type Events } from '.';
|
||||
|
||||
type $$Props = Props;
|
||||
type $$Events = Events;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let variant: $$Props['variant'] = 'default';
|
||||
export let size: $$Props['size'] = 'default';
|
||||
export let builders: $$Props['builders'] = [];
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<ButtonPrimitive.Root
|
||||
{builders}
|
||||
class={cn(buttonVariants({ variant, size, className }))}
|
||||
type="button"
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
>
|
||||
<slot />
|
||||
</ButtonPrimitive.Root>
|
49
web/src/lib/components/ui/button/index.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import type { Button as ButtonPrimitive } from 'bits-ui';
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
import Root from './button.svelte';
|
||||
|
||||
const buttonVariants = tv({
|
||||
base: 'inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
type Variant = VariantProps<typeof buttonVariants>['variant'];
|
||||
type Size = VariantProps<typeof buttonVariants>['size'];
|
||||
|
||||
type Props = ButtonPrimitive.Props & {
|
||||
variant?: Variant;
|
||||
size?: Size;
|
||||
};
|
||||
|
||||
type Events = ButtonPrimitive.Events;
|
||||
|
||||
export {
|
||||
Root,
|
||||
type Props,
|
||||
type Events,
|
||||
//
|
||||
Root as Button,
|
||||
type Props as ButtonProps,
|
||||
type Events as ButtonEvents,
|
||||
buttonVariants
|
||||
};
|
13
web/src/lib/components/ui/card/card-content.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn('p-6 pt-0', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
13
web/src/lib/components/ui/card/card-description.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLParagraphElement>;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<p class={cn('text-sm text-muted-foreground', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</p>
|
13
web/src/lib/components/ui/card/card-footer.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn('flex items-center p-6 pt-0', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
13
web/src/lib/components/ui/card/card-header.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn('flex flex-col space-y-1.5 p-6', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
21
web/src/lib/components/ui/card/card-title.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils';
|
||||
import type { HeadingLevel } from '.';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
||||
tag?: HeadingLevel;
|
||||
};
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let tag: $$Props['tag'] = 'h3';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={tag}
|
||||
class={cn('font-semibold leading-none tracking-tight', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</svelte:element>
|
22
web/src/lib/components/ui/card/card.svelte
Normal file
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={cn('rounded-xl border bg-card text-card-foreground shadow', className)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
>
|
||||
<slot />
|
||||
</div>
|
24
web/src/lib/components/ui/card/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Root from './card.svelte';
|
||||
import Content from './card-content.svelte';
|
||||
import Description from './card-description.svelte';
|
||||
import Footer from './card-footer.svelte';
|
||||
import Header from './card-header.svelte';
|
||||
import Title from './card-title.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle
|
||||
};
|
||||
|
||||
export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
34
web/src/lib/components/ui/checkbox/checkbox.svelte
Normal file
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
|
||||
import { Check, Minus } from 'radix-icons-svelte';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = CheckboxPrimitive.Props;
|
||||
type $$Events = CheckboxPrimitive.Events;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let checked: $$Props['checked'] = false;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<CheckboxPrimitive.Root
|
||||
class={cn(
|
||||
'peer box-content h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[disabled=true]:opacity-50',
|
||||
className
|
||||
)}
|
||||
bind:checked
|
||||
on:click
|
||||
{...$$restProps}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
class={cn('flex h-4 w-4 items-center justify-center text-current')}
|
||||
let:isChecked
|
||||
let:isIndeterminate
|
||||
>
|
||||
{#if isIndeterminate}
|
||||
<Minus class="h-3.5 w-3.5" />
|
||||
{:else}
|
||||
<Check class={cn('h-3.5 w-3.5', !isChecked && 'text-transparent')} />
|
||||
{/if}
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
6
web/src/lib/components/ui/checkbox/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Root from './checkbox.svelte';
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Checkbox
|
||||
};
|
23
web/src/lib/components/ui/command/command-dialog.svelte
Normal file
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import Command from './command.svelte';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import type { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import type { Command as CommandPrimitive } from 'cmdk-sv';
|
||||
|
||||
type $$Props = DialogPrimitive.Props & CommandPrimitive.CommandProps;
|
||||
|
||||
export let open: $$Props['open'] = false;
|
||||
export let value: $$Props['value'] = undefined;
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open {...$$restProps}>
|
||||
<Dialog.Content class="overflow-hidden p-0">
|
||||
<Command
|
||||
class="[&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:font-medium [&_[data-cmdk-group-heading]]:text-muted-foreground [&_[data-cmdk-group]:not([hidden])_~[data-cmdk-group]]:pt-0 [&_[data-cmdk-group]]:px-2 [&_[data-cmdk-input-wrapper]_svg]:h-5 [&_[data-cmdk-input-wrapper]_svg]:w-5 [&_[data-cmdk-input]]:h-12 [&_[data-cmdk-item]]:px-2 [&_[data-cmdk-item]]:py-3 [&_[data-cmdk-item]_svg]:h-5 [&_[data-cmdk-item]_svg]:w-5"
|
||||
{...$$restProps}
|
||||
bind:value
|
||||
>
|
||||
<slot />
|
||||
</Command>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
12
web/src/lib/components/ui/command/command-empty.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'cmdk-sv';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = CommandPrimitive.EmptyProps;
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Empty class={cn('py-6 text-center text-sm', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</CommandPrimitive.Empty>
|
18
web/src/lib/components/ui/command/command-group.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'cmdk-sv';
|
||||
import { cn } from '$lib/utils';
|
||||
type $$Props = CommandPrimitive.GroupProps;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Group
|
||||
class={cn(
|
||||
'overflow-hidden p-1 text-foreground [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:py-1.5 [&_[data-cmdk-group-heading]]:text-xs [&_[data-cmdk-group-heading]]:font-medium [&_[data-cmdk-group-heading]]:text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</CommandPrimitive.Group>
|
23
web/src/lib/components/ui/command/command-input.svelte
Normal file
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'cmdk-sv';
|
||||
import { MagnifyingGlass } from 'radix-icons-svelte';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = CommandPrimitive.InputProps;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
export let value: string = '';
|
||||
</script>
|
||||
|
||||
<div class="flex items-center border-b px-3" data-cmdk-input-wrapper="">
|
||||
<MagnifyingGlass class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
class={cn(
|
||||
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
bind:value
|
||||
/>
|
||||
</div>
|
19
web/src/lib/components/ui/command/command-item.svelte
Normal file
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'cmdk-sv';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = CommandPrimitive.ItemProps;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Item
|
||||
class={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</CommandPrimitive.Item>
|
15
web/src/lib/components/ui/command/command-list.svelte
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'cmdk-sv';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = CommandPrimitive.ListProps;
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.List
|
||||
class={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</CommandPrimitive.List>
|
10
web/src/lib/components/ui/command/command-separator.svelte
Normal file
|
@ -0,0 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'cmdk-sv';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = CommandPrimitive.SeparatorProps;
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Separator class={cn('-mx-1 h-px bg-border', className)} {...$$restProps} />
|
16
web/src/lib/components/ui/command/command-shortcut.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
22
web/src/lib/components/ui/command/command.svelte
Normal file
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'cmdk-sv';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = CommandPrimitive.CommandProps;
|
||||
|
||||
export let value: $$Props['value'] = undefined;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Root
|
||||
class={cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</CommandPrimitive.Root>
|
37
web/src/lib/components/ui/command/index.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Command as CommandPrimitive } from 'cmdk-sv';
|
||||
|
||||
import Root from './command.svelte';
|
||||
import Dialog from './command-dialog.svelte';
|
||||
import Empty from './command-empty.svelte';
|
||||
import Group from './command-group.svelte';
|
||||
import Item from './command-item.svelte';
|
||||
import Input from './command-input.svelte';
|
||||
import List from './command-list.svelte';
|
||||
import Separator from './command-separator.svelte';
|
||||
import Shortcut from './command-shortcut.svelte';
|
||||
|
||||
const Loading = CommandPrimitive.Loading;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Dialog,
|
||||
Empty,
|
||||
Group,
|
||||
Item,
|
||||
Input,
|
||||
List,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Loading,
|
||||
//
|
||||
Root as Command,
|
||||
Dialog as CommandDialog,
|
||||
Empty as CommandEmpty,
|
||||
Group as CommandGroup,
|
||||
Item as CommandItem,
|
||||
Input as CommandInput,
|
||||
List as CommandList,
|
||||
Separator as CommandSeparator,
|
||||
Shortcut as CommandShortcut,
|
||||
Loading as CommandLoading
|
||||
};
|
36
web/src/lib/components/ui/dialog/dialog-content.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import * as Dialog from '.';
|
||||
import { cn, flyAndScale } from '$lib/utils';
|
||||
import { Cross2 } from 'radix-icons-svelte';
|
||||
|
||||
type $$Props = DialogPrimitive.ContentProps;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let transition: $$Props['transition'] = flyAndScale;
|
||||
export let transitionConfig: $$Props['transitionConfig'] = {
|
||||
duration: 200
|
||||
};
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
<DialogPrimitive.Close
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||
>
|
||||
<Cross2 class="h-4 w-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
16
web/src/lib/components/ui/dialog/dialog-description.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = DialogPrimitive.DescriptionProps;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
class={cn('text-sm text-muted-foreground', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DialogPrimitive.Description>
|
16
web/src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
13
web/src/lib/components/ui/dialog/dialog-header.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
21
web/src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
type $$Props = DialogPrimitive.OverlayProps;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let transition: $$Props['transition'] = fade;
|
||||
export let transitionConfig: $$Props['transitionConfig'] = {
|
||||
duration: 150
|
||||
};
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn('fixed inset-0 z-50 bg-background/80 backdrop-blur-sm ', className)}
|
||||
{...$$restProps}
|
||||
/>
|
9
web/src/lib/components/ui/dialog/dialog-portal.svelte
Normal file
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
|
||||
type $$Props = DialogPrimitive.PortalProps;
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Portal {...$$restProps}>
|
||||
<slot />
|
||||
</DialogPrimitive.Portal>
|
16
web/src/lib/components/ui/dialog/dialog-title.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = DialogPrimitive.TitleProps;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
class={cn('text-lg font-semibold leading-none tracking-tight', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DialogPrimitive.Title>
|
34
web/src/lib/components/ui/dialog/index.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
|
||||
const Root = DialogPrimitive.Root;
|
||||
const Trigger = DialogPrimitive.Trigger;
|
||||
|
||||
import Title from './dialog-title.svelte';
|
||||
import Portal from './dialog-portal.svelte';
|
||||
import Footer from './dialog-footer.svelte';
|
||||
import Header from './dialog-header.svelte';
|
||||
import Overlay from './dialog-overlay.svelte';
|
||||
import Content from './dialog-content.svelte';
|
||||
import Description from './dialog-description.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
import { Check } from 'radix-icons-svelte';
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let checked: $$Props['checked'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:checked
|
||||
class={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.CheckboxIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.CheckboxIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn, flyAndScale } from '$lib/utils';
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.ContentProps;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let sideOffset: $$Props['sideOffset'] = 4;
|
||||
export let transition: $$Props['transition'] = flyAndScale;
|
||||
export let transitionConfig: $$Props['transitionConfig'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Content
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
'z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:keydown
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.Content>
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.ItemProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
type $$Events = DropdownMenuPrimitive.ItemEvents;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let inset: $$Props['inset'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Item
|
||||
class={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.Item>
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.LabelProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let inset: $$Props['inset'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Label
|
||||
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.Label>
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
|
||||
|
||||
export let value: $$Props['value'] = undefined;
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.RadioGroup>
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
import { DotFilled } from 'radix-icons-svelte';
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.RadioItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let value: DropdownMenuPrimitive.RadioItemProps['value'];
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
class={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{value}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.RadioIndicator>
|
||||
<DotFilled class="h-4 w-4 fill-current" />
|
||||
</DropdownMenuPrimitive.RadioIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.RadioItem>
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.SeparatorProps;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Separator
|
||||
class={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...$$restProps}
|
||||
/>
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<span class={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</span>
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn, flyAndScale } from '$lib/utils';
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.SubContentProps;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let transition: $$Props['transition'] = flyAndScale;
|
||||
export let transitionConfig: $$Props['transitionConfig'] = {
|
||||
x: -10,
|
||||
y: 0
|
||||
};
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn(
|
||||
'z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:keydown
|
||||
on:focusout
|
||||
on:pointermove
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.SubContent>
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
import { ChevronRight } from 'radix-icons-svelte';
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let inset: $$Props['inset'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
class={cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
48
web/src/lib/components/ui/dropdown-menu/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import Item from './dropdown-menu-item.svelte';
|
||||
import Label from './dropdown-menu-label.svelte';
|
||||
import Content from './dropdown-menu-content.svelte';
|
||||
import Shortcut from './dropdown-menu-shortcut.svelte';
|
||||
import RadioItem from './dropdown-menu-radio-item.svelte';
|
||||
import Separator from './dropdown-menu-separator.svelte';
|
||||
import RadioGroup from './dropdown-menu-radio-group.svelte';
|
||||
import SubContent from './dropdown-menu-sub-content.svelte';
|
||||
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
|
||||
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
|
||||
|
||||
const Sub = DropdownMenuPrimitive.Sub;
|
||||
const Root = DropdownMenuPrimitive.Root;
|
||||
const Trigger = DropdownMenuPrimitive.Trigger;
|
||||
const Group = DropdownMenuPrimitive.Group;
|
||||
|
||||
export {
|
||||
Sub,
|
||||
Root,
|
||||
Item,
|
||||
Label,
|
||||
Group,
|
||||
Trigger,
|
||||
Content,
|
||||
Shortcut,
|
||||
Separator,
|
||||
RadioItem,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
RadioGroup,
|
||||
CheckboxItem,
|
||||
//
|
||||
Root as DropdownMenu,
|
||||
Sub as DropdownMenuSub,
|
||||
Item as DropdownMenuItem,
|
||||
Label as DropdownMenuLabel,
|
||||
Group as DropdownMenuGroup,
|
||||
Content as DropdownMenuContent,
|
||||
Trigger as DropdownMenuTrigger,
|
||||
Shortcut as DropdownMenuShortcut,
|
||||
RadioItem as DropdownMenuRadioItem,
|
||||
Separator as DropdownMenuSeparator,
|
||||
RadioGroup as DropdownMenuRadioGroup,
|
||||
SubContent as DropdownMenuSubContent,
|
||||
SubTrigger as DropdownMenuSubTrigger,
|
||||
CheckboxItem as DropdownMenuCheckboxItem
|
||||
};
|
9
web/src/lib/components/ui/form/form-button.svelte
Normal file
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts">
|
||||
import * as Button from '$lib/components/ui/button';
|
||||
type $$Props = Button.Props;
|
||||
type $$Events = Button.Events;
|
||||
</script>
|
||||
|
||||
<Button.Root type="submit" {...$$restProps} on:click on:keydown>
|
||||
<slot />
|
||||
</Button.Root>
|
26
web/src/lib/components/ui/form/form-checkbox.svelte
Normal file
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts">
|
||||
import { getFormField } from 'formsnap';
|
||||
import type { Checkbox as CheckboxPrimitive } from 'bits-ui';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
type $$Props = CheckboxPrimitive.Props;
|
||||
type $$Events = CheckboxPrimitive.Events;
|
||||
|
||||
export let onCheckedChange: $$Props['onCheckedChange'] = undefined;
|
||||
|
||||
const { name, setValue, attrStore, value } = getFormField();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { name: nameAttr, value: valueAttr, ...rest } = $attrStore;
|
||||
</script>
|
||||
|
||||
<Checkbox
|
||||
{...rest}
|
||||
checked={typeof $value === 'boolean' ? $value : false}
|
||||
onCheckedChange={(v) => {
|
||||
onCheckedChange?.(v);
|
||||
setValue(v);
|
||||
}}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
/>
|
||||
<input hidden {name} value={$value} />
|
16
web/src/lib/components/ui/form/form-description.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Form as FormPrimitive } from 'formsnap';
|
||||
import { cn } from '$lib/utils';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<FormPrimitive.Description
|
||||
class={cn('text-[0.8rem] text-muted-foreground', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</FormPrimitive.Description>
|
28
web/src/lib/components/ui/form/form-input.svelte
Normal file
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { getFormField } from 'formsnap';
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
import { Input, type InputEvents } from '$lib/components/ui/input';
|
||||
|
||||
type $$Props = HTMLInputAttributes;
|
||||
type $$Events = InputEvents;
|
||||
|
||||
const { attrStore, value } = getFormField();
|
||||
</script>
|
||||
|
||||
<Input
|
||||
{...$attrStore}
|
||||
bind:value={$value}
|
||||
{...$$restProps}
|
||||
on:blur
|
||||
on:change
|
||||
on:click
|
||||
on:focus
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:keyup
|
||||
on:mouseover
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:paste
|
||||
on:input
|
||||
/>
|
12
web/src/lib/components/ui/form/form-item.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn('space-y-2', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
17
web/src/lib/components/ui/form/form-label.svelte
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import type { Label as LabelPrimitive } from 'bits-ui';
|
||||
import { getFormField } from 'formsnap';
|
||||
import { cn } from '$lib/utils';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
|
||||
type $$Props = LabelPrimitive.Props;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
|
||||
const { errors, ids } = getFormField();
|
||||
</script>
|
||||
|
||||
<Label for={$ids.input} class={cn($errors && 'text-destructive', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</Label>
|
26
web/src/lib/components/ui/form/form-native-select.svelte
Normal file
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts">
|
||||
import { Form as FormPrimitive } from 'formsnap';
|
||||
import { buttonVariants } from '$lib/components/ui/button';
|
||||
import { cn } from '$lib/utils';
|
||||
import { CaretSort } from 'radix-icons-svelte';
|
||||
import type { HTMLSelectAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLSelectAttributes;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<FormPrimitive.Select
|
||||
class={cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'appearance-none bg-transparent font-normal',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</FormPrimitive.Select>
|
||||
<CaretSort class="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
|
||||
</div>
|