@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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/create-dashboard.js +12 -0
  4. package/dist/cli.d.ts +7 -0
  5. package/dist/cli.js +223 -0
  6. package/dist/configBuilder.d.ts +8 -0
  7. package/dist/configBuilder.js +573 -0
  8. package/dist/generator.d.ts +9 -0
  9. package/dist/generator.js +223 -0
  10. package/dist/presets.d.ts +8 -0
  11. package/dist/presets.js +72 -0
  12. package/dist/prompts.d.ts +2 -0
  13. package/dist/prompts.js +121 -0
  14. package/dist/templateEngine.d.ts +3 -0
  15. package/dist/templateEngine.js +31 -0
  16. package/dist/types.d.ts +54 -0
  17. package/dist/types.js +1 -0
  18. package/dist/validation.d.ts +16 -0
  19. package/dist/validation.js +73 -0
  20. package/package.json +56 -0
  21. package/templates/source/components/controls/Button.tsx +69 -0
  22. package/templates/source/components/controls/Input.tsx +59 -0
  23. package/templates/source/components/controls/SearchBar.tsx +44 -0
  24. package/templates/source/components/controls/Select.tsx +61 -0
  25. package/templates/source/components/controls/ThemeToggle.tsx +23 -0
  26. package/templates/source/components/feedback/NotificationCenter.tsx +151 -0
  27. package/templates/source/components/feedback/Skeleton.tsx +22 -0
  28. package/templates/source/components/feedback/StatusBadge.tsx +35 -0
  29. package/templates/source/components/feedback/statusBadgeStyles.ts +28 -0
  30. package/templates/source/components/icons/IconSet.tsx +235 -0
  31. package/templates/source/components/layout/AppShell.tsx +70 -0
  32. package/templates/source/components/layout/Header.tsx +90 -0
  33. package/templates/source/components/layout/PageHeader.tsx +27 -0
  34. package/templates/source/components/layout/Sidebar.tsx +202 -0
  35. package/templates/source/components/metrics/MetricRow.tsx +61 -0
  36. package/templates/source/components/metrics/StatCard.tsx +49 -0
  37. package/templates/source/components/navigation/NavItem.tsx +75 -0
  38. package/templates/source/components/navigation/ShortcutPill.tsx +39 -0
  39. package/templates/source/components/surfaces/EmptyState.tsx +36 -0
  40. package/templates/source/components/surfaces/HeroSurface.tsx +64 -0
  41. package/templates/source/components/surfaces/Panel.tsx +43 -0
  42. package/templates/source/components/surfaces/PriorityCard.tsx +75 -0
  43. package/templates/source/components/surfaces/QuietListCard.tsx +146 -0
  44. package/templates/source/design-system/animations.css +25 -0
  45. package/templates/source/design-system/tokens.css +74 -0
  46. package/templates/source/design-system/typography.css +34 -0
  47. package/templates/source/theme/ThemeContext.tsx +82 -0
  48. 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;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { GeneratorOptions } from './types.js';
2
+ export declare function promptInteractive(initialProjectName?: string): Promise<GeneratorOptions>;
@@ -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
+ }
@@ -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
+ }