diff --git a/apps/front/old.server.ts b/apps/front/old.server.ts deleted file mode 100644 index cc5d571..0000000 --- a/apps/front/old.server.ts +++ /dev/null @@ -1,151 +0,0 @@ -// @ts-nocheck -/** - * Mobile Proxy Test — Bun Reverse Proxy Server - * - * Sits on port 3000 (what ngrok tunnels) and: - * GET / -> serves our clean mobile viewer (view.html) - * Everything else -> reverse-proxied to ws-scrcpy on port 8000 - * - * WebSocket upgrades are proxied transparently so the ws-scrcpy - * player running inside our iframe can talk to the scrcpy server. - */ - -const WS_SCRCPY_ORIGIN = "http://localhost:8000"; -const PROXY_PORT = 3000; - -const viewHtml = await Bun.file( - new URL("./view.html", import.meta.url).pathname, -).text(); - -const server = Bun.serve({ - port: PROXY_PORT, - hostname: "0.0.0.0", - - async fetch(req, server) { - const url = new URL(req.url); - - // Serve our clean mobile viewer at root only - // (the iframe loads ws-scrcpy's own page via /?scrcpy=1) - if ( - (url.pathname === "/" || url.pathname === "/index.html") && - !url.searchParams.has("scrcpy") - ) { - return new Response(viewHtml, { - headers: { "Content-Type": "text/html; charset=utf-8" }, - }); - } - - // Upgrade WebSocket connections — proxy to ws-scrcpy - if (req.headers.get("upgrade")?.toLowerCase() === "websocket") { - const targetUrl = `${WS_SCRCPY_ORIGIN.replace("http", "ws")}${url.pathname}${url.search}`; - - // Use Bun's WebSocket upgrade to establish a client-side WS to ws-scrcpy - // and bridge the two connections - const success = server.upgrade(req, { - data: { targetUrl }, - }); - - if (success) return undefined; - return new Response("WebSocket upgrade failed", { status: 400 }); - } - - // Proxy all other HTTP requests to ws-scrcpy - const targetUrl = `${WS_SCRCPY_ORIGIN}${url.pathname}${url.search}`; - - try { - const proxyRes = await fetch(targetUrl, { - method: req.method, - headers: req.headers, - body: - req.method !== "GET" && req.method !== "HEAD" - ? req.body - : undefined, - }); - - // Clone response with CORS headers stripped / adjusted - const headers = new Headers(proxyRes.headers); - headers.delete("content-encoding"); // Bun handles this - - return new Response(proxyRes.body, { - status: proxyRes.status, - statusText: proxyRes.statusText, - headers, - }); - } catch (err) { - console.error(`Proxy error for ${url.pathname}:`, err); - return new Response( - "ws-scrcpy not reachable — is it running on port 8000?", - { - status: 502, - }, - ); - } - }, - - websocket: { - async open(ws) { - const { targetUrl } = ws.data as { targetUrl: string }; - - // Open a WebSocket connection to ws-scrcpy - const upstream = new WebSocket(targetUrl); - - // Store upstream reference on ws data for cleanup - (ws.data as any).upstream = upstream; - - upstream.binaryType = "arraybuffer"; - - upstream.onopen = () => { - // Connection established, nothing extra needed - }; - - upstream.onmessage = (event) => { - try { - if (event.data instanceof ArrayBuffer) { - ws.sendBinary(new Uint8Array(event.data)); - } else { - ws.sendText(event.data); - } - } catch { - // Client disconnected - } - }; - - upstream.onclose = () => { - ws.close(); - }; - - upstream.onerror = (err) => { - console.error("Upstream WS error:", err); - ws.close(); - }; - }, - - message(ws, message) { - const upstream = (ws.data as any).upstream as WebSocket | undefined; - if (!upstream || upstream.readyState !== WebSocket.OPEN) return; - - try { - if (typeof message === "string") { - upstream.send(message); - } else { - upstream.send(message); - } - } catch { - // Upstream disconnected - } - }, - - close(ws) { - const upstream = (ws.data as any).upstream as WebSocket | undefined; - if (upstream && upstream.readyState === WebSocket.OPEN) { - upstream.close(); - } - }, - }, -}); - -console.log(`\n Mobile Proxy running on http://localhost:${server.port}`); -console.log(` Proxying to ws-scrcpy at ${WS_SCRCPY_ORIGIN}`); -console.log( - `\n Point ngrok at port ${server.port}, then open the ngrok URL on the user's phone.\n`, -); diff --git a/apps/front/package.json b/apps/front/package.json deleted file mode 100644 index be52692..0000000 --- a/apps/front/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@apps/front", - "type": "module", - "scripts": { - "dev": "PORT=3001 tsx watch src/index.ts", - "build": "tsc", - "prod": "HOST=0.0.0.0 PORT=3000 tsx src/index.ts" - }, - "dependencies": { - "@hono/node-server": "^1.19.9", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/auto-instrumentations-node": "^0.70.1", - "@opentelemetry/exporter-logs-otlp-proto": "^0.212.0", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0", - "@opentelemetry/exporter-trace-otlp-proto": "^0.212.0", - "@opentelemetry/sdk-logs": "^0.212.0", - "@opentelemetry/sdk-metrics": "^2.1.0", - "@opentelemetry/sdk-node": "^0.212.0", - "@pkg/db": "workspace:*", - "@pkg/logger": "workspace:*", - "@pkg/logic": "workspace:*", - "@pkg/result": "workspace:*", - "@pkg/settings": "workspace:*", - "hono": "^4.12.8", - "import-in-the-middle": "^3.0.0", - "valibot": "^1.2.0" - }, - "devDependencies": { - "@types/node": "^25.3.2", - "tsx": "^4.21.0", - "typescript": "^5.9.3" - } -} diff --git a/apps/front/src/core/utils.ts b/apps/front/src/core/utils.ts deleted file mode 100644 index 209b471..0000000 --- a/apps/front/src/core/utils.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context"; -import { ERROR_CODES, errorStatusMap, type Err } from "@pkg/result"; -import { randomUUID } from "node:crypto"; -import type { ContentfulStatusCode } from "hono/utils/http-status"; - -export function buildFlowExecCtx(): FlowExecCtx { - return { flowId: randomUUID() }; -} - -export function normalizeBaseUrl(url: string): string { - return url.endsWith("/") ? url.slice(0, -1) : url; -} - -export function createAppError( - fctx: FlowExecCtx, - code: string, - message: string, - description: string, - detail: string, - actionable?: boolean, -): Err { - return { - flowId: fctx.flowId, - code, - message, - description, - detail, - actionable, - }; -} - -export function jsonError(error: Err, status?: number) { - return { - body: { - data: null, - error: { - ...error, - }, - }, - status: - status || - errorStatusMap[error.code] || - errorStatusMap[ERROR_CODES.INTERNAL_SERVER_ERROR] || - 500, - }; -} - -export function toStatusCode(status: number): ContentfulStatusCode { - return status as ContentfulStatusCode; -} - -export function isErrPayload(value: unknown): value is Err { - return !!value && typeof value === "object" && "code" in value; -} diff --git a/apps/front/src/domains/links/router.ts b/apps/front/src/domains/links/router.ts deleted file mode 100644 index 91b6a3b..0000000 --- a/apps/front/src/domains/links/router.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Hono } from "hono"; -import { buildFlowExecCtx, toStatusCode } from "../../core/utils"; -import { prepareLink, resolveLink } from "./service"; - -export const linksRouter = new Hono() - .get("/links/:token/resolve", async (c) => { - const response = await resolveLink(buildFlowExecCtx(), c.req.param("token")); - return c.json(response.body, toStatusCode(response.status)); - }) - .post("/links/:token/prepare", async (c) => { - const response = await prepareLink(buildFlowExecCtx(), c.req.param("token")); - return c.json(response.body, toStatusCode(response.status)); - }); diff --git a/apps/front/src/domains/links/service.ts b/apps/front/src/domains/links/service.ts deleted file mode 100644 index edbb37d..0000000 --- a/apps/front/src/domains/links/service.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { getLinkController } from "@pkg/logic/domains/link/controller"; -import { DeviceStatus } from "@pkg/logic/domains/device/data"; -import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context"; -import { formatErrorDetail, logDomainEvent } from "@pkg/logger"; -import { ERROR_CODES, type Err } from "@pkg/result"; -import { settings } from "@pkg/settings"; -import { - createAppError, - isErrPayload, - jsonError, - normalizeBaseUrl, -} from "../../core/utils"; - -const linkController = getLinkController(); - -type OrchestratorPreparePayload = { - deviceId: number; - packageName: string; - linkToken: string; -}; - -type OrchestratorPrepareResponse = { - data: { - deviceId: number; - containerId: string; - packageName: string; - serial: string; - status: string; - }; - error: null; -}; - -async function callOrchestratorPrepare( - fctx: FlowExecCtx, - payload: OrchestratorPreparePayload, -): Promise { - const response = await fetch( - `${normalizeBaseUrl(settings.orchestratorApiUrl)}/internal/sessions/prepare`, - { - method: "POST", - headers: { - "content-type": "application/json", - "x-internal-api-key": settings.internalApiKey, - "x-flow-id": fctx.flowId, - }, - body: JSON.stringify(payload), - }, - ); - - let body: unknown = null; - try { - body = await response.json(); - } catch (error) { - throw createAppError( - fctx, - ERROR_CODES.EXTERNAL_SERVICE_ERROR, - "Invalid orchestrator response", - "The orchestrator returned a response that could not be parsed", - formatErrorDetail(error), - ); - } - - if (!response.ok && body && typeof body === "object" && "error" in body) { - const payloadError = (body as { error?: unknown }).error; - if (isErrPayload(payloadError)) { - throw payloadError; - } - } - - if ( - !body || - typeof body !== "object" || - !("data" in body) || - ("error" in body && body.error) - ) { - throw createAppError( - fctx, - ERROR_CODES.EXTERNAL_SERVICE_ERROR, - "Unexpected orchestrator response", - "The orchestrator response was missing the expected data payload", - JSON.stringify(body), - ); - } - - return body as OrchestratorPrepareResponse; -} - -export async function resolveLink(fctx: FlowExecCtx, token: string) { - logDomainEvent({ - event: "front.link_resolve.started", - fctx, - meta: { token }, - }); - - const linkResult = await linkController.validate(fctx, token); - if (linkResult.isErr()) { - logDomainEvent({ - level: "warn", - event: "front.link_resolve.rejected", - fctx, - error: linkResult.error, - meta: { token }, - }); - - return jsonError(linkResult.error); - } - - const link = linkResult.value; - if (!link.device) { - return jsonError( - createAppError( - fctx, - ERROR_CODES.NOT_ALLOWED, - "Link is not assigned to a device", - "This link cannot start a session because no device is assigned", - `token=${token}`, - true, - ), - ); - } - - if (!link.supportedApp) { - return jsonError( - createAppError( - fctx, - ERROR_CODES.NOT_ALLOWED, - "Link is not assigned to an app", - "This link cannot start a session because no app is assigned", - `token=${token}`, - true, - ), - ); - } - - return { - body: { - data: { - link: { - id: link.id, - token: link.token, - status: link.status, - expiresAt: link.expiresAt, - }, - device: { - id: link.device.id, - title: link.device.title, - status: link.device.status, - inUse: link.device.inUse, - isAvailable: - link.device.status === DeviceStatus.ONLINE && !link.device.inUse, - }, - supportedApp: { - id: link.supportedApp.id, - title: link.supportedApp.title, - packageName: link.supportedApp.packageName, - }, - }, - error: null, - }, - status: 200, - }; -} - -export async function prepareLink(fctx: FlowExecCtx, token: string) { - logDomainEvent({ - event: "front.link_prepare.started", - fctx, - meta: { token }, - }); - - const linkResult = await linkController.validate(fctx, token); - if (linkResult.isErr()) { - logDomainEvent({ - level: "warn", - event: "front.link_prepare.rejected", - fctx, - error: linkResult.error, - meta: { token }, - }); - - return jsonError(linkResult.error); - } - - const link = linkResult.value; - if (!link.device) { - return jsonError( - createAppError( - fctx, - ERROR_CODES.NOT_ALLOWED, - "Link is not assigned to a device", - "This link cannot start a session because no device is assigned", - `token=${token}`, - true, - ), - ); - } - - if (!link.supportedApp) { - return jsonError( - createAppError( - fctx, - ERROR_CODES.NOT_ALLOWED, - "Link is not assigned to an app", - "This link cannot start a session because no app is assigned", - `token=${token}`, - true, - ), - ); - } - - if (link.device.status !== DeviceStatus.ONLINE || link.device.inUse) { - return jsonError( - createAppError( - fctx, - ERROR_CODES.NOT_ALLOWED, - "Device is not available", - "The assigned device is currently busy or offline", - `deviceId=${link.device.id} status=${link.device.status} inUse=${link.device.inUse}`, - true, - ), - ); - } - - try { - const orchestratorResponse = await callOrchestratorPrepare(fctx, { - deviceId: link.device.id, - packageName: link.supportedApp.packageName, - linkToken: link.token, - }); - - logDomainEvent({ - event: "front.link_prepare.succeeded", - fctx, - meta: { - token, - deviceId: link.device.id, - packageName: link.supportedApp.packageName, - }, - }); - - return { - body: { - data: { - link: { - id: link.id, - token: link.token, - }, - device: { - id: link.device.id, - title: link.device.title, - host: link.device.host, - wsPort: link.device.wsPort, - }, - supportedApp: { - id: link.supportedApp.id, - title: link.supportedApp.title, - packageName: link.supportedApp.packageName, - }, - session: orchestratorResponse.data, - }, - error: null, - }, - status: 200, - }; - } catch (error) { - const err: Err = isErrPayload(error) - ? error - : createAppError( - fctx, - ERROR_CODES.EXTERNAL_SERVICE_ERROR, - "Failed to prepare session", - "The front server could not prepare the assigned Android session", - formatErrorDetail(error), - ); - - logDomainEvent({ - level: "error", - event: "front.link_prepare.failed", - fctx, - error: err, - meta: { - token, - orchestratorUrl: settings.orchestratorApiUrl, - }, - }); - - return jsonError( - err.flowId - ? err - : { - ...err, - flowId: fctx.flowId, - }, - err.code === ERROR_CODES.EXTERNAL_SERVICE_ERROR ? 502 : undefined, - ); - } -} diff --git a/apps/front/src/index.ts b/apps/front/src/index.ts deleted file mode 100644 index b766601..0000000 --- a/apps/front/src/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import "./instrumentation.js"; - -import { createHttpTelemetryMiddleware } from "@pkg/logic/core/http.telemetry"; -import { serve } from "@hono/node-server"; -import { Hono } from "hono"; -import { linksRouter } from "./domains/links/router"; - -const app = new Hono().use("*", createHttpTelemetryMiddleware("front")); - -const host = process.env.HOST || "0.0.0.0"; -const port = Number(process.env.PORT || "3000"); - -app.get("/health", (c) => { - return c.json({ ok: true }); -}); - -app.get("/ping", (c) => { - return c.text("pong"); -}); - -app.route("/", linksRouter); - -serve( - { - fetch: app.fetch, - port, - hostname: host, - }, - (info) => { - console.log(`Server is running on http://${host}:${info.port}`); - }, -); diff --git a/apps/front/tsconfig.json b/apps/front/tsconfig.json deleted file mode 100644 index fce7dfe..0000000 --- a/apps/front/tsconfig.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "verbatimModuleSyntax": false, - "skipLibCheck": true, - "types": [ - "node" - ], - "baseUrl": ".", - "paths": { - "@pkg/logic": ["../../packages/logic"], - "@pkg/logic/*": ["../../packages/logic/*"], - "@pkg/db": ["../../packages/db"], - "@pkg/db/*": ["../../packages/db/*"], - "@pkg/logger": ["../../packages/logger"], - "@pkg/logger/*": ["../../packages/logger/*"], - "@pkg/result": ["../../packages/result"], - "@pkg/result/*": ["../../packages/result/*"], - "@pkg/settings": ["../../packages/settings"], - "@pkg/settings/*": ["../../packages/settings/*"], - "@/*": ["../../packages/logic/*"], - "@core/*": ["../../packages/logic/core/*"], - "@domains/*": ["../../packages/logic/domains/*"] - }, - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx", - "outDir": "./dist" - }, - "exclude": ["node_modules"] -} diff --git a/apps/front/view.html b/apps/front/view.html deleted file mode 100644 index 03e7f6c..0000000 --- a/apps/front/view.html +++ /dev/null @@ -1,290 +0,0 @@ - - - - - - - - - - Loading... - - - -
-
- Connecting... -
- - - - - - diff --git a/apps/frontend/.gitignore b/apps/frontend/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/apps/frontend/.gitignore @@ -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-* diff --git a/apps/frontend/.npmrc b/apps/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/apps/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/apps/frontend/.prettierignore b/apps/frontend/.prettierignore new file mode 100644 index 0000000..7d74fe2 --- /dev/null +++ b/apps/frontend/.prettierignore @@ -0,0 +1,9 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ diff --git a/apps/frontend/components.json b/apps/frontend/components.json new file mode 100644 index 0000000..1e6a638 --- /dev/null +++ b/apps/frontend/components.json @@ -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" +} diff --git a/apps/frontend/package.json b/apps/frontend/package.json new file mode 100644 index 0000000..b28c10f --- /dev/null +++ b/apps/frontend/package.json @@ -0,0 +1,74 @@ +{ + "name": "@apps/frontend", + "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 node ./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" + }, + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/auto-instrumentations-node": "^0.70.1", + "@opentelemetry/exporter-logs-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.212.0", + "@opentelemetry/sdk-logs": "^0.212.0", + "@opentelemetry/sdk-node": "^0.212.0", + "@pkg/db": "workspace:*", + "@pkg/keystore": "workspace:*", + "@pkg/logger": "workspace:*", + "@pkg/logic": "workspace:*", + "@pkg/result": "workspace:*", + "@pkg/settings": "workspace:*", + "date-fns": "^4.1.0", + "import-in-the-middle": "^3.0.0", + "nanoid": "^5.1.6", + "neverthrow": "^8.2.0", + "valibot": "^1.2.0" + }, + "devDependencies": { + "@iconify/json": "^2.2.434", + "@internationalized/date": "^3.10.0", + "@lucide/svelte": "^0.561.0", + "@sveltejs/adapter-node": "^5.5.4", + "@sveltejs/kit": "^2.53.4", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@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.53.6", + "svelte-check": "^4.4.4", + "svelte-sonner": "^1.0.7", + "sveltekit-superforms": "^2.30.0", + "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" + } +} diff --git a/apps/frontend/src/app.d.ts b/apps/frontend/src/app.d.ts new file mode 100644 index 0000000..7ded0fe --- /dev/null +++ b/apps/frontend/src/app.d.ts @@ -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 {}; diff --git a/apps/frontend/src/app.html b/apps/frontend/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/apps/frontend/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/frontend/src/demo.spec.ts b/apps/frontend/src/demo.spec.ts new file mode 100644 index 0000000..e07cbbd --- /dev/null +++ b/apps/frontend/src/demo.spec.ts @@ -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); + }); +}); diff --git a/apps/frontend/src/hooks.server.ts b/apps/frontend/src/hooks.server.ts new file mode 100644 index 0000000..d18086b --- /dev/null +++ b/apps/frontend/src/hooks.server.ts @@ -0,0 +1,15 @@ +import type { Handle, HandleServerError } from "@sveltejs/kit"; + +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 middleware: Handle = async ({ event, resolve }) => { + event.locals.flowId ||= crypto.randomUUID(); + console.log("[+] Running middleware for : ", event.url.pathname); + return resolve(event); +}; + +export const handle = middleware; diff --git a/apps/front/src/instrumentation.ts b/apps/frontend/src/instrumentation.server.ts similarity index 57% rename from apps/front/src/instrumentation.ts rename to apps/frontend/src/instrumentation.server.ts index 6b3ec48..4184ea0 100644 --- a/apps/front/src/instrumentation.ts +++ b/apps/frontend/src/instrumentation.server.ts @@ -1,8 +1,6 @@ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; -import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"; -import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; -import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; import { createAddHookMessageChannel } from "import-in-the-middle"; import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs"; import { NodeSDK } from "@opentelemetry/sdk-node"; @@ -12,23 +10,9 @@ import { register } from "node:module"; const { registerOptions } = createAddHookMessageChannel(); register("import-in-the-middle/hook.mjs", import.meta.url, registerOptions); -const normalizedEndpoint = settings.otelExporterOtlpHttpEndpoint.startsWith( - "http", -) - ? settings.otelExporterOtlpHttpEndpoint - : `http://${settings.otelExporterOtlpHttpEndpoint}`; - -if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { - process.env.OTEL_EXPORTER_OTLP_ENDPOINT = normalizedEndpoint; -} - const sdk = new NodeSDK({ - serviceName: `${settings.otelServiceName}-processor`, + serviceName: settings.otelServiceName || settings.appName, traceExporter: new OTLPTraceExporter(), - metricReader: new PeriodicExportingMetricReader({ - exporter: new OTLPMetricExporter(), - exportIntervalMillis: 10_000, - }), logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())], instrumentations: [ getNodeAutoInstrumentations({ @@ -41,10 +25,3 @@ const sdk = new NodeSDK({ }); sdk.start(); - -const shutdown = () => { - void sdk.shutdown(); -}; - -process.on("SIGTERM", shutdown); -process.on("SIGINT", shutdown); diff --git a/apps/frontend/src/lib/components/app-sidebar.svelte b/apps/frontend/src/lib/components/app-sidebar.svelte new file mode 100644 index 0000000..f0ed540 --- /dev/null +++ b/apps/frontend/src/lib/components/app-sidebar.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/apps/frontend/src/lib/components/atoms/button-text.svelte b/apps/frontend/src/lib/components/atoms/button-text.svelte new file mode 100644 index 0000000..207d697 --- /dev/null +++ b/apps/frontend/src/lib/components/atoms/button-text.svelte @@ -0,0 +1,17 @@ + + +{#if loading} + + {loadingText} +{:else} + {text} +{/if} diff --git a/apps/frontend/src/lib/components/atoms/icon.svelte b/apps/frontend/src/lib/components/atoms/icon.svelte new file mode 100644 index 0000000..794c944 --- /dev/null +++ b/apps/frontend/src/lib/components/atoms/icon.svelte @@ -0,0 +1,11 @@ + + + diff --git a/apps/frontend/src/lib/components/atoms/title.svelte b/apps/frontend/src/lib/components/atoms/title.svelte new file mode 100644 index 0000000..ed17511 --- /dev/null +++ b/apps/frontend/src/lib/components/atoms/title.svelte @@ -0,0 +1,61 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/molecules/max-width-wrapper.svelte b/apps/frontend/src/lib/components/molecules/max-width-wrapper.svelte new file mode 100644 index 0000000..e3ec428 --- /dev/null +++ b/apps/frontend/src/lib/components/molecules/max-width-wrapper.svelte @@ -0,0 +1,11 @@ + + +
+ {@render children()} +
diff --git a/apps/frontend/src/lib/components/nav-main.svelte b/apps/frontend/src/lib/components/nav-main.svelte new file mode 100644 index 0000000..5968981 --- /dev/null +++ b/apps/frontend/src/lib/components/nav-main.svelte @@ -0,0 +1,81 @@ + + + + + {#each items as mainItem (mainItem.title)} + + + +
+ + + {#snippet child({ props })} + + {#if mainItem.icon} + + {/if} + {mainItem.title} + + {/snippet} + + + + {#if mainItem.items && sidebar.open} + + {#snippet child({ props })} +
+ + Toggle {mainItem.title} submenu +
+ {/snippet} +
+ {/if} +
+ + + + {#if mainItem.items} + + {#each mainItem.items as subItem (subItem.title)} + + + {#snippet child({ props })} + + {subItem.title} + + {/snippet} + + + {/each} + + {/if} + +
+
+ {/each} +
+
diff --git a/apps/frontend/src/lib/components/nav-user.svelte b/apps/frontend/src/lib/components/nav-user.svelte new file mode 100644 index 0000000..0a9e96a --- /dev/null +++ b/apps/frontend/src/lib/components/nav-user.svelte @@ -0,0 +1,150 @@ + + + + + + + {#snippet child({ props })} + + + + + {user.name.slice(0, 2).toUpperCase()} + + +
+ {user.name} + {user.email} +
+ +
+ {/snippet} +
+ + +
+ + + + {user.name.slice(0, 2).toUpperCase()} + + +
+ {user.name} + {user.email} +
+
+
+ + + + {#each secondaryNavTree as item (item.title)} + + + + {item.title} + + + {/each} + + + + + + + + + + Theme + + + setMode("light")}> + + Light + + setMode("dark")}> + + Dark + + resetMode()}> + + System + + + + + + logoutUser()}> + + Log out + +
+
+
+
diff --git a/apps/frontend/src/lib/components/team-switcher.svelte b/apps/frontend/src/lib/components/team-switcher.svelte new file mode 100644 index 0000000..4981212 --- /dev/null +++ b/apps/frontend/src/lib/components/team-switcher.svelte @@ -0,0 +1,80 @@ + + + + + + + {#snippet child({ props })} + +
+ +
+
+ + {activeTeam.name} + + {activeTeam.plan} +
+ +
+ {/snippet} +
+ + Teams + {#each teams as team, index (team.name)} + (activeTeam = team)} + class="gap-2 p-2" + > +
+ +
+ {team.name} + > +
+ {/each} + + +
+ +
+
+ Coming soon +
+
+
+
+
+
diff --git a/apps/frontend/src/lib/components/ui/accordion/accordion-content.svelte b/apps/frontend/src/lib/components/ui/accordion/accordion-content.svelte new file mode 100644 index 0000000..559db3d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/accordion/accordion-content.svelte @@ -0,0 +1,22 @@ + + + +
+ {@render children?.()} +
+
diff --git a/apps/frontend/src/lib/components/ui/accordion/accordion-item.svelte b/apps/frontend/src/lib/components/ui/accordion/accordion-item.svelte new file mode 100644 index 0000000..780545c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/accordion/accordion-item.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte b/apps/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte new file mode 100644 index 0000000..c46c246 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte @@ -0,0 +1,32 @@ + + + + svg]:rotate-180", + className + )} + {...restProps} + > + {@render children?.()} + + + diff --git a/apps/frontend/src/lib/components/ui/accordion/accordion.svelte b/apps/frontend/src/lib/components/ui/accordion/accordion.svelte new file mode 100644 index 0000000..117ee37 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/accordion/accordion.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/accordion/index.ts b/apps/frontend/src/lib/components/ui/accordion/index.ts new file mode 100644 index 0000000..ac343a1 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/accordion/index.ts @@ -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, +}; diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 0000000..a005691 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,18 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 0000000..a7b0cf7 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,18 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 0000000..00bdd9c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,29 @@ + + + + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 0000000..2ec67dc --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 0000000..f78b97a --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 0000000..1835d91 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 0000000..a64ee76 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte new file mode 100644 index 0000000..f0a19a8 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 0000000..7ef2b5f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte new file mode 100644 index 0000000..b22d1d5 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte new file mode 100644 index 0000000..7ea78bb --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/index.ts b/apps/frontend/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..269538e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/index.ts @@ -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, +}; diff --git a/apps/frontend/src/lib/components/ui/alert/alert-description.svelte b/apps/frontend/src/lib/components/ui/alert/alert-description.svelte new file mode 100644 index 0000000..8b56aed --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert/alert-description.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/apps/frontend/src/lib/components/ui/alert/alert-title.svelte b/apps/frontend/src/lib/components/ui/alert/alert-title.svelte new file mode 100644 index 0000000..77e45ad --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert/alert-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/frontend/src/lib/components/ui/alert/alert.svelte b/apps/frontend/src/lib/components/ui/alert/alert.svelte new file mode 100644 index 0000000..2b2eff9 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert/alert.svelte @@ -0,0 +1,44 @@ + + + + + diff --git a/apps/frontend/src/lib/components/ui/alert/index.ts b/apps/frontend/src/lib/components/ui/alert/index.ts new file mode 100644 index 0000000..97e21b4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert/index.ts @@ -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, +}; diff --git a/apps/frontend/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte b/apps/frontend/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte new file mode 100644 index 0000000..815aab0 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/aspect-ratio/index.ts b/apps/frontend/src/lib/components/ui/aspect-ratio/index.ts new file mode 100644 index 0000000..985c75f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/aspect-ratio/index.ts @@ -0,0 +1,3 @@ +import Root from "./aspect-ratio.svelte"; + +export { Root, Root as AspectRatio }; diff --git a/apps/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte b/apps/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte new file mode 100644 index 0000000..249d4a4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/avatar/avatar-image.svelte b/apps/frontend/src/lib/components/ui/avatar/avatar-image.svelte new file mode 100644 index 0000000..2bb9db4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/avatar/avatar-image.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/avatar/avatar.svelte b/apps/frontend/src/lib/components/ui/avatar/avatar.svelte new file mode 100644 index 0000000..e37214d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/avatar/avatar.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/avatar/index.ts b/apps/frontend/src/lib/components/ui/avatar/index.ts new file mode 100644 index 0000000..d06457b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/avatar/index.ts @@ -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, +}; diff --git a/apps/frontend/src/lib/components/ui/badge/badge.svelte b/apps/frontend/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..e3164ba --- /dev/null +++ b/apps/frontend/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,50 @@ + + + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/badge/index.ts b/apps/frontend/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..64e0aa9 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte new file mode 100644 index 0000000..a178cf5 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte new file mode 100644 index 0000000..1a84c4c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte @@ -0,0 +1,20 @@ + + +
  • + {@render children?.()} +
  • diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte new file mode 100644 index 0000000..e6bc17d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte @@ -0,0 +1,31 @@ + + +{#if child} + {@render child({ props: attrs })} +{:else} + + {@render children?.()} + +{/if} diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte new file mode 100644 index 0000000..1272a37 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte @@ -0,0 +1,23 @@ + + +
      + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte new file mode 100644 index 0000000..5fb6979 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte new file mode 100644 index 0000000..84106a1 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb.svelte new file mode 100644 index 0000000..8f8a3e6 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb.svelte @@ -0,0 +1,21 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/index.ts b/apps/frontend/src/lib/components/ui/breadcrumb/index.ts new file mode 100644 index 0000000..dc914ec --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/index.ts @@ -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, +}; diff --git a/apps/frontend/src/lib/components/ui/button-group/button-group-separator.svelte b/apps/frontend/src/lib/components/ui/button-group/button-group-separator.svelte new file mode 100644 index 0000000..86ff8ae --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button-group/button-group-separator.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/button-group/button-group-text.svelte b/apps/frontend/src/lib/components/ui/button-group/button-group-text.svelte new file mode 100644 index 0000000..1be72bb --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button-group/button-group-text.svelte @@ -0,0 +1,30 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
    + {@render mergedProps.children?.()} +
    +{/if} diff --git a/apps/frontend/src/lib/components/ui/button-group/button-group.svelte b/apps/frontend/src/lib/components/ui/button-group/button-group.svelte new file mode 100644 index 0000000..34c8d79 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button-group/button-group.svelte @@ -0,0 +1,46 @@ + + + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/button-group/index.ts b/apps/frontend/src/lib/components/ui/button-group/index.ts new file mode 100644 index 0000000..7f706e3 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button-group/index.ts @@ -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, +}; diff --git a/apps/frontend/src/lib/components/ui/button/button.svelte b/apps/frontend/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..a8296ae --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button/button.svelte @@ -0,0 +1,82 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/apps/frontend/src/lib/components/ui/button/index.ts b/apps/frontend/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..fb585d7 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button/index.ts @@ -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, +}; diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-caption.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-caption.svelte new file mode 100644 index 0000000..5c93037 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-caption.svelte @@ -0,0 +1,76 @@ + + +{#snippet MonthSelect()} + { + if (!placeholder) return; + const v = Number.parseInt(e.currentTarget.value); + const newPlaceholder = placeholder.set({ month: v }); + placeholder = newPlaceholder.subtract({ months: monthIndex }); + }} + /> +{/snippet} + +{#snippet YearSelect()} + +{/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} diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-cell.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-cell.svelte new file mode 100644 index 0000000..4cdb548 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-cell.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-day.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-day.svelte new file mode 100644 index 0000000..19d7bde --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-day.svelte @@ -0,0 +1,35 @@ + + +span]:text-xs [&>span]:opacity-70", + className + )} + {...restProps} +/> diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-grid-body.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-body.svelte new file mode 100644 index 0000000..8cd86de --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-body.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-grid-head.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-head.svelte new file mode 100644 index 0000000..333edc4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-head.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-grid-row.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-row.svelte new file mode 100644 index 0000000..9032236 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-row.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-grid.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-grid.svelte new file mode 100644 index 0000000..e0c8627 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-grid.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-head-cell.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-head-cell.svelte new file mode 100644 index 0000000..131807e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-head-cell.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-header.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-header.svelte new file mode 100644 index 0000000..c39e955 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-header.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-heading.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-heading.svelte new file mode 100644 index 0000000..a9b9810 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-heading.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-month-select.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-month-select.svelte new file mode 100644 index 0000000..8d88deb --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-month-select.svelte @@ -0,0 +1,44 @@ + + + + + {#snippet child({ props, monthItems, selectedMonthItem })} + + + {/snippet} + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-month.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-month.svelte new file mode 100644 index 0000000..e747fae --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-month.svelte @@ -0,0 +1,15 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-months.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-months.svelte new file mode 100644 index 0000000..f717a9d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-months.svelte @@ -0,0 +1,19 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-nav.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-nav.svelte new file mode 100644 index 0000000..27f33d7 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-nav.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-next-button.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-next-button.svelte new file mode 100644 index 0000000..5c5a78d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-next-button.svelte @@ -0,0 +1,31 @@ + + +{#snippet Fallback()} + +{/snippet} + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-prev-button.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-prev-button.svelte new file mode 100644 index 0000000..33cfd63 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-prev-button.svelte @@ -0,0 +1,31 @@ + + +{#snippet Fallback()} + +{/snippet} + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-year-select.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-year-select.svelte new file mode 100644 index 0000000..226efdf --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-year-select.svelte @@ -0,0 +1,43 @@ + + + + + {#snippet child({ props, yearItems, selectedYearItem })} + + + {/snippet} + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar.svelte new file mode 100644 index 0000000..29b6fff --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar.svelte @@ -0,0 +1,115 @@ + + + + + {#snippet children({ months, weekdays })} + + + + + + {#each months as month, monthIndex (month)} + + + + + + + + {#each weekdays as weekday (weekday)} + + {weekday.slice(0, 2)} + + {/each} + + + + {#each month.weeks as weekDates (weekDates)} + + {#each weekDates as date (date)} + + {#if day} + {@render day({ + day: date, + outsideMonth: !isEqualMonth(date, month.value), + })} + {:else} + + {/if} + + {/each} + + {/each} + + + + {/each} + + {/snippet} + diff --git a/apps/frontend/src/lib/components/ui/calendar/index.ts b/apps/frontend/src/lib/components/ui/calendar/index.ts new file mode 100644 index 0000000..f3a16d2 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/index.ts @@ -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, +}; diff --git a/apps/frontend/src/lib/components/ui/card/card-action.svelte b/apps/frontend/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..cc36c56 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/card-content.svelte b/apps/frontend/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..bc90b83 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/card-description.svelte b/apps/frontend/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..9b20ac7 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

    + {@render children?.()} +

    diff --git a/apps/frontend/src/lib/components/ui/card/card-footer.svelte b/apps/frontend/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..2d4d0f2 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/card-header.svelte b/apps/frontend/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..2501788 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/card-title.svelte b/apps/frontend/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..7447231 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/card.svelte b/apps/frontend/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..99448cc --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/index.ts b/apps/frontend/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..4d3fce4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/index.ts @@ -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, +}; diff --git a/apps/frontend/src/lib/components/ui/carousel/carousel-content.svelte b/apps/frontend/src/lib/components/ui/carousel/carousel-content.svelte new file mode 100644 index 0000000..84c71f8 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/carousel-content.svelte @@ -0,0 +1,43 @@ + + +
    +
    + {@render children?.()} +
    +
    diff --git a/apps/frontend/src/lib/components/ui/carousel/carousel-item.svelte b/apps/frontend/src/lib/components/ui/carousel/carousel-item.svelte new file mode 100644 index 0000000..ebf1649 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/carousel-item.svelte @@ -0,0 +1,30 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/carousel/carousel-next.svelte b/apps/frontend/src/lib/components/ui/carousel/carousel-next.svelte new file mode 100644 index 0000000..1aaa1f4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/carousel-next.svelte @@ -0,0 +1,38 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/carousel/carousel-previous.svelte b/apps/frontend/src/lib/components/ui/carousel/carousel-previous.svelte new file mode 100644 index 0000000..dafe4fd --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/carousel-previous.svelte @@ -0,0 +1,38 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/carousel/carousel.svelte b/apps/frontend/src/lib/components/ui/carousel/carousel.svelte new file mode 100644 index 0000000..0492805 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/carousel.svelte @@ -0,0 +1,93 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/carousel/context.ts b/apps/frontend/src/lib/components/ui/carousel/context.ts new file mode 100644 index 0000000..a5fd74f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/context.ts @@ -0,0 +1,58 @@ +import type { WithElementRef } from "$lib/utils.js"; +import type { + EmblaCarouselSvelteType, + default as emblaCarouselSvelte, +} from "embla-carousel-svelte"; +import { getContext, hasContext, setContext } from "svelte"; +import type { HTMLAttributes } from "svelte/elements"; + +export type CarouselAPI = + NonNullable["on:emblaInit"]> extends ( + evt: CustomEvent + ) => void + ? CarouselAPI + : never; + +type EmblaCarouselConfig = NonNullable[1]>; + +export type CarouselOptions = EmblaCarouselConfig["options"]; +export type CarouselPlugins = EmblaCarouselConfig["plugins"]; + +//// + +export type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugins; + setApi?: (api: CarouselAPI | undefined) => void; + orientation?: "horizontal" | "vertical"; +} & WithElementRef>; + +const EMBLA_CAROUSEL_CONTEXT = Symbol("EMBLA_CAROUSEL_CONTEXT"); + +export type EmblaContext = { + api: CarouselAPI | undefined; + orientation: "horizontal" | "vertical"; + scrollNext: () => void; + scrollPrev: () => void; + canScrollNext: boolean; + canScrollPrev: boolean; + handleKeyDown: (e: KeyboardEvent) => void; + options: CarouselOptions; + plugins: CarouselPlugins; + onInit: (e: CustomEvent) => void; + scrollTo: (index: number, jump?: boolean) => void; + scrollSnaps: number[]; + selectedIndex: number; +}; + +export function setEmblaContext(config: EmblaContext): EmblaContext { + setContext(EMBLA_CAROUSEL_CONTEXT, config); + return config; +} + +export function getEmblaContext(name = "This component") { + if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) { + throw new Error(`${name} must be used within a component`); + } + return getContext>(EMBLA_CAROUSEL_CONTEXT); +} diff --git a/apps/frontend/src/lib/components/ui/carousel/index.ts b/apps/frontend/src/lib/components/ui/carousel/index.ts new file mode 100644 index 0000000..957fc74 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/index.ts @@ -0,0 +1,19 @@ +import Root from "./carousel.svelte"; +import Content from "./carousel-content.svelte"; +import Item from "./carousel-item.svelte"; +import Previous from "./carousel-previous.svelte"; +import Next from "./carousel-next.svelte"; + +export { + Root, + Content, + Item, + Previous, + Next, + // + Root as Carousel, + Content as CarouselContent, + Item as CarouselItem, + Previous as CarouselPrevious, + Next as CarouselNext, +}; diff --git a/apps/frontend/src/lib/components/ui/chart/chart-container.svelte b/apps/frontend/src/lib/components/ui/chart/chart-container.svelte new file mode 100644 index 0000000..36c0000 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/chart/chart-container.svelte @@ -0,0 +1,80 @@ + + +
    + + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/chart/chart-style.svelte b/apps/frontend/src/lib/components/ui/chart/chart-style.svelte new file mode 100644 index 0000000..864ecc3 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/chart/chart-style.svelte @@ -0,0 +1,37 @@ + + +{#if themeContents} + {#key id} + + {themeContents} + + {/key} +{/if} diff --git a/apps/frontend/src/lib/components/ui/chart/chart-tooltip.svelte b/apps/frontend/src/lib/components/ui/chart/chart-tooltip.svelte new file mode 100644 index 0000000..6eb66ff --- /dev/null +++ b/apps/frontend/src/lib/components/ui/chart/chart-tooltip.svelte @@ -0,0 +1,159 @@ + + +{#snippet TooltipLabel()} + {#if formattedLabel} +
    + {#if typeof formattedLabel === "function"} + {@render formattedLabel()} + {:else} + {formattedLabel} + {/if} +
    + {/if} +{/snippet} + + +
    + {#if !nestLabel} + {@render TooltipLabel()} + {/if} +
    + {#each tooltipCtx.payload as item, i (item.key + i)} + {@const key = `${nameKey || item.key || item.name || "value"}`} + {@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)} + {@const indicatorColor = color || item.payload?.color || item.color} +
    svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5", + indicator === "dot" && "items-center" + )} + > + {#if formatter && item.value !== undefined && item.name} + {@render formatter({ + value: item.value, + name: item.name, + item, + index: i, + payload: tooltipCtx.payload, + })} + {:else} + {#if itemConfig?.icon} + + {:else if !hideIndicator} +
    + {/if} +
    +
    + {#if nestLabel} + {@render TooltipLabel()} + {/if} + + {itemConfig?.label || item.name} + +
    + {#if item.value !== undefined} + + {item.value.toLocaleString()} + + {/if} +
    + {/if} +
    + {/each} +
    +
    +
    diff --git a/apps/frontend/src/lib/components/ui/chart/chart-utils.ts b/apps/frontend/src/lib/components/ui/chart/chart-utils.ts new file mode 100644 index 0000000..2decbbf --- /dev/null +++ b/apps/frontend/src/lib/components/ui/chart/chart-utils.ts @@ -0,0 +1,66 @@ +import type { Tooltip } from "layerchart"; +import { getContext, setContext, type Component, type ComponentProps, type Snippet } from "svelte"; + +export const THEMES = { light: "", dark: ".dark" } as const; + +export type ChartConfig = { + [k in string]: { + label?: string; + icon?: Component; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; + +export type ExtractSnippetParams = T extends Snippet<[infer P]> ? P : never; + +export type TooltipPayload = ExtractSnippetParams< + ComponentProps["children"] +>["payload"][number]; + +// Helper to extract item config from a payload. +export function getPayloadConfigFromPayload( + config: ChartConfig, + payload: TooltipPayload, + key: string +) { + if (typeof payload !== "object" || payload === null) return undefined; + + const payloadPayload = + "payload" in payload && typeof payload.payload === "object" && payload.payload !== null + ? payload.payload + : undefined; + + let configLabelKey: string = key; + + if (payload.key === key) { + configLabelKey = payload.key; + } else if (payload.name === key) { + configLabelKey = payload.name; + } else if (key in payload && typeof payload[key as keyof typeof payload] === "string") { + configLabelKey = payload[key as keyof typeof payload] as string; + } else if ( + payloadPayload !== undefined && + key in payloadPayload && + typeof payloadPayload[key as keyof typeof payloadPayload] === "string" + ) { + configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string; + } + + return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]; +} + +type ChartContextValue = { + config: ChartConfig; +}; + +const chartContextKey = Symbol("chart-context"); + +export function setChartContext(value: ChartContextValue) { + return setContext(chartContextKey, value); +} + +export function useChart() { + return getContext(chartContextKey); +} diff --git a/apps/frontend/src/lib/components/ui/chart/index.ts b/apps/frontend/src/lib/components/ui/chart/index.ts new file mode 100644 index 0000000..f22375e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/chart/index.ts @@ -0,0 +1,6 @@ +import ChartContainer from "./chart-container.svelte"; +import ChartTooltip from "./chart-tooltip.svelte"; + +export { getPayloadConfigFromPayload, type ChartConfig } from "./chart-utils.js"; + +export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip }; diff --git a/apps/frontend/src/lib/components/ui/checkbox/checkbox.svelte b/apps/frontend/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000..0a2b010 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,36 @@ + + + + {#snippet children({ checked, indeterminate })} +
    + {#if checked} + + {:else if indeterminate} + + {/if} +
    + {/snippet} +
    diff --git a/apps/frontend/src/lib/components/ui/checkbox/index.ts b/apps/frontend/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000..6d92d94 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox, +}; diff --git a/apps/frontend/src/lib/components/ui/collapsible/collapsible-content.svelte b/apps/frontend/src/lib/components/ui/collapsible/collapsible-content.svelte new file mode 100644 index 0000000..bdabb55 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/collapsible/collapsible-content.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/collapsible/collapsible-trigger.svelte b/apps/frontend/src/lib/components/ui/collapsible/collapsible-trigger.svelte new file mode 100644 index 0000000..ece7ad6 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/collapsible/collapsible-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/collapsible/collapsible.svelte b/apps/frontend/src/lib/components/ui/collapsible/collapsible.svelte new file mode 100644 index 0000000..39cdd4e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/collapsible/collapsible.svelte @@ -0,0 +1,11 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/collapsible/index.ts b/apps/frontend/src/lib/components/ui/collapsible/index.ts new file mode 100644 index 0000000..169b479 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/collapsible/index.ts @@ -0,0 +1,13 @@ +import Root from "./collapsible.svelte"; +import Trigger from "./collapsible-trigger.svelte"; +import Content from "./collapsible-content.svelte"; + +export { + Root, + Content, + Trigger, + // + Root as Collapsible, + Content as CollapsibleContent, + Trigger as CollapsibleTrigger, +}; diff --git a/apps/frontend/src/lib/components/ui/command/command-dialog.svelte b/apps/frontend/src/lib/components/ui/command/command-dialog.svelte new file mode 100644 index 0000000..4bdb740 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-dialog.svelte @@ -0,0 +1,40 @@ + + + + + {title} + {description} + + + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-empty.svelte b/apps/frontend/src/lib/components/ui/command/command-empty.svelte new file mode 100644 index 0000000..6726cd8 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-empty.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-group.svelte b/apps/frontend/src/lib/components/ui/command/command-group.svelte new file mode 100644 index 0000000..104f817 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-group.svelte @@ -0,0 +1,32 @@ + + + + {#if heading} + + {heading} + + {/if} + + diff --git a/apps/frontend/src/lib/components/ui/command/command-input.svelte b/apps/frontend/src/lib/components/ui/command/command-input.svelte new file mode 100644 index 0000000..28d3dcf --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-input.svelte @@ -0,0 +1,26 @@ + + +
    + + +
    diff --git a/apps/frontend/src/lib/components/ui/command/command-item.svelte b/apps/frontend/src/lib/components/ui/command/command-item.svelte new file mode 100644 index 0000000..5833416 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-item.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-link-item.svelte b/apps/frontend/src/lib/components/ui/command/command-link-item.svelte new file mode 100644 index 0000000..ada6d2c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-link-item.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-list.svelte b/apps/frontend/src/lib/components/ui/command/command-list.svelte new file mode 100644 index 0000000..2d3a01a --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-list.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-loading.svelte b/apps/frontend/src/lib/components/ui/command/command-loading.svelte new file mode 100644 index 0000000..19dd298 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-loading.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-separator.svelte b/apps/frontend/src/lib/components/ui/command/command-separator.svelte new file mode 100644 index 0000000..35c4c95 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-shortcut.svelte b/apps/frontend/src/lib/components/ui/command/command-shortcut.svelte new file mode 100644 index 0000000..f3d6928 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/command/command.svelte b/apps/frontend/src/lib/components/ui/command/command.svelte new file mode 100644 index 0000000..a1581f1 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command.svelte @@ -0,0 +1,28 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/index.ts b/apps/frontend/src/lib/components/ui/command/index.ts new file mode 100644 index 0000000..5435fbe --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/index.ts @@ -0,0 +1,37 @@ +import Root from "./command.svelte"; +import Loading from "./command-loading.svelte"; +import Dialog from "./command-dialog.svelte"; +import Empty from "./command-empty.svelte"; +import Group from "./command-group.svelte"; +import Item from "./command-item.svelte"; +import Input from "./command-input.svelte"; +import List from "./command-list.svelte"; +import Separator from "./command-separator.svelte"; +import Shortcut from "./command-shortcut.svelte"; +import LinkItem from "./command-link-item.svelte"; + +export { + Root, + Dialog, + Empty, + Group, + Item, + LinkItem, + Input, + List, + Separator, + Shortcut, + Loading, + // + Root as Command, + Dialog as CommandDialog, + Empty as CommandEmpty, + Group as CommandGroup, + Item as CommandItem, + LinkItem as CommandLinkItem, + Input as CommandInput, + List as CommandList, + Separator as CommandSeparator, + Shortcut as CommandShortcut, + Loading as CommandLoading, +}; diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte new file mode 100644 index 0000000..f3b6db3 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte @@ -0,0 +1,40 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-content.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-content.svelte new file mode 100644 index 0000000..20b516d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-content.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-group-heading.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-group-heading.svelte new file mode 100644 index 0000000..2cb6207 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-group-heading.svelte @@ -0,0 +1,21 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-group.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-group.svelte new file mode 100644 index 0000000..c7c1e06 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-item.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-item.svelte new file mode 100644 index 0000000..4e8d224 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-label.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-label.svelte new file mode 100644 index 0000000..5e96107 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-label.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-portal.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-portal.svelte new file mode 100644 index 0000000..96b1e3e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-group.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-group.svelte new file mode 100644 index 0000000..964cb55 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-item.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-item.svelte new file mode 100644 index 0000000..0141b14 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-item.svelte @@ -0,0 +1,33 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-separator.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-separator.svelte new file mode 100644 index 0000000..7f5b237 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-shortcut.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-shortcut.svelte new file mode 100644 index 0000000..6181881 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-content.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-content.svelte new file mode 100644 index 0000000..2b6ca47 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte new file mode 100644 index 0000000..38d74eb --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub.svelte new file mode 100644 index 0000000..a03827b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-trigger.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-trigger.svelte new file mode 100644 index 0000000..3efa857 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu.svelte new file mode 100644 index 0000000..cfaefb3 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/index.ts b/apps/frontend/src/lib/components/ui/context-menu/index.ts new file mode 100644 index 0000000..cbeaee1 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/index.ts @@ -0,0 +1,52 @@ +import Root from "./context-menu.svelte"; +import Sub from "./context-menu-sub.svelte"; +import Portal from "./context-menu-portal.svelte"; +import Trigger from "./context-menu-trigger.svelte"; +import Group from "./context-menu-group.svelte"; +import RadioGroup from "./context-menu-radio-group.svelte"; +import Item from "./context-menu-item.svelte"; +import GroupHeading from "./context-menu-group-heading.svelte"; +import Content from "./context-menu-content.svelte"; +import Shortcut from "./context-menu-shortcut.svelte"; +import RadioItem from "./context-menu-radio-item.svelte"; +import Separator from "./context-menu-separator.svelte"; +import SubContent from "./context-menu-sub-content.svelte"; +import SubTrigger from "./context-menu-sub-trigger.svelte"; +import CheckboxItem from "./context-menu-checkbox-item.svelte"; +import Label from "./context-menu-label.svelte"; + +export { + Root, + Sub, + Portal, + Item, + GroupHeading, + Label, + Group, + Trigger, + Content, + Shortcut, + Separator, + RadioItem, + SubContent, + SubTrigger, + RadioGroup, + CheckboxItem, + // + Root as ContextMenu, + Sub as ContextMenuSub, + Portal as ContextMenuPortal, + Item as ContextMenuItem, + GroupHeading as ContextMenuGroupHeading, + Group as ContextMenuGroup, + Content as ContextMenuContent, + Trigger as ContextMenuTrigger, + Shortcut as ContextMenuShortcut, + RadioItem as ContextMenuRadioItem, + Separator as ContextMenuSeparator, + RadioGroup as ContextMenuRadioGroup, + SubContent as ContextMenuSubContent, + SubTrigger as ContextMenuSubTrigger, + CheckboxItem as ContextMenuCheckboxItem, + Label as ContextMenuLabel, +}; diff --git a/apps/frontend/src/lib/components/ui/data-table/data-table.svelte.ts b/apps/frontend/src/lib/components/ui/data-table/data-table.svelte.ts new file mode 100644 index 0000000..5b7985e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/data-table/data-table.svelte.ts @@ -0,0 +1,142 @@ +import { + type RowData, + type TableOptions, + type TableOptionsResolved, + type TableState, + createTable, +} from "@tanstack/table-core"; + +/** + * Creates a reactive TanStack table object for Svelte. + * @param options Table options to create the table with. + * @returns A reactive table object. + * @example + * ```svelte + * + * + * + * + * {#each table.getHeaderGroups() as headerGroup} + * + * {#each headerGroup.headers as header} + * + * {/each} + * + * {/each} + * + * + *
    + * + *
    + * ``` + */ +export function createSvelteTable(options: TableOptions) { + const resolvedOptions: TableOptionsResolved = mergeObjects( + { + state: {}, + onStateChange() {}, + renderFallbackValue: null, + mergeOptions: ( + defaultOptions: TableOptions, + options: Partial> + ) => { + return mergeObjects(defaultOptions, options); + }, + }, + options + ); + + const table = createTable(resolvedOptions); + let state = $state>(table.initialState); + + function updateOptions() { + table.setOptions((prev) => { + return mergeObjects(prev, options, { + state: mergeObjects(state, options.state || {}), + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onStateChange: (updater: any) => { + if (updater instanceof Function) state = updater(state); + else state = mergeObjects(state, updater); + + options.onStateChange?.(updater); + }, + }); + }); + } + + updateOptions(); + + $effect.pre(() => { + updateOptions(); + }); + + return table; +} + +type MaybeThunk = T | (() => T | null | undefined); +type Intersection = (T extends [infer H, ...infer R] + ? H & Intersection + : unknown) & {}; + +/** + * Lazily merges several objects (or thunks) while preserving + * getter semantics from every source. + * + * Proxy-based to avoid known WebKit recursion issue. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function mergeObjects[]>( + ...sources: Sources +): Intersection<{ [K in keyof Sources]: Sources[K] }> { + const resolve = (src: MaybeThunk): T | undefined => + typeof src === "function" ? (src() ?? undefined) : src; + + const findSourceWithKey = (key: PropertyKey) => { + for (let i = sources.length - 1; i >= 0; i--) { + const obj = resolve(sources[i]); + if (obj && key in obj) return obj; + } + return undefined; + }; + + return new Proxy(Object.create(null), { + get(_, key) { + const src = findSourceWithKey(key); + + return src?.[key as never]; + }, + + has(_, key) { + return !!findSourceWithKey(key); + }, + + ownKeys(): (string | symbol)[] { + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const all = new Set(); + for (const s of sources) { + const obj = resolve(s); + if (obj) { + for (const k of Reflect.ownKeys(obj) as (string | symbol)[]) { + all.add(k); + } + } + } + return [...all]; + }, + + getOwnPropertyDescriptor(_, key) { + const src = findSourceWithKey(key); + if (!src) return undefined; + return { + configurable: true, + enumerable: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: (src as any)[key], + writable: true, + }; + }, + }) as Intersection<{ [K in keyof Sources]: Sources[K] }>; +} diff --git a/apps/frontend/src/lib/components/ui/data-table/flex-render.svelte b/apps/frontend/src/lib/components/ui/data-table/flex-render.svelte new file mode 100644 index 0000000..ac82a58 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/data-table/flex-render.svelte @@ -0,0 +1,40 @@ + + +{#if typeof content === "string"} + {content} +{:else if content instanceof Function} + + + {@const result = content(context as any)} + {#if result instanceof RenderComponentConfig} + {@const { component: Component, props } = result} + + {:else if result instanceof RenderSnippetConfig} + {@const { snippet, params } = result} + {@render snippet({ ...params, attach })} + {:else} + {result} + {/if} +{/if} diff --git a/apps/frontend/src/lib/components/ui/data-table/index.ts b/apps/frontend/src/lib/components/ui/data-table/index.ts new file mode 100644 index 0000000..5f4e77e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/data-table/index.ts @@ -0,0 +1,3 @@ +export { default as FlexRender } from "./flex-render.svelte"; +export { renderComponent, renderSnippet } from "./render-helpers.js"; +export { createSvelteTable } from "./data-table.svelte.js"; diff --git a/apps/frontend/src/lib/components/ui/data-table/render-helpers.ts b/apps/frontend/src/lib/components/ui/data-table/render-helpers.ts new file mode 100644 index 0000000..fa036d6 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/data-table/render-helpers.ts @@ -0,0 +1,111 @@ +import type { Component, ComponentProps, Snippet } from "svelte"; + +/** + * A helper class to make it easy to identify Svelte components in + * `columnDef.cell` and `columnDef.header` properties. + * + * > NOTE: This class should only be used internally by the adapter. If you're + * reading this and you don't know what this is for, you probably don't need it. + * + * @example + * ```svelte + * {@const result = content(context as any)} + * {#if result instanceof RenderComponentConfig} + * {@const { component: Component, props } = result} + * + * {/if} + * ``` + */ +export class RenderComponentConfig { + component: TComponent; + props: ComponentProps | Record; + constructor( + component: TComponent, + props: ComponentProps | Record = {} + ) { + this.component = component; + this.props = props; + } +} + +/** + * A helper class to make it easy to identify Svelte Snippets in `columnDef.cell` and `columnDef.header` properties. + * + * > NOTE: This class should only be used internally by the adapter. If you're + * reading this and you don't know what this is for, you probably don't need it. + * + * @example + * ```svelte + * {@const result = content(context as any)} + * {#if result instanceof RenderSnippetConfig} + * {@const { snippet, params } = result} + * {@render snippet(params)} + * {/if} + * ``` + */ +export class RenderSnippetConfig { + snippet: Snippet<[TProps]>; + params: TProps; + constructor(snippet: Snippet<[TProps]>, params: TProps) { + this.snippet = snippet; + this.params = params; + } +} + +/** + * A helper function to help create cells from Svelte components through ColumnDef's `cell` and `header` properties. + * + * This is only to be used with Svelte Components - use `renderSnippet` for Svelte Snippets. + * + * @param component A Svelte component + * @param props The props to pass to `component` + * @returns A `RenderComponentConfig` object that helps svelte-table know how to render the header/cell component. + * @example + * ```ts + * // +page.svelte + * const defaultColumns = [ + * columnHelper.accessor('name', { + * header: header => renderComponent(SortHeader, { label: 'Name', header }), + * }), + * columnHelper.accessor('state', { + * header: header => renderComponent(SortHeader, { label: 'State', header }), + * }), + * ] + * ``` + * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} + */ +export function renderComponent< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends Component, + Props extends ComponentProps, +>(component: T, props: Props = {} as Props) { + return new RenderComponentConfig(component, props); +} + +/** + * A helper function to help create cells from Svelte Snippets through ColumnDef's `cell` and `header` properties. + * + * The snippet must only take one parameter. + * + * This is only to be used with Snippets - use `renderComponent` for Svelte Components. + * + * @param snippet + * @param params + * @returns - A `RenderSnippetConfig` object that helps svelte-table know how to render the header/cell snippet. + * @example + * ```ts + * // +page.svelte + * const defaultColumns = [ + * columnHelper.accessor('name', { + * cell: cell => renderSnippet(nameSnippet, { name: cell.row.name }), + * }), + * columnHelper.accessor('state', { + * cell: cell => renderSnippet(stateSnippet, { state: cell.row.state }), + * }), + * ] + * ``` + * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} + */ +export function renderSnippet(snippet: Snippet<[TProps]>, params: TProps = {} as TProps) { + return new RenderSnippetConfig(snippet, params); +} diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-close.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 0000000..840b2f6 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-content.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000..ae1a03f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,45 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + + Close + + {/if} + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-description.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000..3845023 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-footer.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000..e7ff446 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-header.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000..4e5c447 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000..f81ad83 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-portal.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 0000000..ccfa79c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-title.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000..e4d4b34 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 0000000..9d1e801 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog.svelte new file mode 100644 index 0000000..211672c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/index.ts b/apps/frontend/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..076cef5 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,34 @@ +import Root from "./dialog.svelte"; +import Portal from "./dialog-portal.svelte"; +import Title from "./dialog-title.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; +import Trigger from "./dialog-trigger.svelte"; +import Close from "./dialog-close.svelte"; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, +}; diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-close.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-close.svelte new file mode 100644 index 0000000..95c2479 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-content.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-content.svelte new file mode 100644 index 0000000..6bb01db --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-content.svelte @@ -0,0 +1,40 @@ + + + + + + + {@render children?.()} + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-description.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-description.svelte new file mode 100644 index 0000000..2763a1a --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-footer.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-footer.svelte new file mode 100644 index 0000000..1691f58 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-header.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-header.svelte new file mode 100644 index 0000000..65d2de5 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-nested.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-nested.svelte new file mode 100644 index 0000000..834af94 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-nested.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-overlay.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-overlay.svelte new file mode 100644 index 0000000..53f78a2 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-portal.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-portal.svelte new file mode 100644 index 0000000..5a0dd74 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-title.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-title.svelte new file mode 100644 index 0000000..a2e5761 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-trigger.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-trigger.svelte new file mode 100644 index 0000000..f1877d8 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer.svelte new file mode 100644 index 0000000..0cb57ff --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/index.ts b/apps/frontend/src/lib/components/ui/drawer/index.ts new file mode 100644 index 0000000..1656cac --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/index.ts @@ -0,0 +1,38 @@ +import Root from "./drawer.svelte"; +import Content from "./drawer-content.svelte"; +import Description from "./drawer-description.svelte"; +import Overlay from "./drawer-overlay.svelte"; +import Footer from "./drawer-footer.svelte"; +import Header from "./drawer-header.svelte"; +import Title from "./drawer-title.svelte"; +import NestedRoot from "./drawer-nested.svelte"; +import Close from "./drawer-close.svelte"; +import Trigger from "./drawer-trigger.svelte"; +import Portal from "./drawer-portal.svelte"; + +export { + Root, + NestedRoot, + Content, + Description, + Overlay, + Footer, + Header, + Title, + Trigger, + Portal, + Close, + + // + Root as Drawer, + NestedRoot as DrawerNestedRoot, + Content as DrawerContent, + Description as DrawerDescription, + Overlay as DrawerOverlay, + Footer as DrawerFooter, + Header as DrawerHeader, + Title as DrawerTitle, + Trigger as DrawerTrigger, + Portal as DrawerPortal, + Close as DrawerClose, +}; diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 0000000..e0e1971 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..6d9ef85 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,43 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..1e96782 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..433540f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..aca1f7b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..04cd110 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..9681c2b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 0000000..274cfef --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..189aef4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..ce2ad09 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,33 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..90f1b6f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..7c6e9c6 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..3f06dc4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..5f49d01 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte new file mode 100644 index 0000000..f044581 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..cb05344 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 0000000..cb4bc62 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/index.ts b/apps/frontend/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..7850c6a --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,54 @@ +import Root from "./dropdown-menu.svelte"; +import Sub from "./dropdown-menu-sub.svelte"; +import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Group from "./dropdown-menu-group.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import Trigger from "./dropdown-menu-trigger.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +import Portal from "./dropdown-menu-portal.svelte"; + +export { + CheckboxGroup, + CheckboxItem, + Content, + Portal, + Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Portal as DropdownMenuPortal, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/apps/frontend/src/lib/components/ui/empty/empty-content.svelte b/apps/frontend/src/lib/components/ui/empty/empty-content.svelte new file mode 100644 index 0000000..f5a9c68 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty-content.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/empty-description.svelte b/apps/frontend/src/lib/components/ui/empty/empty-description.svelte new file mode 100644 index 0000000..85a866c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty-description.svelte @@ -0,0 +1,23 @@ + + +
    a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/empty-header.svelte b/apps/frontend/src/lib/components/ui/empty/empty-header.svelte new file mode 100644 index 0000000..296eaf8 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/empty-media.svelte b/apps/frontend/src/lib/components/ui/empty/empty-media.svelte new file mode 100644 index 0000000..0b4e45d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty-media.svelte @@ -0,0 +1,41 @@ + + + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/empty-title.svelte b/apps/frontend/src/lib/components/ui/empty/empty-title.svelte new file mode 100644 index 0000000..8c237aa --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty-title.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/empty.svelte b/apps/frontend/src/lib/components/ui/empty/empty.svelte new file mode 100644 index 0000000..4ccf060 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/index.ts b/apps/frontend/src/lib/components/ui/empty/index.ts new file mode 100644 index 0000000..ae4c106 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/index.ts @@ -0,0 +1,22 @@ +import Root from "./empty.svelte"; +import Header from "./empty-header.svelte"; +import Media from "./empty-media.svelte"; +import Title from "./empty-title.svelte"; +import Description from "./empty-description.svelte"; +import Content from "./empty-content.svelte"; + +export { + Root, + Header, + Media, + Title, + Description, + Content, + // + Root as Empty, + Header as EmptyHeader, + Media as EmptyMedia, + Title as EmptyTitle, + Description as EmptyDescription, + Content as EmptyContent, +}; diff --git a/apps/frontend/src/lib/components/ui/field/field-content.svelte b/apps/frontend/src/lib/components/ui/field/field-content.svelte new file mode 100644 index 0000000..1b6535b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-content.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/field/field-description.svelte b/apps/frontend/src/lib/components/ui/field/field-description.svelte new file mode 100644 index 0000000..a0c9f06 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-description.svelte @@ -0,0 +1,25 @@ + + +

    a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...restProps} +> + {@render children?.()} +

    diff --git a/apps/frontend/src/lib/components/ui/field/field-error.svelte b/apps/frontend/src/lib/components/ui/field/field-error.svelte new file mode 100644 index 0000000..1d5cc5f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-error.svelte @@ -0,0 +1,58 @@ + + +{#if hasContent} + +{/if} diff --git a/apps/frontend/src/lib/components/ui/field/field-group.svelte b/apps/frontend/src/lib/components/ui/field/field-group.svelte new file mode 100644 index 0000000..e685427 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-group.svelte @@ -0,0 +1,23 @@ + + +
    [data-slot=field-group]]:gap-4", + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/field/field-label.svelte b/apps/frontend/src/lib/components/ui/field/field-label.svelte new file mode 100644 index 0000000..2ee431a --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-label.svelte @@ -0,0 +1,26 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/field/field-legend.svelte b/apps/frontend/src/lib/components/ui/field/field-legend.svelte new file mode 100644 index 0000000..3f1c50f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-legend.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/field/field-separator.svelte b/apps/frontend/src/lib/components/ui/field/field-separator.svelte new file mode 100644 index 0000000..12bcb77 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-separator.svelte @@ -0,0 +1,38 @@ + + +
    + + {#if children} + + {@render children()} + + {/if} +
    diff --git a/apps/frontend/src/lib/components/ui/field/field-set.svelte b/apps/frontend/src/lib/components/ui/field/field-set.svelte new file mode 100644 index 0000000..1d8e233 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-set.svelte @@ -0,0 +1,24 @@ + + +
    [data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/field/field-title.svelte b/apps/frontend/src/lib/components/ui/field/field-title.svelte new file mode 100644 index 0000000..5906044 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-title.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/field/field.svelte b/apps/frontend/src/lib/components/ui/field/field.svelte new file mode 100644 index 0000000..3284203 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field.svelte @@ -0,0 +1,53 @@ + + + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/field/index.ts b/apps/frontend/src/lib/components/ui/field/index.ts new file mode 100644 index 0000000..a644a95 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/index.ts @@ -0,0 +1,33 @@ +import Field from "./field.svelte"; +import Set from "./field-set.svelte"; +import Legend from "./field-legend.svelte"; +import Group from "./field-group.svelte"; +import Content from "./field-content.svelte"; +import Label from "./field-label.svelte"; +import Title from "./field-title.svelte"; +import Description from "./field-description.svelte"; +import Separator from "./field-separator.svelte"; +import Error from "./field-error.svelte"; + +export { + Field, + Set, + Legend, + Group, + Content, + Label, + Title, + Description, + Separator, + Error, + // + Set as FieldSet, + Legend as FieldLegend, + Group as FieldGroup, + Content as FieldContent, + Label as FieldLabel, + Title as FieldTitle, + Description as FieldDescription, + Separator as FieldSeparator, + Error as FieldError, +}; diff --git a/apps/frontend/src/lib/components/ui/form/form-button.svelte b/apps/frontend/src/lib/components/ui/form/form-button.svelte new file mode 100644 index 0000000..48d3936 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/form/form-button.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/input-group/input-group-input.svelte b/apps/frontend/src/lib/components/ui/input-group/input-group-input.svelte new file mode 100644 index 0000000..ded2655 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/input-group/input-group-input.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/input-group/input-group-text.svelte b/apps/frontend/src/lib/components/ui/input-group/input-group-text.svelte new file mode 100644 index 0000000..9c43dc4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/input-group/input-group-text.svelte @@ -0,0 +1,22 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/input-group/input-group-textarea.svelte b/apps/frontend/src/lib/components/ui/input-group/input-group-textarea.svelte new file mode 100644 index 0000000..91850ff --- /dev/null +++ b/apps/frontend/src/lib/components/ui/input-group/input-group-textarea.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/toggle-group/index.ts b/apps/frontend/src/lib/components/ui/toggle-group/index.ts new file mode 100644 index 0000000..12b14b9 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/toggle-group/index.ts @@ -0,0 +1,10 @@ +import Root from "./toggle-group.svelte"; +import Item from "./toggle-group-item.svelte"; + +export { + Root, + Item, + // + Root as ToggleGroup, + Item as ToggleGroupItem, +}; diff --git a/apps/frontend/src/lib/components/ui/toggle-group/toggle-group-item.svelte b/apps/frontend/src/lib/components/ui/toggle-group/toggle-group-item.svelte new file mode 100644 index 0000000..6d60b52 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/toggle-group/toggle-group-item.svelte @@ -0,0 +1,35 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/toggle-group/toggle-group.svelte b/apps/frontend/src/lib/components/ui/toggle-group/toggle-group.svelte new file mode 100644 index 0000000..106561c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/toggle-group/toggle-group.svelte @@ -0,0 +1,59 @@ + + + + + + diff --git a/apps/frontend/src/lib/components/ui/toggle/index.ts b/apps/frontend/src/lib/components/ui/toggle/index.ts new file mode 100644 index 0000000..8cb2936 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/toggle/index.ts @@ -0,0 +1,13 @@ +import Root from "./toggle.svelte"; +export { + toggleVariants, + type ToggleSize, + type ToggleVariant, + type ToggleVariants, +} from "./toggle.svelte"; + +export { + Root, + // + Root as Toggle, +}; diff --git a/apps/frontend/src/lib/components/ui/toggle/toggle.svelte b/apps/frontend/src/lib/components/ui/toggle/toggle.svelte new file mode 100644 index 0000000..56eb86b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/toggle/toggle.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/apps/frontend/src/lib/components/ui/tooltip/index.ts b/apps/frontend/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..1718604 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,19 @@ +import Root from "./tooltip.svelte"; +import Trigger from "./tooltip-trigger.svelte"; +import Content from "./tooltip-content.svelte"; +import Provider from "./tooltip-provider.svelte"; +import Portal from "./tooltip-portal.svelte"; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/apps/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte b/apps/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..788ec34 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,52 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/apps/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte b/apps/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 0000000..d234f7d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte b/apps/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 0000000..8150bef --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/apps/frontend/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..1acdaa4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/tooltip/tooltip.svelte b/apps/frontend/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 0000000..0b0f9ce --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/core/constants.ts b/apps/frontend/src/lib/core/constants.ts new file mode 100644 index 0000000..9d1cd2f --- /dev/null +++ b/apps/frontend/src/lib/core/constants.ts @@ -0,0 +1,6 @@ +import { PUBLIC_WS_SCRCPY_SVC_URL } from "$env/static/public"; + +export const WS_SCRCPY_URL = PUBLIC_WS_SCRCPY_SVC_URL; + +export const TRANSITION_COLORS = "transition-colors duration-150 ease-in-out"; +export const TRANSITION_ALL = "transition-all duration-150 ease-in-out"; diff --git a/apps/frontend/src/lib/core/server.utils.ts b/apps/frontend/src/lib/core/server.utils.ts new file mode 100644 index 0000000..450c7dd --- /dev/null +++ b/apps/frontend/src/lib/core/server.utils.ts @@ -0,0 +1,25 @@ +import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context"; +import type { Err } from "@pkg/result"; + +export async function getFlowExecCtxForRemoteFuncs( + locals: App.Locals, +): Promise { + return { + flowId: locals.flowId || crypto.randomUUID(), + userId: locals.user?.id, + sessionId: locals.session?.id, + }; +} + +export function unauthorized(fctx: FlowExecCtx) { + return { + data: null, + error: { + flowId: fctx.flowId, + code: "UNAUTHORIZED", + message: "User not authenticated", + description: "Please log in", + detail: "No user ID found in session", + } as Err, + }; +} diff --git a/apps/frontend/src/lib/domains/link/data.ts b/apps/frontend/src/lib/domains/link/data.ts new file mode 100644 index 0000000..481aa5e --- /dev/null +++ b/apps/frontend/src/lib/domains/link/data.ts @@ -0,0 +1,22 @@ +import * as v from "valibot"; + +export const tokenSchema = v.object({ + token: v.pipe(v.string(), v.minLength(1)), +}); + +export type OrchestratorPreparePayload = { + deviceId: number; + packageName: string; + linkToken: string; +}; + +export type OrchestratorPrepareResponse = { + data: { + deviceId: number; + containerId: string; + packageName: string; + serial: string; + status: string; + }; + error: null; +}; diff --git a/apps/frontend/src/lib/domains/link/link.remote.ts b/apps/frontend/src/lib/domains/link/link.remote.ts new file mode 100644 index 0000000..da016e4 --- /dev/null +++ b/apps/frontend/src/lib/domains/link/link.remote.ts @@ -0,0 +1,16 @@ +import { getFlowExecCtxForRemoteFuncs } from "$lib/core/server.utils"; +import { command, getRequestEvent, query } from "$app/server"; +import { prepareLinkFlow, resolveLinkFlow } from "./service"; +import { tokenSchema } from "./data"; + +export const resolveLinkSQ = query(tokenSchema, async ({ token }) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + return resolveLinkFlow(fctx, token); +}); + +export const prepareLinkSC = command(tokenSchema, async ({ token }) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + return prepareLinkFlow(fctx, token); +}); diff --git a/apps/frontend/src/lib/domains/link/orchestrator.service.ts b/apps/frontend/src/lib/domains/link/orchestrator.service.ts new file mode 100644 index 0000000..366b5c0 --- /dev/null +++ b/apps/frontend/src/lib/domains/link/orchestrator.service.ts @@ -0,0 +1,63 @@ +import type { + OrchestratorPreparePayload, + OrchestratorPrepareResponse, +} from "./data"; +import { createLinkFlowError, isErrPayload, normalizeBaseUrl } from "./utils"; +import { formatErrorDetail } from "@pkg/logger"; +import { ERROR_CODES } from "@pkg/result"; +import { settings } from "@pkg/settings"; + +export async function prepareOrchestratedSession( + flowId: string, + payload: OrchestratorPreparePayload, +): Promise { + const response = await fetch( + `${normalizeBaseUrl(settings.orchestratorApiUrl)}/internal/sessions/prepare`, + { + method: "POST", + headers: { + "content-type": "application/json", + "x-internal-api-key": settings.internalApiKey, + "x-flow-id": flowId, + }, + body: JSON.stringify(payload), + }, + ); + + let body: unknown = null; + try { + body = await response.json(); + } catch (error) { + throw createLinkFlowError( + flowId, + ERROR_CODES.EXTERNAL_SERVICE_ERROR, + "Invalid orchestrator response", + "The orchestrator returned a response that could not be parsed", + formatErrorDetail(error), + ); + } + + if (!response.ok && body && typeof body === "object" && "error" in body) { + const payloadError = (body as { error?: unknown }).error; + if (isErrPayload(payloadError)) { + throw payloadError; + } + } + + if ( + !body || + typeof body !== "object" || + !("data" in body) || + ("error" in body && body.error) + ) { + throw createLinkFlowError( + flowId, + ERROR_CODES.EXTERNAL_SERVICE_ERROR, + "Unexpected orchestrator response", + "The orchestrator response was missing the expected data payload", + JSON.stringify(body), + ); + } + + return body as OrchestratorPrepareResponse; +} diff --git a/apps/frontend/src/lib/domains/link/service.ts b/apps/frontend/src/lib/domains/link/service.ts new file mode 100644 index 0000000..873c8ab --- /dev/null +++ b/apps/frontend/src/lib/domains/link/service.ts @@ -0,0 +1,224 @@ +import { + createLinkFlowError, + isErrPayload, + missingDeviceError, + missingSupportedAppError, + unavailableDeviceError, +} from "./utils"; +import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context"; +import { getLinkController } from "@pkg/logic/domains/link/controller"; +import { prepareOrchestratedSession } from "./orchestrator.service"; +import { formatErrorDetail, logDomainEvent } from "@pkg/logger"; +import { DeviceStatus } from "@pkg/logic/domains/device/data"; +import { ERROR_CODES, type Err } from "@pkg/result"; +import { settings } from "@pkg/settings"; + +const lc = getLinkController(); + +type LinkSessionShape = { + id: number; + token: string; + status: string; + expiresAt: Date | null; + device: { + id: number; + title: string; + status: string; + inUse: boolean; + host: string; + wsPort: string; + }; + supportedApp: { + id: number; + title: string; + packageName: string; + }; +}; + +async function getPreparedLinkContext(fctx: FlowExecCtx, token: string) { + const linkResult = await lc.validate(fctx, token); + if (linkResult.isErr()) { + return { data: null, error: linkResult.error } as const; + } + + const link = linkResult.value; + + if (!link.device) { + return { + data: null, + error: missingDeviceError(fctx.flowId, token), + } as const; + } + + if (!link.supportedApp) { + return { + data: null, + error: missingSupportedAppError(fctx.flowId, token), + } as const; + } + + return { + data: { + id: link.id, + token: link.token, + status: link.status, + expiresAt: link.expiresAt, + device: { + id: link.device.id, + title: link.device.title, + status: link.device.status, + inUse: link.device.inUse, + host: link.device.host, + wsPort: link.device.wsPort, + }, + supportedApp: { + id: link.supportedApp.id, + title: link.supportedApp.title, + packageName: link.supportedApp.packageName, + }, + } satisfies LinkSessionShape, + error: null, + } as const; +} + +export async function resolveLinkFlow(fctx: FlowExecCtx, token: string) { + logDomainEvent({ + event: "front.link_resolve.started", + fctx, + meta: { token }, + }); + + const prepared = await getPreparedLinkContext(fctx, token); + if (prepared.error) { + logDomainEvent({ + level: "warn", + event: "front.link_resolve.rejected", + fctx, + error: prepared.error, + meta: { token }, + }); + + return { data: null, error: prepared.error }; + } + + const link = prepared.data; + + return { + data: { + link: { + id: link.id, + token: link.token, + status: link.status, + expiresAt: link.expiresAt, + }, + device: { + id: link.device.id, + title: link.device.title, + status: link.device.status, + inUse: link.device.inUse, + isAvailable: + link.device.status === DeviceStatus.ONLINE && + !link.device.inUse, + }, + supportedApp: link.supportedApp, + }, + error: null, + }; +} + +export async function prepareLinkFlow(fctx: FlowExecCtx, token: string) { + logDomainEvent({ + event: "front.link_prepare.started", + fctx, + meta: { token }, + }); + + const prepared = await getPreparedLinkContext(fctx, token); + if (prepared.error) { + logDomainEvent({ + level: "warn", + event: "front.link_prepare.rejected", + fctx, + error: prepared.error, + meta: { token }, + }); + + return { data: null, error: prepared.error }; + } + + const link = prepared.data; + + if (link.device.status !== DeviceStatus.ONLINE || link.device.inUse) { + return { + data: null, + error: unavailableDeviceError(fctx.flowId, link.device), + }; + } + + try { + const session = await prepareOrchestratedSession(fctx.flowId, { + deviceId: link.device.id, + packageName: link.supportedApp.packageName, + linkToken: link.token, + }); + + logDomainEvent({ + event: "front.link_prepare.succeeded", + fctx, + meta: { + token, + deviceId: link.device.id, + packageName: link.supportedApp.packageName, + }, + }); + + return { + data: { + link: { + id: link.id, + token: link.token, + }, + device: { + id: link.device.id, + title: link.device.title, + host: link.device.host, + wsPort: link.device.wsPort, + }, + supportedApp: link.supportedApp, + session: session.data, + }, + error: null, + }; + } catch (error) { + const err: Err = isErrPayload(error) + ? error + : createLinkFlowError( + fctx.flowId, + ERROR_CODES.EXTERNAL_SERVICE_ERROR, + "Failed to prepare session", + "The front server could not prepare the assigned Android session", + formatErrorDetail(error), + ); + + logDomainEvent({ + level: "error", + event: "front.link_prepare.failed", + fctx, + error: err, + meta: { + token, + orchestratorUrl: settings.orchestratorApiUrl, + }, + }); + + return { + data: null, + error: err.flowId + ? err + : { + ...err, + flowId: fctx.flowId, + }, + }; + } +} diff --git a/apps/frontend/src/lib/domains/link/utils.ts b/apps/frontend/src/lib/domains/link/utils.ts new file mode 100644 index 0000000..d491775 --- /dev/null +++ b/apps/frontend/src/lib/domains/link/utils.ts @@ -0,0 +1,63 @@ +import { ERROR_CODES, type Err } from "@pkg/result"; + +export function normalizeBaseUrl(url: string): string { + return url.endsWith("/") ? url.slice(0, -1) : url; +} + +export function createLinkFlowError( + flowId: string, + code: string, + message: string, + description: string, + detail: string, + actionable?: boolean, +): Err { + return { + flowId, + code, + message, + description, + detail, + actionable, + }; +} + +export function isErrPayload(value: unknown): value is Err { + return !!value && typeof value === "object" && "code" in value; +} + +export function missingDeviceError(flowId: string, token: string): Err { + return createLinkFlowError( + flowId, + ERROR_CODES.NOT_ALLOWED, + "Link is not assigned to a device", + "This link cannot start a session because no device is assigned", + `token=${token}`, + true, + ); +} + +export function missingSupportedAppError(flowId: string, token: string): Err { + return createLinkFlowError( + flowId, + ERROR_CODES.NOT_ALLOWED, + "Link is not assigned to an app", + "This link cannot start a session because no app is assigned", + `token=${token}`, + true, + ); +} + +export function unavailableDeviceError( + flowId: string, + device: { id: number; status: string; inUse: boolean }, +): Err { + return createLinkFlowError( + flowId, + ERROR_CODES.NOT_ALLOWED, + "Device is not available", + "The assigned device is currently busy or offline", + `deviceId=${device.id} status=${device.status} inUse=${device.inUse}`, + true, + ); +} diff --git a/apps/frontend/src/lib/global.stores.ts b/apps/frontend/src/lib/global.stores.ts new file mode 100644 index 0000000..88b4e38 --- /dev/null +++ b/apps/frontend/src/lib/global.stores.ts @@ -0,0 +1,14 @@ +import type { Session, User } from "@pkg/logic/domains/user/data"; +import type { AppSidebarItem } from "./core/constants"; +import { writable } from "svelte/store"; +import type { Router } from "$lib/api"; +import type { hc } from "hono/client"; + +export const breadcrumbs = writable>([ + { title: "Dashboard", url: "/dashboard" }, +]); + +export const apiClient = writable>>(undefined); + +export const user = writable(undefined); +export const session = writable(undefined); diff --git a/apps/frontend/src/lib/hooks/is-mobile.svelte.ts b/apps/frontend/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..4829c00 --- /dev/null +++ b/apps/frontend/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from "svelte/reactivity"; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/apps/frontend/src/lib/make-client.ts b/apps/frontend/src/lib/make-client.ts new file mode 100644 index 0000000..999475d --- /dev/null +++ b/apps/frontend/src/lib/make-client.ts @@ -0,0 +1,21 @@ +import type { Router } from "$lib/api"; +import { hc } from "hono/client"; + +let browserClient: ReturnType>; + +export const makeClient = (fetch: Window["fetch"]) => { + const isBrowser = typeof window !== "undefined"; + const origin = isBrowser ? window.location.origin : ""; + + if (isBrowser && browserClient) { + return browserClient; + } + + const client = hc(origin + "/api/v1", { fetch }); + + if (isBrowser) { + browserClient = client; + } + + return client; +}; diff --git a/apps/frontend/src/lib/utils.ts b/apps/frontend/src/lib/utils.ts new file mode 100644 index 0000000..55b3a91 --- /dev/null +++ b/apps/frontend/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/apps/frontend/src/routes/+layout.svelte b/apps/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..a374c05 --- /dev/null +++ b/apps/frontend/src/routes/+layout.svelte @@ -0,0 +1,20 @@ + + + + {$breadcrumbs[$breadcrumbs.length - 1]?.title ?? "Dashboard"} + + + + + + + +{@render children()} diff --git a/apps/frontend/src/routes/+page.svelte b/apps/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..f4d7b91 --- /dev/null +++ b/apps/frontend/src/routes/+page.svelte @@ -0,0 +1,7 @@ + + +base page to show the loading state diff --git a/apps/frontend/src/routes/layout.css b/apps/frontend/src/routes/layout.css new file mode 100644 index 0000000..e245be5 --- /dev/null +++ b/apps/frontend/src/routes/layout.css @@ -0,0 +1,195 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@plugin "@tailwindcss/forms"; +@plugin "@tailwindcss/typography"; + +@font-face { + font-family: "Manrope"; + src: url("/fonts/manrope-variable.ttf") format("truetype"); +} + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(0.994 0 0); + --foreground: oklch(0 0 0); + + --card: oklch(0.994 0 0); + --card-foreground: oklch(0 0 0); + + --popover: oklch(0.991 0 0); + --popover-foreground: oklch(0 0 0); + + /* --- main theme: teal --- */ + --primary: oklch(0.6 0.15 180); /* medium teal */ + --primary-foreground: oklch(0.99 0 0); + + --secondary: oklch(0.93 0.04 178); /* soft pale teal */ + --secondary-foreground: oklch(0.25 0.03 180); + + --muted: oklch(0.96 0.01 175); + --muted-foreground: oklch(0.4 0.01 178); + + --accent: oklch(0.86 0.07 175); /* teal accent */ + --accent-foreground: oklch(0.5 0.12 180); + + --destructive: oklch(0.63 0.18 25); + --destructive-foreground: oklch(1 0 0); + + --border: oklch(0.92 0.02 178); + --input: oklch(0.94 0 0); + --ring: oklch(0.6 0.15 180); + + /* charts — variety within teal spectrum */ + --chart-1: oklch(0.7 0.13 175); + --chart-2: oklch(0.6 0.15 180); + --chart-3: oklch(0.72 0.14 165); /* slightly more green-teal */ + --chart-4: oklch(0.65 0.12 190); /* slightly bluer teal */ + --chart-5: oklch(0.76 0.09 182); + + --sidebar: oklch(0.97 0.01 178); + --sidebar-foreground: oklch(0 0 0); + --sidebar-primary: oklch(0.6 0.15 180); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.92 0.02 178); + --sidebar-accent-foreground: oklch(0.2 0.02 180); + --sidebar-border: oklch(0.92 0.02 178); + --sidebar-ring: oklch(0.6 0.15 180); + + --font-sans: Plus Jakarta Sans, sans-serif; + --font-serif: Lora, serif; + --font-mono: IBM Plex Mono, monospace; + + --radius: 0.69rem; + + --shadow-2xs: 0px 2px 3px 0px hsl(0 0% 0% / 0.08); + --shadow-xs: 0px 2px 3px 0px hsl(0 0% 0% / 0.08); + --shadow-sm: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 1px 2px -1px hsl(0 0% 0% / 0.16); + --shadow: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 1px 2px -1px hsl(0 0% 0% / 0.16); + --shadow-md: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 2px 4px -1px hsl(0 0% 0% / 0.16); + --shadow-lg: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 4px 6px -1px hsl(0 0% 0% / 0.16); + --shadow-xl: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 8px 10px -1px hsl(0 0% 0% / 0.16); + --shadow-2xl: 0px 2px 3px 0px hsl(0 0% 0% / 0.4); + + --tracking-normal: -0.025em; + --spacing: 0.27rem; +} + +.dark { + --background: oklch(0.23 0.01 178); + --foreground: oklch(0.95 0 0); + + --card: oklch(0.25 0.015 178); + --card-foreground: oklch(0.95 0 0); + + --popover: oklch(0.25 0.015 178); + --popover-foreground: oklch(0.95 0 0); + + --primary: oklch(0.56 0.13 180); + --primary-foreground: oklch(0.97 0 0); + + --secondary: oklch(0.35 0.03 180); + --secondary-foreground: oklch(0.92 0 0); + + --muted: oklch(0.33 0.02 178); + --muted-foreground: oklch(0.7 0.01 178); + + --accent: oklch(0.44 0.08 178); + --accent-foreground: oklch(0.88 0.08 180); + + --destructive: oklch(0.7 0.17 25); + --destructive-foreground: oklch(1 0 0); + + --border: oklch(0.34 0.02 178); + --input: oklch(0.34 0.02 178); + --ring: oklch(0.56 0.13 180); + + --chart-1: oklch(0.68 0.12 175); + --chart-2: oklch(0.62 0.15 180); + --chart-3: oklch(0.7 0.11 165); + --chart-4: oklch(0.65 0.13 190); + --chart-5: oklch(0.72 0.09 182); + + --sidebar: oklch(0.2 0.01 178); + --sidebar-foreground: oklch(0.95 0 0); + --sidebar-primary: oklch(0.56 0.13 180); + --sidebar-primary-foreground: oklch(0.97 0 0); + --sidebar-accent: oklch(0.35 0.03 180); + --sidebar-accent-foreground: oklch(0.65 0.15 180); + --sidebar-border: oklch(0.34 0.02 178); + --sidebar-ring: oklch(0.65 0.15 180); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: var(--shadow-2xs); + --shadow-xs: var(--shadow-xs); + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-2xl: var(--shadow-2xl); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + font-family: "Manrope", sans-serif; + letter-spacing: var(--tracking-normal); + } +} diff --git a/apps/frontend/static/fonts/manrope-variable.ttf b/apps/frontend/static/fonts/manrope-variable.ttf new file mode 100644 index 0000000..f39ca39 Binary files /dev/null and b/apps/frontend/static/fonts/manrope-variable.ttf differ diff --git a/apps/frontend/static/images/avatar.png b/apps/frontend/static/images/avatar.png new file mode 100644 index 0000000..0989209 Binary files /dev/null and b/apps/frontend/static/images/avatar.png differ diff --git a/apps/frontend/static/robots.txt b/apps/frontend/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/apps/frontend/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/apps/frontend/svelte.config.js b/apps/frontend/svelte.config.js new file mode 100644 index 0000000..2afacd0 --- /dev/null +++ b/apps/frontend/svelte.config.js @@ -0,0 +1,20 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import adapter from "@sveltejs/adapter-node"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ out: "build" }), + experimental: { + remoteFunctions: true, + tracing: { server: true }, + instrumentation: { server: true }, + }, + }, + compilerOptions: { + experimental: { async: true }, + }, +}; + +export default config; diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/apps/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts new file mode 100644 index 0000000..1f01308 --- /dev/null +++ b/apps/frontend/vite.config.ts @@ -0,0 +1,46 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vitest/config"; +import tailwindcss from "@tailwindcss/vite"; +import Icons from "unplugin-icons/vite"; +import { resolve } from "path"; + +export default defineConfig({ + plugins: [sveltekit(), tailwindcss(), Icons({ compiler: "svelte" })], + + ssr: { + external: [ + "argon2", + "node-gyp-build", + "sharp", + "@img/sharp-linux-x64", + "@img/sharp-linuxmusl-x64", + "@img/sharp-wasm32", + ], + }, + + resolve: { + alias: { + "@core": resolve(__dirname, "../../packages/logic/core"), + "@domains": resolve(__dirname, "../../packages/logic/domains"), + "@/core": resolve(__dirname, "../../packages/logic/core"), + "@/domains": resolve(__dirname, "../../packages/logic/domains"), + }, + }, + + test: { + expect: { requireAssertions: true }, + + projects: [ + { + extends: "./vite.config.ts", + + test: { + name: "server", + environment: "node", + include: ["src/**/*.{test,spec}.{js,ts}"], + exclude: ["src/**/*.svelte.{test,spec}.{js,ts}"], + }, + }, + ], + }, +}); diff --git a/apps/main/src/lib/core/constants.ts b/apps/main/src/lib/core/constants.ts index 1638b68..ae52a54 100644 --- a/apps/main/src/lib/core/constants.ts +++ b/apps/main/src/lib/core/constants.ts @@ -1,4 +1,5 @@ import LayoutDashboard from "@lucide/svelte/icons/layout-dashboard"; +import { PUBLIC_WS_SCRCPY_SVC_URL } from "$env/static/public"; import AppWindow from "@lucide/svelte/icons/app-window"; import { BellRingIcon, Link } from "@lucide/svelte"; import UserCircle from "~icons/lucide/user-circle"; @@ -46,7 +47,7 @@ export const secondaryNavTree = [ }, ] as AppSidebarItem[]; -export const WS_SCRCPY_URL = "https://iotam-ws-scrcpy.snapyra.com"; +export const WS_SCRCPY_URL = PUBLIC_WS_SCRCPY_SVC_URL; export const COMPANY_NAME = "SaaS Template"; export const WEBSITE_URL = "https://company.com"; diff --git a/dockerfiles/front.Dockerfile b/dockerfiles/frontend.Dockerfile similarity index 67% rename from dockerfiles/front.Dockerfile rename to dockerfiles/frontend.Dockerfile index a59d75f..fea09d2 100644 --- a/dockerfiles/front.Dockerfile +++ b/dockerfiles/frontend.Dockerfile @@ -6,13 +6,13 @@ WORKDIR /app COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ -COPY apps/front/package.json ./apps/front/package.json +COPY apps/frontend/package.json ./apps/frontend/package.json COPY packages ./packages RUN pnpm install -COPY apps/front ./apps/front +COPY apps/frontend ./apps/frontend RUN pnpm install @@ -24,4 +24,4 @@ EXPOSE 3000 RUN chmod +x scripts/prod.start.sh -CMD ["/bin/sh", "scripts/prod.start.sh", "apps/front"] +CMD ["/bin/sh", "scripts/prod.start.sh", "apps/frontend"] diff --git a/memory.log.md b/memory.log.md index 155f72a..8c01d2d 100644 --- a/memory.log.md +++ b/memory.log.md @@ -148,3 +148,15 @@ Update rule: - Split `apps/front` into a thin entrypoint, shared `src/core/utils.ts`, and a dedicated `src/domains/links/{router,service}.ts` flow - Moved link resolve/prepare logic and orchestrator calling into the links domain service so `src/index.ts` only wires middleware and routes - Verified the cleaned `apps/front` app compiles cleanly with `tsc --noEmit` + +### 21 — Frontend Remote Function Migration + +- Migrated the legacy front server-side link flow into `apps/frontend` as SvelteKit remote functions in `src/lib/domains/link/link.remote.ts` +- Added frontend request flow IDs in `src/hooks.server.ts` so remote functions build `FlowExecCtx` from `event.locals` consistently +- Preserved the two server operations as a query/command split: link resolve remains read-only and session prepare remains the mutating flow that calls orchestrator + +### 22 — Frontend Link Domain Refactor + +- Reduced `apps/frontend/src/lib/domains/link/link.remote.ts` to thin remote-function entrypoints only +- Split link-domain server responsibilities into dedicated files: `link.data.ts`, `link.utils.ts`, `orchestrator.service.ts`, and `link.service.ts` +- Moved orchestrator calling, link validation shaping, and reusable error creation out of the remote layer for a cleaner domain boundary diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e20738c..ab16f77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,7 +27,167 @@ importers: specifier: ^5.9.3 version: 5.9.3 - apps/front: + apps/frontend: + dependencies: + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.70.1 + version: 0.70.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)) + '@opentelemetry/exporter-logs-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@pkg/db': + specifier: workspace:* + version: link:../../packages/db + '@pkg/keystore': + specifier: workspace:* + version: link:../../packages/keystore + '@pkg/logger': + specifier: workspace:* + version: link:../../packages/logger + '@pkg/logic': + specifier: workspace:* + version: link:../../packages/logic + '@pkg/result': + specifier: workspace:* + version: link:../../packages/result + '@pkg/settings': + specifier: workspace:* + version: link:../../packages/settings + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + import-in-the-middle: + specifier: ^3.0.0 + version: 3.0.0 + nanoid: + specifier: ^5.1.6 + version: 5.1.6 + neverthrow: + specifier: ^8.2.0 + version: 8.2.0 + valibot: + specifier: ^1.2.0 + version: 1.2.0(typescript@5.9.3) + devDependencies: + '@iconify/json': + specifier: ^2.2.434 + version: 2.2.444 + '@internationalized/date': + specifier: ^3.10.0 + version: 3.11.0 + '@lucide/svelte': + specifier: ^0.561.0 + version: 0.561.0(svelte@5.53.6) + '@sveltejs/adapter-node': + specifier: ^5.5.4 + version: 5.5.4(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) + '@sveltejs/kit': + specifier: ^2.53.4 + version: 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.4 + version: 6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tailwindcss/forms': + specifier: ^0.5.10 + version: 0.5.11(tailwindcss@4.2.1) + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.2.1) + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.2.1(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/table-core': + specifier: ^8.21.3 + version: 8.21.3 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 + bits-ui: + specifier: ^2.14.4 + version: 2.16.2(@internationalized/date@3.11.0)(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + embla-carousel-svelte: + specifier: ^8.6.0 + version: 8.6.0(svelte@5.53.6) + formsnap: + specifier: ^2.0.1 + version: 2.0.1(svelte@5.53.6)(sveltekit-superforms@2.30.0(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)) + layerchart: + specifier: 2.0.0-next.43 + version: 2.0.0-next.43(svelte@5.53.6) + mode-watcher: + specifier: ^1.1.0 + version: 1.1.0(svelte@5.53.6) + paneforge: + specifier: ^1.0.2 + version: 1.0.2(svelte@5.53.6) + prettier: + specifier: ^3.7.4 + version: 3.8.1 + prettier-plugin-svelte: + specifier: ^3.4.0 + version: 3.5.0(prettier@3.8.1)(svelte@5.53.6) + prettier-plugin-tailwindcss: + specifier: ^0.7.2 + version: 0.7.2(prettier-plugin-sort-imports@1.8.11(typescript@5.9.3))(prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.6))(prettier@3.8.1) + svelte: + specifier: ^5.53.6 + version: 5.53.6 + svelte-check: + specifier: ^4.4.4 + version: 4.4.4(picomatch@4.0.3)(svelte@5.53.6)(typescript@5.9.3) + svelte-sonner: + specifier: ^1.0.7 + version: 1.0.7(svelte@5.53.6) + sveltekit-superforms: + specifier: ^2.30.0 + version: 2.30.0(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3) + tailwind-merge: + specifier: ^3.4.0 + version: 3.5.0 + tailwind-variants: + specifier: ^3.2.2 + version: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.1) + tailwindcss: + specifier: ^4.1.18 + version: 4.2.1 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + unplugin-icons: + specifier: ^23.0.1 + version: 23.0.1(svelte@5.53.6) + vaul-svelte: + specifier: ^1.0.0-next.7 + version: 1.0.0-next.7(svelte@5.53.6) + vite: + specifier: ^7.2.6 + version: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: + specifier: ^4.0.15 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + + apps/frontlegacy: dependencies: '@hono/node-server': specifier: ^1.19.9