nucleus-core-ts 0.9.42 → 0.9.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Client/Proxy/httpProxy.js +8 -0
- package/infra/scripts/generate-project.ts +99 -43
- package/infra/templates/backend/src/index.ts +1 -1
- package/infra/templates/frontend/app/globals.css +1 -1
- package/infra/templates/frontend/app/ui-docs/page.tsx +52 -0
- package/infra/templates/frontend/code-and-design-principles.md +438 -0
- package/infra/templates/frontend/next.config.ts +7 -14
- package/package.json +1 -1
|
@@ -230,6 +230,11 @@ export function createHttpProxyHandler(config) {
|
|
|
230
230
|
const refreshCookieName = refreshConfig?.refreshCookieName ?? 'refresh_token';
|
|
231
231
|
const refreshToken = cookies[refreshCookieName];
|
|
232
232
|
let refreshSetCookies = [];
|
|
233
|
+
// --- Diagnostic logging for SSE/streaming paths ---
|
|
234
|
+
const isStreamingPath = path.includes('/predict/stream') || path.includes('/stream/resume');
|
|
235
|
+
if (isStreamingPath) {
|
|
236
|
+
logger.info(`[SSE:proxy] ${req.method} ${path} | accessToken: ${accessToken ? 'present' : 'MISSING'} | refreshToken: ${refreshToken ? 'present' : 'MISSING'} | refreshEnabled: ${refreshEnabled}`);
|
|
237
|
+
}
|
|
233
238
|
// --- PROACTIVE REFRESH: no access_token but refresh_token exists ---
|
|
234
239
|
if (refreshEnabled && !accessToken && refreshToken && target.injectTokenFromCookie) {
|
|
235
240
|
logger.info(`[TokenRefresh] No access_token, attempting proactive refresh for ${path}`);
|
|
@@ -334,6 +339,9 @@ export function createHttpProxyHandler(config) {
|
|
|
334
339
|
} catch (error) {
|
|
335
340
|
const duration = performance.now() - startTime;
|
|
336
341
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
342
|
+
if (isStreamingPath) {
|
|
343
|
+
logger.error(`[SSE:proxy] FETCH ERROR for ${path} after ${duration.toFixed(0)}ms: ${err.name} - ${err.message}`);
|
|
344
|
+
}
|
|
337
345
|
if (err.name === 'AbortError') {
|
|
338
346
|
logger.error(`Timeout after ${duration.toFixed(0)}ms: ${path}`);
|
|
339
347
|
config.onError?.(new Error('Request timeout'), path, 504);
|
|
@@ -101,6 +101,19 @@ function replaceTemplateVars(content: string, vars: TemplateVars): string {
|
|
|
101
101
|
return result
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
const BINARY_EXTENSIONS = new Set([
|
|
105
|
+
'.ico', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp',
|
|
106
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
107
|
+
'.mp3', '.mp4', '.wav', '.ogg', '.webm',
|
|
108
|
+
'.zip', '.gz', '.tar', '.pdf',
|
|
109
|
+
'.lock',
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
function isBinaryFile(filePath: string): boolean {
|
|
113
|
+
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase()
|
|
114
|
+
return BINARY_EXTENSIONS.has(ext)
|
|
115
|
+
}
|
|
116
|
+
|
|
104
117
|
function copyTemplateDir(srcDir: string, destDir: string, vars: TemplateVars, skipPatterns: string[] = []) {
|
|
105
118
|
if (!existsSync(srcDir)) return
|
|
106
119
|
|
|
@@ -115,6 +128,8 @@ function copyTemplateDir(srcDir: string, destDir: string, vars: TemplateVars, sk
|
|
|
115
128
|
|
|
116
129
|
if (stat.isDirectory()) {
|
|
117
130
|
copyTemplateDir(srcPath, destPath, vars, skipPatterns)
|
|
131
|
+
} else if (isBinaryFile(srcPath)) {
|
|
132
|
+
writeFileSync(destPath, readFileSync(srcPath))
|
|
118
133
|
} else {
|
|
119
134
|
const content = readFileSync(srcPath, 'utf-8')
|
|
120
135
|
const processed = replaceTemplateVars(content, vars)
|
|
@@ -224,19 +239,24 @@ function generateBackend(
|
|
|
224
239
|
writeFileSync(join(srcDir, 'config.json'), JSON.stringify(config, null, 2))
|
|
225
240
|
logSuccess('src/config.json written')
|
|
226
241
|
|
|
227
|
-
// Write .env for local dev
|
|
228
|
-
const
|
|
242
|
+
// Write .env for local dev — read token secret env var names from config
|
|
243
|
+
const auth = config.authentication as Record<string, Record<string, string>> | undefined
|
|
244
|
+
const accessSecretName = auth?.accessToken?.secret || 'ACCESS_TOKEN_SECRET'
|
|
245
|
+
const refreshSecretName = auth?.refreshToken?.secret || 'REFRESH_TOKEN_SECRET'
|
|
246
|
+
const sessionSecretName = auth?.sessionToken?.secret || 'SESSION_TOKEN_SECRET'
|
|
247
|
+
|
|
248
|
+
const envLines = [
|
|
229
249
|
`DATABASE_URL=postgresql://postgres:postgres@localhost:5432/${projectName}`,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
`${accessSecretName}=${generateSecret()}`,
|
|
251
|
+
`${refreshSecretName}=${generateSecret()}`,
|
|
252
|
+
`${sessionSecretName}=${generateSecret()}`,
|
|
233
253
|
`REDIS_HOST=localhost`,
|
|
234
254
|
`REDIS_PORT=6379`,
|
|
235
255
|
`PORT=9000`,
|
|
236
256
|
`FRONTEND_URL=http://localhost:3000`,
|
|
237
257
|
`USE_DAPR=false`,
|
|
238
|
-
]
|
|
239
|
-
writeFileSync(join(beDir, '.env'),
|
|
258
|
+
]
|
|
259
|
+
writeFileSync(join(beDir, '.env'), envLines.join('\n') + '\n')
|
|
240
260
|
logSuccess('.env written (local dev)')
|
|
241
261
|
|
|
242
262
|
// K8s files are already copied from template with vars replaced
|
|
@@ -280,15 +300,16 @@ function generateFrontend(
|
|
|
280
300
|
logSuccess('lib/api/config.ts written (from config.json)')
|
|
281
301
|
|
|
282
302
|
// Write .env for production build (non-secret build-time vars)
|
|
303
|
+
// NOTE: PORT and HOSTNAME are NOT set here — they come from K8s configmap in prod.
|
|
304
|
+
// In local dev, server.ts defaults to port 4000 and Next.js to 3000 — no conflict.
|
|
283
305
|
const envContent = [
|
|
284
306
|
`# Production build environment variables`,
|
|
285
307
|
`# These are injected via Docker build args in the pipeline`,
|
|
286
308
|
`# DO NOT put secrets here — use K8s Secrets`,
|
|
309
|
+
`# PORT and HOSTNAME are set by K8s configmap, not here`,
|
|
287
310
|
`API_BASE_URL=http://${apiProjectName}-service.${vars.NAMESPACE}.svc.cluster.local:3000`,
|
|
288
311
|
`BACKEND_API_URL=http://${apiProjectName}-service.${vars.NAMESPACE}.svc.cluster.local:3000`,
|
|
289
312
|
`ALLOWED_ORIGINS=${vars.DOMAIN}`,
|
|
290
|
-
`PORT=3000`,
|
|
291
|
-
`HOSTNAME=0.0.0.0`,
|
|
292
313
|
].join('\n')
|
|
293
314
|
writeFileSync(join(feDir, '.env'), envContent + '\n')
|
|
294
315
|
logSuccess('.env written (build/prod)')
|
|
@@ -446,6 +467,62 @@ async function main() {
|
|
|
446
467
|
generateFrontend(outputDir, feProjectName, apiProjectName, config, feVars, feTemplateDir)
|
|
447
468
|
}
|
|
448
469
|
|
|
470
|
+
// ── Post-scaffold automation ──
|
|
471
|
+
logStep('Installing dependencies & setting up...')
|
|
472
|
+
|
|
473
|
+
const beDir = join(outputDir, apiProjectName)
|
|
474
|
+
|
|
475
|
+
// Backend: bun install
|
|
476
|
+
logStep(`[API] bun install`)
|
|
477
|
+
const installBe = Bun.spawnSync(['bun', 'install'], { cwd: beDir, stdout: 'pipe', stderr: 'pipe' })
|
|
478
|
+
if (installBe.exitCode === 0) {
|
|
479
|
+
logSuccess('bun install completed')
|
|
480
|
+
} else {
|
|
481
|
+
logError(`bun install failed: ${installBe.stderr.toString().slice(0, 200)}`)
|
|
482
|
+
process.exit(1)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Backend: create database (best-effort — skip if createdb not found or DB already exists)
|
|
486
|
+
logStep(`[API] Creating database: ${apiProjectName}`)
|
|
487
|
+
const createDb = Bun.spawnSync(['createdb', apiProjectName], { cwd: beDir, stdout: 'pipe', stderr: 'pipe' })
|
|
488
|
+
if (createDb.exitCode === 0) {
|
|
489
|
+
logSuccess(`Database "${apiProjectName}" created`)
|
|
490
|
+
} else {
|
|
491
|
+
const errMsg = createDb.stderr.toString()
|
|
492
|
+
if (errMsg.includes('already exists')) {
|
|
493
|
+
logWarn(`Database "${apiProjectName}" already exists — using existing`)
|
|
494
|
+
} else if (errMsg.includes('not found') || errMsg.includes('No such file')) {
|
|
495
|
+
logWarn('createdb not found — please create the database manually')
|
|
496
|
+
} else {
|
|
497
|
+
logWarn(`createdb: ${errMsg.trim().slice(0, 150)}`)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Backend: nucleus-generate (schema + relations)
|
|
502
|
+
logStep(`[API] Generating Drizzle schema`)
|
|
503
|
+
const generate = Bun.spawnSync(['bunx', 'nucleus-generate', 'src/config.json', 'src/drizzle'], { cwd: beDir, stdout: 'pipe', stderr: 'pipe' })
|
|
504
|
+
if (generate.exitCode === 0) {
|
|
505
|
+
logSuccess('Drizzle schema generated')
|
|
506
|
+
const genOut = generate.stdout.toString()
|
|
507
|
+
if (genOut) log(` ${DIM}${genOut.trim()}${RESET}`)
|
|
508
|
+
} else {
|
|
509
|
+
logError(`nucleus-generate failed: ${generate.stderr.toString().slice(0, 200)}`)
|
|
510
|
+
process.exit(1)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Frontend: bun install
|
|
514
|
+
if (feCount > 0) {
|
|
515
|
+
const feDir = join(outputDir, feProjectName)
|
|
516
|
+
logStep(`[FE] bun install`)
|
|
517
|
+
const installFe = Bun.spawnSync(['bun', 'install'], { cwd: feDir, stdout: 'pipe', stderr: 'pipe' })
|
|
518
|
+
if (installFe.exitCode === 0) {
|
|
519
|
+
logSuccess('bun install completed')
|
|
520
|
+
} else {
|
|
521
|
+
logError(`bun install failed: ${installFe.stderr.toString().slice(0, 200)}`)
|
|
522
|
+
process.exit(1)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
449
526
|
// ── Summary ──
|
|
450
527
|
log(`\n${GREEN}${BOLD}╔══════════════════════════════════════════╗${RESET}`)
|
|
451
528
|
log(`${GREEN}${BOLD}║ Project scaffolding complete! ║${RESET}`)
|
|
@@ -454,48 +531,27 @@ async function main() {
|
|
|
454
531
|
log(`\n ${BOLD}Generated structure:${RESET}`)
|
|
455
532
|
log(` ${outputDir}/`)
|
|
456
533
|
log(` ├── ${apiProjectName}/`)
|
|
457
|
-
log(` │ ├── src
|
|
458
|
-
log(` │
|
|
459
|
-
log(` │ │ └── index.ts`)
|
|
460
|
-
log(` │ ├── k8s/`)
|
|
461
|
-
log(` │ │ ├── Dockerfile`)
|
|
462
|
-
log(` │ │ ├── configmap.yaml`)
|
|
463
|
-
log(` │ │ ├── deployment.yaml`)
|
|
464
|
-
log(` │ │ ├── service.yaml`)
|
|
465
|
-
log(` │ │ └── pvc.yaml`)
|
|
534
|
+
log(` │ ├── src/config.json + drizzle/schema.ts`)
|
|
535
|
+
log(` │ ├── k8s/ (Dockerfile, configmap, deployment, service, pvc)`)
|
|
466
536
|
log(` │ ├── azure-pipelines.yml`)
|
|
467
|
-
log(` │ ├── .env`)
|
|
468
|
-
log(` │
|
|
469
|
-
log(` │ └── package.json`)
|
|
470
|
-
|
|
537
|
+
log(` │ ├── .env + .dockerignore`)
|
|
538
|
+
log(` │ └── node_modules/ ${DIM}(installed)${RESET}`)
|
|
471
539
|
if (feCount > 0) {
|
|
472
540
|
log(` └── ${feProjectName}/`)
|
|
473
|
-
log(` ├── app
|
|
474
|
-
log(` ├──
|
|
475
|
-
log(` ├── k8s/`)
|
|
476
|
-
log(` │ ├── Dockerfile`)
|
|
477
|
-
log(` │ ├── configmap.yaml`)
|
|
478
|
-
log(` │ ├── deployment.yaml`)
|
|
479
|
-
log(` │ ├── service.yaml`)
|
|
480
|
-
log(` │ └── ingress.yaml`)
|
|
541
|
+
log(` ├── app/ + lib/api/config.ts`)
|
|
542
|
+
log(` ├── k8s/ (Dockerfile, configmap, deployment, service, ingress)`)
|
|
481
543
|
log(` ├── azure-pipelines.yml`)
|
|
482
|
-
log(` ├── .env`)
|
|
483
|
-
log(`
|
|
484
|
-
log(` ├── .dockerignore`)
|
|
485
|
-
log(` └── package.json`)
|
|
544
|
+
log(` ├── .env + .env.local + .dockerignore`)
|
|
545
|
+
log(` └── node_modules/ ${DIM}(installed)${RESET}`)
|
|
486
546
|
}
|
|
487
547
|
|
|
488
|
-
log(`\n ${BOLD}
|
|
489
|
-
log(`
|
|
490
|
-
log(` 2. ${CYAN}bunx nucleus-generate src/config.json src/drizzle${RESET}`)
|
|
491
|
-
log(` 3. Start a PostgreSQL + Redis instance`)
|
|
492
|
-
log(` 4. ${CYAN}bun run dev${RESET}`)
|
|
548
|
+
log(`\n ${BOLD}Ready to run:${RESET}`)
|
|
549
|
+
log(` ${CYAN}cd ${beDir} && bun run dev${RESET}`)
|
|
493
550
|
if (feCount > 0) {
|
|
494
|
-
log(`
|
|
495
|
-
log(` 6. ${CYAN}bun run dev${RESET}`)
|
|
551
|
+
log(` ${CYAN}cd ${join(outputDir, feProjectName)} && bun run dev${RESET}`)
|
|
496
552
|
}
|
|
497
|
-
log(`\n ${DIM}
|
|
498
|
-
log(`
|
|
553
|
+
log(`\n ${DIM}Prerequisites: PostgreSQL + Redis running locally.`)
|
|
554
|
+
log(` For deployment, configure Azure DevOps pipeline variables.${RESET}\n`)
|
|
499
555
|
}
|
|
500
556
|
|
|
501
557
|
main().catch((err) => {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import dynamic from "next/dynamic";
|
|
4
|
+
import { Suspense, useEffect, useState } from "react";
|
|
5
|
+
import type { ComponentEntry } from "nucleus-core-ts/fe";
|
|
6
|
+
|
|
7
|
+
const DesignSystemPage = dynamic(
|
|
8
|
+
() => import("nucleus-core-ts/fe").then((mod) => mod.DesignSystemPage),
|
|
9
|
+
{ ssr: false },
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const createDefaultRegistryImport = () =>
|
|
13
|
+
import("nucleus-core-ts/fe").then((mod) => mod.createDefaultRegistry);
|
|
14
|
+
|
|
15
|
+
function UIDocsContent() {
|
|
16
|
+
const [registry, setRegistry] = useState<ComponentEntry[]>([]);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
createDefaultRegistryImport().then((fn) => {
|
|
20
|
+
setRegistry(fn());
|
|
21
|
+
});
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
if (registry.length === 0) {
|
|
25
|
+
return (
|
|
26
|
+
<section className="min-h-screen flex items-center justify-center bg-zinc-50 dark:bg-zinc-950">
|
|
27
|
+
<section className="animate-pulse flex flex-col items-center gap-3">
|
|
28
|
+
<span className="w-10 h-10 rounded-full border-2 border-zinc-300 dark:border-zinc-700 border-t-zinc-900 dark:border-t-white animate-spin" />
|
|
29
|
+
<span className="text-sm text-zinc-400 dark:text-zinc-500">
|
|
30
|
+
Loading components...
|
|
31
|
+
</span>
|
|
32
|
+
</section>
|
|
33
|
+
</section>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return <DesignSystemPage registry={registry} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function UIDocsPage() {
|
|
41
|
+
return (
|
|
42
|
+
<Suspense
|
|
43
|
+
fallback={
|
|
44
|
+
<section className="min-h-screen flex items-center justify-center">
|
|
45
|
+
<span className="animate-spin rounded-full h-8 w-8 border-b-2 border-zinc-900 dark:border-white" />
|
|
46
|
+
</section>
|
|
47
|
+
}
|
|
48
|
+
>
|
|
49
|
+
<UIDocsContent />
|
|
50
|
+
</Suspense>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# Nucleus Front-End Code & Design Principles
|
|
2
|
+
|
|
3
|
+
> **IMPORTANT**: These principles are **absolute, non-negotiable, and mandatory** for all front-end development. Zero exceptions. Any deviation must be corrected immediately — even if it requires extensive refactoring.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Build & Tooling
|
|
8
|
+
|
|
9
|
+
| Requirement | Specification |
|
|
10
|
+
|-------------|---------------|
|
|
11
|
+
| **Package Manager** | Bun exclusively |
|
|
12
|
+
| **Bundler** | Turbopack only (`next dev --turbopack`) |
|
|
13
|
+
| **Forbidden** | Webpack config, custom package resolution entries |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. Responsive Design — Mobile-First, 6 Breakpoints
|
|
18
|
+
|
|
19
|
+
### 2.1 The Rule
|
|
20
|
+
|
|
21
|
+
**Every single component, every single page, every single layout MUST be built mobile-first with all 6 breakpoints.** This is not a suggestion — it is a hard requirement. No component ships without responsive coverage.
|
|
22
|
+
|
|
23
|
+
### 2.2 Breakpoint System
|
|
24
|
+
|
|
25
|
+
| Breakpoint | Name | Tailwind | Target |
|
|
26
|
+
|-----------|------|----------|--------|
|
|
27
|
+
| **0px** | `base` | (default) | Small phones (iPhone SE) |
|
|
28
|
+
| **480px** | `xs` | `xs:` | Large phones (iPhone 14 Pro Max) |
|
|
29
|
+
| **640px** | `sm` | `sm:` | Small tablets (iPad Mini portrait) |
|
|
30
|
+
| **768px** | `md` | `md:` | Large tablets (iPad Pro portrait) |
|
|
31
|
+
| **1024px** | `lg` | `lg:` | Small desktop / tablet landscape |
|
|
32
|
+
| **1280px** | `xl` | `xl:` | Large desktop |
|
|
33
|
+
|
|
34
|
+
### 2.3 How to Write Mobile-First CSS
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// ✅ CORRECT — mobile-first: base styles are for phones, then override upward
|
|
38
|
+
<section className="
|
|
39
|
+
flex flex-col gap-2 p-4 text-sm
|
|
40
|
+
xs:gap-3
|
|
41
|
+
sm:flex-row sm:gap-4 sm:p-6
|
|
42
|
+
md:p-8 md:text-base
|
|
43
|
+
lg:max-w-5xl lg:mx-auto lg:gap-6
|
|
44
|
+
xl:max-w-7xl xl:p-12
|
|
45
|
+
">
|
|
46
|
+
|
|
47
|
+
// ❌ WRONG — desktop-first: writing for desktop then trying to "fix" mobile
|
|
48
|
+
<section className="max-w-7xl mx-auto p-12 flex gap-6 md:flex-col md:p-4 sm:p-2">
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2.4 Enforcement Checklist
|
|
52
|
+
|
|
53
|
+
Before ANY component is considered complete:
|
|
54
|
+
|
|
55
|
+
- [ ] Renders correctly on 320px (small phone)
|
|
56
|
+
- [ ] Renders correctly on 480px (large phone)
|
|
57
|
+
- [ ] Renders correctly on 640px (small tablet)
|
|
58
|
+
- [ ] Renders correctly on 768px (large tablet)
|
|
59
|
+
- [ ] Renders correctly on 1024px (small desktop)
|
|
60
|
+
- [ ] Renders correctly on 1280px (large desktop)
|
|
61
|
+
|
|
62
|
+
**If even ONE breakpoint is broken, the component is not done.**
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 3. Theme System — Light-First, Dark Support
|
|
67
|
+
|
|
68
|
+
### 3.1 The Rule
|
|
69
|
+
|
|
70
|
+
**Light theme is the default. Dark theme is the override. Every visible element MUST have both light AND dark styles.** No element may appear unstyled, invisible, or unreadable in either theme.
|
|
71
|
+
|
|
72
|
+
### 3.2 How to Write Light-First Theme Styles
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// ✅ CORRECT — light-first: base is light, dark: overrides
|
|
76
|
+
<section className="
|
|
77
|
+
bg-white text-gray-900 border-gray-200
|
|
78
|
+
dark:bg-gray-900 dark:text-gray-100 dark:border-gray-800
|
|
79
|
+
">
|
|
80
|
+
|
|
81
|
+
// ❌ WRONG — dark-only: no light mode styles
|
|
82
|
+
<section className="dark:bg-gray-900 dark:text-white">
|
|
83
|
+
|
|
84
|
+
// ❌ WRONG — missing dark variant
|
|
85
|
+
<section className="bg-white text-gray-900 border-gray-200">
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3.3 Theme Enforcement Rules
|
|
89
|
+
|
|
90
|
+
| Rule | Detail |
|
|
91
|
+
|------|--------|
|
|
92
|
+
| **Every `bg-*`** | Must have a `dark:bg-*` counterpart |
|
|
93
|
+
| **Every `text-*`** | Must have a `dark:text-*` counterpart |
|
|
94
|
+
| **Every `border-*`** | Must have a `dark:border-*` counterpart |
|
|
95
|
+
| **Every `shadow-*`** | Must have a `dark:shadow-*` or be theme-neutral |
|
|
96
|
+
| **Every `placeholder-*`** | Must have a `dark:placeholder-*` counterpart |
|
|
97
|
+
| **Hover/focus states** | Must also have `dark:hover:*` / `dark:focus:*` variants |
|
|
98
|
+
|
|
99
|
+
### 3.4 Dark Mode Configuration
|
|
100
|
+
|
|
101
|
+
The project uses Tailwind CSS v4 class-based dark mode:
|
|
102
|
+
```css
|
|
103
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Toggle dark mode by adding/removing `.dark` class on a parent element.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 4. Code Splitting — No Monoliths
|
|
111
|
+
|
|
112
|
+
### 4.1 The Rule
|
|
113
|
+
|
|
114
|
+
**No file exceeds 200 lines. No component does more than one thing. Split aggressively.** A monolithic 500-line component is a failure — it must be decomposed into focused, reusable units.
|
|
115
|
+
|
|
116
|
+
### 4.2 Mandatory Split Structure
|
|
117
|
+
|
|
118
|
+
Every scope (page, feature, domain) uses these subfolders:
|
|
119
|
+
|
|
120
|
+
| Folder | Purpose |
|
|
121
|
+
|--------|---------|
|
|
122
|
+
| `_components/` | UI components for that scope |
|
|
123
|
+
| `_utils/` | Helper utilities |
|
|
124
|
+
| `_store/` | State stores (h-state) |
|
|
125
|
+
| `_hooks/` | Custom hooks |
|
|
126
|
+
|
|
127
|
+
### 4.3 Scope Placement Rules
|
|
128
|
+
|
|
129
|
+
| Usage | Placement |
|
|
130
|
+
|-------|-----------|
|
|
131
|
+
| Used in 1 page only | Inside that page's `_components/` |
|
|
132
|
+
| Used across pages | Elevate to `app/_components/` |
|
|
133
|
+
| Used everywhere | Use nucleus-core-ts component or create in `app/_components/global/` |
|
|
134
|
+
|
|
135
|
+
### 4.4 Component File Structure
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
_components/
|
|
139
|
+
BandMemberCard/
|
|
140
|
+
index.tsx # Component implementation
|
|
141
|
+
types.ts # Type definitions
|
|
142
|
+
utils.ts # Helper functions
|
|
143
|
+
BandMemberSkeleton.tsx # Loading skeleton
|
|
144
|
+
ShowCard/
|
|
145
|
+
index.tsx
|
|
146
|
+
types.ts
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 4.5 Index File Rules
|
|
150
|
+
|
|
151
|
+
- **Barrel files** (`_components/index.ts`): Re-export only. No logic.
|
|
152
|
+
- **Module files** (`BandMemberCard/index.tsx`): Contains the actual implementation.
|
|
153
|
+
- **Named exports only.** Default exports are forbidden.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 5. Nucleus Design System Components
|
|
158
|
+
|
|
159
|
+
### 5.1 Available UI Components
|
|
160
|
+
|
|
161
|
+
The `nucleus-core-ts/fe` package provides ready-to-use, themeable, responsive UI **components**. Use these instead of building from scratch:
|
|
162
|
+
|
|
163
|
+
| Component | Import | Purpose |
|
|
164
|
+
|-----------|--------|---------|
|
|
165
|
+
| **Button** | `Button` | Versatile button with variants, sizes, shapes, loading states |
|
|
166
|
+
| **NucleusTextInput** | `NucleusTextInput` | Text input with label, validation, icons |
|
|
167
|
+
| **Checkbox** | `Checkbox` | Checkbox and switch toggle with colors |
|
|
168
|
+
| **SelectBox** | `SelectBox` | Dropdown select with search, clear, portal-based |
|
|
169
|
+
| **SearchBox** | `SearchBox` | Search input with debounce, dropdown results |
|
|
170
|
+
| **DatePicker** | `DatePicker` | Calendar date picker with range mode, locale |
|
|
171
|
+
| **RangePicker** | `RangePicker` | Numeric range slider with ticks, tooltips |
|
|
172
|
+
| **DataTable** | `DataTable` | Feature-rich table: sorting, resize, selection, infinite scroll, inline edit |
|
|
173
|
+
| **Tooltip** | `Tooltip` | Tooltip with placements, variants, GSAP animation |
|
|
174
|
+
| **Captcha** | `Captcha` | Puzzle captcha with expiration |
|
|
175
|
+
| **NotificationCenter** | `NotificationCenter` | In-app notification bell + panel |
|
|
176
|
+
| **FormBuilder** | `FormBuilder` | Dynamic form generation from schema |
|
|
177
|
+
|
|
178
|
+
All components import from:
|
|
179
|
+
```ts
|
|
180
|
+
import { Button, DataTable, SelectBox, ... } from 'nucleus-core-ts/fe'
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 5.2 Design System Explorer
|
|
184
|
+
|
|
185
|
+
The project includes a `/ui-docs` page that renders the **DesignSystem** explorer — a live, interactive documentation of all available components with prop controls, examples, and theme toggling (light/dark). Use it as reference during development.
|
|
186
|
+
|
|
187
|
+
Visit `http://localhost:3000/ui-docs` to explore.
|
|
188
|
+
|
|
189
|
+
### 5.3 When to Use nucleus-core Components vs Custom
|
|
190
|
+
|
|
191
|
+
| Situation | Action |
|
|
192
|
+
|-----------|--------|
|
|
193
|
+
| Need a button, input, table, select, etc. | **Use nucleus-core component** |
|
|
194
|
+
| Need a complex domain-specific component (e.g., ShowCalendar) | **Build custom**, but compose from nucleus-core atoms (Button, DataTable, etc.) |
|
|
195
|
+
| Need a variant/style not covered by nucleus-core | **Extend via theme props** or `className` override — don't recreate |
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 6. API Integration
|
|
200
|
+
|
|
201
|
+
### 6.1 The Pattern
|
|
202
|
+
|
|
203
|
+
All API calls go through the type-safe `useApiActions` hook:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { useApiActions } from '@/lib/api/hook'
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 6.2 Type Safety — Absolute Rules
|
|
210
|
+
|
|
211
|
+
| Rule | Detail |
|
|
212
|
+
|------|--------|
|
|
213
|
+
| **Zero `as` casts** | All types flow from endpoint definitions |
|
|
214
|
+
| **Zero manual type redefinition** | Never redefine types that nucleus-core provides |
|
|
215
|
+
| **Zero `Record<string, unknown>`** | Extract the real type from the endpoint |
|
|
216
|
+
| **Zero wrapper hooks** | Don't wrap `useApiActions` with extra `useState` |
|
|
217
|
+
|
|
218
|
+
### 6.3 Entity Type Extraction
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import type { AllEndpoints } from '@/lib/api/endpoints'
|
|
222
|
+
|
|
223
|
+
// ✅ CORRECT — type flows from endpoint definition
|
|
224
|
+
type BandMember = NonNullable<AllEndpoints['GET_BAND_MEMBERS']['_success']['data']>['items'][number]
|
|
225
|
+
|
|
226
|
+
// ❌ WRONG — manual type
|
|
227
|
+
type BandMember = { id: string; firstName: string; ... }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 6.4 Action State = Single Source of Truth
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
const actions = useApiActions()
|
|
234
|
+
|
|
235
|
+
// ✅ CORRECT
|
|
236
|
+
const isLoading = actions.GET_BAND_MEMBERS.state.isPending
|
|
237
|
+
const members = actions.GET_BAND_MEMBERS.state.data?.data?.items ?? []
|
|
238
|
+
const meta = actions.GET_BAND_MEMBERS.state.data?.data?.meta
|
|
239
|
+
|
|
240
|
+
// ❌ WRONG — duplicating into separate state
|
|
241
|
+
const [members, setMembers] = useState([])
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 6.5 Fetch Pattern
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
const actions = useApiActions()
|
|
248
|
+
|
|
249
|
+
const fetchMembers = useEffectEvent(() => {
|
|
250
|
+
actions.GET_BAND_MEMBERS.start({
|
|
251
|
+
payload: { page: 1, limit: 20, sort: [{ field: 'displayOrder', direction: 'asc' }] },
|
|
252
|
+
onAfterHandle: (data) => { /* data is fully typed */ },
|
|
253
|
+
onErrorHandle: (error, code) => { /* handle errors */ },
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
useEffect(() => { fetchMembers() }, [])
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 6.6 List Response Structure
|
|
261
|
+
|
|
262
|
+
All list endpoints return:
|
|
263
|
+
```ts
|
|
264
|
+
{
|
|
265
|
+
data: {
|
|
266
|
+
items: T[]
|
|
267
|
+
meta: { page, limit, totalItems, totalPages, hasNextPage, hasPrevPage, ... }
|
|
268
|
+
}
|
|
269
|
+
success: boolean
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Access: `actions.GET_X.state.data?.data?.items` and `.data?.data?.meta`
|
|
274
|
+
|
|
275
|
+
### 6.7 File Upload & CDN
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
// Upload
|
|
279
|
+
const formData = new FormData()
|
|
280
|
+
formData.append('files', selectedFile)
|
|
281
|
+
formData.append('data', JSON.stringify({ original_name: file.name, path: 'uploads', size: file.size, mime_type: file.type, extension: file.name.split('.').pop(), type: 'image' }))
|
|
282
|
+
actions.ADD_FILE.start({ payload: formData })
|
|
283
|
+
|
|
284
|
+
// Display (via CDN rewrite in next.config.ts)
|
|
285
|
+
<img src={`/cdn/${file.id}`} alt={file.originalName} />
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### 6.8 Prohibited Patterns
|
|
289
|
+
|
|
290
|
+
| Pattern | Why Prohibited | Correct Alternative |
|
|
291
|
+
|---------|---------------|-------------------|
|
|
292
|
+
| `as` type casts on API data | Breaks type safety | Let types flow from endpoints |
|
|
293
|
+
| `useState` for API data | Duplicates state | Use `actions.X.state` |
|
|
294
|
+
| `try/catch/await` API calls | Against callback pattern | Use `onAfterHandle` / `onErrorHandle` |
|
|
295
|
+
| Manual entity types | Diverges from schema | Extract from `AllEndpoints` |
|
|
296
|
+
| `Record<string, unknown>` | No intellisense | Use endpoint-derived types |
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## 7. React Patterns
|
|
301
|
+
|
|
302
|
+
### 7.1 Strictly Prohibited
|
|
303
|
+
|
|
304
|
+
- `useCallback`
|
|
305
|
+
- `useMemo`
|
|
306
|
+
- `memo`
|
|
307
|
+
- `as` type assertions
|
|
308
|
+
- `any`, `unknown` types
|
|
309
|
+
|
|
310
|
+
### 7.2 Effect Management
|
|
311
|
+
|
|
312
|
+
- All functions inside `useEffect` **must use `useEffectEvent`**
|
|
313
|
+
|
|
314
|
+
### 7.3 State Management
|
|
315
|
+
|
|
316
|
+
- **h-state** for all shared state management
|
|
317
|
+
- **Prop drilling is strictly prohibited**
|
|
318
|
+
|
|
319
|
+
### 7.4 Animation
|
|
320
|
+
|
|
321
|
+
- **GSAP** is the required animation library
|
|
322
|
+
- Use `@gsap/react` for React integration
|
|
323
|
+
- Animations should be sophisticated and butter-smooth
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## 8. Loading States & Skeletons
|
|
328
|
+
|
|
329
|
+
### 8.1 The Rule
|
|
330
|
+
|
|
331
|
+
**Every page and every data-driven component MUST have a skeleton loader.** Loading states are not optional — they are part of the component contract.
|
|
332
|
+
|
|
333
|
+
### 8.2 Implementation
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
export function BandMemberList() {
|
|
337
|
+
const actions = useApiActions()
|
|
338
|
+
const isLoading = actions.GET_BAND_MEMBERS.state.isPending
|
|
339
|
+
const members = actions.GET_BAND_MEMBERS.state.data?.data?.items ?? []
|
|
340
|
+
|
|
341
|
+
if (isLoading) return <BandMemberListSkeleton count={4} />
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
345
|
+
{members.map(m => <BandMemberCard key={m.id} member={m} />)}
|
|
346
|
+
</section>
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### 8.3 Skeleton Rules
|
|
352
|
+
|
|
353
|
+
- Must mirror exact layout of loaded content
|
|
354
|
+
- Must respect light/dark theme
|
|
355
|
+
- Must follow same responsive breakpoints
|
|
356
|
+
- GSAP shimmer animation preferred
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 9. Design Philosophy
|
|
361
|
+
|
|
362
|
+
### 9.1 Bold Direction
|
|
363
|
+
|
|
364
|
+
Before coding, establish a clear aesthetic direction. Be intentional — not generic.
|
|
365
|
+
|
|
366
|
+
### 9.2 Typography
|
|
367
|
+
|
|
368
|
+
- **Forbidden**: Generic fonts (Arial, Inter, Roboto, system fonts)
|
|
369
|
+
- **Required**: Distinctive, characterful font choices
|
|
370
|
+
|
|
371
|
+
### 9.3 Color & Theme
|
|
372
|
+
|
|
373
|
+
- Cohesive palette with intentional accents
|
|
374
|
+
- CSS variables for consistency
|
|
375
|
+
- **Forbidden**: Clichéd color schemes, AI-slop aesthetics
|
|
376
|
+
|
|
377
|
+
### 9.4 Motion
|
|
378
|
+
|
|
379
|
+
- GSAP for all animations
|
|
380
|
+
- Orchestrated page loads with staggered reveals
|
|
381
|
+
- Scroll-triggered animations
|
|
382
|
+
- One well-crafted animation > scattered micro-interactions
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## 10. snake_case vs camelCase Convention
|
|
387
|
+
|
|
388
|
+
| Layer | Format | Example |
|
|
389
|
+
|-------|--------|---------|
|
|
390
|
+
| Config `table_name` | snake_case | `band_members` |
|
|
391
|
+
| Backend route path | camelCase | `/bandMembers` |
|
|
392
|
+
| Frontend endpoint key | UPPER_SNAKE_CASE | `GET_BAND_MEMBERS` |
|
|
393
|
+
| Query param fields | camelCase | `displayOrder` |
|
|
394
|
+
|
|
395
|
+
**Never** hardcode snake_case in API paths. Use `generateAllEndpoints()` — it handles casing automatically.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## 11. Compliance Checklist
|
|
400
|
+
|
|
401
|
+
Before submitting any code:
|
|
402
|
+
|
|
403
|
+
### Responsive
|
|
404
|
+
- [ ] Mobile-first with all 6 breakpoints tested
|
|
405
|
+
- [ ] Renders correctly from 320px to 1280px+
|
|
406
|
+
|
|
407
|
+
### Theme
|
|
408
|
+
- [ ] Every `bg-*` has `dark:bg-*` counterpart
|
|
409
|
+
- [ ] Every `text-*` has `dark:text-*` counterpart
|
|
410
|
+
- [ ] Every `border-*` has `dark:border-*` counterpart
|
|
411
|
+
- [ ] Tested in both light and dark mode
|
|
412
|
+
|
|
413
|
+
### Architecture
|
|
414
|
+
- [ ] No file exceeds 200 lines
|
|
415
|
+
- [ ] Components are single-responsibility
|
|
416
|
+
- [ ] Scope placement is correct
|
|
417
|
+
- [ ] Named exports only (no default exports)
|
|
418
|
+
|
|
419
|
+
### API
|
|
420
|
+
- [ ] Uses `useApiActions` pattern
|
|
421
|
+
- [ ] Types extracted from endpoints (no manual types)
|
|
422
|
+
- [ ] Action state is single source of truth
|
|
423
|
+
- [ ] No `as` casts, no `try/catch/await`
|
|
424
|
+
|
|
425
|
+
### Components
|
|
426
|
+
- [ ] Nucleus-core components used where available
|
|
427
|
+
- [ ] Custom components compose from nucleus-core atoms
|
|
428
|
+
- [ ] Skeleton loader exists for every data-driven component
|
|
429
|
+
|
|
430
|
+
### Quality
|
|
431
|
+
- [ ] Semantic HTML
|
|
432
|
+
- [ ] GSAP animations
|
|
433
|
+
- [ ] h-state for shared state (no prop drilling)
|
|
434
|
+
- [ ] Production-grade (no TODOs)
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
**Remember**: Every interface should be distinctive, responsive from 320px to 4K, readable in both themes, and crafted with precision. These are not guidelines — they are requirements.
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import type { NextConfig } from 'next'
|
|
2
2
|
|
|
3
3
|
const nextConfig: NextConfig = {
|
|
4
|
+
output: 'standalone',
|
|
5
|
+
logging: {
|
|
6
|
+
browserToTerminal: true,
|
|
7
|
+
},
|
|
4
8
|
async rewrites() {
|
|
9
|
+
const apiUrl = process.env.API_BASE_URL || 'http://localhost:9000'
|
|
5
10
|
return [
|
|
6
11
|
{
|
|
7
|
-
source: '/
|
|
8
|
-
destination:
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
source: '/api/events/:path*',
|
|
12
|
-
destination: 'http://localhost:4000/api/events/:path*',
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
source: '/api/ws/:path*',
|
|
16
|
-
destination: 'http://localhost:4000/api/ws/:path*',
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
source: '/ws/:path*',
|
|
20
|
-
destination: 'http://localhost:4000/ws/:path*',
|
|
12
|
+
source: '/cdn/:path*',
|
|
13
|
+
destination: `${apiUrl}/cdn/:path*`,
|
|
21
14
|
},
|
|
22
15
|
]
|
|
23
16
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.43",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|