diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..12bc7fa7e05091e2d413d235ef695c1d95805c8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# compiled output +dist +tmp +out-tsc +*.tsbuildinfo +.expo +.expo-shared + +# dependencies +node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db + +.cursor/rules/nx-rules.mdc +.github/instructions/nx.instructions.md + +# Replit +.cache/ +.local/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000000000000000000000000000000000..61e34c2e5dde6b5b6b9d1a478945726c27993d4b --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +auto-install-peers=false +strict-peer-dependencies=false diff --git a/.replit b/.replit new file mode 100644 index 0000000000000000000000000000000000000000..8dc8fddff04309d1d915ff3502bb525051956bb4 --- /dev/null +++ b/.replit @@ -0,0 +1,26 @@ +modules = ["nodejs-24"] + +[[artifacts]] +id = "artifacts/api-server" + +[[artifacts]] +id = "artifacts/mockup-sandbox" + +[deployment] +router = "application" +deploymentTarget = "autoscale" + +[deployment.postBuild] +args = ["pnpm", "store", "prune"] +env = { "CI" = "true" } + +[workflows] +runButton = "Project" + +[agent] +stack = "PNPM_WORKSPACE" +expertMode = true + +[postMerge] +path = "scripts/post-merge.sh" +timeoutMs = 20000 diff --git a/.replitignore b/.replitignore new file mode 100644 index 0000000000000000000000000000000000000000..9eb019cd54e4f8ea1737967adb19aa1f53562cf2 --- /dev/null +++ b/.replitignore @@ -0,0 +1,5 @@ +# The format of this file is identical to `.dockerignore`. +# It is used to reduce the size of deployed images to make the process of publishing faster. + +# No need to store the pnpm store twice. +.local diff --git a/artifacts/api-server/.replit-artifact/artifact.toml b/artifacts/api-server/.replit-artifact/artifact.toml new file mode 100644 index 0000000000000000000000000000000000000000..814b9ad51b47b2302b7364ccb37849767aec680e --- /dev/null +++ b/artifacts/api-server/.replit-artifact/artifact.toml @@ -0,0 +1,32 @@ +kind = "api" +previewPath = "/api" # TODO - should be excluded from preview in the first place +title = "API Server" +version = "1.0.0" +id = "3B4_FFSkEVBkAeYMFRJ2e" + +[[services]] +localPort = 8080 +name = "API Server" +paths = ["/api"] + +[services.development] +run = "pnpm --filter @workspace/api-server run dev" + +[services.production] + +[services.production.build] +args = ["pnpm", "--filter", "@workspace/api-server", "run", "build"] + +[services.production.build.env] +NODE_ENV = "production" + +[services.production.run] +# we don't run through pnpm to make startup faster in production +args = ["node", "--enable-source-maps", "artifacts/api-server/dist/index.mjs"] + +[services.production.run.env] +PORT = "8080" +NODE_ENV = "production" + +[services.production.health.startup] +path = "/api/healthz" diff --git a/artifacts/api-server/build.mjs b/artifacts/api-server/build.mjs new file mode 100644 index 0000000000000000000000000000000000000000..86ebf7faaa9322b1ab55cdbbda7febaad27b35cd --- /dev/null +++ b/artifacts/api-server/build.mjs @@ -0,0 +1,126 @@ +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { build as esbuild } from "esbuild"; +import esbuildPluginPino from "esbuild-plugin-pino"; +import { rm } from "node:fs/promises"; + +// Plugins (e.g. 'esbuild-plugin-pino') may use `require` to resolve dependencies +globalThis.require = createRequire(import.meta.url); + +const artifactDir = path.dirname(fileURLToPath(import.meta.url)); + +async function buildAll() { + const distDir = path.resolve(artifactDir, "dist"); + await rm(distDir, { recursive: true, force: true }); + + await esbuild({ + entryPoints: [path.resolve(artifactDir, "src/index.ts")], + platform: "node", + bundle: true, + format: "esm", + outdir: distDir, + outExtension: { ".js": ".mjs" }, + logLevel: "info", + // Some packages may not be bundleable, so we externalize them, we can add more here as needed. + // Some of the packages below may not be imported or installed, but we're adding them in case they are in the future. + // Examples of unbundleable packages: + // - uses native modules and loads them dynamically (e.g. sharp) + // - use path traversal to read files (e.g. @google-cloud/secret-manager loads sibling .proto files) + external: [ + "*.node", + "sharp", + "better-sqlite3", + "sqlite3", + "canvas", + "bcrypt", + "argon2", + "fsevents", + "re2", + "farmhash", + "xxhash-addon", + "bufferutil", + "utf-8-validate", + "ssh2", + "cpu-features", + "dtrace-provider", + "isolated-vm", + "lightningcss", + "pg-native", + "oracledb", + "mongodb-client-encryption", + "nodemailer", + "handlebars", + "knex", + "typeorm", + "protobufjs", + "onnxruntime-node", + "@tensorflow/*", + "@prisma/client", + "@mikro-orm/*", + "@grpc/*", + "@swc/*", + "@aws-sdk/*", + "@azure/*", + "@opentelemetry/*", + "@google-cloud/*", + "@google/*", + "googleapis", + "firebase-admin", + "@parcel/watcher", + "@sentry/profiling-node", + "@tree-sitter/*", + "aws-sdk", + "classic-level", + "dd-trace", + "ffi-napi", + "grpc", + "hiredis", + "kerberos", + "leveldown", + "miniflare", + "mysql2", + "newrelic", + "odbc", + "piscina", + "realm", + "ref-napi", + "rocksdb", + "sass-embedded", + "sequelize", + "serialport", + "snappy", + "tinypool", + "usb", + "workerd", + "wrangler", + "zeromq", + "zeromq-prebuilt", + "playwright", + "puppeteer", + "puppeteer-core", + "electron", + ], + sourcemap: "linked", + plugins: [ + // pino relies on workers to handle logging, instead of externalizing it we use a plugin to handle it + esbuildPluginPino({ transports: ["pino-pretty"] }) + ], + // Make sure packages that are cjs only (e.g. express) but are bundled continue to work in our esm output file + banner: { + js: `import { createRequire as __bannerCrReq } from 'node:module'; +import __bannerPath from 'node:path'; +import __bannerUrl from 'node:url'; + +globalThis.require = __bannerCrReq(import.meta.url); +globalThis.__filename = __bannerUrl.fileURLToPath(import.meta.url); +globalThis.__dirname = __bannerPath.dirname(globalThis.__filename); + `, + }, + }); +} + +buildAll().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/artifacts/api-server/package.json b/artifacts/api-server/package.json new file mode 100644 index 0000000000000000000000000000000000000000..1c15a36a7c274ec5bf992948c958e3d31900ec63 --- /dev/null +++ b/artifacts/api-server/package.json @@ -0,0 +1,32 @@ +{ + "name": "@workspace/api-server", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "export NODE_ENV=development && pnpm run build && pnpm run start", + "build": "node ./build.mjs", + "start": "node --enable-source-maps ./dist/index.mjs", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@workspace/api-zod": "workspace:*", + "@workspace/db": "workspace:*", + "cookie-parser": "^1.4.7", + "cors": "^2", + "drizzle-orm": "catalog:", + "express": "^5", + "pino": "^9", + "pino-http": "^10" + }, + "devDependencies": { + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "catalog:", + "esbuild": "^0.27.3", + "esbuild-plugin-pino": "^2.3.3", + "pino-pretty": "^13", + "thread-stream": "3.1.0" + } +} diff --git a/artifacts/api-server/src/app.ts b/artifacts/api-server/src/app.ts new file mode 100644 index 0000000000000000000000000000000000000000..f32f71eb2abef174efb08a2cc118c7e76c6df951 --- /dev/null +++ b/artifacts/api-server/src/app.ts @@ -0,0 +1,34 @@ +import express, { type Express } from "express"; +import cors from "cors"; +import pinoHttp from "pino-http"; +import router from "./routes"; +import { logger } from "./lib/logger"; + +const app: Express = express(); + +app.use( + pinoHttp({ + logger, + serializers: { + req(req) { + return { + id: req.id, + method: req.method, + url: req.url?.split("?")[0], + }; + }, + res(res) { + return { + statusCode: res.statusCode, + }; + }, + }, + }), +); +app.use(cors()); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +app.use("/api", router); + +export default app; diff --git a/artifacts/api-server/src/index.ts b/artifacts/api-server/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b1f024dd070f40db5058650016bf63746994c0ac --- /dev/null +++ b/artifacts/api-server/src/index.ts @@ -0,0 +1,25 @@ +import app from "./app"; +import { logger } from "./lib/logger"; + +const rawPort = process.env["PORT"]; + +if (!rawPort) { + throw new Error( + "PORT environment variable is required but was not provided.", + ); +} + +const port = Number(rawPort); + +if (Number.isNaN(port) || port <= 0) { + throw new Error(`Invalid PORT value: "${rawPort}"`); +} + +app.listen(port, (err) => { + if (err) { + logger.error({ err }, "Error listening on port"); + process.exit(1); + } + + logger.info({ port }, "Server listening"); +}); diff --git a/artifacts/api-server/src/lib/.gitkeep b/artifacts/api-server/src/lib/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/artifacts/api-server/src/lib/logger.ts b/artifacts/api-server/src/lib/logger.ts new file mode 100644 index 0000000000000000000000000000000000000000..d9c67f79a82872af8336ab384014fa69bbe0695f --- /dev/null +++ b/artifacts/api-server/src/lib/logger.ts @@ -0,0 +1,20 @@ +import pino from "pino"; + +const isProduction = process.env.NODE_ENV === "production"; + +export const logger = pino({ + level: process.env.LOG_LEVEL ?? "info", + redact: [ + "req.headers.authorization", + "req.headers.cookie", + "res.headers['set-cookie']", + ], + ...(isProduction + ? {} + : { + transport: { + target: "pino-pretty", + options: { colorize: true }, + }, + }), +}); diff --git a/artifacts/api-server/src/middlewares/.gitkeep b/artifacts/api-server/src/middlewares/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/artifacts/api-server/src/routes/health.ts b/artifacts/api-server/src/routes/health.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0a144626055cbc728d5d813d63b389ae1e82bee --- /dev/null +++ b/artifacts/api-server/src/routes/health.ts @@ -0,0 +1,11 @@ +import { Router, type IRouter } from "express"; +import { HealthCheckResponse } from "@workspace/api-zod"; + +const router: IRouter = Router(); + +router.get("/healthz", (_req, res) => { + const data = HealthCheckResponse.parse({ status: "ok" }); + res.json(data); +}); + +export default router; diff --git a/artifacts/api-server/src/routes/index.ts b/artifacts/api-server/src/routes/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a1f77abda69515f1657b86f551c97570d05d6a3 --- /dev/null +++ b/artifacts/api-server/src/routes/index.ts @@ -0,0 +1,8 @@ +import { Router, type IRouter } from "express"; +import healthRouter from "./health"; + +const router: IRouter = Router(); + +router.use(healthRouter); + +export default router; diff --git a/artifacts/api-server/tsconfig.json b/artifacts/api-server/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..b60e718d970f7497ab0d119899c3b6b9cc881b32 --- /dev/null +++ b/artifacts/api-server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src"], + "references": [ + { + "path": "../../lib/db" + }, + { + "path": "../../lib/api-zod" + } + ] +} diff --git a/artifacts/mockup-sandbox/.replit-artifact/artifact.toml b/artifacts/mockup-sandbox/.replit-artifact/artifact.toml new file mode 100644 index 0000000000000000000000000000000000000000..4e9156ef3a9f736e06cdf80287c956271eb5fe9d --- /dev/null +++ b/artifacts/mockup-sandbox/.replit-artifact/artifact.toml @@ -0,0 +1,17 @@ +kind = "design" +previewPath = "/__mockup" +title = "Component Preview Server" +version = "1.0.0" +id = "XegfDyZt7HqfW2Bb8Ghoy" + +[[services]] +localPort = 8081 +name = "Component Preview Server" +paths = ["/__mockup"] + +[services.env] +PORT = "8081" +BASE_PATH = "/__mockup" + +[services.development] +run = "pnpm --filter @workspace/mockup-sandbox run dev" diff --git a/artifacts/mockup-sandbox/components.json b/artifacts/mockup-sandbox/components.json new file mode 100644 index 0000000000000000000000000000000000000000..ba0c18c0c790c4270fe1d596a400e4e24dcc643e --- /dev/null +++ b/artifacts/mockup-sandbox/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/artifacts/mockup-sandbox/index.html b/artifacts/mockup-sandbox/index.html new file mode 100644 index 0000000000000000000000000000000000000000..1397a638300ec564af33d63612ff217ea2c4012b --- /dev/null +++ b/artifacts/mockup-sandbox/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + Mockup Canvas + + + + + + + + +
+ + + diff --git a/artifacts/mockup-sandbox/mockupPreviewPlugin.ts b/artifacts/mockup-sandbox/mockupPreviewPlugin.ts new file mode 100644 index 0000000000000000000000000000000000000000..c09ed92cbbc0cbe8e5499340d22a576720eb3c8f --- /dev/null +++ b/artifacts/mockup-sandbox/mockupPreviewPlugin.ts @@ -0,0 +1,180 @@ +import { mkdirSync, writeFileSync } from "fs"; +import path from "path"; +import glob from "fast-glob"; +import chokidar from "chokidar"; +import type { FSWatcher } from "chokidar"; +import type { Plugin } from "vite"; + +const MOCKUPS_DIR = "src/components/mockups"; +const GENERATED_MODULE = "src/.generated/mockup-components.ts"; + +interface DiscoveredComponent { + globKey: string; + importPath: string; +} + +export function mockupPreviewPlugin(): Plugin { + let root = ""; + let currentSource = ""; + let watcher: FSWatcher | null = null; + + function getMockupsAbsDir(): string { + return path.join(root, MOCKUPS_DIR); + } + + function getGeneratedModuleAbsPath(): string { + return path.join(root, GENERATED_MODULE); + } + + function isMockupFile(absolutePath: string): boolean { + const rel = path.relative(getMockupsAbsDir(), absolutePath); + return ( + !rel.startsWith("..") && !path.isAbsolute(rel) && rel.endsWith(".tsx") + ); + } + + function isPreviewTarget(relativeToMockups: string): boolean { + return relativeToMockups + .split(path.sep) + .every((segment) => !segment.startsWith("_")); + } + + async function discoverComponents(): Promise> { + const files = await glob(`${MOCKUPS_DIR}/**/*.tsx`, { + cwd: root, + ignore: ["**/_*/**", "**/_*.tsx"], + }); + + return files.map((f) => ({ + globKey: "./" + f.slice("src/".length), + importPath: path.posix.relative("src/.generated", f), + })); + } + + function generateSource(components: Array): string { + const entries = components + .map( + (c) => + ` ${JSON.stringify(c.globKey)}: () => import(${JSON.stringify(c.importPath)})`, + ) + .join(",\n"); + + return [ + "// This file is auto-generated by mockupPreviewPlugin.ts.", + "type ModuleMap = Record Promise>>;", + "export const modules: ModuleMap = {", + entries, + "};", + "", + ].join("\n"); + } + + function shouldAutoRescan(pathname: string): boolean { + return ( + pathname.includes("/components/mockups/") || + pathname.includes("/.generated/mockup-components") + ); + } + + let refreshInFlight = false; + let refreshQueued = false; + + async function refresh(): Promise { + if (refreshInFlight) { + refreshQueued = true; + return false; + } + + refreshInFlight = true; + let changed = false; + try { + const components = await discoverComponents(); + const newSource = generateSource(components); + if (newSource !== currentSource) { + currentSource = newSource; + const generatedModuleAbsPath = getGeneratedModuleAbsPath(); + mkdirSync(path.dirname(generatedModuleAbsPath), { recursive: true }); + writeFileSync(generatedModuleAbsPath, currentSource); + changed = true; + } + } finally { + refreshInFlight = false; + } + + if (refreshQueued) { + refreshQueued = false; + const followUp = await refresh(); + return changed || followUp; + } + + return changed; + } + + async function onFileAddedOrRemoved(): Promise { + await refresh(); + } + + return { + name: "mockup-preview", + enforce: "pre", + + configResolved(config) { + root = config.root; + }, + + async buildStart() { + await refresh(); + }, + + async configureServer(viteServer) { + await refresh(); + + const mockupsAbsDir = getMockupsAbsDir(); + mkdirSync(mockupsAbsDir, { recursive: true }); + + watcher = chokidar.watch(mockupsAbsDir, { + ignoreInitial: true, + awaitWriteFinish: { + stabilityThreshold: 100, + pollInterval: 50, + }, + }); + + watcher.on("add", (file) => { + if ( + isMockupFile(file) && + isPreviewTarget(path.relative(mockupsAbsDir, file)) + ) { + void onFileAddedOrRemoved(); + } + }); + + watcher.on("unlink", (file) => { + if (isMockupFile(file)) { + void onFileAddedOrRemoved(); + } + }); + + viteServer.middlewares.use((req, res, next) => { + const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1"); + const pathname = requestUrl.pathname; + const originalEnd = res.end.bind(res); + + res.end = ((...args: Parameters) => { + if (res.statusCode === 404 && shouldAutoRescan(pathname)) { + void refresh(); + } + return originalEnd(...args); + }) as typeof res.end; + + next(); + }); + }, + + async closeWatcher() { + if (watcher) { + await watcher.close(); + } + }, + }; +} diff --git a/artifacts/mockup-sandbox/package.json b/artifacts/mockup-sandbox/package.json new file mode 100644 index 0000000000000000000000000000000000000000..20e280611ead90d8869b7460aedbaa2fb14b506b --- /dev/null +++ b/artifacts/mockup-sandbox/package.json @@ -0,0 +1,74 @@ +{ + "name": "@workspace/mockup-sandbox", + "version": "2.0.0", + "type": "module", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "devDependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.8", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toast": "^1.2.7", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@replit/vite-plugin-cartographer": "catalog:", + "@replit/vite-plugin-runtime-error-modal": "catalog:", + "@tailwindcss/vite": "catalog:", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-react": "catalog:", + "chokidar": "^4.0.3", + "class-variance-authority": "catalog:", + "clsx": "catalog:", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "fast-glob": "^3.3.3", + "framer-motion": "catalog:", + "input-otp": "^1.4.2", + "lucide-react": "catalog:", + "next-themes": "^0.4.6", + "react": "catalog:", + "react-day-picker": "^9.11.1", + "react-dom": "catalog:", + "react-hook-form": "^7.66.0", + "react-resizable-panels": "^2.1.9", + "recharts": "^2.15.4", + "sonner": "^2.0.7", + "tailwind-merge": "catalog:", + "tailwindcss": "catalog:", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2", + "vite": "catalog:", + "zod": "catalog:" + } +} diff --git a/artifacts/mockup-sandbox/src/.generated/mockup-components.ts b/artifacts/mockup-sandbox/src/.generated/mockup-components.ts new file mode 100644 index 0000000000000000000000000000000000000000..97c87e1fe941456236fbbaa8a06da5a8dbab649c --- /dev/null +++ b/artifacts/mockup-sandbox/src/.generated/mockup-components.ts @@ -0,0 +1,5 @@ +// This file is auto-generated by mockupPreviewPlugin.ts. +type ModuleMap = Record Promise>>; +export const modules: ModuleMap = { + +}; diff --git a/artifacts/mockup-sandbox/src/App.tsx b/artifacts/mockup-sandbox/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d9c2317f0d952aefe737473c57dc4afe359048a0 --- /dev/null +++ b/artifacts/mockup-sandbox/src/App.tsx @@ -0,0 +1,146 @@ +import { useEffect, useState, type ComponentType } from "react"; + +import { modules as discoveredModules } from "./.generated/mockup-components"; + +type ModuleMap = Record Promise>>; + +function _resolveComponent( + mod: Record, + name: string, +): ComponentType | undefined { + const fns = Object.values(mod).filter( + (v) => typeof v === "function", + ) as ComponentType[]; + return ( + (mod.default as ComponentType) || + (mod.Preview as ComponentType) || + (mod[name] as ComponentType) || + fns[fns.length - 1] + ); +} + +function PreviewRenderer({ + componentPath, + modules, +}: { + componentPath: string; + modules: ModuleMap; +}) { + const [Component, setComponent] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + + setComponent(null); + setError(null); + + async function loadComponent(): Promise { + const key = `./components/mockups/${componentPath}.tsx`; + const loader = modules[key]; + if (!loader) { + setError(`No component found at ${componentPath}.tsx`); + return; + } + + try { + const mod = await loader(); + if (cancelled) { + return; + } + const name = componentPath.split("/").pop()!; + const comp = _resolveComponent(mod, name); + if (!comp) { + setError( + `No exported React component found in ${componentPath}.tsx\n\nMake sure the file has at least one exported function component.`, + ); + return; + } + setComponent(() => comp); + } catch (e) { + if (cancelled) { + return; + } + + const message = e instanceof Error ? e.message : String(e); + setError(`Failed to load preview.\n${message}`); + } + } + + void loadComponent(); + + return () => { + cancelled = true; + }; + }, [componentPath, modules]); + + if (error) { + return ( +
+        {error}
+      
+ ); + } + + if (!Component) return null; + + return ; +} + +function getBasePath(): string { + return import.meta.env.BASE_URL.replace(/\/$/, ""); +} + +function getPreviewExamplePath(): string { + const basePath = getBasePath(); + return `${basePath}/preview/ComponentName`; +} + +function Gallery() { + return ( +
+
+

+ Component Preview Server +

+

+ This server renders individual components for the workspace canvas. +

+

+ Access component previews at{" "} + + {getPreviewExamplePath()} + +

+
+
+ ); +} + +function getPreviewPath(): string | null { + const basePath = getBasePath(); + const { pathname } = window.location; + const local = + basePath && pathname.startsWith(basePath) + ? pathname.slice(basePath.length) || "/" + : pathname; + const match = local.match(/^\/preview\/(.+)$/); + return match ? match[1] : null; +} + +function App() { + const previewPath = getPreviewPath(); + + if (previewPath) { + return ( + + ); + } + + return ; +} + +export default App; diff --git a/artifacts/mockup-sandbox/src/components/ui/accordion.tsx b/artifacts/mockup-sandbox/src/components/ui/accordion.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e1797c93458f3751a2e0f1189ff50d77f20ae3ed --- /dev/null +++ b/artifacts/mockup-sandbox/src/components/ui/accordion.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/artifacts/mockup-sandbox/src/components/ui/alert-dialog.tsx b/artifacts/mockup-sandbox/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fa2b4429f4303210462ad03f9f49280ac5ae07ce --- /dev/null +++ b/artifacts/mockup-sandbox/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/artifacts/mockup-sandbox/src/components/ui/alert.tsx b/artifacts/mockup-sandbox/src/components/ui/alert.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5afd41d142c95c74069ebb5460117e74ead3b9df --- /dev/null +++ b/artifacts/mockup-sandbox/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/artifacts/mockup-sandbox/src/components/ui/aspect-ratio.tsx b/artifacts/mockup-sandbox/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c4abbf37f217c715a0eaade7f45ac78600df419f --- /dev/null +++ b/artifacts/mockup-sandbox/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/artifacts/mockup-sandbox/src/components/ui/avatar.tsx b/artifacts/mockup-sandbox/src/components/ui/avatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..51e507ba9d08bcdbb1fb630498f1cbdf2bf50093 --- /dev/null +++ b/artifacts/mockup-sandbox/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/artifacts/mockup-sandbox/src/components/ui/badge.tsx b/artifacts/mockup-sandbox/src/components/ui/badge.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cfd176a05b00824f7a6a029b10bb935ef38d10bb --- /dev/null +++ b/artifacts/mockup-sandbox/src/components/ui/badge.tsx @@ -0,0 +1,37 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "whitespace-nowrap inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" + + " hover-elevate ", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow-xs", + secondary: + "border-transparent bg-secondary text-secondary-foreground", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow-xs", + outline: "text-foreground border [border-color:var(--badge-outline)]", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/artifacts/mockup-sandbox/src/components/ui/breadcrumb.tsx b/artifacts/mockup-sandbox/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000000000000000000000000000000000000..60e6c96f72f0350d08b47e4730cab8f3975dc853 --- /dev/null +++ b/artifacts/mockup-sandbox/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>