create-template-html-css 1.6.4 → 1.8.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/src/generator.js CHANGED
@@ -40,6 +40,179 @@ async function formatJs(jsContent) {
40
40
  }
41
41
  }
42
42
 
43
+ /**
44
+ * Color scheme presets
45
+ */
46
+ const COLOR_SCHEMES = {
47
+ vibrant: {
48
+ name: "Vibrant",
49
+ primary: "#FF6B6B",
50
+ secondary: "#4ECDC4",
51
+ description: "Bold reds and teals - energetic and modern"
52
+ },
53
+ pastel: {
54
+ name: "Pastel",
55
+ primary: "#FFB3BA",
56
+ secondary: "#FFDFBA",
57
+ description: "Soft pinks and peaches - calm and friendly"
58
+ },
59
+ ocean: {
60
+ name: "Ocean",
61
+ primary: "#0066CC",
62
+ secondary: "#00CCFF",
63
+ description: "Deep blues and cyans - professional and cool"
64
+ },
65
+ sunset: {
66
+ name: "Sunset",
67
+ primary: "#FF6B35",
68
+ secondary: "#FFA500",
69
+ description: "Warm oranges and reds - energetic glow"
70
+ },
71
+ forest: {
72
+ name: "Forest",
73
+ primary: "#2D6A4F",
74
+ secondary: "#40916C",
75
+ description: "Green tones - natural and organic"
76
+ },
77
+ purple: {
78
+ name: "Purple",
79
+ primary: "#7209B7",
80
+ secondary: "#B5179E",
81
+ description: "Deep purples - elegant and premium"
82
+ },
83
+ minimal: {
84
+ name: "Minimal",
85
+ primary: "#1A1A1A",
86
+ secondary: "#666666",
87
+ description: "Grays and blacks - clean and simple"
88
+ },
89
+ coral: {
90
+ name: "Coral",
91
+ primary: "#FF6B9D",
92
+ secondary: "#FFA07A",
93
+ description: "Coral pinks and salmon - warm and playful"
94
+ },
95
+ teal: {
96
+ name: "Teal",
97
+ primary: "#008B8B",
98
+ secondary: "#20B2AA",
99
+ description: "Teal hues - balanced and professional"
100
+ },
101
+ neon: {
102
+ name: "Neon",
103
+ primary: "#FF006E",
104
+ secondary: "#00D9FF",
105
+ description: "Bright neon colors - bold and futuristic"
106
+ }
107
+ };
108
+
109
+ /**
110
+ * Get color scheme by preset name
111
+ */
112
+ function getColorScheme(schemeName) {
113
+ if (COLOR_SCHEMES[schemeName]) {
114
+ return COLOR_SCHEMES[schemeName];
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Apply custom colors to CSS content using CSS variables
121
+ */
122
+ function applyCustomColors(cssContent, primaryColor, secondaryColor) {
123
+ if (!primaryColor && !secondaryColor) {
124
+ return cssContent;
125
+ }
126
+
127
+ // Create CSS variable overrides
128
+ let colorVariables = ":root {\n";
129
+
130
+ if (primaryColor) {
131
+ colorVariables += ` --primary-color: ${primaryColor};\n`;
132
+ // Also add as root variable that can override gradients
133
+ colorVariables += ` --primary-rgb: ${hexToRgb(primaryColor)};\n`;
134
+ }
135
+
136
+ if (secondaryColor) {
137
+ colorVariables += ` --secondary-color: ${secondaryColor};\n`;
138
+ colorVariables += ` --secondary-rgb: ${hexToRgb(secondaryColor)};\n`;
139
+ }
140
+
141
+ colorVariables += "}\n\n";
142
+
143
+ // If CSS doesn't have :root, add it
144
+ if (!cssContent.includes(":root")) {
145
+ return colorVariables + cssContent;
146
+ }
147
+
148
+ // Replace existing :root with extended one
149
+ return cssContent.replace(/:root\s*{/, colorVariables.trim() + "\n\n:root {");
150
+ }
151
+
152
+ /**
153
+ * Convert hex color to RGB values
154
+ */
155
+ function hexToRgb(hex) {
156
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
157
+ if (result) {
158
+ const r = parseInt(result[1], 16);
159
+ const g = parseInt(result[2], 16);
160
+ const b = parseInt(result[3], 16);
161
+ return `${r}, ${g}, ${b}`;
162
+ }
163
+ return "102, 126, 234"; // fallback to default primary
164
+ }
165
+
166
+ /**
167
+ * Add dark mode styles to CSS
168
+ */
169
+ function addDarkModeStyles(cssContent) {
170
+ if (cssContent.includes("prefers-color-scheme")) {
171
+ return cssContent; // Already has dark mode
172
+ }
173
+
174
+ // Create dark mode media query
175
+ const darkModeStyles = `
176
+ /* Dark Mode Support */
177
+ @media (prefers-color-scheme: dark) {
178
+ :root {
179
+ --bg-primary: #1a1a1a;
180
+ --bg-secondary: #2d2d2d;
181
+ --text-primary: #ffffff;
182
+ --text-secondary: #b0b0b0;
183
+ --border-color: #404040;
184
+ }
185
+
186
+ body {
187
+ background-color: var(--bg-primary, #1a1a1a);
188
+ color: var(--text-primary, #ffffff);
189
+ }
190
+
191
+ .container,
192
+ .card,
193
+ .modal-content,
194
+ .form-container {
195
+ background-color: var(--bg-secondary, #2d2d2d);
196
+ color: var(--text-primary, #ffffff);
197
+ border-color: var(--border-color, #404040);
198
+ }
199
+
200
+ input,
201
+ textarea,
202
+ select {
203
+ background-color: var(--bg-primary, #1a1a1a);
204
+ color: var(--text-primary, #ffffff);
205
+ border-color: var(--border-color, #404040);
206
+ }
207
+
208
+ input::placeholder {
209
+ color: var(--text-secondary, #b0b0b0);
210
+ }
211
+ }`;
212
+
213
+ return cssContent + darkModeStyles;
214
+ }
215
+
43
216
  // Security: Validate component name against whitelist
44
217
  const VALID_COMPONENTS = [
45
218
  "button",
@@ -65,6 +238,9 @@ const VALID_COMPONENTS = [
65
238
  "counter",
66
239
  "accordion",
67
240
  "tabs",
241
+ "login",
242
+ "register",
243
+ "skeleton",
68
244
  ];
69
245
 
70
246
  // Security: Sanitize filename to prevent path traversal
@@ -81,8 +257,305 @@ function sanitizeFilename(filename) {
81
257
  return sanitized.replace(/[<>:"|?*]/g, "").trim();
82
258
  }
83
259
 
260
+ /**
261
+ * Filter button variations based on user selection
262
+ */
263
+ function filterButtonVariations(htmlContent, buttonVariations, selectedButtons) {
264
+ if (buttonVariations === "all" || !selectedButtons || selectedButtons.length === 0) {
265
+ return htmlContent; // Return all buttons
266
+ }
267
+
268
+ // Map selection to button classes
269
+ const buttonMap = {
270
+ primary: "btn-primary",
271
+ secondary: "btn-secondary",
272
+ success: "btn-success",
273
+ danger: "btn-danger",
274
+ outlined: "btn-outlined",
275
+ disabled: "disabled"
276
+ };
277
+
278
+ let filteredHtml = htmlContent;
279
+
280
+ // Remove buttons that weren't selected
281
+ for (const [key, className] of Object.entries(buttonMap)) {
282
+ if (!selectedButtons.includes(key)) {
283
+ // Remove button and its comment
284
+ const patterns = [
285
+ new RegExp(`\\s*<!-- ${key.charAt(0).toUpperCase() + key.slice(1)} Button -->\\s*\\n\\s*<button[^>]*${className}[^>]*>.*?<\\/button>\\s*\\n`, 'gis'),
286
+ new RegExp(`\\s*<button[^>]*${className}[^>]*>.*?<\\/button>\\s*\\n`, 'gis')
287
+ ];
288
+
289
+ patterns.forEach(pattern => {
290
+ filteredHtml = filteredHtml.replace(pattern, '\n ');
291
+ });
292
+ }
293
+ }
294
+
295
+ return filteredHtml;
296
+ }
297
+
298
+ /**
299
+ * Filter card variations based on user selection
300
+ */
301
+ function filterCardVariations(htmlContent, cardVariations, selectedCards) {
302
+ if (cardVariations === "all" || !selectedCards || selectedCards.length === 0) {
303
+ return htmlContent; // Return all cards
304
+ }
305
+
306
+ const cardComments = {
307
+ modern: "Card 1 - Basic",
308
+ premium: "Card 2 - With Price",
309
+ blog: "Card 3 - With Tags",
310
+ minimal: "Card 4 - Minimal",
311
+ user: "Card 5 - With Avatar",
312
+ interactive: "Card 6 - Interactive"
313
+ };
314
+
315
+ let filteredHtml = htmlContent;
316
+
317
+ // Remove cards that weren't selected
318
+ for (const [key, comment] of Object.entries(cardComments)) {
319
+ if (!selectedCards.includes(key)) {
320
+ // Remove entire card div including comment
321
+ const pattern = new RegExp(`\\s*<!-- ${comment.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')} -->\\s*\\n\\s*<div class="card[^>]*>.*?<\\/div>\\s*\\n\\s*<\\/div>\\s*\\n`, 'gis');
322
+ filteredHtml = filteredHtml.replace(pattern, '\n ');
323
+ }
324
+ }
325
+
326
+ return filteredHtml;
327
+ }
328
+
329
+ /**
330
+ * Filter spinner variations based on user selection
331
+ */
332
+ function filterSpinnerVariations(htmlContent, spinnerVariations, selectedSpinners) {
333
+ if (spinnerVariations === "all" || !selectedSpinners || selectedSpinners.length === 0) {
334
+ return htmlContent; // Return all spinners
335
+ }
336
+
337
+ const spinnerTypes = {
338
+ circle: "Circle Spinner",
339
+ dots: "Bouncing Dots",
340
+ pulse: "Pulse Loader",
341
+ bars: "Bar Loader",
342
+ gradient: "Gradient Ring"
343
+ };
344
+
345
+ let filteredHtml = htmlContent;
346
+
347
+ // Remove spinners that weren't selected
348
+ for (const [key, title] of Object.entries(spinnerTypes)) {
349
+ if (!selectedSpinners.includes(key)) {
350
+ // Remove entire spinner-group div
351
+ const pattern = new RegExp(`\\s*<!-- ${title} Spinner -->\\s*\\n\\s*<div class="spinner-group">.*?<\\/div>\\s*\\n\\s*<\\/div>\\s*\\n`, 'gis');
352
+ filteredHtml = filteredHtml.replace(pattern, '\n ');
353
+ }
354
+ }
355
+
356
+ return filteredHtml;
357
+ }
358
+
359
+ /**
360
+ * Generate custom navigation items based on user input
361
+ */
362
+ function generateNavigationItems(htmlContent, navItems) {
363
+ // Parse the comma-separated navigation items
364
+ const items = navItems.split(',').map(item => item.trim()).filter(item => item.length > 0);
365
+
366
+ // Generate navigation HTML
367
+ let navHtml = '';
368
+ items.forEach((item, index) => {
369
+ const itemId = item.toLowerCase().replace(/\s+/g, '-');
370
+ const activeClass = index === 0 ? ' active' : '';
371
+ navHtml += ` <li class="nav-item">
372
+ <a href="#${itemId}" class="nav-link${activeClass}">${item}</a>
373
+ </li>\n`;
374
+ });
375
+
376
+ // Replace the navigation items in the HTML
377
+ const navMenuRegex = /<ul class="nav-menu"[^>]*>[\s\S]*?<\/ul>/;
378
+ const replacement = `<ul class="nav-menu" id="navMenu">
379
+ ${navHtml} </ul>`;
380
+
381
+ htmlContent = htmlContent.replace(navMenuRegex, replacement);
382
+
383
+ // Generate sections for each navigation item
384
+ let sectionsHtml = '';
385
+ items.forEach(item => {
386
+ const itemId = item.toLowerCase().replace(/\s+/g, '-');
387
+ sectionsHtml += ` <section id="${itemId}" class="section">
388
+ <h1>${item}</h1>
389
+ <p>Content for ${item} section</p>
390
+ </section>
391
+
392
+ `;
393
+ });
394
+
395
+ // Replace the sections in the HTML
396
+ const mainContentRegex = /<main class="main-content">[\s\S]*?<\/main>/;
397
+ const sectionsReplacement = `<main class="main-content">
398
+ ${sectionsHtml} </main>`;
399
+
400
+ htmlContent = htmlContent.replace(mainContentRegex, sectionsReplacement);
401
+
402
+ return htmlContent;
403
+ }
404
+
405
+ /**
406
+ * Generate custom form fields based on user selection
407
+ */
408
+ function generateFormFields(htmlContent, formFields, customFormFields) {
409
+ let fieldsHtml = '';
410
+
411
+ // Add standard fields
412
+ formFields.forEach(field => {
413
+ switch(field) {
414
+ case 'name':
415
+ fieldsHtml += ` <div class="form-group">
416
+ <label for="name">Full Name</label>
417
+ <input type="text" id="name" name="name" required>
418
+ </div>
419
+
420
+ `;
421
+ break;
422
+ case 'email':
423
+ fieldsHtml += ` <div class="form-group">
424
+ <label for="email">Email</label>
425
+ <input type="email" id="email" name="email" required>
426
+ </div>
427
+
428
+ `;
429
+ break;
430
+ case 'phone':
431
+ fieldsHtml += ` <div class="form-group">
432
+ <label for="phone">Phone</label>
433
+ <input type="tel" id="phone" name="phone">
434
+ </div>
435
+
436
+ `;
437
+ break;
438
+ case 'subject':
439
+ fieldsHtml += ` <div class="form-group">
440
+ <label for="subject">Subject</label>
441
+ <select id="subject" name="subject" required>
442
+ <option value="">Select a subject</option>
443
+ <option value="support">Technical Support</option>
444
+ <option value="sales">Sales</option>
445
+ <option value="general">General</option>
446
+ </select>
447
+ </div>
448
+
449
+ `;
450
+ break;
451
+ case 'message':
452
+ fieldsHtml += ` <div class="form-group">
453
+ <label for="message">Message</label>
454
+ <textarea id="message" name="message" rows="5" required></textarea>
455
+ </div>
456
+
457
+ `;
458
+ break;
459
+ case 'terms':
460
+ fieldsHtml += ` <div class="form-group checkbox-group">
461
+ <input type="checkbox" id="terms" name="terms" required>
462
+ <label for="terms">I agree to the terms of service</label>
463
+ </div>
464
+
465
+ `;
466
+ break;
467
+ }
468
+ });
469
+
470
+ // Add custom fields if provided
471
+ if (customFormFields && customFormFields.trim().length > 0) {
472
+ const customFields = customFormFields.split(',').map(f => f.trim()).filter(f => f.length > 0);
473
+
474
+ customFields.forEach(field => {
475
+ const [type, label] = field.split(':').map(s => s.trim());
476
+ if (type && label) {
477
+ const fieldId = label.toLowerCase().replace(/\s+/g, '-');
478
+
479
+ if (type === 'textarea') {
480
+ fieldsHtml += ` <div class="form-group">
481
+ <label for="${fieldId}">${label}</label>
482
+ <textarea id="${fieldId}" name="${fieldId}" rows="5"></textarea>
483
+ </div>
484
+
485
+ `;
486
+ } else if (type === 'checkbox') {
487
+ fieldsHtml += ` <div class="form-group checkbox-group">
488
+ <input type="checkbox" id="${fieldId}" name="${fieldId}">
489
+ <label for="${fieldId}">${label}</label>
490
+ </div>
491
+
492
+ `;
493
+ } else if (type === 'select') {
494
+ fieldsHtml += ` <div class="form-group">
495
+ <label for="${fieldId}">${label}</label>
496
+ <select id="${fieldId}" name="${fieldId}">
497
+ <option value="">Select an option</option>
498
+ <option value="option1">Option 1</option>
499
+ <option value="option2">Option 2</option>
500
+ </select>
501
+ </div>
502
+
503
+ `;
504
+ } else {
505
+ // Default to input with specified type
506
+ fieldsHtml += ` <div class="form-group">
507
+ <label for="${fieldId}">${label}</label>
508
+ <input type="${type}" id="${fieldId}" name="${fieldId}">
509
+ </div>
510
+
511
+ `;
512
+ }
513
+ }
514
+ });
515
+ }
516
+
517
+ // Replace the form fields in the HTML
518
+ const formRegex = /<form class="form"[^>]*>[\s\S]*?<button type="submit"/;
519
+ const replacement = `<form class="form" id="contactForm">
520
+ <h1 class="form-title">Contact Us</h1>
521
+ <p class="form-subtitle">Fill in the details and we'll get back to you soon</p>
522
+
523
+ ${fieldsHtml} <button type="submit"`;
524
+
525
+ htmlContent = htmlContent.replace(formRegex, replacement);
526
+
527
+ return htmlContent;
528
+ }
529
+
84
530
  async function generateTemplate(options) {
85
- const { component, name, includeJs } = options;
531
+ const {
532
+ component,
533
+ name,
534
+ includeJs,
535
+ navItems,
536
+ formFields,
537
+ customFormFields,
538
+ buttonVariations,
539
+ selectedButtons,
540
+ cardVariations,
541
+ selectedCards,
542
+ spinnerVariations,
543
+ selectedSpinners,
544
+ colorScheme,
545
+ primaryColor,
546
+ secondaryColor,
547
+ darkMode
548
+ } = options;
549
+
550
+ // If colorScheme is provided, use those colors
551
+ let finalPrimaryColor = primaryColor;
552
+ let finalSecondaryColor = secondaryColor;
553
+
554
+ if (colorScheme && COLOR_SCHEMES[colorScheme]) {
555
+ const scheme = COLOR_SCHEMES[colorScheme];
556
+ finalPrimaryColor = scheme.primary;
557
+ finalSecondaryColor = scheme.secondary;
558
+ }
86
559
 
87
560
  // Security: Validate component name
88
561
  if (!VALID_COMPONENTS.includes(component)) {
@@ -117,6 +590,31 @@ async function generateTemplate(options) {
117
590
 
118
591
  // Replace placeholder name
119
592
  htmlContent = htmlContent.replace(/{{name}}/g, safeName);
593
+
594
+ // Filter button variations
595
+ if (component === "button") {
596
+ htmlContent = filterButtonVariations(htmlContent, buttonVariations, selectedButtons);
597
+ }
598
+
599
+ // Filter card variations
600
+ if (component === "card") {
601
+ htmlContent = filterCardVariations(htmlContent, cardVariations, selectedCards);
602
+ }
603
+
604
+ // Filter spinner variations
605
+ if (component === "spinner") {
606
+ htmlContent = filterSpinnerVariations(htmlContent, spinnerVariations, selectedSpinners);
607
+ }
608
+
609
+ // Custom navigation items
610
+ if (component === "navigation" && navItems) {
611
+ htmlContent = generateNavigationItems(htmlContent, navItems);
612
+ }
613
+
614
+ // Custom form fields
615
+ if (component === "form" && formFields) {
616
+ htmlContent = generateFormFields(htmlContent, formFields, customFormFields);
617
+ }
120
618
 
121
619
  // Add script tag if JavaScript is included (pointing to js/ folder)
122
620
  if (includeJs) {
@@ -139,10 +637,21 @@ async function generateTemplate(options) {
139
637
  await fs.writeFile(path.join(outputDir, "index.html"), htmlContent);
140
638
 
141
639
  // Copy CSS file to css/ folder
142
- const cssContent = await fs.readFile(
640
+ let cssContent = await fs.readFile(
143
641
  path.join(templateDir, "style.css"),
144
642
  "utf-8",
145
643
  );
644
+
645
+ // Apply custom colors if provided (either from colorScheme or direct colors)
646
+ if (finalPrimaryColor || finalSecondaryColor) {
647
+ cssContent = applyCustomColors(cssContent, finalPrimaryColor, finalSecondaryColor);
648
+ }
649
+
650
+ // Add dark mode support if requested
651
+ if (darkMode) {
652
+ cssContent = addDarkModeStyles(cssContent);
653
+ }
654
+
146
655
  const formattedCss = await formatCss(cssContent);
147
656
  await fs.writeFile(path.join(cssDir, "style.css"), formattedCss);
148
657
 
@@ -165,4 +674,4 @@ async function generateTemplate(options) {
165
674
  return outputDir;
166
675
  }
167
676
 
168
- module.exports = { generateTemplate };
677
+ module.exports = { generateTemplate, COLOR_SCHEMES, getColorScheme };
package/src/inserter.js CHANGED
@@ -161,8 +161,14 @@ async function insertComponent(options) {
161
161
  );
162
162
  }
163
163
 
164
- // Check if target file exists
164
+ // Security: Prevent path traversal attacks
165
165
  const targetPath = path.resolve(process.cwd(), targetFile);
166
+ const cwd = process.cwd();
167
+ if (!targetPath.startsWith(cwd)) {
168
+ throw new Error(`Security error: Cannot access files outside current directory`);
169
+ }
170
+
171
+ // Check if target file exists
166
172
  try {
167
173
  await fs.access(targetPath);
168
174
  } catch {
@@ -0,0 +1,58 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{name}} - Login</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <div class="login-card">
12
+ <h1 class="login-title">Welcome Back</h1>
13
+ <p class="login-subtitle">Login to your account</p>
14
+
15
+ <form class="login-form" id="loginForm">
16
+ <div class="form-group">
17
+ <label for="email">Email Address</label>
18
+ <input type="email" id="email" name="email" placeholder="you@example.com" required>
19
+ </div>
20
+
21
+ <div class="form-group">
22
+ <label for="password">Password</label>
23
+ <input type="password" id="password" name="password" placeholder="Enter your password" required>
24
+ </div>
25
+
26
+ <div class="form-group remember-forgot">
27
+ <div class="checkbox-wrapper">
28
+ <input type="checkbox" id="remember" name="remember">
29
+ <label for="remember">Remember me</label>
30
+ </div>
31
+ <a href="#" class="forgot-link">Forgot Password?</a>
32
+ </div>
33
+
34
+ <button type="submit" class="login-btn">Login</button>
35
+ </form>
36
+
37
+ <div class="form-divider">
38
+ <span>Or continue with</span>
39
+ </div>
40
+
41
+ <div class="social-buttons">
42
+ <button class="social-btn google-btn">
43
+ <span>Google</span>
44
+ </button>
45
+ <button class="social-btn github-btn">
46
+ <span>GitHub</span>
47
+ </button>
48
+ </div>
49
+
50
+ <p class="signup-link">
51
+ Don't have an account? <a href="#">Sign up here</a>
52
+ </p>
53
+ </div>
54
+ </div>
55
+
56
+ <script src="script.js"></script>
57
+ </body>
58
+ </html>