feat: turborepo monorepo scaffold and provisioning
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import { authOptions } from '@/lib/auth/authOptions';
|
|||||||
import { query } from '@/lib/db-postgres';
|
import { query } from '@/lib/db-postgres';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { createRepo, createWebhook, getRepo, listWebhooks, GITEA_ADMIN_USER_EXPORT } from '@/lib/gitea';
|
import { createRepo, createWebhook, getRepo, listWebhooks, GITEA_ADMIN_USER_EXPORT } from '@/lib/gitea';
|
||||||
|
import { pushTurborepoScaffold } from '@/lib/scaffold';
|
||||||
|
import { createProject as createCoolifyProject, createMonorepoAppService } from '@/lib/coolify';
|
||||||
import { provisionTheiaWorkspace } from '@/lib/cloud-run-workspace';
|
import { provisionTheiaWorkspace } from '@/lib/cloud-run-workspace';
|
||||||
import type { ProjectPhaseData, ProjectPhaseScores } from '@/lib/types/project-artifacts';
|
import type { ProjectPhaseData, ProjectPhaseScores } from '@/lib/types/project-artifacts';
|
||||||
|
|
||||||
@@ -73,7 +75,7 @@ export async function POST(request: Request) {
|
|||||||
repo = await createRepo(repoName, {
|
repo = await createRepo(repoName, {
|
||||||
description: `${projectName} — managed by Vibn`,
|
description: `${projectName} — managed by Vibn`,
|
||||||
private: true,
|
private: true,
|
||||||
auto_init: true,
|
auto_init: false,
|
||||||
});
|
});
|
||||||
console.log(`[API] Gitea repo created: ${GITEA_ADMIN_USER}/${repoName}`);
|
console.log(`[API] Gitea repo created: ${GITEA_ADMIN_USER}/${repoName}`);
|
||||||
} catch (createErr) {
|
} catch (createErr) {
|
||||||
@@ -93,6 +95,10 @@ export async function POST(request: Request) {
|
|||||||
giteaCloneUrl = repo.clone_url;
|
giteaCloneUrl = repo.clone_url;
|
||||||
giteaSshUrl = repo.ssh_url;
|
giteaSshUrl = repo.ssh_url;
|
||||||
|
|
||||||
|
// Push Turborepo monorepo scaffold as initial commit
|
||||||
|
await pushTurborepoScaffold(GITEA_ADMIN_USER, repoName, slug, projectName);
|
||||||
|
console.log(`[API] Turborepo scaffold pushed to ${giteaRepo}`);
|
||||||
|
|
||||||
// Register webhook — skip if one already points to this project
|
// Register webhook — skip if one already points to this project
|
||||||
const webhookUrl = `${APP_URL}/api/webhooks/gitea?projectId=${projectId}`;
|
const webhookUrl = `${APP_URL}/api/webhooks/gitea?projectId=${projectId}`;
|
||||||
const existingHooks = await listWebhooks(GITEA_ADMIN_USER, repoName).catch(() => []);
|
const existingHooks = await listWebhooks(GITEA_ADMIN_USER, repoName).catch(() => []);
|
||||||
@@ -112,6 +118,43 @@ export async function POST(request: Request) {
|
|||||||
console.error('[API] Gitea provisioning failed (non-fatal):', giteaError);
|
console.error('[API] Gitea provisioning failed (non-fatal):', giteaError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 2. Provision Coolify project + per-app services
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
const APP_BASE_DOMAIN = process.env.APP_BASE_DOMAIN ?? 'vibnai.com';
|
||||||
|
const appNames = ['product', 'website', 'admin', 'storybook'] as const;
|
||||||
|
const provisionedApps: Array<{
|
||||||
|
name: string; path: string; coolifyServiceUuid: string | null; domain: string | null;
|
||||||
|
}> = appNames.map(name => ({ name, path: `apps/${name}`, coolifyServiceUuid: null, domain: null }));
|
||||||
|
|
||||||
|
if (giteaCloneUrl) {
|
||||||
|
try {
|
||||||
|
const coolifyProject = await createCoolifyProject(
|
||||||
|
projectName,
|
||||||
|
`Vibn project: ${projectName}`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const app of provisionedApps) {
|
||||||
|
try {
|
||||||
|
const domain = `${app.name}-${slug}.${APP_BASE_DOMAIN}`;
|
||||||
|
const service = await createMonorepoAppService({
|
||||||
|
projectUuid: coolifyProject.uuid,
|
||||||
|
appName: app.name,
|
||||||
|
gitRepo: giteaCloneUrl,
|
||||||
|
domain,
|
||||||
|
});
|
||||||
|
app.coolifyServiceUuid = service.uuid;
|
||||||
|
app.domain = domain;
|
||||||
|
console.log(`[API] Coolify service created: ${app.name} → ${domain}`);
|
||||||
|
} catch (appErr) {
|
||||||
|
console.error(`[API] Coolify service failed for ${app.name}:`, appErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (coolifyErr) {
|
||||||
|
console.error('[API] Coolify project provisioning failed (non-fatal):', coolifyErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
// 3. Provision dedicated Theia workspace
|
// 3. Provision dedicated Theia workspace
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
@@ -173,6 +216,9 @@ export async function POST(request: Request) {
|
|||||||
theiaError,
|
theiaError,
|
||||||
// Context snapshot (kept fresh by webhooks)
|
// Context snapshot (kept fresh by webhooks)
|
||||||
contextSnapshot: null,
|
contextSnapshot: null,
|
||||||
|
// Turborepo monorepo apps — each gets its own Coolify service
|
||||||
|
turboVersion: '2.3.3',
|
||||||
|
apps: provisionedApps,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|||||||
84
app/api/projects/deploy/route.ts
Normal file
84
app/api/projects/deploy/route.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* POST /api/projects/deploy
|
||||||
|
*
|
||||||
|
* Trigger a Coolify deployment for one or all apps in a project's monorepo.
|
||||||
|
*
|
||||||
|
* Body: { projectId: string, appName?: string }
|
||||||
|
* - If appName is omitted, all apps with a coolifyServiceUuid are deployed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth/authOptions';
|
||||||
|
import { query } from '@/lib/db-postgres';
|
||||||
|
import { deployApplication } from '@/lib/coolify';
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { projectId, appName } = await request.json() as {
|
||||||
|
projectId: string;
|
||||||
|
appName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
return NextResponse.json({ error: 'projectId is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await query<{ data: any }>(
|
||||||
|
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
|
||||||
|
[projectId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!rows[0]) {
|
||||||
|
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectData = rows[0].data;
|
||||||
|
|
||||||
|
if (projectData.userId !== session.user.id && projectData.workspace !== session.user.email?.split('@')[0]) {
|
||||||
|
// Allow if email matches workspace owner — loose check
|
||||||
|
}
|
||||||
|
|
||||||
|
const apps: Array<{ name: string; coolifyServiceUuid?: string | null }> =
|
||||||
|
projectData.apps ?? [];
|
||||||
|
|
||||||
|
const targets = appName
|
||||||
|
? apps.filter(a => a.name === appName)
|
||||||
|
: apps;
|
||||||
|
|
||||||
|
if (targets.length === 0) {
|
||||||
|
return NextResponse.json({ error: `No matching apps found${appName ? ` for "${appName}"` : ''}` }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: Array<{ app: string; deploymentUuid?: string; error?: string }> = [];
|
||||||
|
|
||||||
|
for (const app of targets) {
|
||||||
|
if (!app.coolifyServiceUuid) {
|
||||||
|
results.push({ app: app.name, error: 'No Coolify service UUID — app may not be provisioned yet' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const deployment = await deployApplication(app.coolifyServiceUuid);
|
||||||
|
results.push({ app: app.name, deploymentUuid: deployment.deployment_uuid });
|
||||||
|
console.log(`[API] Deploy triggered: ${app.name} → ${deployment.deployment_uuid}`);
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
results.push({ app: app.name, error: msg });
|
||||||
|
console.error(`[API] Deploy failed for ${app.name}:`, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ results });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[POST /api/projects/deploy] Error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to trigger deployment', details: error instanceof Error ? error.message : String(error) },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,11 +100,21 @@ export async function provisionTheiaWorkspace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Step 3: Set environment variables ────────────────────────────────────
|
// ── Step 3: Set environment variables ────────────────────────────────────
|
||||||
|
const giteaBaseUrl = process.env.GITEA_URL ?? 'https://git.vibnai.com';
|
||||||
|
const giteaToken = process.env.GITEA_TOKEN ?? '';
|
||||||
|
// Authenticated clone URL so Theia can git clone on startup
|
||||||
|
const giteaCloneUrl = giteaRepo
|
||||||
|
? `https://${giteaToken ? `oauth2:${giteaToken}@` : ''}${giteaBaseUrl.replace(/^https?:\/\//, '')}/${giteaRepo}.git`
|
||||||
|
: '';
|
||||||
|
|
||||||
const envVars = [
|
const envVars = [
|
||||||
{ key: 'VIBN_PROJECT_ID', value: projectId, is_preview: false },
|
{ key: 'VIBN_PROJECT_ID', value: projectId, is_preview: false },
|
||||||
{ key: 'VIBN_PROJECT_SLUG', value: slug, is_preview: false },
|
{ key: 'VIBN_PROJECT_SLUG', value: slug, is_preview: false },
|
||||||
{ key: 'GITEA_REPO', value: giteaRepo ?? '', is_preview: false },
|
{ key: 'GITEA_REPO', value: giteaRepo ?? '', is_preview: false },
|
||||||
{ key: 'GITEA_API_URL', value: process.env.GITEA_API_URL ?? 'https://git.vibnai.com', is_preview: false },
|
{ key: 'GITEA_CLONE_URL', value: giteaCloneUrl, is_preview: false },
|
||||||
|
{ key: 'GITEA_API_URL', value: giteaBaseUrl, is_preview: false },
|
||||||
|
// Theia opens this path as its workspace root
|
||||||
|
{ key: 'THEIA_WORKSPACE_ROOT', value: `/home/theia/${slug}`, is_preview: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
await fetch(`${COOLIFY_URL}/api/v1/applications/${appUuid}/envs/bulk`, {
|
await fetch(`${COOLIFY_URL}/api/v1/applications/${appUuid}/envs/bulk`, {
|
||||||
|
|||||||
@@ -149,6 +149,45 @@ export async function createApplication(opts: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Coolify service for one app inside a Turborepo monorepo.
|
||||||
|
* Build command uses `turbo run build --filter` to target just that app.
|
||||||
|
*/
|
||||||
|
export async function createMonorepoAppService(opts: {
|
||||||
|
projectUuid: string;
|
||||||
|
appName: string;
|
||||||
|
gitRepo: string;
|
||||||
|
gitBranch?: string;
|
||||||
|
domain: string;
|
||||||
|
serverUuid?: string;
|
||||||
|
environmentName?: string;
|
||||||
|
}): Promise<CoolifyApplication> {
|
||||||
|
const {
|
||||||
|
projectUuid, appName, gitRepo,
|
||||||
|
gitBranch = 'main',
|
||||||
|
domain,
|
||||||
|
serverUuid = '0',
|
||||||
|
environmentName = 'production',
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
return coolifyFetch(`/applications`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
project_uuid: projectUuid,
|
||||||
|
name: appName,
|
||||||
|
git_repository: gitRepo,
|
||||||
|
git_branch: gitBranch,
|
||||||
|
server_uuid: serverUuid,
|
||||||
|
environment_name: environmentName,
|
||||||
|
build_pack: 'nixpacks',
|
||||||
|
build_command: `pnpm install && turbo run build --filter=${appName}`,
|
||||||
|
start_command: `turbo run start --filter=${appName}`,
|
||||||
|
ports_exposes: '3000',
|
||||||
|
fqdn: `https://${domain}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function deployApplication(uuid: string): Promise<{ deployment_uuid: string }> {
|
export async function deployApplication(uuid: string): Promise<{ deployment_uuid: string }> {
|
||||||
return coolifyFetch(`/applications/${uuid}/deploy`, { method: 'POST' });
|
return coolifyFetch(`/applications/${uuid}/deploy`, { method: 'POST' });
|
||||||
}
|
}
|
||||||
|
|||||||
29
lib/gitea.ts
29
lib/gitea.ts
@@ -163,4 +163,33 @@ export async function verifyWebhookSignature(
|
|||||||
return expected === signature;
|
return expected === signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a single file to a repo via the Gitea contents API.
|
||||||
|
* Creates the file if it doesn't exist; updates it if it does.
|
||||||
|
*/
|
||||||
|
export async function giteaPushFile(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
path: string,
|
||||||
|
content: string,
|
||||||
|
message: string,
|
||||||
|
branch = 'main',
|
||||||
|
): Promise<void> {
|
||||||
|
const encoded = Buffer.from(content).toString('base64');
|
||||||
|
|
||||||
|
// Check if file already exists to get its SHA (required for updates)
|
||||||
|
let sha: string | undefined;
|
||||||
|
try {
|
||||||
|
const existing = await giteaFetch(`/repos/${owner}/${repo}/contents/${path}?ref=${branch}`);
|
||||||
|
sha = (existing as any)?.sha;
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist — create it
|
||||||
|
}
|
||||||
|
|
||||||
|
await giteaFetch(`/repos/${owner}/${repo}/contents/${path}`, {
|
||||||
|
method: sha ? 'PUT' : 'POST',
|
||||||
|
body: JSON.stringify({ message, content: encoded, branch, ...(sha ? { sha } : {}) }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const GITEA_ADMIN_USER_EXPORT = GITEA_ADMIN_USER;
|
export const GITEA_ADMIN_USER_EXPORT = GITEA_ADMIN_USER;
|
||||||
|
|||||||
55
lib/scaffold/index.ts
Normal file
55
lib/scaffold/index.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Turborepo scaffold loader
|
||||||
|
*
|
||||||
|
* Reads template files from lib/scaffold/turborepo/, replaces
|
||||||
|
* {{project-slug}} and {{project-name}} placeholders, then pushes
|
||||||
|
* each file to the user's Gitea repo via the contents API.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readdir, readFile } from 'fs/promises';
|
||||||
|
import { join, relative } from 'path';
|
||||||
|
import { giteaPushFile } from '@/lib/gitea';
|
||||||
|
|
||||||
|
const TEMPLATES_DIR = join(process.cwd(), 'lib/scaffold/turborepo');
|
||||||
|
|
||||||
|
const HIDDEN_FILES = ['.gitignore'];
|
||||||
|
|
||||||
|
async function walkDir(dir: string): Promise<string[]> {
|
||||||
|
const entries = await readdir(dir, { withFileTypes: true });
|
||||||
|
const files: string[] = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
files.push(...await walkDir(fullPath));
|
||||||
|
} else {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pushTurborepoScaffold(
|
||||||
|
owner: string,
|
||||||
|
repoName: string,
|
||||||
|
projectSlug: string,
|
||||||
|
projectName: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const allFiles = await walkDir(TEMPLATES_DIR);
|
||||||
|
|
||||||
|
for (const filePath of allFiles) {
|
||||||
|
let relPath = relative(TEMPLATES_DIR, filePath);
|
||||||
|
|
||||||
|
// Restore leading dot for hidden files (e.g. gitignore → .gitignore)
|
||||||
|
const basename = relPath.split('/').pop() ?? '';
|
||||||
|
if (HIDDEN_FILES.includes(`.${basename}`)) {
|
||||||
|
relPath = relPath.replace(basename, `.${basename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = await readFile(filePath, 'utf-8');
|
||||||
|
content = content
|
||||||
|
.replaceAll('{{project-slug}}', projectSlug)
|
||||||
|
.replaceAll('{{project-name}}', projectName);
|
||||||
|
|
||||||
|
await giteaPushFile(owner, repoName, relPath, content, `chore: scaffold ${relPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
lib/scaffold/turborepo/.gitignore
vendored
Normal file
16
lib/scaffold/turborepo/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
dist
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
build
|
||||||
|
storybook-static
|
||||||
|
.turbo
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
*.tsbuildinfo
|
||||||
|
coverage
|
||||||
43
lib/scaffold/turborepo/README.md
Normal file
43
lib/scaffold/turborepo/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# {{project-name}}
|
||||||
|
|
||||||
|
A full-stack monorepo managed by [Vibn](https://vibnai.com), powered by [Turborepo](https://turbo.build).
|
||||||
|
|
||||||
|
## Apps
|
||||||
|
|
||||||
|
| App | Path | Purpose |
|
||||||
|
|-----|------|---------|
|
||||||
|
| `product` | `apps/product` | Core user-facing application |
|
||||||
|
| `website` | `apps/website` | Marketing and landing pages |
|
||||||
|
| `admin` | `apps/admin` | Internal admin tooling |
|
||||||
|
| `storybook` | `apps/storybook` | Component browser and design system |
|
||||||
|
|
||||||
|
## Shared Packages
|
||||||
|
|
||||||
|
| Package | Path | Purpose |
|
||||||
|
|---------|------|---------|
|
||||||
|
| `@{{project-slug}}/ui` | `packages/ui` | Shared React components |
|
||||||
|
| `@{{project-slug}}/tokens` | `packages/tokens` | Design tokens (colors, spacing, typography) |
|
||||||
|
| `@{{project-slug}}/types` | `packages/types` | Shared TypeScript types |
|
||||||
|
| `@{{project-slug}}/config` | `packages/config` | Shared eslint and tsconfig |
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running a specific app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
turbo run dev --filter=product
|
||||||
|
turbo run dev --filter=website
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
# or a single app
|
||||||
|
turbo run build --filter=product
|
||||||
|
```
|
||||||
10
lib/scaffold/turborepo/apps/admin/next.config.ts
Normal file
10
lib/scaffold/turborepo/apps/admin/next.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
transpilePackages: [
|
||||||
|
'@{{project-slug}}/ui',
|
||||||
|
'@{{project-slug}}/tokens',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
27
lib/scaffold/turborepo/apps/admin/package.json
Normal file
27
lib/scaffold/turborepo/apps/admin/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@{{project-slug}}/admin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --port 3002",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@{{project-slug}}/ui": "workspace:*",
|
||||||
|
"@{{project-slug}}/tokens": "workspace:*",
|
||||||
|
"@{{project-slug}}/types": "workspace:*",
|
||||||
|
"next": "^15.1.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@{{project-slug}}/config": "workspace:*",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/scaffold/turborepo/apps/admin/tsconfig.json
Normal file
9
lib/scaffold/turborepo/apps/admin/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@{{project-slug}}/config/tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": { "@/*": ["./src/*"] }
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
10
lib/scaffold/turborepo/apps/product/next.config.ts
Normal file
10
lib/scaffold/turborepo/apps/product/next.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
transpilePackages: [
|
||||||
|
'@{{project-slug}}/ui',
|
||||||
|
'@{{project-slug}}/tokens',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
27
lib/scaffold/turborepo/apps/product/package.json
Normal file
27
lib/scaffold/turborepo/apps/product/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@{{project-slug}}/product",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --port 3000",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@{{project-slug}}/ui": "workspace:*",
|
||||||
|
"@{{project-slug}}/tokens": "workspace:*",
|
||||||
|
"@{{project-slug}}/types": "workspace:*",
|
||||||
|
"next": "^15.1.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@{{project-slug}}/config": "workspace:*",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/scaffold/turborepo/apps/product/tsconfig.json
Normal file
9
lib/scaffold/turborepo/apps/product/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@{{project-slug}}/config/tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": { "@/*": ["./src/*"] }
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
26
lib/scaffold/turborepo/apps/storybook/package.json
Normal file
26
lib/scaffold/turborepo/apps/storybook/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "@{{project-slug}}/storybook",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "storybook dev --port 6006",
|
||||||
|
"build": "storybook build --output-dir storybook-static",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@{{project-slug}}/ui": "workspace:*",
|
||||||
|
"@{{project-slug}}/tokens": "workspace:*",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@{{project-slug}}/config": "workspace:*",
|
||||||
|
"@storybook/addon-essentials": "^8.5.0",
|
||||||
|
"@storybook/react": "^8.5.0",
|
||||||
|
"@storybook/react-vite": "^8.5.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"storybook": "^8.5.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"vite": "^6.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
lib/scaffold/turborepo/apps/website/next.config.ts
Normal file
10
lib/scaffold/turborepo/apps/website/next.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
transpilePackages: [
|
||||||
|
'@{{project-slug}}/ui',
|
||||||
|
'@{{project-slug}}/tokens',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
27
lib/scaffold/turborepo/apps/website/package.json
Normal file
27
lib/scaffold/turborepo/apps/website/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@{{project-slug}}/website",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --port 3001",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@{{project-slug}}/ui": "workspace:*",
|
||||||
|
"@{{project-slug}}/tokens": "workspace:*",
|
||||||
|
"@{{project-slug}}/types": "workspace:*",
|
||||||
|
"next": "^15.1.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@{{project-slug}}/config": "workspace:*",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/scaffold/turborepo/apps/website/tsconfig.json
Normal file
9
lib/scaffold/turborepo/apps/website/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@{{project-slug}}/config/tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": { "@/*": ["./src/*"] }
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
17
lib/scaffold/turborepo/package.json
Normal file
17
lib/scaffold/turborepo/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "{{project-slug}}",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "turbo run build",
|
||||||
|
"dev": "turbo run dev",
|
||||||
|
"lint": "turbo run lint",
|
||||||
|
"type-check": "turbo run type-check",
|
||||||
|
"test": "turbo run test",
|
||||||
|
"clean": "turbo run clean && rm -rf node_modules"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"turbo": "^2.3.3"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.15.0",
|
||||||
|
"workspaces": ["apps/*", "packages/*"]
|
||||||
|
}
|
||||||
9
lib/scaffold/turborepo/packages/config/package.json
Normal file
9
lib/scaffold/turborepo/packages/config/package.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "@{{project-slug}}/config",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"exports": {
|
||||||
|
"./tsconfig.base.json": "./tsconfig.base.json",
|
||||||
|
"./eslint": "./eslint.config.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
lib/scaffold/turborepo/packages/config/tsconfig.base.json
Normal file
18
lib/scaffold/turborepo/packages/config/tsconfig.base.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"incremental": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib/scaffold/turborepo/packages/tokens/package.json
Normal file
14
lib/scaffold/turborepo/packages/tokens/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@{{project-slug}}/tokens",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./css": "./src/tokens.css"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@{{project-slug}}/config": "workspace:*",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
lib/scaffold/turborepo/packages/tokens/src/index.ts
Normal file
27
lib/scaffold/turborepo/packages/tokens/src/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export const colors = {
|
||||||
|
brand: {
|
||||||
|
50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc',
|
||||||
|
400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1',
|
||||||
|
800: '#075985', 900: '#0c4a6e',
|
||||||
|
},
|
||||||
|
neutral: {
|
||||||
|
50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8',
|
||||||
|
400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46',
|
||||||
|
800: '#27272a', 900: '#18181b',
|
||||||
|
},
|
||||||
|
success: { DEFAULT: '#22c55e', light: '#dcfce7', dark: '#15803d' },
|
||||||
|
warning: { DEFAULT: '#f59e0b', light: '#fef3c7', dark: '#b45309' },
|
||||||
|
error: { DEFAULT: '#ef4444', light: '#fee2e2', dark: '#b91c1c' },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const typography = {
|
||||||
|
fontFamily: {
|
||||||
|
sans: 'var(--font-sans, ui-sans-serif, system-ui, sans-serif)',
|
||||||
|
mono: 'var(--font-mono, ui-monospace, monospace)',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const radius = {
|
||||||
|
none: '0', sm: '0.125rem', DEFAULT: '0.25rem', md: '0.375rem',
|
||||||
|
lg: '0.5rem', xl: '0.75rem', '2xl': '1rem', full: '9999px',
|
||||||
|
} as const;
|
||||||
23
lib/scaffold/turborepo/packages/tokens/src/tokens.css
Normal file
23
lib/scaffold/turborepo/packages/tokens/src/tokens.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
:root {
|
||||||
|
--color-brand-50: #f0f9ff; --color-brand-100: #e0f2fe;
|
||||||
|
--color-brand-200: #bae6fd; --color-brand-300: #7dd3fc;
|
||||||
|
--color-brand-400: #38bdf8; --color-brand-500: #0ea5e9;
|
||||||
|
--color-brand-600: #0284c7; --color-brand-700: #0369a1;
|
||||||
|
--color-brand-800: #075985; --color-brand-900: #0c4a6e;
|
||||||
|
|
||||||
|
--color-neutral-50: #fafafa; --color-neutral-100: #f4f4f5;
|
||||||
|
--color-neutral-200: #e4e4e7; --color-neutral-300: #d4d4d8;
|
||||||
|
--color-neutral-400: #a1a1aa; --color-neutral-500: #71717a;
|
||||||
|
--color-neutral-600: #52525b; --color-neutral-700: #3f3f46;
|
||||||
|
--color-neutral-800: #27272a; --color-neutral-900: #18181b;
|
||||||
|
|
||||||
|
--color-success: #22c55e; --color-success-light: #dcfce7;
|
||||||
|
--color-warning: #f59e0b; --color-warning-light: #fef3c7;
|
||||||
|
--color-error: #ef4444; --color-error-light: #fee2e2;
|
||||||
|
|
||||||
|
--font-sans: ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-mono: ui-monospace, monospace;
|
||||||
|
|
||||||
|
--radius-sm: 0.125rem; --radius: 0.25rem; --radius-md: 0.375rem;
|
||||||
|
--radius-lg: 0.5rem; --radius-xl: 0.75rem; --radius-full: 9999px;
|
||||||
|
}
|
||||||
11
lib/scaffold/turborepo/packages/types/package.json
Normal file
11
lib/scaffold/turborepo/packages/types/package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@{{project-slug}}/types",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": { ".": "./src/index.ts" },
|
||||||
|
"devDependencies": {
|
||||||
|
"@{{project-slug}}/config": "workspace:*",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/scaffold/turborepo/packages/types/src/index.ts
Normal file
21
lib/scaffold/turborepo/packages/types/src/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export type ID = string;
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id: ID;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiResponse<T> =
|
||||||
|
| { data: T; error: null }
|
||||||
|
| { data: null; error: { message: string; code?: string } };
|
||||||
|
|
||||||
|
export type PaginatedResponse<T> = {
|
||||||
|
items: T[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
};
|
||||||
22
lib/scaffold/turborepo/packages/ui/package.json
Normal file
22
lib/scaffold/turborepo/packages/ui/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "@{{project-slug}}/ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./styles": "./src/styles.css"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@{{project-slug}}/tokens": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@{{project-slug}}/config": "workspace:*",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/scaffold/turborepo/packages/ui/src/components/Badge.tsx
Normal file
29
lib/scaffold/turborepo/packages/ui/src/components/Badge.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
type BadgeVariant = 'default' | 'success' | 'warning' | 'error' | 'brand';
|
||||||
|
|
||||||
|
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantClasses: Record<BadgeVariant, string> = {
|
||||||
|
default: 'bg-[var(--color-neutral-100)] text-[var(--color-neutral-700)]',
|
||||||
|
success: 'bg-[var(--color-success-light)] text-[var(--color-success-dark,#15803d)]',
|
||||||
|
warning: 'bg-[var(--color-warning-light)] text-[var(--color-warning-dark,#b45309)]',
|
||||||
|
error: 'bg-[var(--color-error-light)] text-[var(--color-error-dark,#b91c1c)]',
|
||||||
|
brand: 'bg-[var(--color-brand-100)] text-[var(--color-brand-700)]',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Badge({ variant = 'default', className = '', children, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
{...props}
|
||||||
|
className={[
|
||||||
|
'inline-flex items-center rounded-[var(--radius-full)] px-2.5 py-0.5 text-xs font-medium',
|
||||||
|
variantClasses[variant], className,
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
lib/scaffold/turborepo/packages/ui/src/components/Button.tsx
Normal file
49
lib/scaffold/turborepo/packages/ui/src/components/Button.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { ButtonHTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
type Variant = 'primary' | 'secondary' | 'ghost' | 'destructive';
|
||||||
|
type Size = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: Variant;
|
||||||
|
size?: Size;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantClasses: Record<Variant, string> = {
|
||||||
|
primary: 'bg-[var(--color-brand-600)] text-white hover:bg-[var(--color-brand-700)]',
|
||||||
|
secondary: 'bg-[var(--color-neutral-100)] text-[var(--color-neutral-900)] hover:bg-[var(--color-neutral-200)]',
|
||||||
|
ghost: 'bg-transparent text-[var(--color-neutral-700)] hover:bg-[var(--color-neutral-100)]',
|
||||||
|
destructive: 'bg-[var(--color-error)] text-white hover:opacity-90',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses: Record<Size, string> = {
|
||||||
|
sm: 'px-3 py-1.5 text-sm',
|
||||||
|
md: 'px-4 py-2 text-sm',
|
||||||
|
lg: 'px-5 py-2.5 text-base',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
variant = 'primary', size = 'md', loading = false,
|
||||||
|
disabled, className = '', children, ...props
|
||||||
|
}: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
disabled={disabled ?? loading}
|
||||||
|
className={[
|
||||||
|
'inline-flex items-center justify-center gap-2 rounded-[var(--radius-md)] font-medium transition-colors',
|
||||||
|
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||||
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||||
|
variantClasses[variant], sizeClasses[size], className,
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{loading && (
|
||||||
|
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
lib/scaffold/turborepo/packages/ui/src/components/Card.tsx
Normal file
21
lib/scaffold/turborepo/packages/ui/src/components/Card.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
const paddingClasses = { none: '', sm: 'p-3', md: 'p-5', lg: 'p-8' };
|
||||||
|
|
||||||
|
export function Card({ padding = 'md', className = '', children, ...props }: CardProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={[
|
||||||
|
'rounded-[var(--radius-xl)] border border-[var(--color-neutral-200)] bg-white shadow-sm',
|
||||||
|
paddingClasses[padding], className,
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
lib/scaffold/turborepo/packages/ui/src/components/Input.tsx
Normal file
35
lib/scaffold/turborepo/packages/ui/src/components/Input.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { InputHTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
hint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Input({ label, error, hint, className = '', id, ...props }: InputProps) {
|
||||||
|
const inputId = id ?? label?.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={inputId} className="text-sm font-medium text-[var(--color-neutral-700)]">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
{...props} id={inputId}
|
||||||
|
className={[
|
||||||
|
'w-full rounded-[var(--radius-md)] border px-3 py-2 text-sm transition-colors',
|
||||||
|
'placeholder:text-[var(--color-neutral-400)]',
|
||||||
|
'focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-500)] focus:border-transparent',
|
||||||
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||||
|
error
|
||||||
|
? 'border-[var(--color-error)] bg-[var(--color-error-light)]'
|
||||||
|
: 'border-[var(--color-neutral-300)] bg-white',
|
||||||
|
className,
|
||||||
|
].join(' ')}
|
||||||
|
/>
|
||||||
|
{error && <p className="text-xs text-[var(--color-error)]">{error}</p>}
|
||||||
|
{hint && !error && <p className="text-xs text-[var(--color-neutral-500)]">{hint}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
lib/scaffold/turborepo/packages/ui/src/index.ts
Normal file
4
lib/scaffold/turborepo/packages/ui/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { Button } from './components/Button.js';
|
||||||
|
export { Card } from './components/Card.js';
|
||||||
|
export { Input } from './components/Input.js';
|
||||||
|
export { Badge } from './components/Badge.js';
|
||||||
1
lib/scaffold/turborepo/packages/ui/src/styles.css
Normal file
1
lib/scaffold/turborepo/packages/ui/src/styles.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import '@{{project-slug}}/tokens/css';
|
||||||
23
lib/scaffold/turborepo/turbo.json
Normal file
23
lib/scaffold/turborepo/turbo.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"tasks": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": [".next/**", "!.next/cache/**", "dist/**", "storybook-static/**"]
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"dependsOn": ["^lint"]
|
||||||
|
},
|
||||||
|
"type-check": {
|
||||||
|
"dependsOn": ["^type-check"]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": ["coverage/**"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,16 @@ export interface ProjectChatContext {
|
|||||||
q3?: string;
|
q3?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
};
|
};
|
||||||
|
/** Gitea monorepo */
|
||||||
|
giteaRepo?: string | null;
|
||||||
|
giteaRepoUrl?: string | null;
|
||||||
|
/** Turborepo apps */
|
||||||
|
apps?: Array<{
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
domain?: string | null;
|
||||||
|
coolifyServiceUuid?: string | null;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Phase-specific artifacts */
|
/** Phase-specific artifacts */
|
||||||
@@ -261,6 +271,9 @@ export async function buildProjectContextForChat(
|
|||||||
githubRepoUrl: projectData.githubRepoUrl ?? null,
|
githubRepoUrl: projectData.githubRepoUrl ?? null,
|
||||||
extensionLinked: projectData.extensionLinked ?? false,
|
extensionLinked: projectData.extensionLinked ?? false,
|
||||||
visionAnswers: projectData.visionAnswers ?? {},
|
visionAnswers: projectData.visionAnswers ?? {},
|
||||||
|
giteaRepo: projectData.giteaRepo ?? null,
|
||||||
|
giteaRepoUrl: projectData.giteaRepoUrl ?? null,
|
||||||
|
apps: projectData.apps ?? [],
|
||||||
},
|
},
|
||||||
phaseData: {
|
phaseData: {
|
||||||
canonicalProductModel: projectData.phaseData?.canonicalProductModel ?? null,
|
canonicalProductModel: projectData.phaseData?.canonicalProductModel ?? null,
|
||||||
@@ -342,6 +355,19 @@ export function formatContextForPrompt(context: ProjectChatContext): string {
|
|||||||
`Phase: ${context.project.currentPhase} (${context.project.phaseStatus})`
|
`Phase: ${context.project.currentPhase} (${context.project.phaseStatus})`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Monorepo info
|
||||||
|
if (context.project.giteaRepo) {
|
||||||
|
sections.push(`\n## Monorepo (Turborepo)`);
|
||||||
|
sections.push(`Repository: ${context.project.giteaRepoUrl ?? context.project.giteaRepo}`);
|
||||||
|
if (context.project.apps && context.project.apps.length > 0) {
|
||||||
|
sections.push(`Apps:`);
|
||||||
|
for (const app of context.project.apps) {
|
||||||
|
const domain = app.domain ? ` → https://${app.domain}` : '';
|
||||||
|
sections.push(` • ${app.name} (${app.path})${domain}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Knowledge summary
|
// Knowledge summary
|
||||||
if (context.knowledgeSummary.totalCount > 0) {
|
if (context.knowledgeSummary.totalCount > 0) {
|
||||||
sections.push(`\nKnowledge Items: ${context.knowledgeSummary.totalCount} total`);
|
sections.push(`\nKnowledge Items: ${context.knowledgeSummary.totalCount} total`);
|
||||||
|
|||||||
Reference in New Issue
Block a user