@mohammad-irfan/create-dashboard-kit 1.0.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/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/create-dashboard.js +12 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +223 -0
- package/dist/configBuilder.d.ts +8 -0
- package/dist/configBuilder.js +573 -0
- package/dist/generator.d.ts +9 -0
- package/dist/generator.js +223 -0
- package/dist/presets.d.ts +8 -0
- package/dist/presets.js +72 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +121 -0
- package/dist/templateEngine.d.ts +3 -0
- package/dist/templateEngine.js +31 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +16 -0
- package/dist/validation.js +73 -0
- package/package.json +56 -0
- package/templates/source/components/controls/Button.tsx +69 -0
- package/templates/source/components/controls/Input.tsx +59 -0
- package/templates/source/components/controls/SearchBar.tsx +44 -0
- package/templates/source/components/controls/Select.tsx +61 -0
- package/templates/source/components/controls/ThemeToggle.tsx +23 -0
- package/templates/source/components/feedback/NotificationCenter.tsx +151 -0
- package/templates/source/components/feedback/Skeleton.tsx +22 -0
- package/templates/source/components/feedback/StatusBadge.tsx +35 -0
- package/templates/source/components/feedback/statusBadgeStyles.ts +28 -0
- package/templates/source/components/icons/IconSet.tsx +235 -0
- package/templates/source/components/layout/AppShell.tsx +70 -0
- package/templates/source/components/layout/Header.tsx +90 -0
- package/templates/source/components/layout/PageHeader.tsx +27 -0
- package/templates/source/components/layout/Sidebar.tsx +202 -0
- package/templates/source/components/metrics/MetricRow.tsx +61 -0
- package/templates/source/components/metrics/StatCard.tsx +49 -0
- package/templates/source/components/navigation/NavItem.tsx +75 -0
- package/templates/source/components/navigation/ShortcutPill.tsx +39 -0
- package/templates/source/components/surfaces/EmptyState.tsx +36 -0
- package/templates/source/components/surfaces/HeroSurface.tsx +64 -0
- package/templates/source/components/surfaces/Panel.tsx +43 -0
- package/templates/source/components/surfaces/PriorityCard.tsx +75 -0
- package/templates/source/components/surfaces/QuietListCard.tsx +146 -0
- package/templates/source/design-system/animations.css +25 -0
- package/templates/source/design-system/tokens.css +74 -0
- package/templates/source/design-system/typography.css +34 -0
- package/templates/source/theme/ThemeContext.tsx +82 -0
- package/templates/source/theme/useTheme.ts +10 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { validateProjectName } from './validation.js';
|
|
5
|
+
import { ensureDir, writeTemplateFile, copyDirRecursive, } from './templateEngine.js';
|
|
6
|
+
import { buildDashboardConfigFile, buildTokensCssFile, buildIndexHtml, buildLogoSvg, buildAppTsx, buildPackageJson, buildProjectReadme, } from './configBuilder.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
/**
|
|
10
|
+
* Finds the source directory containing the generic dashboard starter kit components.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveStarterKitSrc() {
|
|
13
|
+
const candidates = [
|
|
14
|
+
path.resolve(__dirname, '../templates/source'),
|
|
15
|
+
path.resolve(__dirname, '../../dashboard-starter-kit/src'),
|
|
16
|
+
path.resolve(process.cwd(), 'dashboard-starter-kit/src'),
|
|
17
|
+
];
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
if (fs.existsSync(candidate) && fs.existsSync(path.join(candidate, 'components/layout/AppShell.tsx'))) {
|
|
20
|
+
return candidate;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new Error('Unable to locate dashboard-starter-kit/src directory.');
|
|
24
|
+
}
|
|
25
|
+
export function generateProject(options) {
|
|
26
|
+
const validation = validateProjectName(options.projectName, options.targetDir, options.force);
|
|
27
|
+
if (!validation.valid) {
|
|
28
|
+
throw new Error(validation.error || 'Invalid project name.');
|
|
29
|
+
}
|
|
30
|
+
const starterSrc = resolveStarterKitSrc();
|
|
31
|
+
const targetDir = path.resolve(options.targetDir);
|
|
32
|
+
ensureDir(targetDir);
|
|
33
|
+
// 1. Generate package.json & Vite / TS configs
|
|
34
|
+
writeTemplateFile(targetDir, 'package.json', buildPackageJson(options));
|
|
35
|
+
writeTemplateFile(targetDir, 'README.md', buildProjectReadme(options));
|
|
36
|
+
writeTemplateFile(targetDir, 'index.html', buildIndexHtml(options));
|
|
37
|
+
const viteConfigContent = `import tailwindcss from '@tailwindcss/vite'
|
|
38
|
+
import react from '@vitejs/plugin-react'
|
|
39
|
+
import { defineConfig } from 'vite'
|
|
40
|
+
|
|
41
|
+
export default defineConfig({
|
|
42
|
+
plugins: [react(), tailwindcss()],
|
|
43
|
+
})
|
|
44
|
+
`;
|
|
45
|
+
writeTemplateFile(targetDir, 'vite.config.ts', viteConfigContent);
|
|
46
|
+
const tsconfigRootContent = `{
|
|
47
|
+
"files": [],
|
|
48
|
+
"references": [
|
|
49
|
+
{ "path": "./tsconfig.app.json" },
|
|
50
|
+
{ "path": "./tsconfig.node.json" }
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
writeTemplateFile(targetDir, 'tsconfig.json', tsconfigRootContent);
|
|
55
|
+
const tsconfigAppContent = `{
|
|
56
|
+
"compilerOptions": {
|
|
57
|
+
"target": "es2023",
|
|
58
|
+
"lib": ["ES2023", "DOM"],
|
|
59
|
+
"module": "esnext",
|
|
60
|
+
"types": ["vite/client"],
|
|
61
|
+
"allowArbitraryExtensions": true,
|
|
62
|
+
"skipLibCheck": true,
|
|
63
|
+
"moduleResolution": "bundler",
|
|
64
|
+
"allowImportingTsExtensions": true,
|
|
65
|
+
"verbatimModuleSyntax": true,
|
|
66
|
+
"moduleDetection": "force",
|
|
67
|
+
"noEmit": true,
|
|
68
|
+
"jsx": "react-jsx",
|
|
69
|
+
"noUnusedLocals": true,
|
|
70
|
+
"noUnusedParameters": true,
|
|
71
|
+
"noFallthroughCasesInSwitch": true
|
|
72
|
+
},
|
|
73
|
+
"include": ["src"],
|
|
74
|
+
"exclude": ["src/**/*.test.ts"]
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
writeTemplateFile(targetDir, 'tsconfig.app.json', tsconfigAppContent);
|
|
78
|
+
const tsconfigNodeContent = `{
|
|
79
|
+
"compilerOptions": {
|
|
80
|
+
"target": "es2023",
|
|
81
|
+
"lib": ["ES2023"],
|
|
82
|
+
"types": ["node"],
|
|
83
|
+
"skipLibCheck": true,
|
|
84
|
+
"module": "nodenext",
|
|
85
|
+
"allowImportingTsExtensions": true,
|
|
86
|
+
"verbatimModuleSyntax": true,
|
|
87
|
+
"moduleDetection": "force",
|
|
88
|
+
"noEmit": true,
|
|
89
|
+
"noUnusedLocals": true,
|
|
90
|
+
"noUnusedParameters": true,
|
|
91
|
+
"noFallthroughCasesInSwitch": true
|
|
92
|
+
},
|
|
93
|
+
"include": ["vite.config.ts"]
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
writeTemplateFile(targetDir, 'tsconfig.node.json', tsconfigNodeContent);
|
|
97
|
+
// 2. Generate Public Assets
|
|
98
|
+
writeTemplateFile(targetDir, 'public/assets/logo.svg', buildLogoSvg(options));
|
|
99
|
+
// 3. Copy Generic Components from Starter Kit
|
|
100
|
+
const componentsToCopy = [
|
|
101
|
+
'controls',
|
|
102
|
+
'feedback',
|
|
103
|
+
'icons',
|
|
104
|
+
'layout',
|
|
105
|
+
'metrics',
|
|
106
|
+
'navigation',
|
|
107
|
+
'surfaces',
|
|
108
|
+
];
|
|
109
|
+
for (const compDir of componentsToCopy) {
|
|
110
|
+
const srcCompDir = path.join(starterSrc, 'components', compDir);
|
|
111
|
+
const destCompDir = path.join(targetDir, 'src', 'components', compDir);
|
|
112
|
+
copyDirRecursive(srcCompDir, destCompDir);
|
|
113
|
+
}
|
|
114
|
+
// 4. Copy Design System Files & Generate tokens.css
|
|
115
|
+
const destDesignDir = path.join(targetDir, 'src', 'design-system');
|
|
116
|
+
ensureDir(destDesignDir);
|
|
117
|
+
fs.copyFileSync(path.join(starterSrc, 'design-system', 'typography.css'), path.join(destDesignDir, 'typography.css'));
|
|
118
|
+
fs.copyFileSync(path.join(starterSrc, 'design-system', 'animations.css'), path.join(destDesignDir, 'animations.css'));
|
|
119
|
+
writeTemplateFile(targetDir, 'src/design-system/tokens.css', buildTokensCssFile(options));
|
|
120
|
+
// 5. Copy Theme System
|
|
121
|
+
const destThemeDir = path.join(targetDir, 'src', 'theme');
|
|
122
|
+
ensureDir(destThemeDir);
|
|
123
|
+
fs.copyFileSync(path.join(starterSrc, 'theme', 'ThemeContext.tsx'), path.join(destThemeDir, 'ThemeContext.tsx'));
|
|
124
|
+
fs.copyFileSync(path.join(starterSrc, 'theme', 'useTheme.ts'), path.join(destThemeDir, 'useTheme.ts'));
|
|
125
|
+
// 6. Generate Centralized dashboard.config.ts
|
|
126
|
+
writeTemplateFile(targetDir, 'src/config/dashboard.config.ts', buildDashboardConfigFile(options));
|
|
127
|
+
// 7. Generate src/index.css
|
|
128
|
+
const indexCssContent = `@import "tailwindcss";
|
|
129
|
+
|
|
130
|
+
@import "./design-system/tokens.css";
|
|
131
|
+
@import "./design-system/typography.css";
|
|
132
|
+
@import "./design-system/animations.css";
|
|
133
|
+
|
|
134
|
+
@theme inline {
|
|
135
|
+
--color-background: var(--background);
|
|
136
|
+
--color-foreground: var(--foreground);
|
|
137
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
138
|
+
--color-surface: var(--surface);
|
|
139
|
+
--color-surface-elevated: var(--surface-elevated);
|
|
140
|
+
--color-muted: var(--muted);
|
|
141
|
+
--color-border: var(--border);
|
|
142
|
+
--color-input: var(--input);
|
|
143
|
+
--color-canvas: var(--canvas-background);
|
|
144
|
+
|
|
145
|
+
--color-primary: var(--primary);
|
|
146
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
147
|
+
--color-ring: var(--ring);
|
|
148
|
+
|
|
149
|
+
--color-success: var(--success);
|
|
150
|
+
--color-success-fg: var(--success-fg);
|
|
151
|
+
--color-success-bg: var(--success-bg);
|
|
152
|
+
|
|
153
|
+
--color-warning: var(--warning);
|
|
154
|
+
--color-warning-fg: var(--warning-fg);
|
|
155
|
+
--color-warning-bg: var(--warning-bg);
|
|
156
|
+
|
|
157
|
+
--color-danger: var(--danger);
|
|
158
|
+
--color-danger-fg: var(--danger-fg);
|
|
159
|
+
--color-danger-bg: var(--danger-bg);
|
|
160
|
+
|
|
161
|
+
--color-info: var(--info);
|
|
162
|
+
--color-info-fg: var(--info-fg);
|
|
163
|
+
--color-info-bg: var(--info-bg);
|
|
164
|
+
}
|
|
165
|
+
`;
|
|
166
|
+
writeTemplateFile(targetDir, 'src/index.css', indexCssContent);
|
|
167
|
+
// 8. Generate src/main.tsx
|
|
168
|
+
const mainTsxContent = `import { StrictMode } from 'react'
|
|
169
|
+
import { createRoot } from 'react-dom/client'
|
|
170
|
+
import './index.css'
|
|
171
|
+
import App from './App.tsx'
|
|
172
|
+
|
|
173
|
+
createRoot(document.getElementById('root')!).render(
|
|
174
|
+
<StrictMode>
|
|
175
|
+
<App />
|
|
176
|
+
</StrictMode>
|
|
177
|
+
)
|
|
178
|
+
`;
|
|
179
|
+
writeTemplateFile(targetDir, 'src/main.tsx', mainTsxContent);
|
|
180
|
+
// 9. Generate src/App.tsx
|
|
181
|
+
writeTemplateFile(targetDir, 'src/App.tsx', buildAppTsx(options));
|
|
182
|
+
// 10. Generate src/index.ts (barrel exports for consumer)
|
|
183
|
+
const barrelContent = `export * from './components/layout/AppShell.tsx'
|
|
184
|
+
export * from './components/layout/Header.tsx'
|
|
185
|
+
export * from './components/layout/Sidebar.tsx'
|
|
186
|
+
export * from './components/layout/PageHeader.tsx'
|
|
187
|
+
export * from './components/surfaces/HeroSurface.tsx'
|
|
188
|
+
export * from './components/surfaces/Panel.tsx'
|
|
189
|
+
export * from './components/surfaces/PriorityCard.tsx'
|
|
190
|
+
export * from './components/surfaces/QuietListCard.tsx'
|
|
191
|
+
export * from './components/surfaces/EmptyState.tsx'
|
|
192
|
+
export * from './components/metrics/StatCard.tsx'
|
|
193
|
+
export * from './components/metrics/MetricRow.tsx'
|
|
194
|
+
export * from './components/navigation/NavItem.tsx'
|
|
195
|
+
export * from './components/navigation/ShortcutPill.tsx'
|
|
196
|
+
export * from './components/feedback/StatusBadge.tsx'
|
|
197
|
+
export * from './components/feedback/NotificationCenter.tsx'
|
|
198
|
+
export * from './components/feedback/Skeleton.tsx'
|
|
199
|
+
export * from './components/controls/Button.tsx'
|
|
200
|
+
export * from './components/controls/Input.tsx'
|
|
201
|
+
export * from './components/controls/Select.tsx'
|
|
202
|
+
export * from './components/controls/SearchBar.tsx'
|
|
203
|
+
export * from './components/controls/ThemeToggle.tsx'
|
|
204
|
+
export * from './components/icons/IconSet.tsx'
|
|
205
|
+
export * from './theme/ThemeContext.tsx'
|
|
206
|
+
export * from './theme/useTheme.ts'
|
|
207
|
+
export * from './config/dashboard.config.ts'
|
|
208
|
+
`;
|
|
209
|
+
writeTemplateFile(targetDir, 'src/index.ts', barrelContent);
|
|
210
|
+
// 11. Generate Smoke Test in tests/
|
|
211
|
+
const smokeTestContent = `import test from 'node:test'
|
|
212
|
+
import assert from 'node:assert/strict'
|
|
213
|
+
import { dashboardConfig } from '../src/config/dashboard.config.ts'
|
|
214
|
+
|
|
215
|
+
test('dashboard configuration loaded with correct branding and tokens', () => {
|
|
216
|
+
assert.equal(dashboardConfig.branding.name, '${options.brandName.replace(/'/g, "\\'")}')
|
|
217
|
+
assert.ok(dashboardConfig.navigation.length > 0)
|
|
218
|
+
assert.ok(dashboardConfig.colors.primary.light.length > 0)
|
|
219
|
+
})
|
|
220
|
+
`;
|
|
221
|
+
writeTemplateFile(targetDir, 'tests/dashboard.test.ts', smokeTestContent);
|
|
222
|
+
return { success: true, targetDir };
|
|
223
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PresetThemeTokens, ThemePreset } from './types.js';
|
|
2
|
+
export declare const JOBTRACK_PRESET: PresetThemeTokens;
|
|
3
|
+
export declare const CAFE_PRESET: PresetThemeTokens;
|
|
4
|
+
/**
|
|
5
|
+
* Adjusts or computes a dark mode counterpart for a hex color if not provided.
|
|
6
|
+
*/
|
|
7
|
+
export declare function deriveDarkColor(hex: string): string;
|
|
8
|
+
export declare function resolvePresetTokens(preset: ThemePreset, customPrimary?: string, customAccent?: string): PresetThemeTokens;
|
package/dist/presets.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const JOBTRACK_PRESET = {
|
|
2
|
+
name: 'JobTrack Blue',
|
|
3
|
+
brandDefault: 'Acme Workspace',
|
|
4
|
+
taglineDefault: 'Team operations and project command center',
|
|
5
|
+
primaryLight: '#2563eb',
|
|
6
|
+
primaryDark: '#3b82f6',
|
|
7
|
+
accentLight: '#059669',
|
|
8
|
+
accentDark: '#10b981',
|
|
9
|
+
backgroundLight: '#f4f5f7',
|
|
10
|
+
backgroundDark: '#0b0c0e',
|
|
11
|
+
canvasLight: '#eef3fb',
|
|
12
|
+
canvasDark: '#181a1f',
|
|
13
|
+
surfaceLight: '#ffffff',
|
|
14
|
+
surfaceDark: '#151719',
|
|
15
|
+
surfaceElevatedLight: '#ffffff',
|
|
16
|
+
surfaceElevatedDark: '#1c1f22',
|
|
17
|
+
borderLight: '#e3e7ec',
|
|
18
|
+
borderDark: '#2a2f35',
|
|
19
|
+
};
|
|
20
|
+
export const CAFE_PRESET = {
|
|
21
|
+
name: 'Cafe OS Amber',
|
|
22
|
+
brandDefault: 'Cafe OS',
|
|
23
|
+
taglineDefault: 'Artisan Coffeehouse & Kitchen Management Suite',
|
|
24
|
+
primaryLight: '#d97706',
|
|
25
|
+
primaryDark: '#f59e0b',
|
|
26
|
+
accentLight: '#15803d',
|
|
27
|
+
accentDark: '#22c55e',
|
|
28
|
+
backgroundLight: '#fdfbf7',
|
|
29
|
+
backgroundDark: '#0e0d0b',
|
|
30
|
+
canvasLight: '#f5efe6',
|
|
31
|
+
canvasDark: '#1a1815',
|
|
32
|
+
surfaceLight: '#ffffff',
|
|
33
|
+
surfaceDark: '#1e1b17',
|
|
34
|
+
surfaceElevatedLight: '#ffffff',
|
|
35
|
+
surfaceElevatedDark: '#26221d',
|
|
36
|
+
borderLight: '#e8dfd3',
|
|
37
|
+
borderDark: '#383229',
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Adjusts or computes a dark mode counterpart for a hex color if not provided.
|
|
41
|
+
*/
|
|
42
|
+
export function deriveDarkColor(hex) {
|
|
43
|
+
// Simple luminosity shift or fallback to lighter tone if user supplied custom color
|
|
44
|
+
return hex;
|
|
45
|
+
}
|
|
46
|
+
export function resolvePresetTokens(preset, customPrimary, customAccent) {
|
|
47
|
+
if (preset === 'cafe') {
|
|
48
|
+
return CAFE_PRESET;
|
|
49
|
+
}
|
|
50
|
+
if (preset === 'custom' && customPrimary) {
|
|
51
|
+
return {
|
|
52
|
+
name: 'Custom Theme',
|
|
53
|
+
brandDefault: 'My Dashboard',
|
|
54
|
+
taglineDefault: 'Custom Operational Dashboard',
|
|
55
|
+
primaryLight: customPrimary,
|
|
56
|
+
primaryDark: deriveDarkColor(customPrimary),
|
|
57
|
+
accentLight: customAccent || '#059669',
|
|
58
|
+
accentDark: customAccent || '#10b981',
|
|
59
|
+
backgroundLight: '#f4f5f7',
|
|
60
|
+
backgroundDark: '#0b0c0e',
|
|
61
|
+
canvasLight: '#f1f5f9',
|
|
62
|
+
canvasDark: '#181a1f',
|
|
63
|
+
surfaceLight: '#ffffff',
|
|
64
|
+
surfaceDark: '#151719',
|
|
65
|
+
surfaceElevatedLight: '#ffffff',
|
|
66
|
+
surfaceElevatedDark: '#1c1f22',
|
|
67
|
+
borderLight: '#e2e8f0',
|
|
68
|
+
borderDark: '#2a2f35',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return JOBTRACK_PRESET;
|
|
72
|
+
}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
import { validateHexColor } from './validation.js';
|
|
4
|
+
export async function promptInteractive(initialProjectName) {
|
|
5
|
+
const rl = readline.createInterface({ input, output });
|
|
6
|
+
try {
|
|
7
|
+
console.log('\n✨ Welcome to create-dashboard-kit!');
|
|
8
|
+
console.log('Scaffold a modern, editorial dashboard extracted from JobTrack.\n');
|
|
9
|
+
// 1. Project Name
|
|
10
|
+
let projectName = initialProjectName || '';
|
|
11
|
+
while (projectName.trim().length === 0) {
|
|
12
|
+
projectName = await rl.question('? Project name: ');
|
|
13
|
+
if (!projectName.trim()) {
|
|
14
|
+
console.log(' ⚠️ Project name is required.');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
projectName = projectName.trim();
|
|
18
|
+
// 2. Brand Name
|
|
19
|
+
const defaultBrand = projectName
|
|
20
|
+
.split(/[-_]/)
|
|
21
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
22
|
+
.join(' ');
|
|
23
|
+
const brandInput = await rl.question(`? Display / Brand name (${defaultBrand}): `);
|
|
24
|
+
const brandName = brandInput.trim() || defaultBrand;
|
|
25
|
+
// 3. Theme Preset
|
|
26
|
+
console.log('\n? Choose a theme preset:');
|
|
27
|
+
console.log(' 1) JobTrack Blue (Modern SaaS, Royal Blue & Slate)');
|
|
28
|
+
console.log(' 2) Cafe OS Amber (Artisan F&B, Roast Amber & Cream Linen)');
|
|
29
|
+
console.log(' 3) Custom Palette');
|
|
30
|
+
const presetChoice = await rl.question(' Select preset [1-3] (1): ');
|
|
31
|
+
let preset = 'jobtrack';
|
|
32
|
+
if (presetChoice.trim() === '2') {
|
|
33
|
+
preset = 'cafe';
|
|
34
|
+
}
|
|
35
|
+
else if (presetChoice.trim() === '3') {
|
|
36
|
+
preset = 'custom';
|
|
37
|
+
}
|
|
38
|
+
// 4. Primary & Accent Colors
|
|
39
|
+
let primaryColor = preset === 'cafe' ? '#d97706' : '#2563eb';
|
|
40
|
+
let accentColor = preset === 'cafe' ? '#15803d' : '#059669';
|
|
41
|
+
if (preset === 'custom') {
|
|
42
|
+
let customPrimary = '';
|
|
43
|
+
while (!validateHexColor(customPrimary)) {
|
|
44
|
+
customPrimary = await rl.question('? Primary hex color (e.g. #7c3aed): ');
|
|
45
|
+
if (!validateHexColor(customPrimary)) {
|
|
46
|
+
console.log(' ⚠️ Please enter a valid hex color starting with # (e.g. #7c3aed)');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
primaryColor = customPrimary.trim();
|
|
50
|
+
let customAccent = '';
|
|
51
|
+
while (!validateHexColor(customAccent)) {
|
|
52
|
+
customAccent = await rl.question('? Accent hex color (e.g. #06b6d4): ');
|
|
53
|
+
if (!validateHexColor(customAccent)) {
|
|
54
|
+
console.log(' ⚠️ Please enter a valid hex color starting with # (e.g. #06b6d4)');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
accentColor = customAccent.trim();
|
|
58
|
+
}
|
|
59
|
+
// 5. Default Theme Mode
|
|
60
|
+
console.log('\n? Default color mode:');
|
|
61
|
+
console.log(' 1) System (follows user preference)');
|
|
62
|
+
console.log(' 2) Dark');
|
|
63
|
+
console.log(' 3) Light');
|
|
64
|
+
const modeChoice = await rl.question(' Select mode [1-3] (1): ');
|
|
65
|
+
let defaultMode = 'system';
|
|
66
|
+
if (modeChoice.trim() === '2')
|
|
67
|
+
defaultMode = 'dark';
|
|
68
|
+
if (modeChoice.trim() === '3')
|
|
69
|
+
defaultMode = 'light';
|
|
70
|
+
// 6. Button Shape
|
|
71
|
+
console.log('\n? Button shape:');
|
|
72
|
+
console.log(' 1) Pill (rounded-full - Signature JobTrack style)');
|
|
73
|
+
console.log(' 2) Rounded (rounded-xl)');
|
|
74
|
+
console.log(' 3) Square (rounded-md)');
|
|
75
|
+
const shapeChoice = await rl.question(' Select shape [1-3] (1): ');
|
|
76
|
+
let buttonShape = 'pill';
|
|
77
|
+
if (shapeChoice.trim() === '2')
|
|
78
|
+
buttonShape = 'rounded';
|
|
79
|
+
if (shapeChoice.trim() === '3')
|
|
80
|
+
buttonShape = 'square';
|
|
81
|
+
// 7. Card Radius
|
|
82
|
+
console.log('\n? Card border radius:');
|
|
83
|
+
console.log(' 1) Large (rounded-2xl - Signature JobTrack style)');
|
|
84
|
+
console.log(' 2) Medium (rounded-xl)');
|
|
85
|
+
console.log(' 3) Small (rounded-lg)');
|
|
86
|
+
const radiusChoice = await rl.question(' Select radius [1-3] (1): ');
|
|
87
|
+
let cardRadius = 'large';
|
|
88
|
+
if (radiusChoice.trim() === '2')
|
|
89
|
+
cardRadius = 'medium';
|
|
90
|
+
if (radiusChoice.trim() === '3')
|
|
91
|
+
cardRadius = 'small';
|
|
92
|
+
// 8. Sidebar State
|
|
93
|
+
console.log('\n? Sidebar default desktop state:');
|
|
94
|
+
console.log(' 1) Expanded (w-64)');
|
|
95
|
+
console.log(' 2) Collapsed (w-16 compact icon rail)');
|
|
96
|
+
const sidebarChoice = await rl.question(' Select state [1-2] (1): ');
|
|
97
|
+
const sidebarState = sidebarChoice.trim() === '2' ? 'collapsed' : 'expanded';
|
|
98
|
+
// 9. Include Demo Data
|
|
99
|
+
const demoChoice = await rl.question('\n? Include demo data & prebuilt panels? [Y/n]: ');
|
|
100
|
+
const includeDemo = !demoChoice.trim().toLowerCase().startsWith('n');
|
|
101
|
+
// 10. Framework (Locked to React + Vite as per specification)
|
|
102
|
+
const targetDir = `./${projectName}`;
|
|
103
|
+
return {
|
|
104
|
+
projectName,
|
|
105
|
+
targetDir,
|
|
106
|
+
brandName,
|
|
107
|
+
preset,
|
|
108
|
+
primaryColor,
|
|
109
|
+
accentColor,
|
|
110
|
+
defaultMode,
|
|
111
|
+
buttonShape,
|
|
112
|
+
cardRadius,
|
|
113
|
+
sidebarState,
|
|
114
|
+
includeDemo,
|
|
115
|
+
framework: 'react-vite',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
rl.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function ensureDir(dirPath: string): void;
|
|
2
|
+
export declare function writeTemplateFile(baseDir: string, relativePath: string, content: string): void;
|
|
3
|
+
export declare function copyDirRecursive(srcDir: string, destDir: string, filter?: (fileName: string, fullPath: string) => boolean): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sanitizeTargetPath } from './validation.js';
|
|
4
|
+
export function ensureDir(dirPath) {
|
|
5
|
+
if (!fs.existsSync(dirPath)) {
|
|
6
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function writeTemplateFile(baseDir, relativePath, content) {
|
|
10
|
+
const targetPath = sanitizeTargetPath(baseDir, relativePath);
|
|
11
|
+
ensureDir(path.dirname(targetPath));
|
|
12
|
+
fs.writeFileSync(targetPath, content, 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
export function copyDirRecursive(srcDir, destDir, filter) {
|
|
15
|
+
ensureDir(destDir);
|
|
16
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
19
|
+
const destPath = path.join(destDir, entry.name);
|
|
20
|
+
if (filter && !filter(entry.name, srcPath)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
copyDirRecursive(srcPath, destPath, filter);
|
|
25
|
+
}
|
|
26
|
+
else if (entry.isFile()) {
|
|
27
|
+
ensureDir(path.dirname(destPath));
|
|
28
|
+
fs.copyFileSync(srcPath, destPath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type ThemePreset = 'jobtrack' | 'cafe' | 'custom';
|
|
2
|
+
export type ThemeMode = 'light' | 'dark' | 'system';
|
|
3
|
+
export type ButtonShape = 'pill' | 'rounded' | 'square';
|
|
4
|
+
export type CardRadius = 'small' | 'medium' | 'large';
|
|
5
|
+
export type SidebarState = 'expanded' | 'collapsed';
|
|
6
|
+
export interface GeneratorOptions {
|
|
7
|
+
projectName: string;
|
|
8
|
+
targetDir: string;
|
|
9
|
+
brandName: string;
|
|
10
|
+
tagline?: string;
|
|
11
|
+
preset: ThemePreset;
|
|
12
|
+
primaryColor: string;
|
|
13
|
+
accentColor: string;
|
|
14
|
+
defaultMode: ThemeMode;
|
|
15
|
+
buttonShape: ButtonShape;
|
|
16
|
+
cardRadius: CardRadius;
|
|
17
|
+
sidebarState: SidebarState;
|
|
18
|
+
includeDemo: boolean;
|
|
19
|
+
framework: 'react-vite';
|
|
20
|
+
force?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface CliFlags {
|
|
23
|
+
brand?: string;
|
|
24
|
+
preset?: string;
|
|
25
|
+
primary?: string;
|
|
26
|
+
accent?: string;
|
|
27
|
+
mode?: string;
|
|
28
|
+
shape?: string;
|
|
29
|
+
radius?: string;
|
|
30
|
+
sidebar?: string;
|
|
31
|
+
demo?: boolean;
|
|
32
|
+
force?: boolean;
|
|
33
|
+
help?: boolean;
|
|
34
|
+
version?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface PresetThemeTokens {
|
|
37
|
+
name: string;
|
|
38
|
+
brandDefault: string;
|
|
39
|
+
taglineDefault: string;
|
|
40
|
+
primaryLight: string;
|
|
41
|
+
primaryDark: string;
|
|
42
|
+
accentLight: string;
|
|
43
|
+
accentDark: string;
|
|
44
|
+
backgroundLight: string;
|
|
45
|
+
backgroundDark: string;
|
|
46
|
+
canvasLight: string;
|
|
47
|
+
canvasDark: string;
|
|
48
|
+
surfaceLight: string;
|
|
49
|
+
surfaceDark: string;
|
|
50
|
+
surfaceElevatedLight: string;
|
|
51
|
+
surfaceElevatedDark: string;
|
|
52
|
+
borderLight: string;
|
|
53
|
+
borderDark: string;
|
|
54
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates whether a project name conforms to npm package naming rules
|
|
3
|
+
* and is filesystem-safe.
|
|
4
|
+
*/
|
|
5
|
+
export declare function validateProjectName(name: string, targetDir: string, force?: boolean): {
|
|
6
|
+
valid: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Validates a 3, 6, or 8 digit hex color string (e.g. #2563eb, #fff).
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateHexColor(color: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Sanitizes a target filesystem path and ensures it stays strictly within the resolved base directory.
|
|
15
|
+
*/
|
|
16
|
+
export declare function sanitizeTargetPath(baseDir: string, relativePath: string): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Validates whether a project name conforms to npm package naming rules
|
|
5
|
+
* and is filesystem-safe.
|
|
6
|
+
*/
|
|
7
|
+
export function validateProjectName(name, targetDir, force = false) {
|
|
8
|
+
if (!name || name.trim().length === 0) {
|
|
9
|
+
return { valid: false, error: 'Project name cannot be empty.' };
|
|
10
|
+
}
|
|
11
|
+
const trimmed = name.trim();
|
|
12
|
+
// Guard against path traversal patterns and absolute paths in project name
|
|
13
|
+
if (trimmed.includes('..') || trimmed.includes('\0') || trimmed.startsWith('/') || trimmed.startsWith('\\')) {
|
|
14
|
+
return { valid: false, error: 'Project name cannot contain path traversal characters (.., /, \\).' };
|
|
15
|
+
}
|
|
16
|
+
// npm naming convention: lowercase alphanumeric, hyphens, underscores
|
|
17
|
+
const npmSafeRegex = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
18
|
+
if (!npmSafeRegex.test(trimmed)) {
|
|
19
|
+
return {
|
|
20
|
+
valid: false,
|
|
21
|
+
error: `Project name "${trimmed}" must be URL/npm-safe (lowercase letters, numbers, hyphens, or underscores).`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Reserved filesystem and npm names
|
|
25
|
+
const reservedNames = new Set([
|
|
26
|
+
'node_modules',
|
|
27
|
+
'favicon.ico',
|
|
28
|
+
'public',
|
|
29
|
+
'src',
|
|
30
|
+
'dist',
|
|
31
|
+
'build',
|
|
32
|
+
'package.json',
|
|
33
|
+
]);
|
|
34
|
+
if (reservedNames.has(trimmed.toLowerCase())) {
|
|
35
|
+
return { valid: false, error: `"${trimmed}" is a reserved directory/package name.` };
|
|
36
|
+
}
|
|
37
|
+
// Check if directory already exists
|
|
38
|
+
if (fs.existsSync(targetDir)) {
|
|
39
|
+
try {
|
|
40
|
+
const files = fs.readdirSync(targetDir);
|
|
41
|
+
if (files.length > 0 && !force) {
|
|
42
|
+
return {
|
|
43
|
+
valid: false,
|
|
44
|
+
error: `Target directory "${path.basename(targetDir)}" already exists and is not empty. Use --force to overwrite.`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// If unable to read directory, treat as error
|
|
50
|
+
return { valid: false, error: `Cannot access target directory "${targetDir}".` };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { valid: true };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validates a 3, 6, or 8 digit hex color string (e.g. #2563eb, #fff).
|
|
57
|
+
*/
|
|
58
|
+
export function validateHexColor(color) {
|
|
59
|
+
if (!color)
|
|
60
|
+
return false;
|
|
61
|
+
return /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(color.trim());
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sanitizes a target filesystem path and ensures it stays strictly within the resolved base directory.
|
|
65
|
+
*/
|
|
66
|
+
export function sanitizeTargetPath(baseDir, relativePath) {
|
|
67
|
+
const resolvedBase = path.resolve(baseDir);
|
|
68
|
+
const resolvedTarget = path.resolve(resolvedBase, relativePath);
|
|
69
|
+
if (!resolvedTarget.startsWith(resolvedBase)) {
|
|
70
|
+
throw new Error(`Security Violation: Path traversal detected outside "${resolvedBase}".`);
|
|
71
|
+
}
|
|
72
|
+
return resolvedTarget;
|
|
73
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mohammad-irfan/create-dashboard-kit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Project generator for modern, customizable dashboards based on the JobTrack design system.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"types": "dist/cli.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"create-dashboard-kit": "bin/create-dashboard.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"dist",
|
|
14
|
+
"templates",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"prepack": "npm run build",
|
|
24
|
+
"test": "node --test tests/cli.test.ts",
|
|
25
|
+
"start": "node bin/create-dashboard.js"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"create-dashboard-kit",
|
|
29
|
+
"create-dashboard",
|
|
30
|
+
"dashboard",
|
|
31
|
+
"starter-kit",
|
|
32
|
+
"cli",
|
|
33
|
+
"generator",
|
|
34
|
+
"react",
|
|
35
|
+
"vite",
|
|
36
|
+
"tailwind",
|
|
37
|
+
"design-system"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/mr-irfan1/create-dashboard-kit.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/mr-irfan1/create-dashboard-kit#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/mr-irfan1/create-dashboard-kit/issues"
|
|
46
|
+
},
|
|
47
|
+
"author": "JobTrack Team",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^24.13.3",
|
|
54
|
+
"typescript": "~6.0.2"
|
|
55
|
+
}
|
|
56
|
+
}
|