start-vibing-stacks 2.3.0 → 2.4.0
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/detector.js +19 -5
- package/dist/index.js +15 -3
- package/dist/scanner.js +23 -2
- package/dist/setup.js +17 -1
- package/dist/types.d.ts +4 -0
- package/dist/ui.js +6 -5
- package/package.json +1 -1
- package/stacks/_shared/config/security-rules.json +27 -5
- package/stacks/frontend/react/skills/preline-ui/SKILL.md +31 -35
- package/stacks/frontend/react/skills/react-standards/SKILL.md +20 -20
- package/stacks/frontend/react/skills/react-ui-patterns/SKILL.md +78 -42
- package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +1 -1
- package/stacks/frontend/react/skills/zod-validation/SKILL.md +84 -18
- package/stacks/nodejs/skills/nextjs-app-router/SKILL.md +101 -0
- package/stacks/nodejs/stack.json +43 -121
- package/templates/CLAUDE-nodejs.md +323 -0
- package/templates/CLAUDE-php.md +131 -10
package/dist/detector.js
CHANGED
|
@@ -25,10 +25,6 @@ const STACK_SIGNATURES = [
|
|
|
25
25
|
{ id: 'python', files: ['pyproject.toml'], weight: 90, reason: 'pyproject.toml detected' },
|
|
26
26
|
{ id: 'python', files: ['Pipfile'], weight: 85, reason: 'Pipfile detected' },
|
|
27
27
|
{ id: 'python', files: ['setup.py'], weight: 75, reason: 'setup.py detected' },
|
|
28
|
-
// Rust
|
|
29
|
-
{ id: 'rust', files: ['Cargo.toml'], weight: 95, reason: 'Cargo.toml detected' },
|
|
30
|
-
// Go
|
|
31
|
-
{ id: 'go', files: ['go.mod'], weight: 95, reason: 'go.mod detected' },
|
|
32
28
|
];
|
|
33
29
|
export function detectProject(projectDir) {
|
|
34
30
|
const result = {
|
|
@@ -95,8 +91,26 @@ export function detectNodeFramework(projectDir) {
|
|
|
95
91
|
return 'nextjs';
|
|
96
92
|
if (existsSync(join(projectDir, 'nuxt.config.ts')))
|
|
97
93
|
return 'nuxt';
|
|
98
|
-
if (existsSync(join(projectDir, 'astro.config.mjs')))
|
|
94
|
+
if (existsSync(join(projectDir, 'astro.config.mjs')) || existsSync(join(projectDir, 'astro.config.ts')))
|
|
99
95
|
return 'astro';
|
|
96
|
+
if (existsSync(join(projectDir, 'svelte.config.js')) || existsSync(join(projectDir, 'svelte.config.ts')))
|
|
97
|
+
return 'svelte';
|
|
98
|
+
const pkgPath = join(projectDir, 'package.json');
|
|
99
|
+
if (existsSync(pkgPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
102
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
103
|
+
if (deps['express'])
|
|
104
|
+
return 'express';
|
|
105
|
+
if (deps['fastify'])
|
|
106
|
+
return 'fastify';
|
|
107
|
+
if (deps['hono'])
|
|
108
|
+
return 'hono';
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// ignore parse errors
|
|
112
|
+
}
|
|
113
|
+
}
|
|
100
114
|
return null;
|
|
101
115
|
}
|
|
102
116
|
export function detectPythonFramework(projectDir) {
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* Detects project stack, validates requirements, and configures agents.
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, readFileSync } from 'fs';
|
|
9
|
-
import { join, basename } from 'path';
|
|
9
|
+
import { join, basename, dirname, resolve } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
10
11
|
import inquirer from 'inquirer';
|
|
11
12
|
import chalk from 'chalk';
|
|
12
13
|
import * as ui from './ui.js';
|
|
@@ -15,6 +16,10 @@ import { autoInstall, installComposer, installClaudeCode } from './installer.js'
|
|
|
15
16
|
import { loadStackConfig, setupProject } from './setup.js';
|
|
16
17
|
import { selectMcpServers, installMcpServers } from './mcp.js';
|
|
17
18
|
import { scanProjectStandards } from './scanner.js';
|
|
19
|
+
const __cli_filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __cli_dirname = dirname(__cli_filename);
|
|
21
|
+
const CLI_ROOT = resolve(__cli_dirname, '..');
|
|
22
|
+
const PKG_VERSION = JSON.parse(readFileSync(join(CLI_ROOT, 'package.json'), 'utf8')).version;
|
|
18
23
|
// =============================================================================
|
|
19
24
|
// CLI Arguments
|
|
20
25
|
// =============================================================================
|
|
@@ -28,7 +33,7 @@ const FLAGS = {
|
|
|
28
33
|
version: args.includes('--version') || args.includes('-v'),
|
|
29
34
|
};
|
|
30
35
|
if (FLAGS.version) {
|
|
31
|
-
console.log(
|
|
36
|
+
console.log(PKG_VERSION);
|
|
32
37
|
process.exit(0);
|
|
33
38
|
}
|
|
34
39
|
if (FLAGS.help) {
|
|
@@ -158,12 +163,15 @@ async function main() {
|
|
|
158
163
|
: stackId === 'python'
|
|
159
164
|
? detectPythonFramework(projectDir)
|
|
160
165
|
: null;
|
|
166
|
+
const defaultFramework = detectedFramework ||
|
|
167
|
+
stackConfig.frameworks.find((f) => f.default)?.id ||
|
|
168
|
+
undefined;
|
|
161
169
|
const { framework } = await inquirer.prompt([
|
|
162
170
|
{
|
|
163
171
|
type: 'list',
|
|
164
172
|
name: 'framework',
|
|
165
173
|
message: 'Select framework:',
|
|
166
|
-
default:
|
|
174
|
+
default: defaultFramework,
|
|
167
175
|
choices: stackConfig.frameworks.map((f) => ({
|
|
168
176
|
name: `${f.icon} ${f.name}`,
|
|
169
177
|
value: f.id,
|
|
@@ -185,11 +193,13 @@ async function main() {
|
|
|
185
193
|
]);
|
|
186
194
|
// ─── Step 5: Frontend ──────────────────────────────────────────────────
|
|
187
195
|
const compatibleFrontends = stackConfig.frontendOptions.filter((f) => !f.frameworks || f.frameworks.includes(framework));
|
|
196
|
+
const defaultFrontend = compatibleFrontends.find((f) => f.default)?.id || undefined;
|
|
188
197
|
const { frontend } = await inquirer.prompt([
|
|
189
198
|
{
|
|
190
199
|
type: 'list',
|
|
191
200
|
name: 'frontend',
|
|
192
201
|
message: 'Frontend included?',
|
|
202
|
+
default: defaultFrontend,
|
|
193
203
|
choices: compatibleFrontends.map((f) => ({
|
|
194
204
|
name: `${f.icon} ${f.name}`,
|
|
195
205
|
value: f.id,
|
|
@@ -249,12 +259,14 @@ async function main() {
|
|
|
249
259
|
}
|
|
250
260
|
}
|
|
251
261
|
// ─── Step 7: Show Summary & Confirm ────────────────────────────────────
|
|
262
|
+
const selectedFrontend = compatibleFrontends.find((f) => f.id === frontend);
|
|
252
263
|
const config = {
|
|
253
264
|
name: projectName,
|
|
254
265
|
stack: stackId,
|
|
255
266
|
framework,
|
|
256
267
|
database,
|
|
257
268
|
frontend,
|
|
269
|
+
frontendSkillsDir: selectedFrontend?.skillsDir,
|
|
258
270
|
deploy,
|
|
259
271
|
path: projectDir,
|
|
260
272
|
createdAt: new Date().toISOString(),
|
package/dist/scanner.js
CHANGED
|
@@ -102,6 +102,8 @@ const NPM_PACKAGES = {
|
|
|
102
102
|
// Media
|
|
103
103
|
'uploadthing': { category: 'media', name: 'UploadThing file uploads' },
|
|
104
104
|
'sharp': { category: 'media', name: 'Sharp image processing' },
|
|
105
|
+
// Design System
|
|
106
|
+
'preline': { category: 'ui', name: 'Preline UI design system' },
|
|
105
107
|
};
|
|
106
108
|
function readFileIfExists(path) {
|
|
107
109
|
if (!existsSync(path))
|
|
@@ -424,8 +426,14 @@ function scanProjectFiles(projectDir) {
|
|
|
424
426
|
patterns.push({ category: 'quality', name: 'Prettier config present', confidence: 100 });
|
|
425
427
|
}
|
|
426
428
|
// Environment files (works for both PHP and Node.js projects)
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
+
const envFiles = ['.env.example', '.env', '.env.local', '.env.development', '.env.production'];
|
|
430
|
+
let envContent = null;
|
|
431
|
+
for (const envFile of envFiles) {
|
|
432
|
+
const content = readFileIfExists(join(projectDir, envFile));
|
|
433
|
+
if (content) {
|
|
434
|
+
envContent = (envContent || '') + '\n' + content;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
429
437
|
if (envContent) {
|
|
430
438
|
if (/REDIS_HOST|REDIS_URL/i.test(envContent)) {
|
|
431
439
|
patterns.push({ category: 'cache', name: 'Redis configured', confidence: 85 });
|
|
@@ -442,6 +450,19 @@ function scanProjectFiles(projectDir) {
|
|
|
442
450
|
if (/STRIPE_SECRET|STRIPE_KEY/i.test(envContent)) {
|
|
443
451
|
patterns.push({ category: 'billing', name: 'Stripe payments configured', confidence: 85 });
|
|
444
452
|
}
|
|
453
|
+
// Security: detect NEXT_PUBLIC_ with sensitive values
|
|
454
|
+
const sensitivePublicEnv = envContent.match(/^NEXT_PUBLIC_\w*(SECRET|TOKEN|PRIVATE|PASSWORD|CREDENTIAL|OPENAI|API_KEY|DATABASE)\w*\s*=/gmi);
|
|
455
|
+
if (sensitivePublicEnv) {
|
|
456
|
+
for (const match of sensitivePublicEnv) {
|
|
457
|
+
const varName = match.split('=')[0].trim();
|
|
458
|
+
patterns.push({
|
|
459
|
+
category: 'security',
|
|
460
|
+
name: `EXPOSED SECRET: ${varName} is public (visible in browser)`,
|
|
461
|
+
confidence: 100,
|
|
462
|
+
detail: `${varName} uses NEXT_PUBLIC_ prefix which embeds the value in the client JS bundle. Remove NEXT_PUBLIC_ and access via Route Handler or Server Action.`,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
445
466
|
}
|
|
446
467
|
if (patterns.length === 0)
|
|
447
468
|
return null;
|
package/dist/setup.js
CHANGED
|
@@ -7,6 +7,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSy
|
|
|
7
7
|
import { join, dirname, resolve } from 'path';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import ora from 'ora';
|
|
10
|
+
import * as ui from './ui.js';
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = dirname(__filename);
|
|
12
13
|
const PACKAGE_ROOT = resolve(__dirname, '..');
|
|
@@ -89,7 +90,8 @@ export async function setupProject(projectDir, config, options = {}) {
|
|
|
89
90
|
copyDirRecursive(stackConfigDir, join(claudeDir, 'config'), options.force);
|
|
90
91
|
// 8. Copy frontend-specific skills if applicable
|
|
91
92
|
if (config.frontend && config.frontend !== 'none') {
|
|
92
|
-
const
|
|
93
|
+
const frontendId = config.frontendSkillsDir || config.frontend;
|
|
94
|
+
const frontendDir = join(PACKAGE_ROOT, 'stacks', 'frontend', frontendId);
|
|
93
95
|
if (existsSync(frontendDir)) {
|
|
94
96
|
const feSkillCount = copyDirRecursive(join(frontendDir, 'skills'), join(claudeDir, 'skills'), options.force);
|
|
95
97
|
spinner.text = `Loaded ${feSkillCount} frontend skills`;
|
|
@@ -286,6 +288,20 @@ export async function setupProject(projectDir, config, options = {}) {
|
|
|
286
288
|
settings.quality_gates = gates;
|
|
287
289
|
}
|
|
288
290
|
writeFileSync(join(claudeDir, 'settings.json'), JSON.stringify(settings, null, '\t'));
|
|
291
|
+
// 14. Detect Preline and suggest official agent skills
|
|
292
|
+
const projectPkgPath = join(projectDir, 'package.json');
|
|
293
|
+
if (existsSync(projectPkgPath)) {
|
|
294
|
+
try {
|
|
295
|
+
const projectPkg = JSON.parse(readFileSync(projectPkgPath, 'utf8'));
|
|
296
|
+
const allDeps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
|
|
297
|
+
if (allDeps['preline'] && !existsSync(join(claudeDir, 'skills', 'preline-theme-generator'))) {
|
|
298
|
+
ui.info('Preline UI detected — run: npx skills add htmlstreamofficial/preline');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// package.json parse error, skip
|
|
303
|
+
}
|
|
304
|
+
}
|
|
289
305
|
spinner.succeed(`Setup complete: ${agentCount} agents, ${sharedSkillCount + stackSkillCount} skills, ${hookCount} hooks`);
|
|
290
306
|
return true;
|
|
291
307
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface FrameworkOption {
|
|
|
32
32
|
icon: string;
|
|
33
33
|
detectFiles?: string[];
|
|
34
34
|
skills?: string[];
|
|
35
|
+
default?: boolean;
|
|
35
36
|
extra?: Record<string, unknown>;
|
|
36
37
|
}
|
|
37
38
|
export interface DatabaseOption {
|
|
@@ -44,6 +45,8 @@ export interface FrontendOption {
|
|
|
44
45
|
name: string;
|
|
45
46
|
icon: string;
|
|
46
47
|
frameworks?: string[];
|
|
48
|
+
skillsDir?: string;
|
|
49
|
+
default?: boolean;
|
|
47
50
|
}
|
|
48
51
|
export interface DeployTarget {
|
|
49
52
|
id: string;
|
|
@@ -67,6 +70,7 @@ export interface ProjectConfig {
|
|
|
67
70
|
framework: string;
|
|
68
71
|
database: string;
|
|
69
72
|
frontend: string;
|
|
73
|
+
frontendSkillsDir?: string;
|
|
70
74
|
deploy: string;
|
|
71
75
|
path: string;
|
|
72
76
|
createdAt: string;
|
package/dist/ui.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Start Vibing Stacks — Terminal UI
|
|
3
3
|
*/
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { join, dirname, resolve } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
4
7
|
import chalk from 'chalk';
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
return text.split('').map((c, i) => colors[i % colors.length](c)).join('');
|
|
9
|
-
};
|
|
8
|
+
const __ui_filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __ui_dirname = dirname(__ui_filename);
|
|
10
|
+
const VERSION = JSON.parse(readFileSync(join(resolve(__ui_dirname, '..'), 'package.json'), 'utf8')).version;
|
|
10
11
|
export const LOGO = `
|
|
11
12
|
${chalk.hex('#9F7AEA')(' ╔═══════════════════════════════════════════════╗')}
|
|
12
13
|
${chalk.hex('#9F7AEA')(' ║')} ${chalk.hex('#9F7AEA')('║')}
|
package/package.json
CHANGED
|
@@ -5,17 +5,39 @@
|
|
|
5
5
|
},
|
|
6
6
|
"sensitivePatterns": {
|
|
7
7
|
"forbidden": [
|
|
8
|
-
"eval(",
|
|
9
|
-
"exec(",
|
|
10
|
-
"system(",
|
|
11
|
-
"passthru(",
|
|
12
|
-
"shell_exec(",
|
|
13
8
|
"password:",
|
|
14
9
|
"passwordHash",
|
|
15
10
|
"apiKey:",
|
|
16
11
|
"secret:"
|
|
17
12
|
]
|
|
18
13
|
},
|
|
14
|
+
"envExposure": {
|
|
15
|
+
"$comment": "NEXT_PUBLIC_ vars are embedded in browser JS bundle. These patterns indicate secrets being exposed to the client.",
|
|
16
|
+
"forbiddenPublicEnvPatterns": [
|
|
17
|
+
"NEXT_PUBLIC_.*SECRET",
|
|
18
|
+
"NEXT_PUBLIC_.*TOKEN",
|
|
19
|
+
"NEXT_PUBLIC_.*PRIVATE",
|
|
20
|
+
"NEXT_PUBLIC_.*PASSWORD",
|
|
21
|
+
"NEXT_PUBLIC_.*CREDENTIAL"
|
|
22
|
+
],
|
|
23
|
+
"forbiddenPublicEnvExact": [
|
|
24
|
+
"NEXT_PUBLIC_OPENAI_KEY",
|
|
25
|
+
"NEXT_PUBLIC_OPENAI_API_KEY",
|
|
26
|
+
"NEXT_PUBLIC_STRIPE_SECRET",
|
|
27
|
+
"NEXT_PUBLIC_STRIPE_SECRET_KEY",
|
|
28
|
+
"NEXT_PUBLIC_DATABASE_URL",
|
|
29
|
+
"NEXT_PUBLIC_SUPABASE_SERVICE_KEY",
|
|
30
|
+
"NEXT_PUBLIC_FIREBASE_ADMIN_KEY"
|
|
31
|
+
],
|
|
32
|
+
"safePublicEnvPatterns": [
|
|
33
|
+
"NEXT_PUBLIC_.*URL",
|
|
34
|
+
"NEXT_PUBLIC_.*ID",
|
|
35
|
+
"NEXT_PUBLIC_STRIPE_KEY",
|
|
36
|
+
"NEXT_PUBLIC_GA_",
|
|
37
|
+
"NEXT_PUBLIC_SENTRY_DSN"
|
|
38
|
+
],
|
|
39
|
+
"rule": "API keys, secrets, and tokens MUST stay server-side. Use Route Handlers or Server Actions as proxy."
|
|
40
|
+
},
|
|
19
41
|
"cookies": {
|
|
20
42
|
"httpOnly": true,
|
|
21
43
|
"secure": true,
|
|
@@ -10,7 +10,7 @@ Preline is a **semantic token-based design system** built on TailwindCSS. It pro
|
|
|
10
10
|
- Theme generator for custom color schemes
|
|
11
11
|
- Light + dark mode via `data-theme` + `.dark`
|
|
12
12
|
|
|
13
|
-
## Installation
|
|
13
|
+
## Installation
|
|
14
14
|
|
|
15
15
|
### Step 1: Install
|
|
16
16
|
|
|
@@ -21,7 +21,7 @@ npm install preline @tailwindcss/forms
|
|
|
21
21
|
### Step 2: CSS Config
|
|
22
22
|
|
|
23
23
|
```css
|
|
24
|
-
/*
|
|
24
|
+
/* src/app/globals.css (Next.js) or styles/globals.css */
|
|
25
25
|
@import "tailwindcss";
|
|
26
26
|
|
|
27
27
|
/* Preline — MUST be in this order */
|
|
@@ -31,39 +31,35 @@ npm install preline @tailwindcss/forms
|
|
|
31
31
|
@import "./node_modules/preline/themes/theme.css"; /* Base theme */
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
### Step 3:
|
|
34
|
+
### Step 3: Init Preline on Route Changes (MANDATORY)
|
|
35
35
|
|
|
36
|
-
```
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
import laravel from 'laravel-vite-plugin';
|
|
40
|
-
import react from '@vitejs/plugin-react';
|
|
36
|
+
```tsx
|
|
37
|
+
// src/components/PrelineInit.tsx
|
|
38
|
+
'use client';
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
laravel({ input: ['resources/css/app.css', 'resources/js/app.tsx'], refresh: true }),
|
|
45
|
-
react(),
|
|
46
|
-
],
|
|
47
|
-
});
|
|
48
|
-
```
|
|
40
|
+
import { usePathname } from 'next/navigation';
|
|
41
|
+
import { useEffect } from 'react';
|
|
49
42
|
|
|
50
|
-
|
|
43
|
+
export function PrelineInit() {
|
|
44
|
+
const pathname = usePathname();
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
import
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
import('preline/preline').then(({ HSStaticMethods }) => {
|
|
49
|
+
HSStaticMethods.autoInit();
|
|
50
|
+
});
|
|
51
|
+
}, 100);
|
|
52
|
+
return () => clearTimeout(timer);
|
|
53
|
+
}, [pathname]);
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add to root layout:
|
|
59
|
+
// <PrelineInit />
|
|
64
60
|
```
|
|
65
61
|
|
|
66
|
-
**Rule:** Without `HSStaticMethods.autoInit()`, dropdowns, modals, and accordions will NOT work after
|
|
62
|
+
**Rule:** Without `HSStaticMethods.autoInit()`, dropdowns, modals, and accordions will NOT work after client-side navigation.
|
|
67
63
|
|
|
68
64
|
## Templates & Components (840+ free)
|
|
69
65
|
|
|
@@ -80,11 +76,11 @@ router.on('navigate', () => {
|
|
|
80
76
|
|
|
81
77
|
1. Browse https://preline.co/examples.html
|
|
82
78
|
2. Click a block → copy the HTML/JSX
|
|
83
|
-
3. Adapt to React
|
|
84
|
-
- Replace `<a href>` with `<Link href>` (
|
|
79
|
+
3. Adapt to React:
|
|
80
|
+
- Replace `<a href>` with Next.js `<Link href>` (from `next/link`)
|
|
85
81
|
- Replace `class=` with `className=`
|
|
86
82
|
- Add Preline `data-*` attributes for interactive components
|
|
87
|
-
-
|
|
83
|
+
- Pass data via props or fetch with TanStack Query / Server Components
|
|
88
84
|
|
|
89
85
|
### Example: Copy a Hero Block
|
|
90
86
|
|
|
@@ -245,7 +241,7 @@ function ThemeToggle() {
|
|
|
245
241
|
}
|
|
246
242
|
```
|
|
247
243
|
|
|
248
|
-
## Using Components with React
|
|
244
|
+
## Using Components with React
|
|
249
245
|
|
|
250
246
|
### Navbar
|
|
251
247
|
|
|
@@ -327,7 +323,7 @@ function ThemeToggle() {
|
|
|
327
323
|
|
|
328
324
|
```bash
|
|
329
325
|
# Generate theme from config
|
|
330
|
-
npx preline-theme-generator /tmp/config.json ./
|
|
326
|
+
npx preline-theme-generator /tmp/config.json ./src/styles/themes/brand.css
|
|
331
327
|
|
|
332
328
|
# Config format:
|
|
333
329
|
{
|
|
@@ -359,4 +355,4 @@ npx preline-theme-generator /tmp/config.json ./resources/css/themes/brand.css
|
|
|
359
355
|
| Force HTML class changes | Theme activation via `data-theme` only |
|
|
360
356
|
| Invent token names | Follow Preline's naming system |
|
|
361
357
|
| `@apply` for component styles | React components with token classes |
|
|
362
|
-
| Skip `HSStaticMethods.autoInit()` | Always re-init after
|
|
358
|
+
| Skip `HSStaticMethods.autoInit()` | Always re-init after client-side navigation |
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
- **ReactJS >= 19** — MANDATORY
|
|
6
6
|
- **TailwindCSS >= 4** — MANDATORY
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Label Constants Pattern
|
|
9
9
|
|
|
10
10
|
```tsx
|
|
11
|
-
// ✅
|
|
11
|
+
// ✅ Labels as CONST at the top, BEFORE hooks
|
|
12
12
|
const LABELS = {
|
|
13
|
-
title:
|
|
14
|
-
save:
|
|
15
|
-
cancel:
|
|
16
|
-
errorRequired:
|
|
17
|
-
};
|
|
13
|
+
title: 'Dashboard',
|
|
14
|
+
save: 'Save',
|
|
15
|
+
cancel: 'Cancel',
|
|
16
|
+
errorRequired: 'This field is required',
|
|
17
|
+
} as const;
|
|
18
18
|
|
|
19
19
|
export default function Dashboard() {
|
|
20
20
|
const [data, setData] = useState(null);
|
|
@@ -22,14 +22,14 @@ export default function Dashboard() {
|
|
|
22
22
|
return <h1>{LABELS.title}</h1>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// ❌ NEVER
|
|
26
|
-
return <h1>
|
|
25
|
+
// ❌ NEVER scatter string literals across JSX
|
|
26
|
+
return <h1>Dashboard</h1>; // ❌ Duplicated, hard to maintain
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
**Rules:**
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
- Error strings centralized in
|
|
30
|
+
- Labels in `CONST` objects before state hooks for stable references
|
|
31
|
+
- For i18n projects, use `next-intl` or `i18next` — same CONST pattern applies with `t()` calls
|
|
32
|
+
- Error strings centralized in a shared constants file
|
|
33
33
|
|
|
34
34
|
## Debug Logging
|
|
35
35
|
|
|
@@ -65,11 +65,11 @@ export default function Dashboard() {
|
|
|
65
65
|
|
|
66
66
|
```tsx
|
|
67
67
|
// ═══════════════════════════════════════════
|
|
68
|
-
// 1.
|
|
68
|
+
// 1. LABELS (before hooks)
|
|
69
69
|
// ═══════════════════════════════════════════
|
|
70
70
|
const LABELS = {
|
|
71
|
-
title:
|
|
72
|
-
save:
|
|
71
|
+
title: 'Dashboard',
|
|
72
|
+
save: 'Save',
|
|
73
73
|
} as const;
|
|
74
74
|
|
|
75
75
|
// ═══════════════════════════════════════════
|
|
@@ -156,7 +156,7 @@ import { cn } from '@/lib/utils';
|
|
|
156
156
|
5. **Shared styles** — create a `styles.ts` file for cross-component constants
|
|
157
157
|
|
|
158
158
|
```tsx
|
|
159
|
-
//
|
|
159
|
+
// src/styles.ts — shared across components
|
|
160
160
|
export const SHARED_STYLES = {
|
|
161
161
|
page: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8',
|
|
162
162
|
btnPrimary: 'px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary-hover ...',
|
|
@@ -187,10 +187,10 @@ export default function Bad() {
|
|
|
187
187
|
## SVG Icons
|
|
188
188
|
|
|
189
189
|
```tsx
|
|
190
|
-
// ✅ Separate files, import with ?react
|
|
191
|
-
//
|
|
192
|
-
import CheckIcon from '@/
|
|
193
|
-
import { CheckIcon, AlertIcon } from '@/
|
|
190
|
+
// ✅ Separate files, import with ?react (Vite) or as components
|
|
191
|
+
// src/components/icons/CheckIcon.svg
|
|
192
|
+
import CheckIcon from '@/components/icons/CheckIcon.svg?react';
|
|
193
|
+
import { CheckIcon, AlertIcon } from '@/components/icons';
|
|
194
194
|
|
|
195
195
|
// ❌ Inline SVG
|
|
196
196
|
<svg viewBox="0 0 24 24">...</svg> // ❌ Bloats JSX
|