& so it begins
This commit is contained in:
61
.dockerignore
Normal file
61
.dockerignore
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
.zed
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
__pycache__
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# ignore generated log files
|
||||||
|
**/logs/**.log
|
||||||
|
**/logs/**.log.gz
|
||||||
|
**/logs/**-audit.json
|
||||||
|
|
||||||
|
**/data/credentials/**
|
||||||
|
**/testdocs/**
|
||||||
|
|
||||||
|
ot_res.json
|
||||||
|
out.json
|
||||||
|
payload.json
|
||||||
|
|
||||||
|
screenshots/*.jpeg
|
||||||
|
screenshots/*.png
|
||||||
|
screenshots/*.jpg
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Turbo
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# Build Outputs
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
creds.md
|
||||||
|
|
||||||
|
onlydevs
|
||||||
49
.env.example
Normal file
49
.env.example
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
APP_NAME=${{project.APP_NAME}}
|
||||||
|
NODE_ENV=${{project.NODE_ENV}}
|
||||||
|
|
||||||
|
REDIS_URL=${{project.REDIS_URL}}
|
||||||
|
DATABASE_URL=${{project.DATABASE_URL}}
|
||||||
|
|
||||||
|
COUCHBASE_CONNECTION_STRING=${{project.COUCHBASE_CONNECTION_STRING}}
|
||||||
|
COUCHBASE_USERNAME=${{project.COUCHBASE_USERNAME}}
|
||||||
|
COUCHBASE_PASSWORD=${{project.COUCHBASE_PASSWORD}}
|
||||||
|
|
||||||
|
QDRANT_URL=${{project.QDRANT_URL}}
|
||||||
|
QDRANT_API_KEY=${{project.QDRANT_API_KEY}}
|
||||||
|
|
||||||
|
BETTER_AUTH_SECRET=${{project.BETTER_AUTH_SECRET}}
|
||||||
|
BETTER_AUTH_URL=${{project.BETTER_AUTH_URL}}
|
||||||
|
|
||||||
|
GOOGLE_CLIENT_ID=${{project.GOOGLE_CLIENT_ID}}
|
||||||
|
GOOGLE_CLIENT_SECRET=${{project.GOOGLE_CLIENT_SECRET}}
|
||||||
|
GOOGLE_OAUTH_SERVER_URL=${{project.GOOGLE_OAUTH_SERVER_URL}}
|
||||||
|
GOOGLE_OAUTH_SERVER_PORT=${{project.GOOGLE_OAUTH_SERVER_PORT}}
|
||||||
|
|
||||||
|
GEMINI_API_KEY=${{project.GEMINI_API_KEY}}
|
||||||
|
|
||||||
|
RESEND_API_KEY=${{project.RESEND_API_KEY}}
|
||||||
|
FROM_EMAIL=${{project.FROM_EMAIL}}
|
||||||
|
|
||||||
|
INTERNAL_API_KEY="supersecretkey"
|
||||||
|
|
||||||
|
PROCESSOR_API_URL=${{project.PROCESSOR_API_URL}}
|
||||||
|
|
||||||
|
OPENROUTER_API_KEY=${{project.OPENROUTER_API_KEY}}
|
||||||
|
MODEL_NAME=${{project.MODEL_NAME}}
|
||||||
|
|
||||||
|
OCR_SERVICE_URL=${{project.OCR_SERVICE_URL}}
|
||||||
|
OCR_SERVICE_TIMEOUT=${{project.OCR_SERVICE_TIMEOUT}}
|
||||||
|
OCR_SERVICE_MAX_RETRIES=${{project.OCR_SERVICE_MAX_RETRIES}}
|
||||||
|
OCR_FORCE_OCR=${{project.OCR_FORCE_OCR}}
|
||||||
|
OCR_DEBUG=${{project.OCR_DEBUG}}
|
||||||
|
|
||||||
|
R2_BUCKET_NAME=${{project.R2_BUCKET_NAME}}
|
||||||
|
R2_REGION=${{project.R2_REGION}}
|
||||||
|
R2_ENDPOINT=${{project.R2_ENDPOINT}}
|
||||||
|
R2_ACCESS_KEY=${{project.R2_ACCESS_KEY}}
|
||||||
|
R2_SECRET_KEY=${{project.R2_SECRET_KEY}}
|
||||||
|
R2_PUBLIC_URL=${{project.R2_PUBLIC_URL}}
|
||||||
|
|
||||||
|
MAX_FILE_SIZE=${{project.MAX_FILE_SIZE}}
|
||||||
|
ALLOWED_MIME_TYPES=${{project.ALLOWED_MIME_TYPES}}
|
||||||
|
ALLOWED_EXTENSIONS=${{project.ALLOWED_EXTENSIONS}}
|
||||||
63
.gitignore
vendored
Normal file
63
.gitignore
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
.zed
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
__pycache__
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# ignore generated log files
|
||||||
|
**/logs/**.log
|
||||||
|
**/logs/**.log.gz
|
||||||
|
**/logs/**-audit.json
|
||||||
|
|
||||||
|
**/data/credentials/**
|
||||||
|
**/testdocs/**
|
||||||
|
|
||||||
|
scripts/whatsapp.req.sh
|
||||||
|
|
||||||
|
ot_res.json
|
||||||
|
out.json
|
||||||
|
payload.json
|
||||||
|
|
||||||
|
screenshots/*.jpeg
|
||||||
|
screenshots/*.png
|
||||||
|
screenshots/*.jpg
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Turbo
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# Build Outputs
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
creds.md
|
||||||
|
|
||||||
|
onlydevs
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# bare web application monorepo
|
||||||
|
|
||||||
|
for practice and showcase purpose
|
||||||
|
|
||||||
|
---
|
||||||
23
apps/main/.gitignore
vendored
Normal file
23
apps/main/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
1
apps/main/.npmrc
Normal file
1
apps/main/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
9
apps/main/.prettierignore
Normal file
9
apps/main/.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
bun.lock
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/static/
|
||||||
38
apps/main/README.md
Normal file
38
apps/main/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# sv
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# create a new project in the current directory
|
||||||
|
npx sv create
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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://svelte.dev/docs/kit/adapters) for your target environment.
|
||||||
16
apps/main/components.json
Normal file
16
apps/main/components.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/routes/layout.css",
|
||||||
|
"baseColor": "neutral"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks",
|
||||||
|
"lib": "$lib"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
|
}
|
||||||
71
apps/main/package.json
Normal file
71
apps/main/package.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "@apps/main",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev --port 5173",
|
||||||
|
"build": "NODE_ENV=build vite build",
|
||||||
|
"prod": "HOST=0.0.0.0 PORT=3000 bun ./build/index.js",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "prettier --check .",
|
||||||
|
"test:unit": "vitest",
|
||||||
|
"test": "npm run test:unit -- --run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@pkg/db": "workspace:*",
|
||||||
|
"@pkg/logger": "workspace:*",
|
||||||
|
"@pkg/logic": "workspace:*",
|
||||||
|
"@pkg/result": "workspace:*",
|
||||||
|
"@tanstack/svelte-query": "^6.0.10",
|
||||||
|
"better-auth": "^1.4.7",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"hono": "^4.11.1",
|
||||||
|
"marked": "^17.0.1",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
|
"neverthrow": "^8.2.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"valibot": "^1.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify/json": "^2.2.434",
|
||||||
|
"@internationalized/date": "^3.10.0",
|
||||||
|
"@lucide/svelte": "^0.561.0",
|
||||||
|
"@sveltejs/adapter-auto": "^7.0.0",
|
||||||
|
"@sveltejs/kit": "^2.49.1",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@tanstack/table-core": "^8.21.3",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"bits-ui": "^2.14.4",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"embla-carousel-svelte": "^8.6.0",
|
||||||
|
"formsnap": "^2.0.1",
|
||||||
|
"layerchart": "2.0.0-next.43",
|
||||||
|
"mode-watcher": "^1.1.0",
|
||||||
|
"paneforge": "^1.0.2",
|
||||||
|
"prettier": "^3.7.4",
|
||||||
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
|
"svelte": "^5.45.6",
|
||||||
|
"svelte-adapter-bun": "^1.0.1",
|
||||||
|
"svelte-check": "^4.3.4",
|
||||||
|
"svelte-sonner": "^1.0.7",
|
||||||
|
"sveltekit-superforms": "^2.28.1",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwind-variants": "^3.2.2",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"unplugin-icons": "^23.0.1",
|
||||||
|
"vaul-svelte": "^1.0.0-next.7",
|
||||||
|
"vite": "^7.2.6",
|
||||||
|
"vitest": "^4.0.15"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
apps/main/src/app.d.ts
vendored
Normal file
20
apps/main/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Session, User } from "@pkg/logic/domains/user/data";
|
||||||
|
import "unplugin-icons/types/svelte";
|
||||||
|
|
||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
interface Locals {
|
||||||
|
flowId?: string;
|
||||||
|
session?: Session;
|
||||||
|
user?: User;
|
||||||
|
}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
11
apps/main/src/app.html
Normal file
11
apps/main/src/app.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
apps/main/src/demo.spec.ts
Normal file
7
apps/main/src/demo.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('sum test', () => {
|
||||||
|
it('adds 1 + 2 to equal 3', () => {
|
||||||
|
expect(1 + 2).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
79
apps/main/src/hooks.server.ts
Normal file
79
apps/main/src/hooks.server.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { checkInitial2FaRequired } from "@pkg/logic/domains/2fa/sensitive-actions";
|
||||||
|
import type { Handle, HandleServerError } from "@sveltejs/kit";
|
||||||
|
import { auth } from "@pkg/logic/domains/auth/config.base";
|
||||||
|
import { svelteKitHandler } from "better-auth/svelte-kit";
|
||||||
|
import type { User } from "@pkg/logic/domains/user/data";
|
||||||
|
import { sequence } from "@sveltejs/kit/hooks";
|
||||||
|
import { building } from "$app/environment";
|
||||||
|
|
||||||
|
export const handleError: HandleServerError = async ({ error, event }) => {
|
||||||
|
console.log("[-] Running error middleware for : ", event.url.pathname);
|
||||||
|
console.log(error);
|
||||||
|
return { message: (error as Error).message ?? "Internal Server Error" };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const zero: Handle = async ({ event, resolve }) => {
|
||||||
|
return svelteKitHandler({ event, resolve, auth, building });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const first: Handle = async ({ event, resolve }) => {
|
||||||
|
if (
|
||||||
|
event.url.pathname.includes("/api/v1") ||
|
||||||
|
event.url.pathname.includes("/api/auth") ||
|
||||||
|
event.url.pathname.includes("/api/debug") ||
|
||||||
|
event.url.pathname.includes("/api/chat") ||
|
||||||
|
event.url.pathname.includes("/legal") ||
|
||||||
|
event.url.pathname.includes("/auth")
|
||||||
|
) {
|
||||||
|
return await resolve(event);
|
||||||
|
}
|
||||||
|
console.log("[+] Running middleware for : ", event.url.pathname);
|
||||||
|
const baseUrl = event.url.origin;
|
||||||
|
const signInUrl = baseUrl + "/auth/login";
|
||||||
|
const isSignInPage = event.url.pathname === "/auth/login";
|
||||||
|
const redirectResponse = new Response(null, {
|
||||||
|
status: 302,
|
||||||
|
headers: { Location: signInUrl },
|
||||||
|
});
|
||||||
|
|
||||||
|
const u = await auth.api.getSession({ headers: event.request.headers });
|
||||||
|
if (!u || !u.user || !u.session) {
|
||||||
|
return redirectResponse;
|
||||||
|
}
|
||||||
|
if (u.user && isSignInPage) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 302,
|
||||||
|
headers: { Location: baseUrl },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Setting user & session to locals");
|
||||||
|
|
||||||
|
const fid = crypto.randomUUID();
|
||||||
|
|
||||||
|
event.locals.flowId = fid;
|
||||||
|
event.locals.session = u.session;
|
||||||
|
event.locals.user = u.user as any as User;
|
||||||
|
|
||||||
|
const needs2FA = await checkInitial2FaRequired(
|
||||||
|
{
|
||||||
|
flowId: fid,
|
||||||
|
userId: u.user.id,
|
||||||
|
sessionId: u.session.id,
|
||||||
|
},
|
||||||
|
u.user as any,
|
||||||
|
u.session.id,
|
||||||
|
);
|
||||||
|
if (needs2FA && !event.url.pathname.includes("/auth/2fa")) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 302,
|
||||||
|
headers: {
|
||||||
|
Location: `/auth/2fa?redirect=${encodeURIComponent(event.url.pathname)}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handle = sequence(zero, first);
|
||||||
15
apps/main/src/lib/api.ts
Normal file
15
apps/main/src/lib/api.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { notificationsRouter } from "@pkg/logic/domains/notifications/router";
|
||||||
|
import { usersRouter } from "@pkg/logic/domains/user/router";
|
||||||
|
import { twofaRouter } from "@pkg/logic/domains/2fa/router";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
|
||||||
|
const baseRouter = new Hono()
|
||||||
|
.route("/users", usersRouter)
|
||||||
|
.route("/twofactor", twofaRouter)
|
||||||
|
.route("/notifications", notificationsRouter);
|
||||||
|
|
||||||
|
export const apiBasePath = "/api/v1";
|
||||||
|
|
||||||
|
export const api = new Hono().route(apiBasePath, baseRouter);
|
||||||
|
|
||||||
|
export type Router = typeof baseRouter;
|
||||||
21
apps/main/src/lib/auth.client.ts
Normal file
21
apps/main/src/lib/auth.client.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
adminClient,
|
||||||
|
customSessionClient,
|
||||||
|
inferAdditionalFields,
|
||||||
|
magicLinkClient,
|
||||||
|
multiSessionClient,
|
||||||
|
usernameClient,
|
||||||
|
} from "better-auth/client/plugins";
|
||||||
|
import type { auth } from "@pkg/logic/domains/auth/config.base";
|
||||||
|
import { createAuthClient } from "better-auth/svelte";
|
||||||
|
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
plugins: [
|
||||||
|
usernameClient(),
|
||||||
|
adminClient(),
|
||||||
|
magicLinkClient(),
|
||||||
|
multiSessionClient(),
|
||||||
|
customSessionClient<typeof auth>(),
|
||||||
|
inferAdditionalFields<typeof auth>(),
|
||||||
|
],
|
||||||
|
});
|
||||||
35
apps/main/src/lib/components/app-sidebar.svelte
Normal file
35
apps/main/src/lib/components/app-sidebar.svelte
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import BriefcaseIcon from "@lucide/svelte/icons/briefcase";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import NavMain from "$lib/components/nav-main.svelte";
|
||||||
|
import NavUser from "$lib/components/nav-user.svelte";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import { mainNavTree } from "$lib/core/constants";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
const data = $state({
|
||||||
|
teams: [{ name: "Personal", logo: BriefcaseIcon, plan: "Standard" }],
|
||||||
|
navItems: mainNavTree,
|
||||||
|
});
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
collapsible = "icon",
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof Sidebar.Root> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Root bind:ref {collapsible} {...restProps}>
|
||||||
|
<!-- <Sidebar.Header>
|
||||||
|
<TeamSwitcher teams={data.teams} />
|
||||||
|
</Sidebar.Header> -->
|
||||||
|
<Sidebar.Content class="pt-2">
|
||||||
|
<NavMain items={data.navItems} />
|
||||||
|
</Sidebar.Content>
|
||||||
|
<Sidebar.Footer>
|
||||||
|
<NavUser />
|
||||||
|
</Sidebar.Footer>
|
||||||
|
<Sidebar.Rail />
|
||||||
|
</Sidebar.Root>
|
||||||
17
apps/main/src/lib/components/atoms/button-text.svelte
Normal file
17
apps/main/src/lib/components/atoms/button-text.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import LoaderCircleIcon from "~icons/lucide/loader-circle";
|
||||||
|
import Icon from "./icon.svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
loading,
|
||||||
|
loadingText,
|
||||||
|
text,
|
||||||
|
}: { loading: boolean; loadingText: string; text: string } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<Icon icon={LoaderCircleIcon} cls="h-5 w-auto animate-spin" />
|
||||||
|
{loadingText}
|
||||||
|
{:else}
|
||||||
|
{text}
|
||||||
|
{/if}
|
||||||
11
apps/main/src/lib/components/atoms/icon.svelte
Normal file
11
apps/main/src/lib/components/atoms/icon.svelte
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
icon: Icon,
|
||||||
|
cls,
|
||||||
|
}: {
|
||||||
|
icon: any;
|
||||||
|
cls?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Icon class={cls} />
|
||||||
61
apps/main/src/lib/components/atoms/title.svelte
Normal file
61
apps/main/src/lib/components/atoms/title.svelte
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils";
|
||||||
|
|
||||||
|
type TitleSize = "h1" | "h2" | "h3" | "h4" | "h5" | "p";
|
||||||
|
type TitleFontWeight = "bold" | "semibold" | "normal" | "medium";
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
theme: "text-primary",
|
||||||
|
white: "text-white",
|
||||||
|
black: "text-black",
|
||||||
|
primaryLight: "text-primary/80",
|
||||||
|
};
|
||||||
|
|
||||||
|
const weights = {
|
||||||
|
bold: "font-bold",
|
||||||
|
semibold: "font-semibold",
|
||||||
|
medium: "font-medium",
|
||||||
|
normal: "font-normal",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
h1: "text-6xl md:text-7xl",
|
||||||
|
h2: "text-5xl lg:text-6xl",
|
||||||
|
h3: "text-4xl lg:text-5xl",
|
||||||
|
h4: "text-2xl lg:text-3xl",
|
||||||
|
h5: "text-xl lg:text-2xl",
|
||||||
|
p: "text-lg lg:text-xl",
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
size = "h2",
|
||||||
|
weight = "semibold",
|
||||||
|
capitalize = true,
|
||||||
|
color = "theme",
|
||||||
|
center = false,
|
||||||
|
id = undefined,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
size?: TitleSize;
|
||||||
|
weight?: TitleFontWeight;
|
||||||
|
capitalize?: boolean;
|
||||||
|
color?: keyof typeof colors;
|
||||||
|
center?: boolean;
|
||||||
|
id?: string;
|
||||||
|
children?: any;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={size}
|
||||||
|
class={cn(
|
||||||
|
sizes[size] || sizes.p,
|
||||||
|
weights[weight ?? "bold"],
|
||||||
|
capitalize && "capitalize",
|
||||||
|
colors[color],
|
||||||
|
center && "text-center",
|
||||||
|
)}
|
||||||
|
{id}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</svelte:element>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { authVM } from "$lib/domains/security/auth.vm.svelte";
|
||||||
|
import GoogleIcon from "~icons/devicon/google";
|
||||||
|
import Icon from "../atoms/icon.svelte";
|
||||||
|
import Button from "../ui/button/button.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
class="w-full"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => authVM.handleGoogleOAuthLogin()}
|
||||||
|
disabled={authVM.loggingIn}
|
||||||
|
>
|
||||||
|
<Icon icon={GoogleIcon} cls="h-6 w-6" />
|
||||||
|
<p class="">Sign in with Google</p>
|
||||||
|
</Button>
|
||||||
183
apps/main/src/lib/components/molecules/markdown-renderer.svelte
Normal file
183
apps/main/src/lib/components/molecules/markdown-renderer.svelte
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import hljs from "highlight.js/lib/core";
|
||||||
|
import { marked } from "marked";
|
||||||
|
// Import common languages for syntax highlighting
|
||||||
|
import bash from "highlight.js/lib/languages/bash";
|
||||||
|
import css from "highlight.js/lib/languages/css";
|
||||||
|
import javascript from "highlight.js/lib/languages/javascript";
|
||||||
|
import json from "highlight.js/lib/languages/json";
|
||||||
|
import python from "highlight.js/lib/languages/python";
|
||||||
|
import sql from "highlight.js/lib/languages/sql";
|
||||||
|
import typescript from "highlight.js/lib/languages/typescript";
|
||||||
|
import xml from "highlight.js/lib/languages/xml";
|
||||||
|
|
||||||
|
// Register languages
|
||||||
|
hljs.registerLanguage("javascript", javascript);
|
||||||
|
hljs.registerLanguage("typescript", typescript);
|
||||||
|
hljs.registerLanguage("python", python);
|
||||||
|
hljs.registerLanguage("sql", sql);
|
||||||
|
hljs.registerLanguage("json", json);
|
||||||
|
hljs.registerLanguage("xml", xml);
|
||||||
|
hljs.registerLanguage("css", css);
|
||||||
|
hljs.registerLanguage("bash", bash);
|
||||||
|
hljs.registerLanguage("html", xml); // HTML is an alias for XML
|
||||||
|
hljs.registerLanguage("js", javascript); // JS is an alias for JavaScript
|
||||||
|
hljs.registerLanguage("ts", typescript); // TS is an alias for TypeScript
|
||||||
|
hljs.registerLanguage("sh", bash); // SH is an alias for Bash
|
||||||
|
hljs.registerLanguage("shell", bash); // alias
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { content, class: className = "" }: Props = $props();
|
||||||
|
|
||||||
|
// Configure marked with custom extensions
|
||||||
|
marked.use({
|
||||||
|
renderer: {
|
||||||
|
codespan(token: any) {
|
||||||
|
return `<code class="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-sm font-mono">${token.text}</code>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
blockquote(token: any) {
|
||||||
|
return `<blockquote class="border-l-4 border-gray-300 dark:border-gray-600 pl-4 italic text-gray-700 dark:text-gray-300 my-4">${this.parser.parseInline(token.tokens)}</blockquote>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
table(token: any) {
|
||||||
|
let header = "";
|
||||||
|
let body = "";
|
||||||
|
|
||||||
|
// Process header
|
||||||
|
if (token.header && token.header.length > 0) {
|
||||||
|
header = "<thead><tr>";
|
||||||
|
for (const cell of token.header) {
|
||||||
|
header += `<th class="px-4 py-2 bg-secondary dark:bg-secondary font-semibold text-left">${this.parser.parseInline(cell.tokens)}</th>`;
|
||||||
|
}
|
||||||
|
header += "</tr></thead>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process rows
|
||||||
|
if (token.rows && token.rows.length > 0) {
|
||||||
|
body = "<tbody>";
|
||||||
|
for (const row of token.rows) {
|
||||||
|
body += "<tr class='border-b border-border";
|
||||||
|
for (const cell of row) {
|
||||||
|
body += `<td class="px-4 py-2">${this.parser.parseInline(cell.tokens)}</td>`;
|
||||||
|
}
|
||||||
|
body += "</tr>";
|
||||||
|
}
|
||||||
|
body += "</tbody>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<div class="overflow-x-auto my-4"><table class="min-w-full border border-border">${header}${body}</table></div>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gfm: true,
|
||||||
|
breaks: true,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Include highlight.js CSS for syntax highlighting -->
|
||||||
|
<svelte:head>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"
|
||||||
|
media="(prefers-color-scheme: light)"
|
||||||
|
/>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="prose prose-sm dark:prose-invert max-w-none {className}"
|
||||||
|
class:prose-gray={true}
|
||||||
|
>
|
||||||
|
{@html marked(content)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@reference "../../../app.css";
|
||||||
|
|
||||||
|
:global(.prose h1) {
|
||||||
|
@apply mt-6 mb-4 text-2xl font-bold text-gray-900 dark:text-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose h2) {
|
||||||
|
@apply mt-5 mb-3 text-xl font-semibold text-gray-900 dark:text-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose h3) {
|
||||||
|
@apply mt-4 mb-2 text-lg font-medium text-gray-900 dark:text-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose p) {
|
||||||
|
@apply mb-3 leading-relaxed text-gray-700 dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose a) {
|
||||||
|
@apply text-blue-600 underline hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose strong) {
|
||||||
|
@apply font-semibold text-gray-900 dark:text-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose em) {
|
||||||
|
@apply text-gray-700 italic dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose ul) {
|
||||||
|
@apply my-4 ml-4 list-inside list-disc space-y-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose ol) {
|
||||||
|
@apply my-4 ml-4 list-inside list-decimal space-y-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose li) {
|
||||||
|
@apply leading-relaxed text-gray-700 dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose ul ul) {
|
||||||
|
@apply mt-1 mb-1 ml-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose ol ol) {
|
||||||
|
@apply mt-1 mb-1 ml-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose ul ol) {
|
||||||
|
@apply mt-1 mb-1 ml-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose ol ul) {
|
||||||
|
@apply mt-1 mb-1 ml-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code block scrollbar styling */
|
||||||
|
:global(.prose pre) {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose pre::-webkit-scrollbar) {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose pre::-webkit-scrollbar-track) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose pre::-webkit-scrollbar-thumb) {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.prose pre::-webkit-scrollbar-thumb:hover) {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils";
|
||||||
|
|
||||||
|
let { cls, children }: { cls: string; children: any } = $props();
|
||||||
|
|
||||||
|
let clsss = cn("mx-auto h-full w-full max-w-screen-xl", cls);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={clsss}>
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
81
apps/main/src/lib/components/nav-main.svelte
Normal file
81
apps/main/src/lib/components/nav-main.svelte
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import type { AppSidebarItem } from "$lib/core/constants";
|
||||||
|
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||||
|
import Icon from "./atoms/icon.svelte";
|
||||||
|
|
||||||
|
let { items }: { items: AppSidebarItem[] } = $props();
|
||||||
|
|
||||||
|
const sidebar = Sidebar.useSidebar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Group>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
{#each items as mainItem (mainItem.title)}
|
||||||
|
<Collapsible.Root
|
||||||
|
open={mainItem.isActive}
|
||||||
|
class="group/collapsible"
|
||||||
|
>
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<!-- Split the behavior: main button redirects, chevron expands -->
|
||||||
|
<div class="flex w-full items-center">
|
||||||
|
<!-- Main navigation button that redirects -->
|
||||||
|
<Sidebar.MenuButton class="flex-1 pr-1">
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href={mainItem.url} {...props}>
|
||||||
|
{#if mainItem.icon}
|
||||||
|
<mainItem.icon />
|
||||||
|
{/if}
|
||||||
|
<span>{mainItem.title}</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
|
||||||
|
<!-- Chevron button that only toggles the collapsible content -->
|
||||||
|
{#if mainItem.items && sidebar.open}
|
||||||
|
<Collapsible.Trigger class="p-2">
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
class="bg-background hover:bg-accent flex cursor-pointer items-center justify-center rounded-sm p-1"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={ChevronRight}
|
||||||
|
cls="w-4 h-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||||
|
/>
|
||||||
|
<span class="sr-only"
|
||||||
|
>Toggle {mainItem.title} submenu</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsible submenu content -->
|
||||||
|
<Collapsible.Content>
|
||||||
|
{#if mainItem.items}
|
||||||
|
<Sidebar.MenuSub>
|
||||||
|
{#each mainItem.items as subItem (subItem.title)}
|
||||||
|
<Sidebar.MenuSubItem>
|
||||||
|
<Sidebar.MenuSubButton>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a
|
||||||
|
href={subItem.url}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span>{subItem.title}</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuSubButton>
|
||||||
|
</Sidebar.MenuSubItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.MenuSub>
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
</Collapsible.Root>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.Group>
|
||||||
150
apps/main/src/lib/components/nav-user.svelte
Normal file
150
apps/main/src/lib/components/nav-user.svelte
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Avatar from "$lib/components/ui/avatar/index.js";
|
||||||
|
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import { useSidebar } from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import { secondaryNavTree } from "$lib/core/constants";
|
||||||
|
import { sessionsVM } from "$lib/domains/account/sessions/sessions.vm.svelte";
|
||||||
|
import { user as userStore } from "$lib/global.stores";
|
||||||
|
import { type User } from "@pkg/logic/domains/user/data";
|
||||||
|
import { resetMode, setMode } from "mode-watcher";
|
||||||
|
import ChevronsUpDown from "~icons/lucide/chevrons-up-down";
|
||||||
|
import LogOut from "~icons/lucide/log-out";
|
||||||
|
import Monitor from "~icons/lucide/monitor";
|
||||||
|
import Moon from "~icons/lucide/moon";
|
||||||
|
import Sun from "~icons/lucide/sun";
|
||||||
|
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
|
||||||
|
let user = $state({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
image: "",
|
||||||
|
...($userStore || {}),
|
||||||
|
} as any as User);
|
||||||
|
|
||||||
|
userStore.subscribe((value) => {
|
||||||
|
if (value) {
|
||||||
|
user = value;
|
||||||
|
}
|
||||||
|
if (user.name.length < 2) {
|
||||||
|
// @ts-ignore
|
||||||
|
user.name = "User";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$inspect(user);
|
||||||
|
|
||||||
|
async function logoutUser() {
|
||||||
|
await sessionsVM.logout();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Menu>
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Sidebar.MenuButton
|
||||||
|
size="lg"
|
||||||
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Avatar.Root class="h-8 w-8 rounded-lg">
|
||||||
|
<Avatar.Image src={user.image} alt={user.name} />
|
||||||
|
<Avatar.Fallback class="rounded-lg">
|
||||||
|
{user.name.slice(0, 2).toUpperCase()}
|
||||||
|
</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
|
<div
|
||||||
|
class="grid flex-1 text-left text-sm leading-tight"
|
||||||
|
>
|
||||||
|
<span class="truncate font-semibold"
|
||||||
|
>{user.name}</span
|
||||||
|
>
|
||||||
|
<span class="truncate text-xs">{user.email}</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown class="ml-auto size-4" />
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-[var(--bits-dropdown-menu-anchor-width)] min-w-56 rounded-lg"
|
||||||
|
side={sidebar.isMobile ? "bottom" : "right"}
|
||||||
|
align="end"
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Label class="p-0 font-normal">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 px-1 py-1.5 text-left text-sm"
|
||||||
|
>
|
||||||
|
<Avatar.Root class="h-8 w-8 rounded-lg">
|
||||||
|
<Avatar.Image src={user.image} alt={user.name} />
|
||||||
|
<Avatar.Fallback class="rounded-lg">
|
||||||
|
{user.name.slice(0, 2).toUpperCase()}
|
||||||
|
</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
|
<div
|
||||||
|
class="grid flex-1 text-left text-sm leading-tight"
|
||||||
|
>
|
||||||
|
<span class="truncate font-semibold"
|
||||||
|
>{user.name}</span
|
||||||
|
>
|
||||||
|
<span class="truncate text-xs">{user.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
{#each secondaryNavTree as item (item.title)}
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<a
|
||||||
|
href={item.url}
|
||||||
|
class="flex h-full w-full items-center gap-2"
|
||||||
|
>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</a>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/each}
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
|
||||||
|
<!-- Theme switcher section -->
|
||||||
|
<DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.SubTrigger>
|
||||||
|
<Sun
|
||||||
|
class="h-4 w-4 scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90"
|
||||||
|
/>
|
||||||
|
<Moon
|
||||||
|
class="absolute h-4 w-4 scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0"
|
||||||
|
/>
|
||||||
|
<span>Theme</span>
|
||||||
|
</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.SubContent>
|
||||||
|
<DropdownMenu.Item onclick={() => setMode("light")}>
|
||||||
|
<Sun class="mr-2 h-4 w-4" />
|
||||||
|
<span>Light</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onclick={() => setMode("dark")}>
|
||||||
|
<Moon class="mr-2 h-4 w-4" />
|
||||||
|
<span>Dark</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onclick={() => resetMode()}>
|
||||||
|
<Monitor class="mr-2 h-4 w-4" />
|
||||||
|
<span>System</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item onclick={() => logoutUser()}>
|
||||||
|
<LogOut />
|
||||||
|
Log out
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
</Sidebar.Menu>
|
||||||
80
apps/main/src/lib/components/team-switcher.svelte
Normal file
80
apps/main/src/lib/components/team-switcher.svelte
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import { useSidebar } from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import ChevronsUpDown from "@lucide/svelte/icons/chevrons-up-down";
|
||||||
|
import Plus from "@lucide/svelte/icons/plus";
|
||||||
|
|
||||||
|
// This should be `Component` after @lucide/svelte updates types
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let { teams }: { teams: { name: string; logo: any; plan: string }[] } =
|
||||||
|
$props();
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
|
||||||
|
let activeTeam = $state(teams[0]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Menu>
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Sidebar.MenuButton
|
||||||
|
{...props}
|
||||||
|
size="lg"
|
||||||
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"
|
||||||
|
>
|
||||||
|
<activeTeam.logo class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-semibold">
|
||||||
|
{activeTeam.name}
|
||||||
|
</span>
|
||||||
|
<span class="truncate text-xs">{activeTeam.plan}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown class="ml-auto" />
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-[var(--bits-dropdown-menu-anchor-width)] min-w-56 rounded-lg"
|
||||||
|
align="start"
|
||||||
|
side={sidebar.isMobile ? "bottom" : "right"}
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Label class="text-muted-foreground text-xs"
|
||||||
|
>Teams</DropdownMenu.Label
|
||||||
|
>
|
||||||
|
{#each teams as team, index (team.name)}
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => (activeTeam = team)}
|
||||||
|
class="gap-2 p-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex size-6 items-center justify-center rounded-sm border"
|
||||||
|
>
|
||||||
|
<team.logo class="size-4 shrink-0" />
|
||||||
|
</div>
|
||||||
|
{team.name}
|
||||||
|
>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/each}
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item class="gap-2 p-2">
|
||||||
|
<div
|
||||||
|
class="bg-background flex size-6 items-center justify-center rounded-md border"
|
||||||
|
>
|
||||||
|
<Plus class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground font-medium">
|
||||||
|
Coming soon
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
</Sidebar.Menu>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="accordion-content"
|
||||||
|
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<div class={cn("pt-0 pb-4", className)}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AccordionPrimitive.ItemProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
data-slot="accordion-item"
|
||||||
|
class={cn("border-b last:border-b-0", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
level = 3,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
|
||||||
|
level?: AccordionPrimitive.HeaderProps["level"];
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Header {level} class="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-start text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
|
||||||
|
/>
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
16
apps/main/src/lib/components/ui/accordion/accordion.svelte
Normal file
16
apps/main/src/lib/components/ui/accordion/accordion.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
...restProps
|
||||||
|
}: AccordionPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
bind:value={value as never}
|
||||||
|
data-slot="accordion"
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
16
apps/main/src/lib/components/ui/accordion/index.ts
Normal file
16
apps/main/src/lib/components/ui/accordion/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Root from "./accordion.svelte";
|
||||||
|
import Content from "./accordion-content.svelte";
|
||||||
|
import Item from "./accordion-item.svelte";
|
||||||
|
import Trigger from "./accordion-trigger.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Item,
|
||||||
|
Trigger,
|
||||||
|
//
|
||||||
|
Root as Accordion,
|
||||||
|
Content as AccordionContent,
|
||||||
|
Item as AccordionItem,
|
||||||
|
Trigger as AccordionTrigger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.ActionProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-action"
|
||||||
|
class={cn(buttonVariants(), className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.CancelProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-cancel"
|
||||||
|
class={cn(buttonVariants({ variant: "outline" }), className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import AlertDialogPortal from "./alert-dialog-portal.svelte";
|
||||||
|
import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
|
||||||
|
import { cn, type WithoutChild, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
portalProps,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof AlertDialogPortal>>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPortal {...portalProps}>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
class={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed start-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.DescriptionProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-description"
|
||||||
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-dialog-footer"
|
||||||
|
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-dialog-header"
|
||||||
|
class={cn("flex flex-col gap-2 text-center sm:text-start", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.OverlayProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-overlay"
|
||||||
|
class={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ...restProps }: AlertDialogPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Portal {...restProps} />
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.TitleProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-title"
|
||||||
|
class={cn("text-lg font-semibold", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: AlertDialogPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Trigger bind:ref data-slot="alert-dialog-trigger" {...restProps} />
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { open = $bindable(false), ...restProps }: AlertDialogPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Root bind:open {...restProps} />
|
||||||
37
apps/main/src/lib/components/ui/alert-dialog/index.ts
Normal file
37
apps/main/src/lib/components/ui/alert-dialog/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import Root from "./alert-dialog.svelte";
|
||||||
|
import Portal from "./alert-dialog-portal.svelte";
|
||||||
|
import Trigger from "./alert-dialog-trigger.svelte";
|
||||||
|
import Title from "./alert-dialog-title.svelte";
|
||||||
|
import Action from "./alert-dialog-action.svelte";
|
||||||
|
import Cancel from "./alert-dialog-cancel.svelte";
|
||||||
|
import Footer from "./alert-dialog-footer.svelte";
|
||||||
|
import Header from "./alert-dialog-header.svelte";
|
||||||
|
import Overlay from "./alert-dialog-overlay.svelte";
|
||||||
|
import Content from "./alert-dialog-content.svelte";
|
||||||
|
import Description from "./alert-dialog-description.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Title,
|
||||||
|
Action,
|
||||||
|
Cancel,
|
||||||
|
Portal,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Trigger,
|
||||||
|
Overlay,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
//
|
||||||
|
Root as AlertDialog,
|
||||||
|
Title as AlertDialogTitle,
|
||||||
|
Action as AlertDialogAction,
|
||||||
|
Cancel as AlertDialogCancel,
|
||||||
|
Portal as AlertDialogPortal,
|
||||||
|
Footer as AlertDialogFooter,
|
||||||
|
Header as AlertDialogHeader,
|
||||||
|
Trigger as AlertDialogTrigger,
|
||||||
|
Overlay as AlertDialogOverlay,
|
||||||
|
Content as AlertDialogContent,
|
||||||
|
Description as AlertDialogDescription,
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-description"
|
||||||
|
class={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
20
apps/main/src/lib/components/ui/alert/alert-title.svelte
Normal file
20
apps/main/src/lib/components/ui/alert/alert-title.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-title"
|
||||||
|
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
44
apps/main/src/lib/components/ui/alert/alert.svelte
Normal file
44
apps/main/src/lib/components/ui/alert/alert.svelte
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const alertVariants = tv({
|
||||||
|
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
variant?: AlertVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert"
|
||||||
|
class={cn(alertVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
14
apps/main/src/lib/components/ui/alert/index.ts
Normal file
14
apps/main/src/lib/components/ui/alert/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Root from "./alert.svelte";
|
||||||
|
import Description from "./alert-description.svelte";
|
||||||
|
import Title from "./alert-title.svelte";
|
||||||
|
export { alertVariants, type AlertVariant } from "./alert.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Description,
|
||||||
|
Title,
|
||||||
|
//
|
||||||
|
Root as Alert,
|
||||||
|
Description as AlertDescription,
|
||||||
|
Title as AlertTitle,
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AspectRatio as AspectRatioPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: AspectRatioPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AspectRatioPrimitive.Root bind:ref data-slot="aspect-ratio" {...restProps} />
|
||||||
3
apps/main/src/lib/components/ui/aspect-ratio/index.ts
Normal file
3
apps/main/src/lib/components/ui/aspect-ratio/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import Root from "./aspect-ratio.svelte";
|
||||||
|
|
||||||
|
export { Root, Root as AspectRatio };
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AvatarPrimitive.FallbackProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
bind:ref
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
class={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
17
apps/main/src/lib/components/ui/avatar/avatar-image.svelte
Normal file
17
apps/main/src/lib/components/ui/avatar/avatar-image.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AvatarPrimitive.ImageProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
bind:ref
|
||||||
|
data-slot="avatar-image"
|
||||||
|
class={cn("aspect-square size-full", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
19
apps/main/src/lib/components/ui/avatar/avatar.svelte
Normal file
19
apps/main/src/lib/components/ui/avatar/avatar.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
loadingStatus = $bindable("loading"),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AvatarPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
bind:loadingStatus
|
||||||
|
data-slot="avatar"
|
||||||
|
class={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
13
apps/main/src/lib/components/ui/avatar/index.ts
Normal file
13
apps/main/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,
|
||||||
|
};
|
||||||
50
apps/main/src/lib/components/ui/badge/badge.svelte
Normal file
50
apps/main/src/lib/components/ui/badge/badge.svelte
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const badgeVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||||
|
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
href,
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={href ? "a" : "span"}
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="badge"
|
||||||
|
{href}
|
||||||
|
class={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</svelte:element>
|
||||||
2
apps/main/src/lib/components/ui/badge/index.ts
Normal file
2
apps/main/src/lib/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from "./badge.svelte";
|
||||||
|
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb-ellipsis"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
class={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<EllipsisIcon class="size-4" />
|
||||||
|
<span class="sr-only">More</span>
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLLiAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb-item"
|
||||||
|
class={cn("inline-flex items-center gap-1.5", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</li>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
href = undefined,
|
||||||
|
child,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
child?: Snippet<[{ props: HTMLAnchorAttributes }]>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const attrs = $derived({
|
||||||
|
"data-slot": "breadcrumb-link",
|
||||||
|
class: cn("hover:text-foreground transition-colors", className),
|
||||||
|
href,
|
||||||
|
...restProps,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if child}
|
||||||
|
{@render child({ props: attrs })}
|
||||||
|
{:else}
|
||||||
|
<a bind:this={ref} {...attrs}>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLOlAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLOlAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ol
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb-list"
|
||||||
|
class={cn(
|
||||||
|
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</ol>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb-page"
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
class={cn("text-foreground font-normal", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLLiAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb-separator"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
class={cn("[&>svg]:size-3.5", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#if children}
|
||||||
|
{@render children?.()}
|
||||||
|
{:else}
|
||||||
|
<ChevronRightIcon />
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
21
apps/main/src/lib/components/ui/breadcrumb/breadcrumb.svelte
Normal file
21
apps/main/src/lib/components/ui/breadcrumb/breadcrumb.svelte
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb"
|
||||||
|
class={className}
|
||||||
|
aria-label="breadcrumb"
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</nav>
|
||||||
25
apps/main/src/lib/components/ui/breadcrumb/index.ts
Normal file
25
apps/main/src/lib/components/ui/breadcrumb/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Root from "./breadcrumb.svelte";
|
||||||
|
import Ellipsis from "./breadcrumb-ellipsis.svelte";
|
||||||
|
import Item from "./breadcrumb-item.svelte";
|
||||||
|
import Separator from "./breadcrumb-separator.svelte";
|
||||||
|
import Link from "./breadcrumb-link.svelte";
|
||||||
|
import List from "./breadcrumb-list.svelte";
|
||||||
|
import Page from "./breadcrumb-page.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Ellipsis,
|
||||||
|
Item,
|
||||||
|
Separator,
|
||||||
|
Link,
|
||||||
|
List,
|
||||||
|
Page,
|
||||||
|
//
|
||||||
|
Root as Breadcrumb,
|
||||||
|
Ellipsis as BreadcrumbEllipsis,
|
||||||
|
Item as BreadcrumbItem,
|
||||||
|
Separator as BreadcrumbSeparator,
|
||||||
|
Link as BreadcrumbLink,
|
||||||
|
List as BreadcrumbList,
|
||||||
|
Page as BreadcrumbPage,
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof Separator> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Separator
|
||||||
|
bind:ref
|
||||||
|
data-slot="button-group-separator"
|
||||||
|
{orientation}
|
||||||
|
class={cn("bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
child,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const mergedProps = $derived({
|
||||||
|
...restProps,
|
||||||
|
class: cn(
|
||||||
|
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if child}
|
||||||
|
{@render child({ props: mergedProps })}
|
||||||
|
{:else}
|
||||||
|
<div bind:this={ref} {...mergedProps}>
|
||||||
|
{@render mergedProps.children?.()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { tv, type VariantProps } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const buttonGroupVariants = tv({
|
||||||
|
base: "flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-e-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
horizontal:
|
||||||
|
"[&>*:not(:first-child)]:rounded-s-none [&>*:not(:first-child)]:border-s-0 [&>*:not(:last-child)]:rounded-e-none",
|
||||||
|
vertical:
|
||||||
|
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
orientation: "horizontal",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonGroupOrientation = VariantProps<typeof buttonGroupVariants>["orientation"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
orientation = "horizontal",
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
orientation?: ButtonGroupOrientation;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
role="group"
|
||||||
|
data-slot="button-group"
|
||||||
|
data-orientation={orientation}
|
||||||
|
class={cn(buttonGroupVariants({ orientation }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
13
apps/main/src/lib/components/ui/button-group/index.ts
Normal file
13
apps/main/src/lib/components/ui/button-group/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import Root from "./button-group.svelte";
|
||||||
|
import Text from "./button-group-text.svelte";
|
||||||
|
import Separator from "./button-group-separator.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Text,
|
||||||
|
Separator,
|
||||||
|
//
|
||||||
|
Root as ButtonGroup,
|
||||||
|
Text as ButtonGroupText,
|
||||||
|
Separator as ButtonGroupSeparator,
|
||||||
|
};
|
||||||
82
apps/main/src/lib/components/ui/button/button.svelte
Normal file
82
apps/main/src/lib/components/ui/button/button.svelte
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
|
||||||
|
outline:
|
||||||
|
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = "button",
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
href={disabled ? undefined : href}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
role={disabled ? "link" : undefined}
|
||||||
|
tabindex={disabled ? -1 : undefined}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{disabled}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
17
apps/main/src/lib/components/ui/button/index.ts
Normal file
17
apps/main/src/lib/components/ui/button/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Root, {
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
buttonVariants,
|
||||||
|
} from "./button.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type ButtonProps as Props,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
buttonVariants,
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import type Calendar from "./calendar.svelte";
|
||||||
|
import CalendarMonthSelect from "./calendar-month-select.svelte";
|
||||||
|
import CalendarYearSelect from "./calendar-year-select.svelte";
|
||||||
|
import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date";
|
||||||
|
|
||||||
|
let {
|
||||||
|
captionLayout,
|
||||||
|
months,
|
||||||
|
monthFormat,
|
||||||
|
years,
|
||||||
|
yearFormat,
|
||||||
|
month,
|
||||||
|
locale,
|
||||||
|
placeholder = $bindable(),
|
||||||
|
monthIndex = 0,
|
||||||
|
}: {
|
||||||
|
captionLayout: ComponentProps<typeof Calendar>["captionLayout"];
|
||||||
|
months: ComponentProps<typeof CalendarMonthSelect>["months"];
|
||||||
|
monthFormat: ComponentProps<typeof CalendarMonthSelect>["monthFormat"];
|
||||||
|
years: ComponentProps<typeof CalendarYearSelect>["years"];
|
||||||
|
yearFormat: ComponentProps<typeof CalendarYearSelect>["yearFormat"];
|
||||||
|
month: DateValue;
|
||||||
|
placeholder: DateValue | undefined;
|
||||||
|
locale: string;
|
||||||
|
monthIndex: number;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function formatYear(date: DateValue) {
|
||||||
|
const dateObj = date.toDate(getLocalTimeZone());
|
||||||
|
if (typeof yearFormat === "function") return yearFormat(dateObj.getFullYear());
|
||||||
|
return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMonth(date: DateValue) {
|
||||||
|
const dateObj = date.toDate(getLocalTimeZone());
|
||||||
|
if (typeof monthFormat === "function") return monthFormat(dateObj.getMonth() + 1);
|
||||||
|
return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet MonthSelect()}
|
||||||
|
<CalendarMonthSelect
|
||||||
|
{months}
|
||||||
|
{monthFormat}
|
||||||
|
value={month.month}
|
||||||
|
onchange={(e) => {
|
||||||
|
if (!placeholder) return;
|
||||||
|
const v = Number.parseInt(e.currentTarget.value);
|
||||||
|
const newPlaceholder = placeholder.set({ month: v });
|
||||||
|
placeholder = newPlaceholder.subtract({ months: monthIndex });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet YearSelect()}
|
||||||
|
<CalendarYearSelect {years} {yearFormat} value={month.year} />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#if captionLayout === "dropdown"}
|
||||||
|
{@render MonthSelect()}
|
||||||
|
{@render YearSelect()}
|
||||||
|
{:else if captionLayout === "dropdown-months"}
|
||||||
|
{@render MonthSelect()}
|
||||||
|
{#if placeholder}
|
||||||
|
{formatYear(placeholder)}
|
||||||
|
{/if}
|
||||||
|
{:else if captionLayout === "dropdown-years"}
|
||||||
|
{#if placeholder}
|
||||||
|
{formatMonth(placeholder)}
|
||||||
|
{/if}
|
||||||
|
{@render YearSelect()}
|
||||||
|
{:else}
|
||||||
|
{formatMonth(month)} {formatYear(month)}
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.CellProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.Cell
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
"relative size-(--cell-size) p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-s-md [&:last-child[data-selected]_[data-bits-day]]:rounded-e-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
35
apps/main/src/lib/components/ui/calendar/calendar-day.svelte
Normal file
35
apps/main/src/lib/components/ui/calendar/calendar-day.svelte
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.DayProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.Day
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
buttonVariants({ variant: "ghost" }),
|
||||||
|
"flex size-(--cell-size) flex-col items-center justify-center gap-1 p-0 leading-none font-normal whitespace-nowrap select-none",
|
||||||
|
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground",
|
||||||
|
"data-[selected]:bg-primary dark:data-[selected]:hover:bg-accent/50 data-[selected]:text-primary-foreground",
|
||||||
|
// Outside months
|
||||||
|
"[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground",
|
||||||
|
// Disabled
|
||||||
|
"data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
// Unavailable
|
||||||
|
"data-[unavailable]:text-muted-foreground data-[unavailable]:line-through",
|
||||||
|
// hover
|
||||||
|
"dark:hover:text-accent-foreground",
|
||||||
|
// focus
|
||||||
|
"focus:border-ring focus:ring-ring/50 focus:relative",
|
||||||
|
// inner spans
|
||||||
|
"[&>span]:text-xs [&>span]:opacity-70",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.GridBodyProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.GridBody bind:ref class={cn(className)} {...restProps} />
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.GridHeadProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.GridHead bind:ref class={cn(className)} {...restProps} />
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.GridRowProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} />
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.GridProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.Grid
|
||||||
|
bind:ref
|
||||||
|
class={cn("mt-4 flex w-full border-collapse flex-col gap-1", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.HeadCellProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.HeadCell
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
"text-muted-foreground w-(--cell-size) rounded-md text-[0.8rem] font-normal",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.HeaderProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.Header
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.HeadingProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.Heading
|
||||||
|
bind:ref
|
||||||
|
class={cn("px-(--cell-size) text-sm font-medium", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value,
|
||||||
|
onchange,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<CalendarPrimitive.MonthSelectProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class={cn(
|
||||||
|
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarPrimitive.MonthSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
|
||||||
|
{#snippet child({ props, monthItems, selectedMonthItem })}
|
||||||
|
<select {...props} {value} {onchange}>
|
||||||
|
{#each monthItems as monthItem (monthItem.value)}
|
||||||
|
<option
|
||||||
|
value={monthItem.value}
|
||||||
|
selected={value !== undefined
|
||||||
|
? monthItem.value === value
|
||||||
|
: monthItem.value === selectedMonthItem.value}
|
||||||
|
>
|
||||||
|
{monthItem.label}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<span
|
||||||
|
class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
|
||||||
|
<ChevronDownIcon class="size-4" />
|
||||||
|
</span>
|
||||||
|
{/snippet}
|
||||||
|
</CalendarPrimitive.MonthSelect>
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type WithElementRef, cn } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div {...restProps} bind:this={ref} class={cn("flex flex-col", className)}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn("relative flex flex-col gap-4 md:flex-row", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
19
apps/main/src/lib/components/ui/calendar/calendar-nav.svelte
Normal file
19
apps/main/src/lib/components/ui/calendar/calendar-nav.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
{...restProps}
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", className)}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</nav>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||||
|
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
variant = "ghost",
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.NextButtonProps & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet Fallback()}
|
||||||
|
<ChevronRightIcon class="size-4" />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<CalendarPrimitive.NextButton
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
buttonVariants({ variant }),
|
||||||
|
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
children={children || Fallback}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
|
||||||
|
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
variant = "ghost",
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.PrevButtonProps & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet Fallback()}
|
||||||
|
<ChevronLeftIcon class="size-4" />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<CalendarPrimitive.PrevButton
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
buttonVariants({ variant }),
|
||||||
|
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
children={children || Fallback}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<CalendarPrimitive.YearSelectProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class={cn(
|
||||||
|
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarPrimitive.YearSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
|
||||||
|
{#snippet child({ props, yearItems, selectedYearItem })}
|
||||||
|
<select {...props} {value}>
|
||||||
|
{#each yearItems as yearItem (yearItem.value)}
|
||||||
|
<option
|
||||||
|
value={yearItem.value}
|
||||||
|
selected={value !== undefined
|
||||||
|
? yearItem.value === value
|
||||||
|
: yearItem.value === selectedYearItem.value}
|
||||||
|
>
|
||||||
|
{yearItem.label}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<span
|
||||||
|
class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
|
||||||
|
<ChevronDownIcon class="size-4" />
|
||||||
|
</span>
|
||||||
|
{/snippet}
|
||||||
|
</CalendarPrimitive.YearSelect>
|
||||||
|
</span>
|
||||||
115
apps/main/src/lib/components/ui/calendar/calendar.svelte
Normal file
115
apps/main/src/lib/components/ui/calendar/calendar.svelte
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
|
import * as Calendar from "./index.js";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import type { ButtonVariant } from "../button/button.svelte";
|
||||||
|
import { isEqualMonth, type DateValue } from "@internationalized/date";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
placeholder = $bindable(),
|
||||||
|
class: className,
|
||||||
|
weekdayFormat = "short",
|
||||||
|
buttonVariant = "ghost",
|
||||||
|
captionLayout = "label",
|
||||||
|
locale = "en-US",
|
||||||
|
months: monthsProp,
|
||||||
|
years,
|
||||||
|
monthFormat: monthFormatProp,
|
||||||
|
yearFormat = "numeric",
|
||||||
|
day,
|
||||||
|
disableDaysOutsideMonth = false,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & {
|
||||||
|
buttonVariant?: ButtonVariant;
|
||||||
|
captionLayout?: "dropdown" | "dropdown-months" | "dropdown-years" | "label";
|
||||||
|
months?: CalendarPrimitive.MonthSelectProps["months"];
|
||||||
|
years?: CalendarPrimitive.YearSelectProps["years"];
|
||||||
|
monthFormat?: CalendarPrimitive.MonthSelectProps["monthFormat"];
|
||||||
|
yearFormat?: CalendarPrimitive.YearSelectProps["yearFormat"];
|
||||||
|
day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const monthFormat = $derived.by(() => {
|
||||||
|
if (monthFormatProp) return monthFormatProp;
|
||||||
|
if (captionLayout.startsWith("dropdown")) return "short";
|
||||||
|
return "long";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Discriminated Unions + Destructing (required for bindable) do not
|
||||||
|
get along, so we shut typescript up by casting `value` to `never`.
|
||||||
|
-->
|
||||||
|
<CalendarPrimitive.Root
|
||||||
|
bind:value={value as never}
|
||||||
|
bind:ref
|
||||||
|
bind:placeholder
|
||||||
|
{weekdayFormat}
|
||||||
|
{disableDaysOutsideMonth}
|
||||||
|
class={cn(
|
||||||
|
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{locale}
|
||||||
|
{monthFormat}
|
||||||
|
{yearFormat}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ months, weekdays })}
|
||||||
|
<Calendar.Months>
|
||||||
|
<Calendar.Nav>
|
||||||
|
<Calendar.PrevButton variant={buttonVariant} />
|
||||||
|
<Calendar.NextButton variant={buttonVariant} />
|
||||||
|
</Calendar.Nav>
|
||||||
|
{#each months as month, monthIndex (month)}
|
||||||
|
<Calendar.Month>
|
||||||
|
<Calendar.Header>
|
||||||
|
<Calendar.Caption
|
||||||
|
{captionLayout}
|
||||||
|
months={monthsProp}
|
||||||
|
{monthFormat}
|
||||||
|
{years}
|
||||||
|
{yearFormat}
|
||||||
|
month={month.value}
|
||||||
|
bind:placeholder
|
||||||
|
{locale}
|
||||||
|
{monthIndex}
|
||||||
|
/>
|
||||||
|
</Calendar.Header>
|
||||||
|
<Calendar.Grid>
|
||||||
|
<Calendar.GridHead>
|
||||||
|
<Calendar.GridRow class="select-none">
|
||||||
|
{#each weekdays as weekday (weekday)}
|
||||||
|
<Calendar.HeadCell>
|
||||||
|
{weekday.slice(0, 2)}
|
||||||
|
</Calendar.HeadCell>
|
||||||
|
{/each}
|
||||||
|
</Calendar.GridRow>
|
||||||
|
</Calendar.GridHead>
|
||||||
|
<Calendar.GridBody>
|
||||||
|
{#each month.weeks as weekDates (weekDates)}
|
||||||
|
<Calendar.GridRow class="mt-2 w-full">
|
||||||
|
{#each weekDates as date (date)}
|
||||||
|
<Calendar.Cell {date} month={month.value}>
|
||||||
|
{#if day}
|
||||||
|
{@render day({
|
||||||
|
day: date,
|
||||||
|
outsideMonth: !isEqualMonth(date, month.value),
|
||||||
|
})}
|
||||||
|
{:else}
|
||||||
|
<Calendar.Day />
|
||||||
|
{/if}
|
||||||
|
</Calendar.Cell>
|
||||||
|
{/each}
|
||||||
|
</Calendar.GridRow>
|
||||||
|
{/each}
|
||||||
|
</Calendar.GridBody>
|
||||||
|
</Calendar.Grid>
|
||||||
|
</Calendar.Month>
|
||||||
|
{/each}
|
||||||
|
</Calendar.Months>
|
||||||
|
{/snippet}
|
||||||
|
</CalendarPrimitive.Root>
|
||||||
40
apps/main/src/lib/components/ui/calendar/index.ts
Normal file
40
apps/main/src/lib/components/ui/calendar/index.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import Root from "./calendar.svelte";
|
||||||
|
import Cell from "./calendar-cell.svelte";
|
||||||
|
import Day from "./calendar-day.svelte";
|
||||||
|
import Grid from "./calendar-grid.svelte";
|
||||||
|
import Header from "./calendar-header.svelte";
|
||||||
|
import Months from "./calendar-months.svelte";
|
||||||
|
import GridRow from "./calendar-grid-row.svelte";
|
||||||
|
import Heading from "./calendar-heading.svelte";
|
||||||
|
import GridBody from "./calendar-grid-body.svelte";
|
||||||
|
import GridHead from "./calendar-grid-head.svelte";
|
||||||
|
import HeadCell from "./calendar-head-cell.svelte";
|
||||||
|
import NextButton from "./calendar-next-button.svelte";
|
||||||
|
import PrevButton from "./calendar-prev-button.svelte";
|
||||||
|
import MonthSelect from "./calendar-month-select.svelte";
|
||||||
|
import YearSelect from "./calendar-year-select.svelte";
|
||||||
|
import Month from "./calendar-month.svelte";
|
||||||
|
import Nav from "./calendar-nav.svelte";
|
||||||
|
import Caption from "./calendar-caption.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Day,
|
||||||
|
Cell,
|
||||||
|
Grid,
|
||||||
|
Header,
|
||||||
|
Months,
|
||||||
|
GridRow,
|
||||||
|
Heading,
|
||||||
|
GridBody,
|
||||||
|
GridHead,
|
||||||
|
HeadCell,
|
||||||
|
NextButton,
|
||||||
|
PrevButton,
|
||||||
|
Nav,
|
||||||
|
Month,
|
||||||
|
YearSelect,
|
||||||
|
MonthSelect,
|
||||||
|
Caption,
|
||||||
|
//
|
||||||
|
Root as Calendar,
|
||||||
|
};
|
||||||
20
apps/main/src/lib/components/ui/card/card-action.svelte
Normal file
20
apps/main/src/lib/components/ui/card/card-action.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-action"
|
||||||
|
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
15
apps/main/src/lib/components/ui/card/card-content.svelte
Normal file
15
apps/main/src/lib/components/ui/card/card-content.svelte
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
20
apps/main/src/lib/components/ui/card/card-description.svelte
Normal file
20
apps/main/src/lib/components/ui/card/card-description.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-description"
|
||||||
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</p>
|
||||||
20
apps/main/src/lib/components/ui/card/card-footer.svelte
Normal file
20
apps/main/src/lib/components/ui/card/card-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-footer"
|
||||||
|
class={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
23
apps/main/src/lib/components/ui/card/card-header.svelte
Normal file
23
apps/main/src/lib/components/ui/card/card-header.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-header"
|
||||||
|
class={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
20
apps/main/src/lib/components/ui/card/card-title.svelte
Normal file
20
apps/main/src/lib/components/ui/card/card-title.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-title"
|
||||||
|
class={cn("leading-none font-semibold", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
23
apps/main/src/lib/components/ui/card/card.svelte
Normal file
23
apps/main/src/lib/components/ui/card/card.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card"
|
||||||
|
class={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
25
apps/main/src/lib/components/ui/card/index.ts
Normal file
25
apps/main/src/lib/components/ui/card/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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";
|
||||||
|
import Action from "./card-action.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Title,
|
||||||
|
Action,
|
||||||
|
//
|
||||||
|
Root as Card,
|
||||||
|
Content as CardContent,
|
||||||
|
Description as CardDescription,
|
||||||
|
Footer as CardFooter,
|
||||||
|
Header as CardHeader,
|
||||||
|
Title as CardTitle,
|
||||||
|
Action as CardAction,
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import emblaCarouselSvelte from "embla-carousel-svelte";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { getEmblaContext } from "./context.js";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
|
||||||
|
const emblaCtx = getEmblaContext("<Carousel.Content/>");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
data-slot="carousel-content"
|
||||||
|
class="overflow-hidden"
|
||||||
|
use:emblaCarouselSvelte={{
|
||||||
|
options: {
|
||||||
|
container: "[data-embla-container]",
|
||||||
|
slides: "[data-embla-slide]",
|
||||||
|
...emblaCtx.options,
|
||||||
|
axis: emblaCtx.orientation === "horizontal" ? "x" : "y",
|
||||||
|
},
|
||||||
|
plugins: emblaCtx.plugins,
|
||||||
|
}}
|
||||||
|
onemblaInit={emblaCtx.onInit}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn(
|
||||||
|
"flex",
|
||||||
|
emblaCtx.orientation === "horizontal" ? "-ms-4" : "-mt-4 flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
data-embla-container=""
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { getEmblaContext } from "./context.js";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
|
||||||
|
const emblaCtx = getEmblaContext("<Carousel.Item/>");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="carousel-item"
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
class={cn(
|
||||||
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
|
emblaCtx.orientation === "horizontal" ? "ps-4" : "pt-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
data-embla-slide=""
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ArrowRightIcon from "@lucide/svelte/icons/arrow-right";
|
||||||
|
import type { WithoutChildren } from "bits-ui";
|
||||||
|
import { getEmblaContext } from "./context.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import { Button, type Props } from "$lib/components/ui/button/index.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
variant = "outline",
|
||||||
|
size = "icon",
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<Props> = $props();
|
||||||
|
|
||||||
|
const emblaCtx = getEmblaContext("<Carousel.Next/>");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
data-slot="carousel-next"
|
||||||
|
{variant}
|
||||||
|
{size}
|
||||||
|
aria-disabled={!emblaCtx.canScrollNext}
|
||||||
|
class={cn(
|
||||||
|
"absolute size-8 rounded-full",
|
||||||
|
emblaCtx.orientation === "horizontal"
|
||||||
|
? "-end-12 top-1/2 -translate-y-1/2"
|
||||||
|
: "start-1/2 -bottom-12 -translate-x-1/2 rotate-90",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onclick={emblaCtx.scrollNext}
|
||||||
|
onkeydown={emblaCtx.handleKeyDown}
|
||||||
|
bind:ref
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ArrowRightIcon class="size-4" />
|
||||||
|
<span class="sr-only">Next slide</span>
|
||||||
|
</Button>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user