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.
@@ -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 envContent = [
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
- `JWT_SECRET=${generateSecret()}`,
231
- `JWT_REFRESH_SECRET=${generateSecret()}`,
232
- `JWT_SESSION_SECRET=${generateSecret()}`,
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
- ].join('\n')
239
- writeFileSync(join(beDir, '.env'), envContent + '\n')
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(` │ ├── config.json`)
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(` │ ├── .dockerignore`)
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(` ├── lib/api/config.ts`)
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(` ├── .env.local`)
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}Next steps:${RESET}`)
489
- log(` 1. ${CYAN}cd ${join(outputDir, apiProjectName)} && bun install${RESET}`)
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(` 5. ${CYAN}cd ${join(outputDir, feProjectName)} && bun install${RESET}`)
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}For deployment, configure Azure DevOps pipeline variables`)
498
- log(` and push to trigger the CI/CD pipeline.${RESET}\n`)
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) => {
@@ -5,7 +5,7 @@ async function main() {
5
5
  new Elysia()
6
6
  .use(
7
7
  await NucleusElysiaPlugin({
8
- options: './config.json',
8
+ options: './src/config.json',
9
9
  schema: './src/drizzle/schema.ts',
10
10
  relations: './src/drizzle/relations.ts',
11
11
  swagger: {
@@ -2,7 +2,7 @@
2
2
 
3
3
  @custom-variant dark (&:where(.dark, .dark *));
4
4
 
5
- @source "../node_modules/nucleus-core/dist/fe/**/*.js";
5
+ @source "../node_modules/nucleus-core-ts/dist/fe/**/*.js";
6
6
 
7
7
  :root {
8
8
  --background: #ffffff;
@@ -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: '/file-proxy/:path*',
8
- destination: 'http://localhost:4000/file-proxy/:path*',
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.42",
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",