create-velocity-astro 1.1.1 → 1.2.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/index.js CHANGED
@@ -291,7 +291,47 @@ function toTitle(slug) {
291
291
  function toRouteId(slug) {
292
292
  return slug.replace(/-/g, "_");
293
293
  }
294
- function addRouteEntry(targetDir, pageName) {
294
+ function getNextNavOrder(content) {
295
+ const orderMatches = [...content.matchAll(/order:\s*(\d+)/g)];
296
+ let maxOrder = 0;
297
+ for (const match of orderMatches) {
298
+ const orderStr = match[1];
299
+ if (orderStr) {
300
+ const order = parseInt(orderStr, 10);
301
+ if (order > maxOrder) {
302
+ maxOrder = order;
303
+ }
304
+ }
305
+ }
306
+ return maxOrder + 10;
307
+ }
308
+ function addBaseRouteEntry(targetDir, pageName) {
309
+ const routesPath = join(targetDir, "src", "config", "routes.ts");
310
+ if (!existsSync2(routesPath)) {
311
+ return;
312
+ }
313
+ const routeId = toRouteId(pageName);
314
+ const title = toTitle(pageName);
315
+ const content = readFileSync(routesPath, "utf-8");
316
+ if (content.includes(`${routeId}:`)) {
317
+ return;
318
+ }
319
+ const insertPoint = content.indexOf("} as const satisfies");
320
+ if (insertPoint === -1) {
321
+ return;
322
+ }
323
+ const navOrder = getNextNavOrder(content);
324
+ const newRoute = `
325
+ // Custom page: ${pageName}
326
+ ${routeId}: {
327
+ path: '/${pageName}',
328
+ nav: { show: true, order: ${navOrder}, label: '${title}' },
329
+ },
330
+ `;
331
+ const newContent = content.slice(0, insertPoint) + newRoute + content.slice(insertPoint);
332
+ writeFileSync(routesPath, newContent);
333
+ }
334
+ function addI18nRouteEntry(targetDir, pageName) {
295
335
  const routesPath = join(targetDir, "src", "i18n", "routes.ts");
296
336
  if (!existsSync2(routesPath)) {
297
337
  return;
@@ -305,11 +345,54 @@ function addRouteEntry(targetDir, pageName) {
305
345
  if (insertPoint === -1) {
306
346
  return;
307
347
  }
308
- const newRoute = ` // Custom page: ${pageName}
309
- ${routeId}: { en: '${pageName}', es: '${pageName}', fr: '${pageName}' },
348
+ const navOrder = getNextNavOrder(content);
349
+ const title = toTitle(pageName);
350
+ const newRoute = `
351
+ // Custom page: ${pageName}
352
+ ${routeId}: {
353
+ en: '${pageName}', es: '${pageName}', fr: '${pageName}',
354
+ nav: { show: true, order: ${navOrder}, label: 'nav.${routeId}' },
355
+ },
310
356
  `;
311
357
  const newContent = content.slice(0, insertPoint) + newRoute + content.slice(insertPoint);
312
358
  writeFileSync(routesPath, newContent);
359
+ addI18nTranslationKeys(targetDir, routeId, title);
360
+ }
361
+ function addI18nTranslationKeys(targetDir, routeId, title) {
362
+ const locales = ["en", "es", "fr"];
363
+ for (const locale of locales) {
364
+ const translationPath = join(targetDir, "src", "i18n", "translations", `${locale}.ts`);
365
+ if (!existsSync2(translationPath)) {
366
+ continue;
367
+ }
368
+ let content = readFileSync(translationPath, "utf-8");
369
+ if (!content.includes(`${routeId}:`)) {
370
+ const navSectionMatch = content.match(/nav:\s*\{([^}]+)\}/);
371
+ if (navSectionMatch) {
372
+ const navSection = navSectionMatch[0];
373
+ const insertPoint = navSection.lastIndexOf("}");
374
+ const newNavSection = navSection.slice(0, insertPoint) + ` ${routeId}: '${title}',
375
+ ` + navSection.slice(insertPoint);
376
+ content = content.replace(navSection, newNavSection);
377
+ }
378
+ }
379
+ const pageKeyPattern = new RegExp(`^\\s*${routeId}:\\s*\\{`, "m");
380
+ if (!pageKeyPattern.test(content)) {
381
+ const insertPoint = content.lastIndexOf("} as const");
382
+ if (insertPoint !== -1) {
383
+ const pageTranslations = `
384
+ // ${title} page
385
+ ${routeId}: {
386
+ title: '${title}',
387
+ description: 'Add your ${title.toLowerCase()} page description here.',
388
+ },
389
+
390
+ `;
391
+ content = content.slice(0, insertPoint) + pageTranslations + content.slice(insertPoint);
392
+ }
393
+ }
394
+ writeFileSync(translationPath, content);
395
+ }
313
396
  }
314
397
  function generatePageTemplate(pageName, layout) {
315
398
  const title = toTitle(pageName);
@@ -415,6 +498,9 @@ async function generatePages(targetDir, pages, layout, isI18n) {
415
498
  const template = generatePageTemplate(pageName, layout);
416
499
  writeFileSync(filePath, template);
417
500
  generatedFiles.push(`src/pages/${pageName}.astro`);
501
+ if (!isI18n) {
502
+ addBaseRouteEntry(targetDir, pageName);
503
+ }
418
504
  }
419
505
  if (isI18n) {
420
506
  const langDir = join(pagesDir, "[lang]");
@@ -427,14 +513,14 @@ async function generatePages(targetDir, pages, layout, isI18n) {
427
513
  const template = generateI18nPageTemplate(pageName, layout);
428
514
  writeFileSync(filePath, template);
429
515
  generatedFiles.push(`src/pages/[lang]/[...${routeId}].astro`);
430
- addRouteEntry(targetDir, pageName);
516
+ addI18nRouteEntry(targetDir, pageName);
431
517
  }
432
518
  }
433
519
  return generatedFiles;
434
520
  }
435
521
 
436
522
  // src/scaffold.ts
437
- var TEMPLATE_REPO = "github:southwellmedia-dev/velocity";
523
+ var TEMPLATE_REPO = "github:southwellmedia/velocity";
438
524
  var CLEANUP_ITEMS = [
439
525
  "pnpm-lock.yaml",
440
526
  "package-lock.json",
@@ -443,9 +529,15 @@ var CLEANUP_ITEMS = [
443
529
  ".git"
444
530
  ];
445
531
  var DEMO_CONTENT = [
532
+ // Landing page components
446
533
  "src/components/landing",
534
+ "src/components/hero",
535
+ // Landing-specific pages
536
+ "src/pages/index.astro",
447
537
  "src/pages/about.astro",
448
538
  "src/pages/contact.astro",
539
+ "src/pages/components.astro",
540
+ // Demo content
449
541
  "src/content/blog",
450
542
  "src/content/faqs",
451
543
  "src/content/authors",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/prompts.ts","../src/utils/validate.ts","../src/utils/package-manager.ts","../src/scaffold.ts","../src/template.ts","../src/utils/git.ts","../src/features/pages.ts","../src/utils/fs.ts","../src/index.ts"],"sourcesContent":["import mri from 'mri';\nimport { resolve } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport * as p from '@clack/prompts';\nimport pc from 'picocolors';\nimport type { CliOptions } from './types.js';\nimport { runPrompts, showIntro, showOutro, showError } from './prompts.js';\nimport { scaffold } from './scaffold.js';\nimport { isEmptyDir } from './utils/fs.js';\nimport { toValidProjectName } from './utils/validate.js';\n\nconst HELP_TEXT = `\n${pc.bold('create-velocity-astro')} - Create a new Velocity project\n\n${pc.bold('Usage:')}\n npm create velocity-astro@latest [project-name] [options]\n pnpm create velocity-astro [project-name] [options]\n yarn create velocity-astro [project-name] [options]\n bun create velocity-astro [project-name] [options]\n\n${pc.bold('Options:')}\n --demo Include demo landing page and sample content\n --components Include UI component library\n --i18n Add internationalization support\n --pages Prompt for starter pages to generate\n --yes, -y Skip prompts and use defaults\n --help, -h Show this help message\n --version, -v Show version number\n\n${pc.bold('Examples:')}\n npm create velocity-astro@latest my-site\n npm create velocity-astro@latest my-site --demo --components\n npm create velocity-astro@latest my-site --i18n --pages\n pnpm create velocity-astro my-site -y\n`;\n\nconst VERSION = '1.1.0';\n\nexport async function run(argv: string[]): Promise<void> {\n const args = mri<CliOptions>(argv, {\n boolean: ['demo', 'components', 'i18n', 'pages', 'help', 'version', 'yes'],\n alias: {\n h: 'help',\n v: 'version',\n y: 'yes',\n },\n });\n\n // Handle help\n if (args.help) {\n console.log(HELP_TEXT);\n return;\n }\n\n // Handle version\n if (args.version) {\n console.log(VERSION);\n return;\n }\n\n showIntro();\n\n // Get project name from args or prompt\n const argProjectName = args._[0] as string | undefined;\n\n // Skip prompts mode\n if (args.yes) {\n const projectName = toValidProjectName(argProjectName || 'my-velocity-site');\n const targetDir = resolve(process.cwd(), projectName);\n\n if (existsSync(targetDir) && !isEmptyDir(targetDir)) {\n showError(`Directory \"${projectName}\" already exists and is not empty.`);\n process.exit(1);\n }\n\n await scaffold({\n projectName,\n targetDir,\n demo: args.demo || false,\n components: args.components !== false, // Default to true\n i18n: args.i18n || false,\n pages: [],\n pageLayout: 'page',\n packageManager: 'pnpm',\n });\n\n showOutro(projectName, 'pnpm');\n return;\n }\n\n // Interactive mode\n const answers = await runPrompts({\n projectName: argProjectName,\n demo: args.demo,\n components: args.components,\n i18n: args.i18n,\n pages: args.pages,\n });\n\n // User cancelled\n if (typeof answers === 'symbol') {\n return;\n }\n\n const { projectName, demo, components, i18n, pages, pageLayout, packageManager } = answers;\n const targetDir = resolve(process.cwd(), projectName);\n\n // Check if directory exists and is not empty\n if (existsSync(targetDir) && !isEmptyDir(targetDir)) {\n const shouldOverwrite = await p.confirm({\n message: `Directory \"${projectName}\" already exists. Continue and overwrite?`,\n initialValue: false,\n });\n\n if (!shouldOverwrite || p.isCancel(shouldOverwrite)) {\n p.cancel('Operation cancelled.');\n process.exit(0);\n }\n }\n\n // Run scaffold\n try {\n await scaffold({\n projectName,\n targetDir,\n demo,\n components,\n i18n,\n pages,\n pageLayout,\n packageManager,\n });\n\n showOutro(projectName, packageManager);\n } catch (error) {\n showError(error instanceof Error ? error.message : 'An unexpected error occurred');\n process.exit(1);\n }\n}\n","import * as p from '@clack/prompts';\nimport pc from 'picocolors';\nimport type { PackageManager, PageLayout, PromptAnswers } from './types.js';\nimport { validateProjectName, toValidProjectName } from './utils/validate.js';\nimport { detectPackageManager } from './utils/package-manager.js';\n\ninterface PromptDefaults {\n projectName?: string;\n demo?: boolean;\n components?: boolean;\n i18n?: boolean;\n pages?: boolean;\n}\n\n/**\n * Parses comma-separated page names into an array of valid page slugs\n */\nfunction parsePageNames(input: string): string[] {\n if (!input.trim()) return [];\n\n return input\n .split(',')\n .map((name) => name.trim().toLowerCase())\n .filter((name) => name.length > 0)\n .map((name) => name.replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''))\n .filter((name) => name.length > 0 && !['index', 'blog', '404', 'rss'].includes(name));\n}\n\nexport async function runPrompts(defaults: PromptDefaults = {}): Promise<PromptAnswers | symbol> {\n const detectedPm = detectPackageManager();\n\n const answers = await p.group(\n {\n projectName: () =>\n p.text({\n message: 'What is your project name?',\n placeholder: defaults.projectName || 'my-velocity-site',\n defaultValue: defaults.projectName,\n validate: (value) => {\n const name = value || defaults.projectName || 'my-velocity-site';\n const result = validateProjectName(toValidProjectName(name));\n if (!result.valid) return result.message;\n },\n }),\n\n demo:\n defaults.demo !== undefined\n ? () => Promise.resolve(defaults.demo)\n : () =>\n p.select({\n message: 'Include demo landing page and sample content?',\n options: [\n {\n value: false,\n label: 'No',\n hint: 'Minimal starter with basic pages',\n },\n {\n value: true,\n label: 'Yes',\n hint: 'Full demo with landing page, blog posts',\n },\n ],\n initialValue: false,\n }),\n\n components:\n defaults.components !== undefined\n ? () => Promise.resolve(defaults.components)\n : () =>\n p.select({\n message: 'Include UI component library?',\n options: [\n {\n value: false,\n label: 'No',\n hint: 'Just the basics',\n },\n {\n value: true,\n label: 'Yes',\n hint: 'Buttons, forms, cards, dialogs, etc.',\n },\n ],\n initialValue: true,\n }),\n\n i18n:\n defaults.i18n !== undefined\n ? () => Promise.resolve(defaults.i18n)\n : () =>\n p.select({\n message: 'Add internationalization (i18n)?',\n options: [\n {\n value: false,\n label: 'No',\n hint: 'English only',\n },\n {\n value: true,\n label: 'Yes',\n hint: 'Locale routing, translations',\n },\n ],\n initialValue: false,\n }),\n\n generatePages:\n defaults.pages !== undefined\n ? () => Promise.resolve(defaults.pages)\n : () =>\n p.select({\n message: 'Generate starter pages?',\n options: [\n {\n value: false,\n label: 'No',\n hint: 'Create pages manually later',\n },\n {\n value: true,\n label: 'Yes',\n hint: 'Auto-generate page files with layout',\n },\n ],\n initialValue: false,\n }),\n\n pageNames: ({ results }) =>\n results.generatePages\n ? p.text({\n message: 'Enter page names (comma-separated):',\n placeholder: 'about, pricing, faq, contact',\n validate: (value) => {\n const pages = parsePageNames(value);\n if (pages.length === 0) {\n return 'Please enter at least one valid page name';\n }\n },\n })\n : Promise.resolve(''),\n\n pageLayout: ({ results }) =>\n results.generatePages\n ? p.select({\n message: 'Select layout for pages:',\n options: [\n {\n value: 'page' as PageLayout,\n label: 'PageLayout',\n hint: 'Standard content pages (Header + Footer)',\n },\n {\n value: 'landing' as PageLayout,\n label: 'LandingLayout',\n hint: 'Marketing pages (Navbar + LandingFooter)',\n },\n ],\n initialValue: 'page' as PageLayout,\n })\n : Promise.resolve('page' as PageLayout),\n\n packageManager: () =>\n p.select({\n message: 'Which package manager?',\n options: [\n {\n value: 'pnpm' as PackageManager,\n label: 'pnpm',\n hint: detectedPm === 'pnpm' ? 'detected' : 'recommended',\n },\n {\n value: 'npm' as PackageManager,\n label: 'npm',\n hint: detectedPm === 'npm' ? 'detected' : undefined,\n },\n {\n value: 'yarn' as PackageManager,\n label: 'yarn',\n hint: detectedPm === 'yarn' ? 'detected' : undefined,\n },\n {\n value: 'bun' as PackageManager,\n label: 'bun',\n hint: detectedPm === 'bun' ? 'detected' : undefined,\n },\n ],\n initialValue: detectedPm,\n }),\n },\n {\n onCancel: () => {\n p.cancel('Operation cancelled.');\n process.exit(0);\n },\n }\n );\n\n return {\n projectName: toValidProjectName(answers.projectName || defaults.projectName || 'my-velocity-site'),\n demo: answers.demo as boolean,\n components: answers.components as boolean,\n i18n: answers.i18n as boolean,\n pages: parsePageNames(answers.pageNames as string),\n pageLayout: (answers.pageLayout as PageLayout) || 'page',\n packageManager: answers.packageManager as PackageManager,\n };\n}\n\nexport function showIntro(): void {\n console.log();\n p.intro(pc.bgCyan(pc.black(' Create Velocity ')));\n}\n\nexport function showOutro(projectName: string, packageManager: PackageManager): void {\n const runCmd = packageManager === 'npm' ? 'npm run' : packageManager;\n\n p.note(\n [\n `cd ${projectName}`,\n `${runCmd} dev`,\n ].join('\\n'),\n 'Next steps'\n );\n\n p.outro(pc.green('Happy building!'));\n}\n\nexport function showError(message: string): void {\n p.log.error(pc.red(message));\n}\n\nexport function showWarning(message: string): void {\n p.log.warn(pc.yellow(message));\n}\n\nexport function showSuccess(message: string): void {\n p.log.success(pc.green(message));\n}\n\nexport function showStep(message: string): void {\n p.log.step(message);\n}\n","/**\n * Validates a project name for npm package naming conventions\n */\nexport function validateProjectName(name: string): { valid: boolean; message?: string } {\n if (!name || name.trim() === '') {\n return { valid: false, message: 'Project name cannot be empty' };\n }\n\n // Must be lowercase\n if (name !== name.toLowerCase()) {\n return { valid: false, message: 'Project name must be lowercase' };\n }\n\n // Cannot start with . or _\n if (name.startsWith('.') || name.startsWith('_')) {\n return { valid: false, message: 'Project name cannot start with . or _' };\n }\n\n // Cannot contain spaces\n if (/\\s/.test(name)) {\n return { valid: false, message: 'Project name cannot contain spaces' };\n }\n\n // Cannot contain special characters except - and @/\n if (!/^(@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name)) {\n return {\n valid: false,\n message: 'Project name can only contain lowercase letters, numbers, hyphens, and underscores',\n };\n }\n\n // Length check\n if (name.length > 214) {\n return { valid: false, message: 'Project name must be 214 characters or fewer' };\n }\n\n return { valid: true };\n}\n\n/**\n * Sanitizes a string to be a valid project name\n */\nexport function toValidProjectName(name: string): string {\n return name\n .trim()\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9-_~.]/g, '-')\n .replace(/^[-._]+/, '')\n .replace(/[-._]+$/, '')\n .replace(/-+/g, '-');\n}\n","import type { PackageManager } from '../types.js';\n\n/**\n * Detects the package manager used to run this command\n */\nexport function detectPackageManager(): PackageManager {\n const userAgent = process.env.npm_config_user_agent || '';\n\n if (userAgent.startsWith('pnpm')) return 'pnpm';\n if (userAgent.startsWith('yarn')) return 'yarn';\n if (userAgent.startsWith('bun')) return 'bun';\n return 'npm';\n}\n\n/**\n * Gets the install command for a package manager\n */\nexport function getInstallCommand(pm: PackageManager): string {\n switch (pm) {\n case 'pnpm':\n return 'pnpm install';\n case 'yarn':\n return 'yarn';\n case 'bun':\n return 'bun install';\n case 'npm':\n default:\n return 'npm install';\n }\n}\n\n/**\n * Gets the run command for a package manager\n */\nexport function getRunCommand(pm: PackageManager): string {\n switch (pm) {\n case 'pnpm':\n return 'pnpm';\n case 'yarn':\n return 'yarn';\n case 'bun':\n return 'bun';\n case 'npm':\n default:\n return 'npm run';\n }\n}\n","import { existsSync, mkdirSync, readdirSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport * as p from '@clack/prompts';\nimport { execa } from 'execa';\nimport { downloadTemplate } from 'giget';\nimport type { ScaffoldOptions } from './types.js';\nimport { getI18nTemplatePath, getBaseTemplatePath } from './template.js';\nimport { getInstallCommand } from './utils/package-manager.js';\nimport { initGit } from './utils/git.js';\nimport { showSuccess, showWarning } from './prompts.js';\nimport { generatePages } from './features/pages.js';\n\n// GitHub repository for the Velocity template\nconst TEMPLATE_REPO = 'github:southwellmedia-dev/velocity';\n\n// Files/directories to remove after download\nconst CLEANUP_ITEMS = [\n 'pnpm-lock.yaml',\n 'package-lock.json',\n 'yarn.lock',\n 'bun.lockb',\n '.git',\n];\n\n// Demo-specific content to remove when --demo is false\nconst DEMO_CONTENT = [\n 'src/components/landing',\n 'src/pages/about.astro',\n 'src/pages/contact.astro',\n 'src/content/blog',\n 'src/content/faqs',\n 'src/content/authors',\n 'src/content/pages',\n];\n\n// UI component library content to remove when --components is false\nconst COMPONENTS_CONTENT = [\n 'src/components/ui',\n 'src/components/patterns',\n 'src/pages/components.astro',\n];\n\n/**\n * Copies template files recursively\n */\nfunction copyTemplateFiles(src: string, dest: string): void {\n if (!existsSync(dest)) {\n mkdirSync(dest, { recursive: true });\n }\n\n const entries = readdirSync(src, { withFileTypes: true });\n\n for (const entry of entries) {\n const srcPath = join(src, entry.name);\n const destPath = join(dest, entry.name);\n\n if (entry.isDirectory()) {\n copyTemplateFiles(srcPath, destPath);\n } else {\n copyFileSync(srcPath, destPath);\n }\n }\n}\n\n/**\n * Removes files/directories from the target\n */\nfunction removeItems(targetDir: string, items: string[]): void {\n for (const item of items) {\n const itemPath = join(targetDir, item);\n if (existsSync(itemPath)) {\n try {\n rmSync(itemPath, { recursive: true, force: true });\n } catch {\n // Ignore errors - item may not exist or be locked\n }\n }\n }\n}\n\n/**\n * Updates the package.json with the new project name\n */\nfunction updatePackageJson(targetDir: string, projectName: string): void {\n const pkgPath = join(targetDir, 'package.json');\n\n if (!existsSync(pkgPath)) {\n throw new Error('package.json not found in template');\n }\n\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n pkg.name = projectName;\n pkg.version = '0.1.0';\n delete pkg.repository;\n delete pkg.bugs;\n delete pkg.homepage;\n\n writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\\n');\n}\n\n/**\n * Applies base template (minimal pages) when demo is not selected\n */\nfunction applyBaseTemplate(targetDir: string): void {\n const baseTemplate = getBaseTemplatePath();\n if (existsSync(baseTemplate)) {\n copyTemplateFiles(baseTemplate, targetDir);\n }\n}\n\n/**\n * Applies the i18n overlay to the project\n */\nfunction applyI18nOverlay(targetDir: string): void {\n const i18nTemplate = getI18nTemplatePath();\n copyTemplateFiles(i18nTemplate, targetDir);\n}\n\n/**\n * Creates empty content directories with .gitkeep files\n */\nfunction createContentDirectories(targetDir: string): void {\n const contentDirs = [\n 'src/content/blog',\n ];\n\n for (const dir of contentDirs) {\n const dirPath = join(targetDir, dir);\n if (!existsSync(dirPath)) {\n mkdirSync(dirPath, { recursive: true });\n writeFileSync(join(dirPath, '.gitkeep'), '');\n }\n }\n}\n\n/**\n * Main scaffold function\n */\nexport async function scaffold(options: ScaffoldOptions): Promise<void> {\n const { projectName, targetDir, demo, components, i18n, pages, pageLayout, packageManager } = options;\n const spinner = p.spinner();\n\n // Step 1: Download base template from GitHub\n spinner.start('Downloading template from GitHub...');\n\n try {\n await downloadTemplate(TEMPLATE_REPO, {\n dir: targetDir,\n force: true,\n });\n removeItems(targetDir, CLEANUP_ITEMS);\n spinner.stop('Template downloaded');\n } catch (error) {\n spinner.stop('Failed to download template');\n throw new Error(\n `Could not download template from GitHub. Please check your internet connection.\\n${error instanceof Error ? error.message : ''}`\n );\n }\n\n // Step 2: Remove demo content if not requested\n if (!demo) {\n spinner.start('Configuring minimal template...');\n removeItems(targetDir, DEMO_CONTENT);\n applyBaseTemplate(targetDir);\n createContentDirectories(targetDir);\n spinner.stop('Minimal template configured');\n }\n\n // Step 3: Remove UI components if not requested\n if (!components) {\n spinner.start('Removing UI component library...');\n removeItems(targetDir, COMPONENTS_CONTENT);\n spinner.stop('UI components removed');\n }\n\n // Step 4: Apply i18n overlay if requested\n if (i18n) {\n spinner.start('Adding i18n support...');\n try {\n applyI18nOverlay(targetDir);\n spinner.stop('i18n support added');\n } catch (error) {\n spinner.stop('Failed to add i18n support');\n throw error;\n }\n }\n\n // Step 5: Generate starter pages if requested\n if (pages.length > 0) {\n spinner.start(`Generating ${pages.length} starter page${pages.length > 1 ? 's' : ''}...`);\n try {\n const generatedFiles = await generatePages(targetDir, pages, pageLayout, i18n);\n spinner.stop(`Generated ${generatedFiles.length} page file${generatedFiles.length > 1 ? 's' : ''}`);\n } catch (error) {\n spinner.stop('Failed to generate pages');\n throw error;\n }\n }\n\n // Step 6: Update package.json\n spinner.start('Configuring project...');\n try {\n updatePackageJson(targetDir, projectName);\n spinner.stop('Project configured');\n } catch (error) {\n spinner.stop('Failed to configure project');\n throw error;\n }\n\n // Step 7: Initialize git\n spinner.start('Initializing git repository...');\n const gitInitialized = await initGit(targetDir);\n if (gitInitialized) {\n spinner.stop('Git repository initialized');\n } else {\n spinner.stop('Git not available, skipping');\n }\n\n // Step 8: Install dependencies\n spinner.start(`Installing dependencies with ${packageManager}...`);\n try {\n const installCmd = getInstallCommand(packageManager);\n const [cmd, ...args] = installCmd.split(' ');\n await execa(cmd!, args, { cwd: targetDir });\n spinner.stop('Dependencies installed');\n } catch {\n spinner.stop('Failed to install dependencies');\n showWarning(`Run \"${getInstallCommand(packageManager)}\" manually to install dependencies`);\n }\n\n showSuccess(`Project \"${projectName}\" created successfully!`);\n}\n","import { existsSync } from 'node:fs';\nimport { resolve, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Resolves the path to the base template (minimal pages)\n */\nexport function getBaseTemplatePath(): string {\n const templatePath = resolve(__dirname, '..', 'templates', 'base');\n\n if (existsSync(templatePath)) {\n return templatePath;\n }\n\n throw new Error('Could not find base template. Package may be corrupted.');\n}\n\n/**\n * Resolves the path to the i18n overlay template\n */\nexport function getI18nTemplatePath(): string {\n const templatePath = resolve(__dirname, '..', 'templates', 'i18n');\n\n if (existsSync(templatePath)) {\n return templatePath;\n }\n\n throw new Error('Could not find i18n template. Package may be corrupted.');\n}\n","import { execa } from 'execa';\n\n/**\n * Initializes a git repository in the target directory\n */\nexport async function initGit(targetDir: string): Promise<boolean> {\n try {\n await execa('git', ['init'], { cwd: targetDir });\n await execa('git', ['add', '-A'], { cwd: targetDir });\n await execa('git', ['commit', '-m', 'Initial commit from create-velocity'], {\n cwd: targetDir,\n });\n return true;\n } catch {\n // Git may not be installed or configured\n return false;\n }\n}\n\n/**\n * Checks if git is available\n */\nexport async function isGitInstalled(): Promise<boolean> {\n try {\n await execa('git', ['--version']);\n return true;\n } catch {\n return false;\n }\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { PageLayout } from '../types.js';\n\n/**\n * Converts a page slug to a display title\n * e.g., 'about-us' -> 'About Us'\n */\nfunction toTitle(slug: string): string {\n return slug\n .split('-')\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\n/**\n * Converts a page slug to a route ID (snake_case)\n * e.g., 'about-us' -> 'about_us'\n */\nfunction toRouteId(slug: string): string {\n return slug.replace(/-/g, '_');\n}\n\n/**\n * Adds a new route entry to routes.ts\n * Creates the route with the same English slug for all locales (user customizes later)\n */\nfunction addRouteEntry(targetDir: string, pageName: string): void {\n const routesPath = join(targetDir, 'src', 'i18n', 'routes.ts');\n\n if (!existsSync(routesPath)) {\n return; // routes.ts doesn't exist, skip\n }\n\n const routeId = toRouteId(pageName);\n const content = readFileSync(routesPath, 'utf-8');\n\n // Check if route already exists\n if (content.includes(`${routeId}:`)) {\n return; // Route already defined\n }\n\n // Find the closing of routes object (before \"} as const satisfies\")\n const insertPoint = content.indexOf('} as const satisfies');\n if (insertPoint === -1) {\n return; // Can't find insertion point\n }\n\n // Create new route entry with same slug for all locales\n const newRoute = ` // Custom page: ${pageName}\\n ${routeId}: { en: '${pageName}', es: '${pageName}', fr: '${pageName}' },\\n`;\n\n const newContent = content.slice(0, insertPoint) + newRoute + content.slice(insertPoint);\n writeFileSync(routesPath, newContent);\n}\n\n/**\n * Generates the standard page template (non-i18n)\n */\nfunction generatePageTemplate(pageName: string, layout: PageLayout): string {\n const title = toTitle(pageName);\n const layoutName = layout === 'landing' ? 'LandingLayout' : 'PageLayout';\n\n return `---\nimport ${layoutName} from '@/layouts/${layoutName}.astro';\n---\n\n<${layoutName}\n title=\"${title}\"\n description=\"Add your description here\"\n>\n <!-- Hero Section -->\n <section class=\"py-20 bg-secondary\">\n <div class=\"container\">\n <h1 class=\"text-4xl font-bold text-foreground\">${title}</h1>\n <p class=\"mt-4 text-foreground-muted max-w-2xl\">\n Add your content here.\n </p>\n </div>\n </section>\n\n <!-- Content Section -->\n <section class=\"py-16\">\n <div class=\"container\">\n <!-- Your content -->\n </div>\n </section>\n</${layoutName}>\n`;\n}\n\n/**\n * Generates the i18n-aware page template with translated URL support\n */\nfunction generateI18nPageTemplate(pageName: string, layout: PageLayout): string {\n const title = toTitle(pageName);\n const layoutName = layout === 'landing' ? 'LandingLayout' : 'PageLayout';\n const routeId = toRouteId(pageName);\n const titleKey = `${routeId}.title`;\n const descKey = `${routeId}.description`;\n\n return `---\nimport ${layoutName} from '@/layouts/${layoutName}.astro';\nimport { locales, isValidLocale, defaultLocale, type Locale } from '@/i18n/config';\nimport { useTranslations } from '@/i18n/index';\nimport { routes } from '@/i18n/routes';\n\nexport function getStaticPaths() {\n return locales\n .filter((lang) => lang !== defaultLocale)\n .map((lang) => ({\n params: {\n lang,\n ${routeId}: routes.${routeId}[lang],\n },\n }));\n}\n\nconst { lang } = Astro.params;\n\nif (!lang || !isValidLocale(lang)) {\n return Astro.redirect('/');\n}\n\nconst locale = lang as Locale;\nconst t = useTranslations(locale);\n---\n\n<${layoutName}\n title={t('${titleKey}') || '${title}'}\n description={t('${descKey}') || 'Add your description here'}\n lang={locale}\n routeId=\"${routeId}\"\n>\n <!-- Hero Section -->\n <section class=\"py-20 bg-secondary\">\n <div class=\"container\">\n <h1 class=\"text-4xl font-bold text-foreground\">\n {t('${titleKey}') || '${title}'}\n </h1>\n <p class=\"mt-4 text-foreground-muted max-w-2xl\">\n {t('${descKey}') || 'Add your content here.'}\n </p>\n </div>\n </section>\n\n <!-- Content Section -->\n <section class=\"py-16\">\n <div class=\"container\">\n <!-- Your content -->\n </div>\n </section>\n</${layoutName}>\n`;\n}\n\n/**\n * Generates pages in the target directory\n */\nexport async function generatePages(\n targetDir: string,\n pages: string[],\n layout: PageLayout,\n isI18n: boolean\n): Promise<string[]> {\n const generatedFiles: string[] = [];\n\n if (pages.length === 0) {\n return generatedFiles;\n }\n\n // Ensure pages directory exists\n const pagesDir = join(targetDir, 'src', 'pages');\n if (!existsSync(pagesDir)) {\n mkdirSync(pagesDir, { recursive: true });\n }\n\n // Generate standard pages\n for (const pageName of pages) {\n const filePath = join(pagesDir, `${pageName}.astro`);\n const template = generatePageTemplate(pageName, layout);\n writeFileSync(filePath, template);\n generatedFiles.push(`src/pages/${pageName}.astro`);\n }\n\n // Generate i18n pages if enabled\n if (isI18n) {\n const langDir = join(pagesDir, '[lang]');\n if (!existsSync(langDir)) {\n mkdirSync(langDir, { recursive: true });\n }\n\n for (const pageName of pages) {\n const routeId = toRouteId(pageName);\n // Use rest parameter syntax for translated URL slugs\n const filePath = join(langDir, `[...${routeId}].astro`);\n const template = generateI18nPageTemplate(pageName, layout);\n writeFileSync(filePath, template);\n generatedFiles.push(`src/pages/[lang]/[...${routeId}].astro`);\n\n // Add route entry to routes.ts\n addRouteEntry(targetDir, pageName);\n }\n }\n\n return generatedFiles;\n}\n\n/**\n * List of page-related files that could be generated\n */\nexport const PAGES_FILES = [\n 'src/pages/{pageName}.astro',\n 'src/pages/[lang]/[...{routeId}].astro',\n];\n","import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\n\n/**\n * Recursively copies a directory\n */\nexport function copyDirectory(src: string, dest: string, overwrite = false): void {\n if (!existsSync(src)) {\n throw new Error(`Source directory does not exist: ${src}`);\n }\n\n if (!existsSync(dest)) {\n mkdirSync(dest, { recursive: true });\n }\n\n const entries = readdirSync(src, { withFileTypes: true });\n\n for (const entry of entries) {\n const srcPath = join(src, entry.name);\n const destPath = join(dest, entry.name);\n\n if (entry.isDirectory()) {\n copyDirectory(srcPath, destPath, overwrite);\n } else {\n if (overwrite || !existsSync(destPath)) {\n const destDir = dirname(destPath);\n if (!existsSync(destDir)) {\n mkdirSync(destDir, { recursive: true });\n }\n copyFileSync(srcPath, destPath);\n }\n }\n }\n}\n\n/**\n * Checks if a directory is empty\n */\nexport function isEmptyDir(path: string): boolean {\n if (!existsSync(path)) return true;\n const files = readdirSync(path);\n return files.length === 0 || (files.length === 1 && files[0] === '.git');\n}\n\n/**\n * Reads a JSON file and parses it\n */\nexport function readJson<T = Record<string, unknown>>(path: string): T {\n const content = readFileSync(path, 'utf-8');\n return JSON.parse(content) as T;\n}\n\n/**\n * Writes an object as JSON to a file\n */\nexport function writeJson(path: string, data: unknown): void {\n writeFileSync(path, JSON.stringify(data, null, 2) + '\\n');\n}\n\n/**\n * Checks if path exists and is a directory\n */\nexport function isDirectory(path: string): boolean {\n return existsSync(path) && statSync(path).isDirectory();\n}\n","import { run } from './cli.js';\n\nrun(process.argv.slice(2)).catch((error) => {\n console.error(error);\n process.exit(1);\n});\n"],"mappings":"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","names":["resolve","existsSync","p","pc","existsSync","mkdirSync","readFileSync","writeFileSync","join","p","execa","__dirname","existsSync","existsSync","mkdirSync","join","readFileSync","writeFileSync","spinner","execa","existsSync","mkdirSync","readdirSync","copyFileSync","readFileSync","writeFileSync","join","dirname","existsSync","readdirSync","pc","projectName","targetDir","resolve","existsSync"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/prompts.ts","../src/utils/validate.ts","../src/utils/package-manager.ts","../src/scaffold.ts","../src/template.ts","../src/utils/git.ts","../src/features/pages.ts","../src/utils/fs.ts","../src/index.ts"],"sourcesContent":["import mri from 'mri';\nimport { resolve } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport * as p from '@clack/prompts';\nimport pc from 'picocolors';\nimport type { CliOptions } from './types.js';\nimport { runPrompts, showIntro, showOutro, showError } from './prompts.js';\nimport { scaffold } from './scaffold.js';\nimport { isEmptyDir } from './utils/fs.js';\nimport { toValidProjectName } from './utils/validate.js';\n\nconst HELP_TEXT = `\n${pc.bold('create-velocity-astro')} - Create a new Velocity project\n\n${pc.bold('Usage:')}\n npm create velocity-astro@latest [project-name] [options]\n pnpm create velocity-astro [project-name] [options]\n yarn create velocity-astro [project-name] [options]\n bun create velocity-astro [project-name] [options]\n\n${pc.bold('Options:')}\n --demo Include demo landing page and sample content\n --components Include UI component library\n --i18n Add internationalization support\n --pages Prompt for starter pages to generate\n --yes, -y Skip prompts and use defaults\n --help, -h Show this help message\n --version, -v Show version number\n\n${pc.bold('Examples:')}\n npm create velocity-astro@latest my-site\n npm create velocity-astro@latest my-site --demo --components\n npm create velocity-astro@latest my-site --i18n --pages\n pnpm create velocity-astro my-site -y\n`;\n\nconst VERSION = '1.1.0';\n\nexport async function run(argv: string[]): Promise<void> {\n const args = mri<CliOptions>(argv, {\n boolean: ['demo', 'components', 'i18n', 'pages', 'help', 'version', 'yes'],\n alias: {\n h: 'help',\n v: 'version',\n y: 'yes',\n },\n });\n\n // Handle help\n if (args.help) {\n console.log(HELP_TEXT);\n return;\n }\n\n // Handle version\n if (args.version) {\n console.log(VERSION);\n return;\n }\n\n showIntro();\n\n // Get project name from args or prompt\n const argProjectName = args._[0] as string | undefined;\n\n // Skip prompts mode\n if (args.yes) {\n const projectName = toValidProjectName(argProjectName || 'my-velocity-site');\n const targetDir = resolve(process.cwd(), projectName);\n\n if (existsSync(targetDir) && !isEmptyDir(targetDir)) {\n showError(`Directory \"${projectName}\" already exists and is not empty.`);\n process.exit(1);\n }\n\n await scaffold({\n projectName,\n targetDir,\n demo: args.demo || false,\n components: args.components !== false, // Default to true\n i18n: args.i18n || false,\n pages: [],\n pageLayout: 'page',\n packageManager: 'pnpm',\n });\n\n showOutro(projectName, 'pnpm');\n return;\n }\n\n // Interactive mode\n const answers = await runPrompts({\n projectName: argProjectName,\n demo: args.demo,\n components: args.components,\n i18n: args.i18n,\n pages: args.pages,\n });\n\n // User cancelled\n if (typeof answers === 'symbol') {\n return;\n }\n\n const { projectName, demo, components, i18n, pages, pageLayout, packageManager } = answers;\n const targetDir = resolve(process.cwd(), projectName);\n\n // Check if directory exists and is not empty\n if (existsSync(targetDir) && !isEmptyDir(targetDir)) {\n const shouldOverwrite = await p.confirm({\n message: `Directory \"${projectName}\" already exists. Continue and overwrite?`,\n initialValue: false,\n });\n\n if (!shouldOverwrite || p.isCancel(shouldOverwrite)) {\n p.cancel('Operation cancelled.');\n process.exit(0);\n }\n }\n\n // Run scaffold\n try {\n await scaffold({\n projectName,\n targetDir,\n demo,\n components,\n i18n,\n pages,\n pageLayout,\n packageManager,\n });\n\n showOutro(projectName, packageManager);\n } catch (error) {\n showError(error instanceof Error ? error.message : 'An unexpected error occurred');\n process.exit(1);\n }\n}\n","import * as p from '@clack/prompts';\nimport pc from 'picocolors';\nimport type { PackageManager, PageLayout, PromptAnswers } from './types.js';\nimport { validateProjectName, toValidProjectName } from './utils/validate.js';\nimport { detectPackageManager } from './utils/package-manager.js';\n\ninterface PromptDefaults {\n projectName?: string;\n demo?: boolean;\n components?: boolean;\n i18n?: boolean;\n pages?: boolean;\n}\n\n/**\n * Parses comma-separated page names into an array of valid page slugs\n */\nfunction parsePageNames(input: string): string[] {\n if (!input.trim()) return [];\n\n return input\n .split(',')\n .map((name) => name.trim().toLowerCase())\n .filter((name) => name.length > 0)\n .map((name) => name.replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''))\n .filter((name) => name.length > 0 && !['index', 'blog', '404', 'rss'].includes(name));\n}\n\nexport async function runPrompts(defaults: PromptDefaults = {}): Promise<PromptAnswers | symbol> {\n const detectedPm = detectPackageManager();\n\n const answers = await p.group(\n {\n projectName: () =>\n p.text({\n message: 'What is your project name?',\n placeholder: defaults.projectName || 'my-velocity-site',\n defaultValue: defaults.projectName,\n validate: (value) => {\n const name = value || defaults.projectName || 'my-velocity-site';\n const result = validateProjectName(toValidProjectName(name));\n if (!result.valid) return result.message;\n },\n }),\n\n demo:\n defaults.demo !== undefined\n ? () => Promise.resolve(defaults.demo)\n : () =>\n p.select({\n message: 'Include demo landing page and sample content?',\n options: [\n {\n value: false,\n label: 'No',\n hint: 'Minimal starter with basic pages',\n },\n {\n value: true,\n label: 'Yes',\n hint: 'Full demo with landing page, blog posts',\n },\n ],\n initialValue: false,\n }),\n\n components:\n defaults.components !== undefined\n ? () => Promise.resolve(defaults.components)\n : () =>\n p.select({\n message: 'Include UI component library?',\n options: [\n {\n value: false,\n label: 'No',\n hint: 'Just the basics',\n },\n {\n value: true,\n label: 'Yes',\n hint: 'Buttons, forms, cards, dialogs, etc.',\n },\n ],\n initialValue: true,\n }),\n\n i18n:\n defaults.i18n !== undefined\n ? () => Promise.resolve(defaults.i18n)\n : () =>\n p.select({\n message: 'Add internationalization (i18n)?',\n options: [\n {\n value: false,\n label: 'No',\n hint: 'English only',\n },\n {\n value: true,\n label: 'Yes',\n hint: 'Locale routing, translations',\n },\n ],\n initialValue: false,\n }),\n\n generatePages:\n defaults.pages !== undefined\n ? () => Promise.resolve(defaults.pages)\n : () =>\n p.select({\n message: 'Generate starter pages?',\n options: [\n {\n value: false,\n label: 'No',\n hint: 'Create pages manually later',\n },\n {\n value: true,\n label: 'Yes',\n hint: 'Auto-generate page files with layout',\n },\n ],\n initialValue: false,\n }),\n\n pageNames: ({ results }) =>\n results.generatePages\n ? p.text({\n message: 'Enter page names (comma-separated):',\n placeholder: 'about, pricing, faq, contact',\n validate: (value) => {\n const pages = parsePageNames(value);\n if (pages.length === 0) {\n return 'Please enter at least one valid page name';\n }\n },\n })\n : Promise.resolve(''),\n\n pageLayout: ({ results }) =>\n results.generatePages\n ? p.select({\n message: 'Select layout for pages:',\n options: [\n {\n value: 'page' as PageLayout,\n label: 'PageLayout',\n hint: 'Standard content pages (Header + Footer)',\n },\n {\n value: 'landing' as PageLayout,\n label: 'LandingLayout',\n hint: 'Marketing pages (Navbar + LandingFooter)',\n },\n ],\n initialValue: 'page' as PageLayout,\n })\n : Promise.resolve('page' as PageLayout),\n\n packageManager: () =>\n p.select({\n message: 'Which package manager?',\n options: [\n {\n value: 'pnpm' as PackageManager,\n label: 'pnpm',\n hint: detectedPm === 'pnpm' ? 'detected' : 'recommended',\n },\n {\n value: 'npm' as PackageManager,\n label: 'npm',\n hint: detectedPm === 'npm' ? 'detected' : undefined,\n },\n {\n value: 'yarn' as PackageManager,\n label: 'yarn',\n hint: detectedPm === 'yarn' ? 'detected' : undefined,\n },\n {\n value: 'bun' as PackageManager,\n label: 'bun',\n hint: detectedPm === 'bun' ? 'detected' : undefined,\n },\n ],\n initialValue: detectedPm,\n }),\n },\n {\n onCancel: () => {\n p.cancel('Operation cancelled.');\n process.exit(0);\n },\n }\n );\n\n return {\n projectName: toValidProjectName(answers.projectName || defaults.projectName || 'my-velocity-site'),\n demo: answers.demo as boolean,\n components: answers.components as boolean,\n i18n: answers.i18n as boolean,\n pages: parsePageNames(answers.pageNames as string),\n pageLayout: (answers.pageLayout as PageLayout) || 'page',\n packageManager: answers.packageManager as PackageManager,\n };\n}\n\nexport function showIntro(): void {\n console.log();\n p.intro(pc.bgCyan(pc.black(' Create Velocity ')));\n}\n\nexport function showOutro(projectName: string, packageManager: PackageManager): void {\n const runCmd = packageManager === 'npm' ? 'npm run' : packageManager;\n\n p.note(\n [\n `cd ${projectName}`,\n `${runCmd} dev`,\n ].join('\\n'),\n 'Next steps'\n );\n\n p.outro(pc.green('Happy building!'));\n}\n\nexport function showError(message: string): void {\n p.log.error(pc.red(message));\n}\n\nexport function showWarning(message: string): void {\n p.log.warn(pc.yellow(message));\n}\n\nexport function showSuccess(message: string): void {\n p.log.success(pc.green(message));\n}\n\nexport function showStep(message: string): void {\n p.log.step(message);\n}\n","/**\n * Validates a project name for npm package naming conventions\n */\nexport function validateProjectName(name: string): { valid: boolean; message?: string } {\n if (!name || name.trim() === '') {\n return { valid: false, message: 'Project name cannot be empty' };\n }\n\n // Must be lowercase\n if (name !== name.toLowerCase()) {\n return { valid: false, message: 'Project name must be lowercase' };\n }\n\n // Cannot start with . or _\n if (name.startsWith('.') || name.startsWith('_')) {\n return { valid: false, message: 'Project name cannot start with . or _' };\n }\n\n // Cannot contain spaces\n if (/\\s/.test(name)) {\n return { valid: false, message: 'Project name cannot contain spaces' };\n }\n\n // Cannot contain special characters except - and @/\n if (!/^(@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name)) {\n return {\n valid: false,\n message: 'Project name can only contain lowercase letters, numbers, hyphens, and underscores',\n };\n }\n\n // Length check\n if (name.length > 214) {\n return { valid: false, message: 'Project name must be 214 characters or fewer' };\n }\n\n return { valid: true };\n}\n\n/**\n * Sanitizes a string to be a valid project name\n */\nexport function toValidProjectName(name: string): string {\n return name\n .trim()\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9-_~.]/g, '-')\n .replace(/^[-._]+/, '')\n .replace(/[-._]+$/, '')\n .replace(/-+/g, '-');\n}\n","import type { PackageManager } from '../types.js';\n\n/**\n * Detects the package manager used to run this command\n */\nexport function detectPackageManager(): PackageManager {\n const userAgent = process.env.npm_config_user_agent || '';\n\n if (userAgent.startsWith('pnpm')) return 'pnpm';\n if (userAgent.startsWith('yarn')) return 'yarn';\n if (userAgent.startsWith('bun')) return 'bun';\n return 'npm';\n}\n\n/**\n * Gets the install command for a package manager\n */\nexport function getInstallCommand(pm: PackageManager): string {\n switch (pm) {\n case 'pnpm':\n return 'pnpm install';\n case 'yarn':\n return 'yarn';\n case 'bun':\n return 'bun install';\n case 'npm':\n default:\n return 'npm install';\n }\n}\n\n/**\n * Gets the run command for a package manager\n */\nexport function getRunCommand(pm: PackageManager): string {\n switch (pm) {\n case 'pnpm':\n return 'pnpm';\n case 'yarn':\n return 'yarn';\n case 'bun':\n return 'bun';\n case 'npm':\n default:\n return 'npm run';\n }\n}\n","import { existsSync, mkdirSync, readdirSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport * as p from '@clack/prompts';\nimport { execa } from 'execa';\nimport { downloadTemplate } from 'giget';\nimport type { ScaffoldOptions } from './types.js';\nimport { getI18nTemplatePath, getBaseTemplatePath } from './template.js';\nimport { getInstallCommand } from './utils/package-manager.js';\nimport { initGit } from './utils/git.js';\nimport { showSuccess, showWarning } from './prompts.js';\nimport { generatePages } from './features/pages.js';\n\n// GitHub repository for the Velocity template\nconst TEMPLATE_REPO = 'github:southwellmedia/velocity';\n\n// Files/directories to remove after download\nconst CLEANUP_ITEMS = [\n 'pnpm-lock.yaml',\n 'package-lock.json',\n 'yarn.lock',\n 'bun.lockb',\n '.git',\n];\n\n// Demo-specific content to remove when --demo is false\nconst DEMO_CONTENT = [\n // Landing page components\n 'src/components/landing',\n 'src/components/hero',\n // Landing-specific pages\n 'src/pages/index.astro',\n 'src/pages/about.astro',\n 'src/pages/contact.astro',\n 'src/pages/components.astro',\n // Demo content\n 'src/content/blog',\n 'src/content/faqs',\n 'src/content/authors',\n 'src/content/pages',\n];\n\n// UI component library content to remove when --components is false\nconst COMPONENTS_CONTENT = [\n 'src/components/ui',\n 'src/components/patterns',\n 'src/pages/components.astro',\n];\n\n/**\n * Copies template files recursively\n */\nfunction copyTemplateFiles(src: string, dest: string): void {\n if (!existsSync(dest)) {\n mkdirSync(dest, { recursive: true });\n }\n\n const entries = readdirSync(src, { withFileTypes: true });\n\n for (const entry of entries) {\n const srcPath = join(src, entry.name);\n const destPath = join(dest, entry.name);\n\n if (entry.isDirectory()) {\n copyTemplateFiles(srcPath, destPath);\n } else {\n copyFileSync(srcPath, destPath);\n }\n }\n}\n\n/**\n * Removes files/directories from the target\n */\nfunction removeItems(targetDir: string, items: string[]): void {\n for (const item of items) {\n const itemPath = join(targetDir, item);\n if (existsSync(itemPath)) {\n try {\n rmSync(itemPath, { recursive: true, force: true });\n } catch {\n // Ignore errors - item may not exist or be locked\n }\n }\n }\n}\n\n/**\n * Updates the package.json with the new project name\n */\nfunction updatePackageJson(targetDir: string, projectName: string): void {\n const pkgPath = join(targetDir, 'package.json');\n\n if (!existsSync(pkgPath)) {\n throw new Error('package.json not found in template');\n }\n\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n pkg.name = projectName;\n pkg.version = '0.1.0';\n delete pkg.repository;\n delete pkg.bugs;\n delete pkg.homepage;\n\n writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\\n');\n}\n\n/**\n * Applies base template (minimal pages) when demo is not selected\n */\nfunction applyBaseTemplate(targetDir: string): void {\n const baseTemplate = getBaseTemplatePath();\n if (existsSync(baseTemplate)) {\n copyTemplateFiles(baseTemplate, targetDir);\n }\n}\n\n/**\n * Applies the i18n overlay to the project\n */\nfunction applyI18nOverlay(targetDir: string): void {\n const i18nTemplate = getI18nTemplatePath();\n copyTemplateFiles(i18nTemplate, targetDir);\n}\n\n/**\n * Creates empty content directories with .gitkeep files\n */\nfunction createContentDirectories(targetDir: string): void {\n const contentDirs = [\n 'src/content/blog',\n ];\n\n for (const dir of contentDirs) {\n const dirPath = join(targetDir, dir);\n if (!existsSync(dirPath)) {\n mkdirSync(dirPath, { recursive: true });\n writeFileSync(join(dirPath, '.gitkeep'), '');\n }\n }\n}\n\n/**\n * Main scaffold function\n */\nexport async function scaffold(options: ScaffoldOptions): Promise<void> {\n const { projectName, targetDir, demo, components, i18n, pages, pageLayout, packageManager } = options;\n const spinner = p.spinner();\n\n // Step 1: Download base template from GitHub\n spinner.start('Downloading template from GitHub...');\n\n try {\n await downloadTemplate(TEMPLATE_REPO, {\n dir: targetDir,\n force: true,\n });\n removeItems(targetDir, CLEANUP_ITEMS);\n spinner.stop('Template downloaded');\n } catch (error) {\n spinner.stop('Failed to download template');\n throw new Error(\n `Could not download template from GitHub. Please check your internet connection.\\n${error instanceof Error ? error.message : ''}`\n );\n }\n\n // Step 2: Remove demo content if not requested\n if (!demo) {\n spinner.start('Configuring minimal template...');\n removeItems(targetDir, DEMO_CONTENT);\n applyBaseTemplate(targetDir);\n createContentDirectories(targetDir);\n spinner.stop('Minimal template configured');\n }\n\n // Step 3: Remove UI components if not requested\n if (!components) {\n spinner.start('Removing UI component library...');\n removeItems(targetDir, COMPONENTS_CONTENT);\n spinner.stop('UI components removed');\n }\n\n // Step 4: Apply i18n overlay if requested\n if (i18n) {\n spinner.start('Adding i18n support...');\n try {\n applyI18nOverlay(targetDir);\n spinner.stop('i18n support added');\n } catch (error) {\n spinner.stop('Failed to add i18n support');\n throw error;\n }\n }\n\n // Step 5: Generate starter pages if requested\n if (pages.length > 0) {\n spinner.start(`Generating ${pages.length} starter page${pages.length > 1 ? 's' : ''}...`);\n try {\n const generatedFiles = await generatePages(targetDir, pages, pageLayout, i18n);\n spinner.stop(`Generated ${generatedFiles.length} page file${generatedFiles.length > 1 ? 's' : ''}`);\n } catch (error) {\n spinner.stop('Failed to generate pages');\n throw error;\n }\n }\n\n // Step 6: Update package.json\n spinner.start('Configuring project...');\n try {\n updatePackageJson(targetDir, projectName);\n spinner.stop('Project configured');\n } catch (error) {\n spinner.stop('Failed to configure project');\n throw error;\n }\n\n // Step 7: Initialize git\n spinner.start('Initializing git repository...');\n const gitInitialized = await initGit(targetDir);\n if (gitInitialized) {\n spinner.stop('Git repository initialized');\n } else {\n spinner.stop('Git not available, skipping');\n }\n\n // Step 8: Install dependencies\n spinner.start(`Installing dependencies with ${packageManager}...`);\n try {\n const installCmd = getInstallCommand(packageManager);\n const [cmd, ...args] = installCmd.split(' ');\n await execa(cmd!, args, { cwd: targetDir });\n spinner.stop('Dependencies installed');\n } catch {\n spinner.stop('Failed to install dependencies');\n showWarning(`Run \"${getInstallCommand(packageManager)}\" manually to install dependencies`);\n }\n\n showSuccess(`Project \"${projectName}\" created successfully!`);\n}\n","import { existsSync } from 'node:fs';\nimport { resolve, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Resolves the path to the base template (minimal pages)\n */\nexport function getBaseTemplatePath(): string {\n const templatePath = resolve(__dirname, '..', 'templates', 'base');\n\n if (existsSync(templatePath)) {\n return templatePath;\n }\n\n throw new Error('Could not find base template. Package may be corrupted.');\n}\n\n/**\n * Resolves the path to the i18n overlay template\n */\nexport function getI18nTemplatePath(): string {\n const templatePath = resolve(__dirname, '..', 'templates', 'i18n');\n\n if (existsSync(templatePath)) {\n return templatePath;\n }\n\n throw new Error('Could not find i18n template. Package may be corrupted.');\n}\n","import { execa } from 'execa';\n\n/**\n * Initializes a git repository in the target directory\n */\nexport async function initGit(targetDir: string): Promise<boolean> {\n try {\n await execa('git', ['init'], { cwd: targetDir });\n await execa('git', ['add', '-A'], { cwd: targetDir });\n await execa('git', ['commit', '-m', 'Initial commit from create-velocity'], {\n cwd: targetDir,\n });\n return true;\n } catch {\n // Git may not be installed or configured\n return false;\n }\n}\n\n/**\n * Checks if git is available\n */\nexport async function isGitInstalled(): Promise<boolean> {\n try {\n await execa('git', ['--version']);\n return true;\n } catch {\n return false;\n }\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { PageLayout } from '../types.js';\n\n/**\n * Converts a page slug to a display title\n * e.g., 'about-us' -> 'About Us'\n */\nfunction toTitle(slug: string): string {\n return slug\n .split('-')\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\n/**\n * Converts a page slug to a route ID (snake_case)\n * e.g., 'about-us' -> 'about_us'\n */\nfunction toRouteId(slug: string): string {\n return slug.replace(/-/g, '_');\n}\n\n/**\n * Calculates the next available nav order by finding the highest existing order\n */\nfunction getNextNavOrder(content: string): number {\n // Match all order values in nav configs\n const orderMatches = [...content.matchAll(/order:\\s*(\\d+)/g)];\n let maxOrder = 0;\n\n for (const match of orderMatches) {\n const orderStr = match[1];\n if (orderStr) {\n const order = parseInt(orderStr, 10);\n if (order > maxOrder) {\n maxOrder = order;\n }\n }\n }\n\n // Return the next order (10 more than current max for room to insert)\n return maxOrder + 10;\n}\n\n/**\n * Adds a new route entry to base routes.ts (non-i18n projects)\n * Includes nav config so the page appears in navigation\n */\nfunction addBaseRouteEntry(targetDir: string, pageName: string): void {\n const routesPath = join(targetDir, 'src', 'config', 'routes.ts');\n\n if (!existsSync(routesPath)) {\n return; // routes.ts doesn't exist, skip\n }\n\n const routeId = toRouteId(pageName);\n const title = toTitle(pageName);\n const content = readFileSync(routesPath, 'utf-8');\n\n // Check if route already exists\n if (content.includes(`${routeId}:`)) {\n return; // Route already defined\n }\n\n // Find the closing of routes object (before \"} as const satisfies\")\n const insertPoint = content.indexOf('} as const satisfies');\n if (insertPoint === -1) {\n return; // Can't find insertion point\n }\n\n // Calculate the next nav order\n const navOrder = getNextNavOrder(content);\n\n // Create new route entry with nav config\n const newRoute = `\n // Custom page: ${pageName}\n ${routeId}: {\n path: '/${pageName}',\n nav: { show: true, order: ${navOrder}, label: '${title}' },\n },\n`;\n\n const newContent = content.slice(0, insertPoint) + newRoute + content.slice(insertPoint);\n writeFileSync(routesPath, newContent);\n}\n\n/**\n * Adds a new route entry to i18n routes.ts\n * Creates the route with the same English slug for all locales (user customizes later)\n * Includes nav config so the page appears in navigation\n */\nfunction addI18nRouteEntry(targetDir: string, pageName: string): void {\n const routesPath = join(targetDir, 'src', 'i18n', 'routes.ts');\n\n if (!existsSync(routesPath)) {\n return; // routes.ts doesn't exist, skip\n }\n\n const routeId = toRouteId(pageName);\n const content = readFileSync(routesPath, 'utf-8');\n\n // Check if route already exists\n if (content.includes(`${routeId}:`)) {\n return; // Route already defined\n }\n\n // Find the closing of routes object (before \"} as const satisfies\")\n const insertPoint = content.indexOf('} as const satisfies');\n if (insertPoint === -1) {\n return; // Can't find insertion point\n }\n\n // Calculate the next nav order\n const navOrder = getNextNavOrder(content);\n\n // Create new route entry with nav config\n // Uses the route ID as the translation key (user should add translation)\n const title = toTitle(pageName);\n const newRoute = `\n // Custom page: ${pageName}\n ${routeId}: {\n en: '${pageName}', es: '${pageName}', fr: '${pageName}',\n nav: { show: true, order: ${navOrder}, label: 'nav.${routeId}' },\n },\n`;\n\n const newContent = content.slice(0, insertPoint) + newRoute + content.slice(insertPoint);\n writeFileSync(routesPath, newContent);\n\n // Also add translation keys to translation files\n addI18nTranslationKeys(targetDir, routeId, title);\n}\n\n/**\n * Adds translation keys for a new route to all i18n translation files\n */\nfunction addI18nTranslationKeys(targetDir: string, routeId: string, title: string): void {\n const locales = ['en', 'es', 'fr'];\n\n for (const locale of locales) {\n const translationPath = join(targetDir, 'src', 'i18n', 'translations', `${locale}.ts`);\n\n if (!existsSync(translationPath)) {\n continue;\n }\n\n let content = readFileSync(translationPath, 'utf-8');\n\n // Add nav translation if not exists\n if (!content.includes(`${routeId}:`)) {\n // Find the nav section and add the key\n const navSectionMatch = content.match(/nav:\\s*\\{([^}]+)\\}/);\n if (navSectionMatch) {\n const navSection = navSectionMatch[0];\n const insertPoint = navSection.lastIndexOf('}');\n const newNavSection =\n navSection.slice(0, insertPoint) +\n ` ${routeId}: '${title}',\\n ` +\n navSection.slice(insertPoint);\n content = content.replace(navSection, newNavSection);\n }\n }\n\n // Add page-specific translations if not exists\n const pageKeyPattern = new RegExp(`^\\\\s*${routeId}:\\\\s*\\\\{`, 'm');\n if (!pageKeyPattern.test(content)) {\n // Find a good insertion point (before the closing export)\n const insertPoint = content.lastIndexOf('} as const');\n if (insertPoint !== -1) {\n const pageTranslations = `\n // ${title} page\n ${routeId}: {\n title: '${title}',\n description: 'Add your ${title.toLowerCase()} page description here.',\n },\n\n`;\n content = content.slice(0, insertPoint) + pageTranslations + content.slice(insertPoint);\n }\n }\n\n writeFileSync(translationPath, content);\n }\n}\n\n/**\n * Generates the standard page template (non-i18n)\n */\nfunction generatePageTemplate(pageName: string, layout: PageLayout): string {\n const title = toTitle(pageName);\n const layoutName = layout === 'landing' ? 'LandingLayout' : 'PageLayout';\n\n return `---\nimport ${layoutName} from '@/layouts/${layoutName}.astro';\n---\n\n<${layoutName}\n title=\"${title}\"\n description=\"Add your description here\"\n>\n <!-- Hero Section -->\n <section class=\"py-20 bg-secondary\">\n <div class=\"container\">\n <h1 class=\"text-4xl font-bold text-foreground\">${title}</h1>\n <p class=\"mt-4 text-foreground-muted max-w-2xl\">\n Add your content here.\n </p>\n </div>\n </section>\n\n <!-- Content Section -->\n <section class=\"py-16\">\n <div class=\"container\">\n <!-- Your content -->\n </div>\n </section>\n</${layoutName}>\n`;\n}\n\n/**\n * Generates the i18n-aware page template with translated URL support\n */\nfunction generateI18nPageTemplate(pageName: string, layout: PageLayout): string {\n const title = toTitle(pageName);\n const layoutName = layout === 'landing' ? 'LandingLayout' : 'PageLayout';\n const routeId = toRouteId(pageName);\n const titleKey = `${routeId}.title`;\n const descKey = `${routeId}.description`;\n\n return `---\nimport ${layoutName} from '@/layouts/${layoutName}.astro';\nimport { locales, isValidLocale, defaultLocale, type Locale } from '@/i18n/config';\nimport { useTranslations } from '@/i18n/index';\nimport { routes } from '@/i18n/routes';\n\nexport function getStaticPaths() {\n return locales\n .filter((lang) => lang !== defaultLocale)\n .map((lang) => ({\n params: {\n lang,\n ${routeId}: routes.${routeId}[lang],\n },\n }));\n}\n\nconst { lang } = Astro.params;\n\nif (!lang || !isValidLocale(lang)) {\n return Astro.redirect('/');\n}\n\nconst locale = lang as Locale;\nconst t = useTranslations(locale);\n---\n\n<${layoutName}\n title={t('${titleKey}') || '${title}'}\n description={t('${descKey}') || 'Add your description here'}\n lang={locale}\n routeId=\"${routeId}\"\n>\n <!-- Hero Section -->\n <section class=\"py-20 bg-secondary\">\n <div class=\"container\">\n <h1 class=\"text-4xl font-bold text-foreground\">\n {t('${titleKey}') || '${title}'}\n </h1>\n <p class=\"mt-4 text-foreground-muted max-w-2xl\">\n {t('${descKey}') || 'Add your content here.'}\n </p>\n </div>\n </section>\n\n <!-- Content Section -->\n <section class=\"py-16\">\n <div class=\"container\">\n <!-- Your content -->\n </div>\n </section>\n</${layoutName}>\n`;\n}\n\n/**\n * Generates pages in the target directory\n */\nexport async function generatePages(\n targetDir: string,\n pages: string[],\n layout: PageLayout,\n isI18n: boolean\n): Promise<string[]> {\n const generatedFiles: string[] = [];\n\n if (pages.length === 0) {\n return generatedFiles;\n }\n\n // Ensure pages directory exists\n const pagesDir = join(targetDir, 'src', 'pages');\n if (!existsSync(pagesDir)) {\n mkdirSync(pagesDir, { recursive: true });\n }\n\n // Generate standard pages (English / default locale)\n for (const pageName of pages) {\n const filePath = join(pagesDir, `${pageName}.astro`);\n const template = generatePageTemplate(pageName, layout);\n writeFileSync(filePath, template);\n generatedFiles.push(`src/pages/${pageName}.astro`);\n\n // Add route entry to base routes.ts (for non-i18n nav)\n if (!isI18n) {\n addBaseRouteEntry(targetDir, pageName);\n }\n }\n\n // Generate i18n pages if enabled\n if (isI18n) {\n const langDir = join(pagesDir, '[lang]');\n if (!existsSync(langDir)) {\n mkdirSync(langDir, { recursive: true });\n }\n\n for (const pageName of pages) {\n const routeId = toRouteId(pageName);\n // Use rest parameter syntax for translated URL slugs\n const filePath = join(langDir, `[...${routeId}].astro`);\n const template = generateI18nPageTemplate(pageName, layout);\n writeFileSync(filePath, template);\n generatedFiles.push(`src/pages/[lang]/[...${routeId}].astro`);\n\n // Add route entry to i18n routes.ts\n addI18nRouteEntry(targetDir, pageName);\n }\n }\n\n return generatedFiles;\n}\n\n/**\n * List of page-related files that could be generated\n */\nexport const PAGES_FILES = [\n 'src/pages/{pageName}.astro',\n 'src/pages/[lang]/[...{routeId}].astro',\n];\n","import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\n\n/**\n * Recursively copies a directory\n */\nexport function copyDirectory(src: string, dest: string, overwrite = false): void {\n if (!existsSync(src)) {\n throw new Error(`Source directory does not exist: ${src}`);\n }\n\n if (!existsSync(dest)) {\n mkdirSync(dest, { recursive: true });\n }\n\n const entries = readdirSync(src, { withFileTypes: true });\n\n for (const entry of entries) {\n const srcPath = join(src, entry.name);\n const destPath = join(dest, entry.name);\n\n if (entry.isDirectory()) {\n copyDirectory(srcPath, destPath, overwrite);\n } else {\n if (overwrite || !existsSync(destPath)) {\n const destDir = dirname(destPath);\n if (!existsSync(destDir)) {\n mkdirSync(destDir, { recursive: true });\n }\n copyFileSync(srcPath, destPath);\n }\n }\n }\n}\n\n/**\n * Checks if a directory is empty\n */\nexport function isEmptyDir(path: string): boolean {\n if (!existsSync(path)) return true;\n const files = readdirSync(path);\n return files.length === 0 || (files.length === 1 && files[0] === '.git');\n}\n\n/**\n * Reads a JSON file and parses it\n */\nexport function readJson<T = Record<string, unknown>>(path: string): T {\n const content = readFileSync(path, 'utf-8');\n return JSON.parse(content) as T;\n}\n\n/**\n * Writes an object as JSON to a file\n */\nexport function writeJson(path: string, data: unknown): void {\n writeFileSync(path, JSON.stringify(data, null, 2) + '\\n');\n}\n\n/**\n * Checks if path exists and is a directory\n */\nexport function isDirectory(path: string): boolean {\n return existsSync(path) && statSync(path).isDirectory();\n}\n","import { run } from './cli.js';\n\nrun(process.argv.slice(2)).catch((error) => {\n console.error(error);\n process.exit(1);\n});\n"],"mappings":";;;AAAA,OAAO,SAAS;AAChB,SAAS,WAAAA,gBAAe;AACxB,SAAS,cAAAC,mBAAkB;AAC3B,YAAYC,QAAO;AACnB,OAAOC,SAAQ;;;ACJf,YAAY,OAAO;AACnB,OAAO,QAAQ;;;ACER,SAAS,oBAAoB,MAAoD;AACtF,MAAI,CAAC,QAAQ,KAAK,KAAK,MAAM,IAAI;AAC/B,WAAO,EAAE,OAAO,OAAO,SAAS,+BAA+B;AAAA,EACjE;AAGA,MAAI,SAAS,KAAK,YAAY,GAAG;AAC/B,WAAO,EAAE,OAAO,OAAO,SAAS,iCAAiC;AAAA,EACnE;AAGA,MAAI,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,GAAG,GAAG;AAChD,WAAO,EAAE,OAAO,OAAO,SAAS,wCAAwC;AAAA,EAC1E;AAGA,MAAI,KAAK,KAAK,IAAI,GAAG;AACnB,WAAO,EAAE,OAAO,OAAO,SAAS,qCAAqC;AAAA,EACvE;AAGA,MAAI,CAAC,yDAAyD,KAAK,IAAI,GAAG;AACxE,WAAO;AAAA,MACL,OAAO;AAAA,MACP,SAAS;AAAA,IACX;AAAA,EACF;AAGA,MAAI,KAAK,SAAS,KAAK;AACrB,WAAO,EAAE,OAAO,OAAO,SAAS,+CAA+C;AAAA,EACjF;AAEA,SAAO,EAAE,OAAO,KAAK;AACvB;AAKO,SAAS,mBAAmB,MAAsB;AACvD,SAAO,KACJ,KAAK,EACL,YAAY,EACZ,QAAQ,QAAQ,GAAG,EACnB,QAAQ,kBAAkB,GAAG,EAC7B,QAAQ,WAAW,EAAE,EACrB,QAAQ,WAAW,EAAE,EACrB,QAAQ,OAAO,GAAG;AACvB;;;AC9CO,SAAS,uBAAuC;AACrD,QAAM,YAAY,QAAQ,IAAI,yBAAyB;AAEvD,MAAI,UAAU,WAAW,MAAM,EAAG,QAAO;AACzC,MAAI,UAAU,WAAW,MAAM,EAAG,QAAO;AACzC,MAAI,UAAU,WAAW,KAAK,EAAG,QAAO;AACxC,SAAO;AACT;AAKO,SAAS,kBAAkB,IAA4B;AAC5D,UAAQ,IAAI;AAAA,IACV,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;;;AFZA,SAAS,eAAe,OAAyB;AAC/C,MAAI,CAAC,MAAM,KAAK,EAAG,QAAO,CAAC;AAE3B,SAAO,MACJ,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,KAAK,EAAE,YAAY,CAAC,EACvC,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,EAChC,IAAI,CAAC,SAAS,KAAK,QAAQ,eAAe,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,UAAU,EAAE,CAAC,EACxF,OAAO,CAAC,SAAS,KAAK,SAAS,KAAK,CAAC,CAAC,SAAS,QAAQ,OAAO,KAAK,EAAE,SAAS,IAAI,CAAC;AACxF;AAEA,eAAsB,WAAW,WAA2B,CAAC,GAAoC;AAC/F,QAAM,aAAa,qBAAqB;AAExC,QAAM,UAAU,MAAQ;AAAA,IACtB;AAAA,MACE,aAAa,MACT,OAAK;AAAA,QACL,SAAS;AAAA,QACT,aAAa,SAAS,eAAe;AAAA,QACrC,cAAc,SAAS;AAAA,QACvB,UAAU,CAAC,UAAU;AACnB,gBAAM,OAAO,SAAS,SAAS,eAAe;AAC9C,gBAAM,SAAS,oBAAoB,mBAAmB,IAAI,CAAC;AAC3D,cAAI,CAAC,OAAO,MAAO,QAAO,OAAO;AAAA,QACnC;AAAA,MACF,CAAC;AAAA,MAEH,MACE,SAAS,SAAS,SACd,MAAM,QAAQ,QAAQ,SAAS,IAAI,IACnC,MACI,SAAO;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,UACP;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM;AAAA,UACR;AAAA,UACA;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM;AAAA,UACR;AAAA,QACF;AAAA,QACA,cAAc;AAAA,MAChB,CAAC;AAAA,MAET,YACE,SAAS,eAAe,SACpB,MAAM,QAAQ,QAAQ,SAAS,UAAU,IACzC,MACI,SAAO;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,UACP;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM;AAAA,UACR;AAAA,UACA;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM;AAAA,UACR;AAAA,QACF;AAAA,QACA,cAAc;AAAA,MAChB,CAAC;AAAA,MAET,MACE,SAAS,SAAS,SACd,MAAM,QAAQ,QAAQ,SAAS,IAAI,IACnC,MACI,SAAO;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,UACP;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM;AAAA,UACR;AAAA,UACA;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM;AAAA,UACR;AAAA,QACF;AAAA,QACA,cAAc;AAAA,MAChB,CAAC;AAAA,MAET,eACE,SAAS,UAAU,SACf,MAAM,QAAQ,QAAQ,SAAS,KAAK,IACpC,MACI,SAAO;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,UACP;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM;AAAA,UACR;AAAA,UACA;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM;AAAA,UACR;AAAA,QACF;AAAA,QACA,cAAc;AAAA,MAChB,CAAC;AAAA,MAET,WAAW,CAAC,EAAE,QAAQ,MACpB,QAAQ,gBACF,OAAK;AAAA,QACL,SAAS;AAAA,QACT,aAAa;AAAA,QACb,UAAU,CAAC,UAAU;AACnB,gBAAM,QAAQ,eAAe,KAAK;AAClC,cAAI,MAAM,WAAW,GAAG;AACtB,mBAAO;AAAA,UACT;AAAA,QACF;AAAA,MACF,CAAC,IACD,QAAQ,QAAQ,EAAE;AAAA,MAExB,YAAY,CAAC,EAAE,QAAQ,MACrB,QAAQ,gBACF,SAAO;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,UACP;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM;AAAA,UACR;AAAA,UACA;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM;AAAA,UACR;AAAA,QACF;AAAA,QACA,cAAc;AAAA,MAChB,CAAC,IACD,QAAQ,QAAQ,MAAoB;AAAA,MAE1C,gBAAgB,MACZ,SAAO;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,UACP;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM,eAAe,SAAS,aAAa;AAAA,UAC7C;AAAA,UACA;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM,eAAe,QAAQ,aAAa;AAAA,UAC5C;AAAA,UACA;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM,eAAe,SAAS,aAAa;AAAA,UAC7C;AAAA,UACA;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM,eAAe,QAAQ,aAAa;AAAA,UAC5C;AAAA,QACF;AAAA,QACA,cAAc;AAAA,MAChB,CAAC;AAAA,IACL;AAAA,IACA;AAAA,MACE,UAAU,MAAM;AACd,QAAE,SAAO,sBAAsB;AAC/B,gBAAQ,KAAK,CAAC;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,aAAa,mBAAmB,QAAQ,eAAe,SAAS,eAAe,kBAAkB;AAAA,IACjG,MAAM,QAAQ;AAAA,IACd,YAAY,QAAQ;AAAA,IACpB,MAAM,QAAQ;AAAA,IACd,OAAO,eAAe,QAAQ,SAAmB;AAAA,IACjD,YAAa,QAAQ,cAA6B;AAAA,IAClD,gBAAgB,QAAQ;AAAA,EAC1B;AACF;AAEO,SAAS,YAAkB;AAChC,UAAQ,IAAI;AACZ,EAAE,QAAM,GAAG,OAAO,GAAG,MAAM,mBAAmB,CAAC,CAAC;AAClD;AAEO,SAAS,UAAU,aAAqB,gBAAsC;AACnF,QAAM,SAAS,mBAAmB,QAAQ,YAAY;AAEtD,EAAE;AAAA,IACA;AAAA,MACE,MAAM,WAAW;AAAA,MACjB,GAAG,MAAM;AAAA,IACX,EAAE,KAAK,IAAI;AAAA,IACX;AAAA,EACF;AAEA,EAAE,QAAM,GAAG,MAAM,iBAAiB,CAAC;AACrC;AAEO,SAAS,UAAU,SAAuB;AAC/C,EAAE,MAAI,MAAM,GAAG,IAAI,OAAO,CAAC;AAC7B;AAEO,SAAS,YAAY,SAAuB;AACjD,EAAE,MAAI,KAAK,GAAG,OAAO,OAAO,CAAC;AAC/B;AAEO,SAAS,YAAY,SAAuB;AACjD,EAAE,MAAI,QAAQ,GAAG,MAAM,OAAO,CAAC;AACjC;;;AG/OA,SAAS,cAAAC,aAAY,aAAAC,YAAW,aAAa,cAAc,gBAAAC,eAAc,iBAAAC,gBAAe,cAAc;AACtG,SAAS,QAAAC,aAAY;AACrB,YAAYC,QAAO;AACnB,SAAS,SAAAC,cAAa;AACtB,SAAS,wBAAwB;;;ACJjC,SAAS,kBAAkB;AAC3B,SAAS,SAAS,eAAe;AACjC,SAAS,qBAAqB;AAE9B,IAAMC,aAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AAKjD,SAAS,sBAA8B;AAC5C,QAAM,eAAe,QAAQA,YAAW,MAAM,aAAa,MAAM;AAEjE,MAAI,WAAW,YAAY,GAAG;AAC5B,WAAO;AAAA,EACT;AAEA,QAAM,IAAI,MAAM,yDAAyD;AAC3E;AAKO,SAAS,sBAA8B;AAC5C,QAAM,eAAe,QAAQA,YAAW,MAAM,aAAa,MAAM;AAEjE,MAAI,WAAW,YAAY,GAAG;AAC5B,WAAO;AAAA,EACT;AAEA,QAAM,IAAI,MAAM,yDAAyD;AAC3E;;;AC9BA,SAAS,aAAa;AAKtB,eAAsB,QAAQ,WAAqC;AACjE,MAAI;AACF,UAAM,MAAM,OAAO,CAAC,MAAM,GAAG,EAAE,KAAK,UAAU,CAAC;AAC/C,UAAM,MAAM,OAAO,CAAC,OAAO,IAAI,GAAG,EAAE,KAAK,UAAU,CAAC;AACpD,UAAM,MAAM,OAAO,CAAC,UAAU,MAAM,qCAAqC,GAAG;AAAA,MAC1E,KAAK;AAAA,IACP,CAAC;AACD,WAAO;AAAA,EACT,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;;;ACjBA,SAAS,cAAAC,aAAY,WAAW,cAAc,qBAAqB;AACnE,SAAS,YAAY;AAOrB,SAAS,QAAQ,MAAsB;AACrC,SAAO,KACJ,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,GAAG;AACb;AAMA,SAAS,UAAU,MAAsB;AACvC,SAAO,KAAK,QAAQ,MAAM,GAAG;AAC/B;AAKA,SAAS,gBAAgB,SAAyB;AAEhD,QAAM,eAAe,CAAC,GAAG,QAAQ,SAAS,iBAAiB,CAAC;AAC5D,MAAI,WAAW;AAEf,aAAW,SAAS,cAAc;AAChC,UAAM,WAAW,MAAM,CAAC;AACxB,QAAI,UAAU;AACZ,YAAM,QAAQ,SAAS,UAAU,EAAE;AACnC,UAAI,QAAQ,UAAU;AACpB,mBAAW;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAGA,SAAO,WAAW;AACpB;AAMA,SAAS,kBAAkB,WAAmB,UAAwB;AACpE,QAAM,aAAa,KAAK,WAAW,OAAO,UAAU,WAAW;AAE/D,MAAI,CAACA,YAAW,UAAU,GAAG;AAC3B;AAAA,EACF;AAEA,QAAM,UAAU,UAAU,QAAQ;AAClC,QAAM,QAAQ,QAAQ,QAAQ;AAC9B,QAAM,UAAU,aAAa,YAAY,OAAO;AAGhD,MAAI,QAAQ,SAAS,GAAG,OAAO,GAAG,GAAG;AACnC;AAAA,EACF;AAGA,QAAM,cAAc,QAAQ,QAAQ,sBAAsB;AAC1D,MAAI,gBAAgB,IAAI;AACtB;AAAA,EACF;AAGA,QAAM,WAAW,gBAAgB,OAAO;AAGxC,QAAM,WAAW;AAAA,oBACC,QAAQ;AAAA,IACxB,OAAO;AAAA,cACG,QAAQ;AAAA,gCACU,QAAQ,aAAa,KAAK;AAAA;AAAA;AAIxD,QAAM,aAAa,QAAQ,MAAM,GAAG,WAAW,IAAI,WAAW,QAAQ,MAAM,WAAW;AACvF,gBAAc,YAAY,UAAU;AACtC;AAOA,SAAS,kBAAkB,WAAmB,UAAwB;AACpE,QAAM,aAAa,KAAK,WAAW,OAAO,QAAQ,WAAW;AAE7D,MAAI,CAACA,YAAW,UAAU,GAAG;AAC3B;AAAA,EACF;AAEA,QAAM,UAAU,UAAU,QAAQ;AAClC,QAAM,UAAU,aAAa,YAAY,OAAO;AAGhD,MAAI,QAAQ,SAAS,GAAG,OAAO,GAAG,GAAG;AACnC;AAAA,EACF;AAGA,QAAM,cAAc,QAAQ,QAAQ,sBAAsB;AAC1D,MAAI,gBAAgB,IAAI;AACtB;AAAA,EACF;AAGA,QAAM,WAAW,gBAAgB,OAAO;AAIxC,QAAM,QAAQ,QAAQ,QAAQ;AAC9B,QAAM,WAAW;AAAA,oBACC,QAAQ;AAAA,IACxB,OAAO;AAAA,WACA,QAAQ,WAAW,QAAQ,WAAW,QAAQ;AAAA,gCACzB,QAAQ,iBAAiB,OAAO;AAAA;AAAA;AAI9D,QAAM,aAAa,QAAQ,MAAM,GAAG,WAAW,IAAI,WAAW,QAAQ,MAAM,WAAW;AACvF,gBAAc,YAAY,UAAU;AAGpC,yBAAuB,WAAW,SAAS,KAAK;AAClD;AAKA,SAAS,uBAAuB,WAAmB,SAAiB,OAAqB;AACvF,QAAM,UAAU,CAAC,MAAM,MAAM,IAAI;AAEjC,aAAW,UAAU,SAAS;AAC5B,UAAM,kBAAkB,KAAK,WAAW,OAAO,QAAQ,gBAAgB,GAAG,MAAM,KAAK;AAErF,QAAI,CAACA,YAAW,eAAe,GAAG;AAChC;AAAA,IACF;AAEA,QAAI,UAAU,aAAa,iBAAiB,OAAO;AAGnD,QAAI,CAAC,QAAQ,SAAS,GAAG,OAAO,GAAG,GAAG;AAEpC,YAAM,kBAAkB,QAAQ,MAAM,oBAAoB;AAC1D,UAAI,iBAAiB;AACnB,cAAM,aAAa,gBAAgB,CAAC;AACpC,cAAM,cAAc,WAAW,YAAY,GAAG;AAC9C,cAAM,gBACJ,WAAW,MAAM,GAAG,WAAW,IAC/B,OAAO,OAAO,MAAM,KAAK;AAAA,MACzB,WAAW,MAAM,WAAW;AAC9B,kBAAU,QAAQ,QAAQ,YAAY,aAAa;AAAA,MACrD;AAAA,IACF;AAGA,UAAM,iBAAiB,IAAI,OAAO,QAAQ,OAAO,YAAY,GAAG;AAChE,QAAI,CAAC,eAAe,KAAK,OAAO,GAAG;AAEjC,YAAM,cAAc,QAAQ,YAAY,YAAY;AACpD,UAAI,gBAAgB,IAAI;AACtB,cAAM,mBAAmB;AAAA,OAC1B,KAAK;AAAA,IACR,OAAO;AAAA,cACG,KAAK;AAAA,6BACU,MAAM,YAAY,CAAC;AAAA;AAAA;AAAA;AAIxC,kBAAU,QAAQ,MAAM,GAAG,WAAW,IAAI,mBAAmB,QAAQ,MAAM,WAAW;AAAA,MACxF;AAAA,IACF;AAEA,kBAAc,iBAAiB,OAAO;AAAA,EACxC;AACF;AAKA,SAAS,qBAAqB,UAAkB,QAA4B;AAC1E,QAAM,QAAQ,QAAQ,QAAQ;AAC9B,QAAM,aAAa,WAAW,YAAY,kBAAkB;AAE5D,SAAO;AAAA,SACA,UAAU,oBAAoB,UAAU;AAAA;AAAA;AAAA,GAG9C,UAAU;AAAA,WACF,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uDAMuC,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAaxD,UAAU;AAAA;AAEd;AAKA,SAAS,yBAAyB,UAAkB,QAA4B;AAC9E,QAAM,QAAQ,QAAQ,QAAQ;AAC9B,QAAM,aAAa,WAAW,YAAY,kBAAkB;AAC5D,QAAM,UAAU,UAAU,QAAQ;AAClC,QAAM,WAAW,GAAG,OAAO;AAC3B,QAAM,UAAU,GAAG,OAAO;AAE1B,SAAO;AAAA,SACA,UAAU,oBAAoB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAWvC,OAAO,YAAY,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAejC,UAAU;AAAA,cACC,QAAQ,UAAU,KAAK;AAAA,oBACjB,OAAO;AAAA;AAAA,aAEd,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAMN,QAAQ,UAAU,KAAK;AAAA;AAAA;AAAA,cAGvB,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAWjB,UAAU;AAAA;AAEd;AAKA,eAAsB,cACpB,WACA,OACA,QACA,QACmB;AACnB,QAAM,iBAA2B,CAAC;AAElC,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO;AAAA,EACT;AAGA,QAAM,WAAW,KAAK,WAAW,OAAO,OAAO;AAC/C,MAAI,CAACA,YAAW,QAAQ,GAAG;AACzB,cAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,EACzC;AAGA,aAAW,YAAY,OAAO;AAC5B,UAAM,WAAW,KAAK,UAAU,GAAG,QAAQ,QAAQ;AACnD,UAAM,WAAW,qBAAqB,UAAU,MAAM;AACtD,kBAAc,UAAU,QAAQ;AAChC,mBAAe,KAAK,aAAa,QAAQ,QAAQ;AAGjD,QAAI,CAAC,QAAQ;AACX,wBAAkB,WAAW,QAAQ;AAAA,IACvC;AAAA,EACF;AAGA,MAAI,QAAQ;AACV,UAAM,UAAU,KAAK,UAAU,QAAQ;AACvC,QAAI,CAACA,YAAW,OAAO,GAAG;AACxB,gBAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AAAA,IACxC;AAEA,eAAW,YAAY,OAAO;AAC5B,YAAM,UAAU,UAAU,QAAQ;AAElC,YAAM,WAAW,KAAK,SAAS,OAAO,OAAO,SAAS;AACtD,YAAM,WAAW,yBAAyB,UAAU,MAAM;AAC1D,oBAAc,UAAU,QAAQ;AAChC,qBAAe,KAAK,wBAAwB,OAAO,SAAS;AAG5D,wBAAkB,WAAW,QAAQ;AAAA,IACvC;AAAA,EACF;AAEA,SAAO;AACT;;;AHxUA,IAAM,gBAAgB;AAGtB,IAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,eAAe;AAAA;AAAA,EAEnB;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,qBAAqB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF;AAKA,SAAS,kBAAkB,KAAa,MAAoB;AAC1D,MAAI,CAACC,YAAW,IAAI,GAAG;AACrB,IAAAC,WAAU,MAAM,EAAE,WAAW,KAAK,CAAC;AAAA,EACrC;AAEA,QAAM,UAAU,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AAExD,aAAW,SAAS,SAAS;AAC3B,UAAM,UAAUC,MAAK,KAAK,MAAM,IAAI;AACpC,UAAM,WAAWA,MAAK,MAAM,MAAM,IAAI;AAEtC,QAAI,MAAM,YAAY,GAAG;AACvB,wBAAkB,SAAS,QAAQ;AAAA,IACrC,OAAO;AACL,mBAAa,SAAS,QAAQ;AAAA,IAChC;AAAA,EACF;AACF;AAKA,SAAS,YAAY,WAAmB,OAAuB;AAC7D,aAAW,QAAQ,OAAO;AACxB,UAAM,WAAWA,MAAK,WAAW,IAAI;AACrC,QAAIF,YAAW,QAAQ,GAAG;AACxB,UAAI;AACF,eAAO,UAAU,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MACnD,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;AAKA,SAAS,kBAAkB,WAAmB,aAA2B;AACvE,QAAM,UAAUE,MAAK,WAAW,cAAc;AAE9C,MAAI,CAACF,YAAW,OAAO,GAAG;AACxB,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AAEA,QAAM,MAAM,KAAK,MAAMG,cAAa,SAAS,OAAO,CAAC;AACrD,MAAI,OAAO;AACX,MAAI,UAAU;AACd,SAAO,IAAI;AACX,SAAO,IAAI;AACX,SAAO,IAAI;AAEX,EAAAC,eAAc,SAAS,KAAK,UAAU,KAAK,MAAM,CAAC,IAAI,IAAI;AAC5D;AAKA,SAAS,kBAAkB,WAAyB;AAClD,QAAM,eAAe,oBAAoB;AACzC,MAAIJ,YAAW,YAAY,GAAG;AAC5B,sBAAkB,cAAc,SAAS;AAAA,EAC3C;AACF;AAKA,SAAS,iBAAiB,WAAyB;AACjD,QAAM,eAAe,oBAAoB;AACzC,oBAAkB,cAAc,SAAS;AAC3C;AAKA,SAAS,yBAAyB,WAAyB;AACzD,QAAM,cAAc;AAAA,IAClB;AAAA,EACF;AAEA,aAAW,OAAO,aAAa;AAC7B,UAAM,UAAUE,MAAK,WAAW,GAAG;AACnC,QAAI,CAACF,YAAW,OAAO,GAAG;AACxB,MAAAC,WAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AACtC,MAAAG,eAAcF,MAAK,SAAS,UAAU,GAAG,EAAE;AAAA,IAC7C;AAAA,EACF;AACF;AAKA,eAAsB,SAAS,SAAyC;AACtE,QAAM,EAAE,aAAa,WAAW,MAAM,YAAY,MAAM,OAAO,YAAY,eAAe,IAAI;AAC9F,QAAMG,WAAY,WAAQ;AAG1B,EAAAA,SAAQ,MAAM,qCAAqC;AAEnD,MAAI;AACF,UAAM,iBAAiB,eAAe;AAAA,MACpC,KAAK;AAAA,MACL,OAAO;AAAA,IACT,CAAC;AACD,gBAAY,WAAW,aAAa;AACpC,IAAAA,SAAQ,KAAK,qBAAqB;AAAA,EACpC,SAAS,OAAO;AACd,IAAAA,SAAQ,KAAK,6BAA6B;AAC1C,UAAM,IAAI;AAAA,MACR;AAAA,EAAoF,iBAAiB,QAAQ,MAAM,UAAU,EAAE;AAAA,IACjI;AAAA,EACF;AAGA,MAAI,CAAC,MAAM;AACT,IAAAA,SAAQ,MAAM,iCAAiC;AAC/C,gBAAY,WAAW,YAAY;AACnC,sBAAkB,SAAS;AAC3B,6BAAyB,SAAS;AAClC,IAAAA,SAAQ,KAAK,6BAA6B;AAAA,EAC5C;AAGA,MAAI,CAAC,YAAY;AACf,IAAAA,SAAQ,MAAM,kCAAkC;AAChD,gBAAY,WAAW,kBAAkB;AACzC,IAAAA,SAAQ,KAAK,uBAAuB;AAAA,EACtC;AAGA,MAAI,MAAM;AACR,IAAAA,SAAQ,MAAM,wBAAwB;AACtC,QAAI;AACF,uBAAiB,SAAS;AAC1B,MAAAA,SAAQ,KAAK,oBAAoB;AAAA,IACnC,SAAS,OAAO;AACd,MAAAA,SAAQ,KAAK,4BAA4B;AACzC,YAAM;AAAA,IACR;AAAA,EACF;AAGA,MAAI,MAAM,SAAS,GAAG;AACpB,IAAAA,SAAQ,MAAM,cAAc,MAAM,MAAM,gBAAgB,MAAM,SAAS,IAAI,MAAM,EAAE,KAAK;AACxF,QAAI;AACF,YAAM,iBAAiB,MAAM,cAAc,WAAW,OAAO,YAAY,IAAI;AAC7E,MAAAA,SAAQ,KAAK,aAAa,eAAe,MAAM,aAAa,eAAe,SAAS,IAAI,MAAM,EAAE,EAAE;AAAA,IACpG,SAAS,OAAO;AACd,MAAAA,SAAQ,KAAK,0BAA0B;AACvC,YAAM;AAAA,IACR;AAAA,EACF;AAGA,EAAAA,SAAQ,MAAM,wBAAwB;AACtC,MAAI;AACF,sBAAkB,WAAW,WAAW;AACxC,IAAAA,SAAQ,KAAK,oBAAoB;AAAA,EACnC,SAAS,OAAO;AACd,IAAAA,SAAQ,KAAK,6BAA6B;AAC1C,UAAM;AAAA,EACR;AAGA,EAAAA,SAAQ,MAAM,gCAAgC;AAC9C,QAAM,iBAAiB,MAAM,QAAQ,SAAS;AAC9C,MAAI,gBAAgB;AAClB,IAAAA,SAAQ,KAAK,4BAA4B;AAAA,EAC3C,OAAO;AACL,IAAAA,SAAQ,KAAK,6BAA6B;AAAA,EAC5C;AAGA,EAAAA,SAAQ,MAAM,gCAAgC,cAAc,KAAK;AACjE,MAAI;AACF,UAAM,aAAa,kBAAkB,cAAc;AACnD,UAAM,CAAC,KAAK,GAAG,IAAI,IAAI,WAAW,MAAM,GAAG;AAC3C,UAAMC,OAAM,KAAM,MAAM,EAAE,KAAK,UAAU,CAAC;AAC1C,IAAAD,SAAQ,KAAK,wBAAwB;AAAA,EACvC,QAAQ;AACN,IAAAA,SAAQ,KAAK,gCAAgC;AAC7C,gBAAY,QAAQ,kBAAkB,cAAc,CAAC,oCAAoC;AAAA,EAC3F;AAEA,cAAY,YAAY,WAAW,yBAAyB;AAC9D;;;AI7OA,SAAS,cAAAE,aAAY,aAAAC,YAAW,eAAAC,cAAa,UAAU,gBAAAC,eAAc,gBAAAC,eAAc,iBAAAC,sBAAqB;AACxG,SAAS,QAAAC,OAAM,WAAAC,gBAAe;AAqCvB,SAAS,WAAW,MAAuB;AAChD,MAAI,CAACC,YAAW,IAAI,EAAG,QAAO;AAC9B,QAAM,QAAQC,aAAY,IAAI;AAC9B,SAAO,MAAM,WAAW,KAAM,MAAM,WAAW,KAAK,MAAM,CAAC,MAAM;AACnE;;;AR/BA,IAAM,YAAY;AAAA,EAChBC,IAAG,KAAK,uBAAuB,CAAC;AAAA;AAAA,EAEhCA,IAAG,KAAK,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjBA,IAAG,KAAK,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASnBA,IAAG,KAAK,WAAW,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAOtB,IAAM,UAAU;AAEhB,eAAsB,IAAI,MAA+B;AACvD,QAAM,OAAO,IAAgB,MAAM;AAAA,IACjC,SAAS,CAAC,QAAQ,cAAc,QAAQ,SAAS,QAAQ,WAAW,KAAK;AAAA,IACzE,OAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EACF,CAAC;AAGD,MAAI,KAAK,MAAM;AACb,YAAQ,IAAI,SAAS;AACrB;AAAA,EACF;AAGA,MAAI,KAAK,SAAS;AAChB,YAAQ,IAAI,OAAO;AACnB;AAAA,EACF;AAEA,YAAU;AAGV,QAAM,iBAAiB,KAAK,EAAE,CAAC;AAG/B,MAAI,KAAK,KAAK;AACZ,UAAMC,eAAc,mBAAmB,kBAAkB,kBAAkB;AAC3E,UAAMC,aAAYC,SAAQ,QAAQ,IAAI,GAAGF,YAAW;AAEpD,QAAIG,YAAWF,UAAS,KAAK,CAAC,WAAWA,UAAS,GAAG;AACnD,gBAAU,cAAcD,YAAW,oCAAoC;AACvE,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,UAAM,SAAS;AAAA,MACb,aAAAA;AAAA,MACA,WAAAC;AAAA,MACA,MAAM,KAAK,QAAQ;AAAA,MACnB,YAAY,KAAK,eAAe;AAAA;AAAA,MAChC,MAAM,KAAK,QAAQ;AAAA,MACnB,OAAO,CAAC;AAAA,MACR,YAAY;AAAA,MACZ,gBAAgB;AAAA,IAClB,CAAC;AAED,cAAUD,cAAa,MAAM;AAC7B;AAAA,EACF;AAGA,QAAM,UAAU,MAAM,WAAW;AAAA,IAC/B,aAAa;AAAA,IACb,MAAM,KAAK;AAAA,IACX,YAAY,KAAK;AAAA,IACjB,MAAM,KAAK;AAAA,IACX,OAAO,KAAK;AAAA,EACd,CAAC;AAGD,MAAI,OAAO,YAAY,UAAU;AAC/B;AAAA,EACF;AAEA,QAAM,EAAE,aAAa,MAAM,YAAY,MAAM,OAAO,YAAY,eAAe,IAAI;AACnF,QAAM,YAAYE,SAAQ,QAAQ,IAAI,GAAG,WAAW;AAGpD,MAAIC,YAAW,SAAS,KAAK,CAAC,WAAW,SAAS,GAAG;AACnD,UAAM,kBAAkB,MAAQ,WAAQ;AAAA,MACtC,SAAS,cAAc,WAAW;AAAA,MAClC,cAAc;AAAA,IAChB,CAAC;AAED,QAAI,CAAC,mBAAqB,YAAS,eAAe,GAAG;AACnD,MAAE,UAAO,sBAAsB;AAC/B,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF;AAGA,MAAI;AACF,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,cAAU,aAAa,cAAc;AAAA,EACvC,SAAS,OAAO;AACd,cAAU,iBAAiB,QAAQ,MAAM,UAAU,8BAA8B;AACjF,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;;;ASxIA,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,UAAU;AAC1C,UAAQ,MAAM,KAAK;AACnB,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":["resolve","existsSync","p","pc","existsSync","mkdirSync","readFileSync","writeFileSync","join","p","execa","__dirname","existsSync","existsSync","mkdirSync","join","readFileSync","writeFileSync","spinner","execa","existsSync","mkdirSync","readdirSync","copyFileSync","readFileSync","writeFileSync","join","dirname","existsSync","readdirSync","pc","projectName","targetDir","resolve","existsSync"]}
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "create-velocity-astro",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Create Velocity - A CLI to scaffold production-ready Astro 6 + Tailwind v4 projects",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Southwell Media <info@southwellmedia.com>",
8
- "homepage": "https://github.com/southwellmedia-dev/velocity#readme",
8
+ "homepage": "https://github.com/southwellmedia/create-velocity-astro#readme",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/southwellmedia-dev/velocity.git"
11
+ "url": "git+https://github.com/southwellmedia/create-velocity-astro.git"
12
12
  },
13
13
  "bugs": {
14
- "url": "https://github.com/southwellmedia-dev/velocity/issues"
14
+ "url": "https://github.com/southwellmedia/create-velocity-astro/issues"
15
15
  },
16
16
  "keywords": [
17
17
  "create",
@@ -4,7 +4,7 @@ import { getCollection } from 'astro:content';
4
4
 
5
5
  const posts = await getCollection('blog', ({ data }) => !data.draft);
6
6
  const sortedPosts = posts.sort(
7
- (a, b) => new Date(b.data.publishDate).getTime() - new Date(a.data.publishDate).getTime()
7
+ (a, b) => new Date(b.data.publishedAt).getTime() - new Date(a.data.publishedAt).getTime()
8
8
  );
9
9
  ---
10
10
 
@@ -42,7 +42,7 @@ const sortedPosts = posts.sort(
42
42
  {post.data.description}
43
43
  </p>
44
44
  <time class="mt-2 text-sm text-muted-foreground block">
45
- {new Date(post.data.publishDate).toLocaleDateString('en-US', {
45
+ {new Date(post.data.publishedAt).toLocaleDateString('en-US', {
46
46
  year: 'numeric',
47
47
  month: 'long',
48
48
  day: 'numeric',
@@ -1,23 +1,31 @@
1
1
  ---
2
+ /**
3
+ * Navbar Component (i18n-aware)
4
+ * Dynamic navigation that reads routes from routes.ts
5
+ * Includes LanguageSwitcher for locale switching
6
+ */
2
7
  import Button from '@/components/ui/Button.astro';
3
8
  import Icon from '@/components/ui/Icon.astro';
4
9
  import ThemeToggle from '@/components/layout/ThemeToggle.astro';
5
10
  import LanguageSwitcher from '@/components/i18n/LanguageSwitcher.astro';
6
- import { getLocaleFromPath, defaultLocale, localePath } from '@/i18n/config';
11
+ import { getLocaleFromPath, defaultLocale } from '@/i18n/config';
12
+ import { useTranslations, getLocalizedPath, getNavRoutes } from '@/i18n/index';
7
13
 
8
14
  // Get current locale from URL
9
15
  const currentPath = Astro.url.pathname;
10
16
  const currentLocale = getLocaleFromPath(currentPath);
17
+ const t = useTranslations(currentLocale);
18
+
19
+ // Get navigation routes dynamically from routes.ts
20
+ const navRoutes = getNavRoutes();
11
21
 
12
22
  // Check if we're on the landing page to determine link behavior
13
23
  const isLandingPage = currentPath === '/' || currentPath === `/${currentLocale}` || currentPath === `/${currentLocale}/`;
14
24
 
15
25
  // Build locale-aware links
16
- const homeLink = currentLocale === defaultLocale ? '/' : `/${currentLocale}`;
26
+ const homeLink = getLocalizedPath('home', currentLocale);
17
27
  const featuresLink = isLandingPage ? '#features' : `${homeLink}#features`;
18
28
  const ctaLink = isLandingPage ? '#cta' : `${homeLink}#cta`;
19
- const componentsLink = localePath('/components', currentLocale);
20
- const blogLink = localePath('/blog', currentLocale);
21
29
  ---
22
30
 
23
31
  <nav class="fixed top-0 left-0 right-0 z-50 border-b border-border bg-background/80 backdrop-blur-md">
@@ -30,10 +38,24 @@ const blogLink = localePath('/blog', currentLocale);
30
38
 
31
39
  <!-- Desktop Menu -->
32
40
  <div class="hidden items-center gap-8 md:flex">
33
- <a href={featuresLink} class="text-sm font-medium text-foreground-muted hover:text-foreground transition-colors">Features</a>
34
- <a href={componentsLink} class="text-sm font-medium text-foreground-muted hover:text-foreground transition-colors">Components</a>
35
- <a href={blogLink} class="text-sm font-medium text-foreground-muted hover:text-foreground transition-colors">Blog</a>
36
- <a href={blogLink} class="text-sm font-medium text-foreground-muted hover:text-foreground transition-colors">Docs</a>
41
+ <!-- Features link (anchor to landing page section) -->
42
+ <a
43
+ href={featuresLink}
44
+ class="text-sm font-medium text-foreground-muted hover:text-foreground transition-colors"
45
+ >
46
+ {t('nav.features')}
47
+ </a>
48
+
49
+ <!-- Dynamic navigation from routes.ts -->
50
+ {navRoutes.map(({ routeId, label }) => (
51
+ <a
52
+ href={getLocalizedPath(routeId, currentLocale)}
53
+ class="text-sm font-medium text-foreground-muted hover:text-foreground transition-colors"
54
+ >
55
+ {t(label as any)}
56
+ </a>
57
+ ))}
58
+
37
59
  <div class="flex items-center gap-2">
38
60
  <LanguageSwitcher />
39
61
  <ThemeToggle />
@@ -47,7 +69,7 @@ const blogLink = localePath('/blog', currentLocale);
47
69
  >
48
70
  <Icon name="github" size="sm" />
49
71
  </Button>
50
- <Button size="sm" href={ctaLink}>Get Started</Button>
72
+ <Button size="sm" href={ctaLink}>{t('nav.getStarted')}</Button>
51
73
  </div>
52
74
  </div>
53
75
 
@@ -70,15 +92,29 @@ const blogLink = localePath('/blog', currentLocale);
70
92
 
71
93
  <!-- Mobile Menu -->
72
94
  <div id="mobile-menu" class="hidden absolute top-16 left-0 w-full bg-background border-b border-border p-6 md:hidden flex-col gap-4 shadow-lg">
73
- <a href={featuresLink} class="text-base font-medium text-foreground-secondary hover:text-foreground transition-colors">Features</a>
74
- <a href={componentsLink} class="text-base font-medium text-foreground-secondary hover:text-foreground transition-colors">Components</a>
75
- <a href={blogLink} class="text-base font-medium text-foreground-secondary hover:text-foreground transition-colors">Blog</a>
76
- <a href={blogLink} class="text-base font-medium text-foreground-secondary hover:text-foreground transition-colors">Docs</a>
95
+ <!-- Features link (anchor to landing page section) -->
96
+ <a
97
+ href={featuresLink}
98
+ class="text-base font-medium text-foreground-secondary hover:text-foreground transition-colors"
99
+ >
100
+ {t('nav.features')}
101
+ </a>
102
+
103
+ <!-- Dynamic navigation from routes.ts -->
104
+ {navRoutes.map(({ routeId, label }) => (
105
+ <a
106
+ href={getLocalizedPath(routeId, currentLocale)}
107
+ class="text-base font-medium text-foreground-secondary hover:text-foreground transition-colors"
108
+ >
109
+ {t(label as any)}
110
+ </a>
111
+ ))}
112
+
77
113
  <div class="flex items-center gap-3 pt-2 border-t border-border">
78
114
  <LanguageSwitcher />
79
115
  <ThemeToggle />
80
116
  </div>
81
- <Button fullWidth href={ctaLink}>Get Started</Button>
117
+ <Button fullWidth href={ctaLink}>{t('nav.getStarted')}</Button>
82
118
  </div>
83
119
  </nav>
84
120
 
@@ -0,0 +1,148 @@
1
+ ---
2
+ /**
3
+ * Footer Component (i18n-aware)
4
+ * Standard footer for non-landing pages with translated links and labels
5
+ */
6
+ import { cn } from '@/lib/cn';
7
+ import siteConfig from '@/config/site.config';
8
+ import { type Locale, defaultLocale, getLocaleFromPath } from '@/i18n/config';
9
+ import { useTranslations, getLocalizedPath } from '@/i18n/index';
10
+
11
+ interface Props {
12
+ class?: string;
13
+ locale?: Locale;
14
+ }
15
+
16
+ const { class: className, locale: localeProp } = Astro.props;
17
+
18
+ // Get locale from prop or infer from URL
19
+ const locale = localeProp || getLocaleFromPath(Astro.url.pathname);
20
+ const t = useTranslations(locale);
21
+
22
+ const currentYear = new Date().getFullYear();
23
+ ---
24
+
25
+ <footer class={cn('border-t border-border', className)}>
26
+ <div class="container py-16">
27
+ <div class="grid grid-cols-2 gap-8 md:grid-cols-4 lg:grid-cols-5">
28
+ <!-- Brand -->
29
+ <div class="col-span-2 lg:col-span-2">
30
+ <a href={getLocalizedPath('home', locale)} class="inline-flex items-center gap-2 font-semibold text-foreground">
31
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
32
+ <path d="M12 2L2 19.5h20L12 2zm0 4l7 12H5l7-12z" />
33
+ </svg>
34
+ <span>{t('site.name')}</span>
35
+ </a>
36
+ <p class="mt-4 text-sm text-foreground-muted max-w-xs leading-relaxed">
37
+ {t('site.description')}
38
+ </p>
39
+
40
+ <!-- Social Links -->
41
+ <div class="mt-6 flex items-center gap-3">
42
+ <a
43
+ href={siteConfig.links.github}
44
+ target="_blank"
45
+ rel="noopener noreferrer"
46
+ class={cn(
47
+ 'inline-flex items-center justify-center rounded-md p-2',
48
+ 'text-foreground-muted hover:text-foreground hover:bg-secondary',
49
+ 'transition-colors'
50
+ )}
51
+ aria-label="GitHub"
52
+ >
53
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
54
+ <path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
55
+ </svg>
56
+ </a>
57
+ <a
58
+ href={siteConfig.links.twitter}
59
+ target="_blank"
60
+ rel="noopener noreferrer"
61
+ class={cn(
62
+ 'inline-flex items-center justify-center rounded-md p-2',
63
+ 'text-foreground-muted hover:text-foreground hover:bg-secondary',
64
+ 'transition-colors'
65
+ )}
66
+ aria-label="Twitter"
67
+ >
68
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
69
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
70
+ </svg>
71
+ </a>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- Navigation Links -->
76
+ <div>
77
+ <h3 class="text-sm font-semibold text-foreground">{t('nav.home')}</h3>
78
+ <ul class="mt-4 space-y-3">
79
+ <li>
80
+ <a href={getLocalizedPath('home', locale)} class="text-sm text-foreground-muted hover:text-foreground transition-colors">
81
+ {t('nav.home')}
82
+ </a>
83
+ </li>
84
+ <li>
85
+ <a href={getLocalizedPath('blog', locale)} class="text-sm text-foreground-muted hover:text-foreground transition-colors">
86
+ {t('nav.blog')}
87
+ </a>
88
+ </li>
89
+ <li>
90
+ <a href="/rss.xml" class="text-sm text-foreground-muted hover:text-foreground transition-colors inline-flex items-center gap-1">
91
+ <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
92
+ <path d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93V10.1z"/>
93
+ </svg>
94
+ RSS Feed
95
+ </a>
96
+ </li>
97
+ </ul>
98
+ </div>
99
+
100
+ <div>
101
+ <h3 class="text-sm font-semibold text-foreground">{t('nav.about')}</h3>
102
+ <ul class="mt-4 space-y-3">
103
+ <li>
104
+ <a href={getLocalizedPath('about', locale)} class="text-sm text-foreground-muted hover:text-foreground transition-colors">
105
+ {t('nav.about')}
106
+ </a>
107
+ </li>
108
+ <li>
109
+ <a href={getLocalizedPath('contact', locale)} class="text-sm text-foreground-muted hover:text-foreground transition-colors">
110
+ {t('nav.contact')}
111
+ </a>
112
+ </li>
113
+ </ul>
114
+ </div>
115
+
116
+ <div>
117
+ <h3 class="text-sm font-semibold text-foreground">{t('footer.links.documentation')}</h3>
118
+ <ul class="mt-4 space-y-3">
119
+ <li>
120
+ <a href={siteConfig.links.github} target="_blank" rel="noopener noreferrer" class="text-sm text-foreground-muted hover:text-foreground transition-colors">
121
+ {t('footer.links.github')}
122
+ </a>
123
+ </li>
124
+ <li>
125
+ <a href={siteConfig.links.twitter} target="_blank" rel="noopener noreferrer" class="text-sm text-foreground-muted hover:text-foreground transition-colors">
126
+ {t('footer.links.twitter')}
127
+ </a>
128
+ </li>
129
+ </ul>
130
+ </div>
131
+ </div>
132
+
133
+ <!-- Bottom -->
134
+ <div class="mt-16 pt-8 border-t border-border flex flex-col sm:flex-row justify-between items-center gap-4">
135
+ <p class="text-sm text-foreground-muted">
136
+ {t('footer.copyright', { year: currentYear })}
137
+ </p>
138
+ <div class="flex items-center gap-4">
139
+ <a href={siteConfig.links.github} target="_blank" rel="noopener noreferrer" class="text-sm text-foreground-muted hover:text-foreground transition-colors inline-flex items-center gap-1">
140
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
141
+ <path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
142
+ </svg>
143
+ {t('footer.links.github')}
144
+ </a>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </footer>
@@ -58,21 +58,31 @@ function buildPathLookup(): Map<string, { routeId: RouteId; locale: Locale }> {
58
58
 
59
59
  const pathLookup = buildPathLookup();
60
60
 
61
+ /**
62
+ * Result from resolving a route from a path
63
+ */
64
+ export type ResolvedRoute = {
65
+ routeId: RouteId;
66
+ locale: Locale;
67
+ /** Extra path segments after the route (e.g., blog post slug) */
68
+ extra?: string;
69
+ };
70
+
61
71
  /**
62
72
  * Resolve route information from a URL path
63
73
  *
64
74
  * @param pathname - The URL pathname (e.g., '/es/sobre-nosotros')
65
- * @returns The route ID and locale, or null if not a known route
75
+ * @returns The route ID, locale, and any extra path segments, or null if not a known route
66
76
  *
67
77
  * @example
68
78
  * resolveRouteFromPath('/about') // → { routeId: 'about', locale: 'en' }
69
79
  * resolveRouteFromPath('/es/sobre-nosotros') // → { routeId: 'about', locale: 'es' }
70
80
  * resolveRouteFromPath('/fr/a-propos') // → { routeId: 'about', locale: 'fr' }
81
+ * resolveRouteFromPath('/blog/my-post') // → { routeId: 'blog', locale: 'en', extra: 'my-post' }
82
+ * resolveRouteFromPath('/es/blog/my-post') // → { routeId: 'blog', locale: 'es', extra: 'my-post' }
71
83
  * resolveRouteFromPath('/unknown') // → null
72
84
  */
73
- export function resolveRouteFromPath(
74
- pathname: string
75
- ): { routeId: RouteId; locale: Locale } | null {
85
+ export function resolveRouteFromPath(pathname: string): ResolvedRoute | null {
76
86
  // Clean the path
77
87
  const cleanPath = pathname.replace(/^\/+|\/+$/g, '');
78
88
  const segments = cleanPath.split('/').filter(Boolean);
@@ -106,6 +116,37 @@ export function resolveRouteFromPath(
106
116
  return { routeId: 'home', locale: segments[0] as Locale };
107
117
  }
108
118
 
119
+ // Handle nested routes like blog posts: /blog/my-post or /es/blog/my-post
120
+ // Check if the first slug segment matches a known route
121
+ if (slugSegments.length > 1) {
122
+ const firstSlug = slugSegments[0];
123
+ const firstSlugLookupKey = `${locale}/${firstSlug}`;
124
+ const parentMatch = pathLookup.get(firstSlugLookupKey);
125
+
126
+ if (parentMatch) {
127
+ // Found a parent route, the rest is extra path info
128
+ const extra = slugSegments.slice(1).join('/');
129
+ return {
130
+ routeId: parentMatch.routeId,
131
+ locale,
132
+ extra,
133
+ };
134
+ }
135
+
136
+ // Also check for translated blog slugs (e.g., /fr/blogue/my-post)
137
+ // We need to find which route this slug belongs to
138
+ for (const [routeId, routeSlugs] of Object.entries(routes)) {
139
+ if (routeSlugs[locale] === firstSlug) {
140
+ const extra = slugSegments.slice(1).join('/');
141
+ return {
142
+ routeId: routeId as RouteId,
143
+ locale,
144
+ extra,
145
+ };
146
+ }
147
+ }
148
+ }
149
+
109
150
  return null;
110
151
  }
111
152
 
@@ -117,16 +158,27 @@ export function resolveRouteFromPath(
117
158
  * @returns The new path in the target locale, or the home page if route unknown
118
159
  *
119
160
  * @example
120
- * switchLocale('/about', 'es') // → '/es/sobre-nosotros'
121
- * switchLocale('/es/sobre-nosotros', 'fr') // → '/fr/a-propos'
122
- * switchLocale('/es/sobre-nosotros', 'en') // → '/about'
123
- * switchLocale('/unknown', 'es') // → '/es' (fallback to home)
161
+ * switchLocale('/about', 'es') // → '/es/sobre-nosotros'
162
+ * switchLocale('/es/sobre-nosotros', 'fr') // → '/fr/a-propos'
163
+ * switchLocale('/es/sobre-nosotros', 'en') // → '/about'
164
+ * switchLocale('/blog/my-post', 'es') // → '/es/blog/my-post'
165
+ * switchLocale('/es/blog/my-post', 'fr') // → '/fr/blogue/my-post'
166
+ * switchLocale('/unknown', 'es') // → '/es' (fallback to home)
124
167
  */
125
168
  export function switchLocale(currentPath: string, targetLocale: Locale): string {
126
169
  const resolved = resolveRouteFromPath(currentPath);
127
170
 
128
171
  if (resolved) {
129
- return getLocalizedPath(resolved.routeId, targetLocale);
172
+ const basePath = getLocalizedPath(resolved.routeId, targetLocale);
173
+
174
+ // If there's extra path info (like blog post slug), append it
175
+ if (resolved.extra) {
176
+ // Ensure we don't double-slash
177
+ const separator = basePath.endsWith('/') ? '' : '/';
178
+ return `${basePath}${separator}${resolved.extra}`;
179
+ }
180
+
181
+ return basePath;
130
182
  }
131
183
 
132
184
  // Fallback: if we can't resolve the route, go to home page for target locale
@@ -182,5 +234,39 @@ export function isRoute(pathname: string, routeId: RouteId): boolean {
182
234
  return resolved?.routeId === routeId;
183
235
  }
184
236
 
237
+ /**
238
+ * Navigation route information
239
+ */
240
+ export type NavRoute = {
241
+ routeId: RouteId;
242
+ label: string;
243
+ order: number;
244
+ };
245
+
246
+ /**
247
+ * Get routes that should appear in navigation, sorted by order
248
+ *
249
+ * @returns Array of navigation routes with their translation keys
250
+ *
251
+ * @example
252
+ * const navRoutes = getNavRoutes();
253
+ * // → [
254
+ * // { routeId: 'components', label: 'nav.components', order: 1 },
255
+ * // { routeId: 'blog', label: 'nav.blog', order: 2 },
256
+ * // { routeId: 'about', label: 'nav.about', order: 3 },
257
+ * // { routeId: 'contact', label: 'nav.contact', order: 4 },
258
+ * // ]
259
+ */
260
+ export function getNavRoutes(): NavRoute[] {
261
+ return Object.entries(routes)
262
+ .filter(([_, route]) => route.nav?.show === true)
263
+ .map(([routeId, route]) => ({
264
+ routeId: routeId as RouteId,
265
+ label: route.nav!.label,
266
+ order: route.nav!.order,
267
+ }))
268
+ .sort((a, b) => a.order - b.order);
269
+ }
270
+
185
271
  // Re-export route types for convenience
186
272
  export { type RouteId, isValidRouteId } from './routes';
@@ -85,7 +85,7 @@ export function getTranslations(locale: Locale) {
85
85
  export { locales, defaultLocale, type Locale } from './config';
86
86
 
87
87
  // Re-export routes and helpers for translated URLs
88
- export { routes, type RouteId, routeIds, isValidRouteId } from './routes';
88
+ export { routes, type RouteId, routeIds, isValidRouteId, type NavConfig } from './routes';
89
89
  export {
90
90
  getLocalizedPath,
91
91
  resolveRouteFromPath,
@@ -93,4 +93,7 @@ export {
93
93
  getRouteTranslations,
94
94
  getRouteSlug,
95
95
  isRoute,
96
+ getNavRoutes,
97
+ type NavRoute,
98
+ type ResolvedRoute,
96
99
  } from './helpers';
@@ -13,16 +13,31 @@
13
13
 
14
14
  import type { Locale } from './config';
15
15
 
16
+ /**
17
+ * Navigation configuration for a route
18
+ */
19
+ export type NavConfig = {
20
+ /** Whether to show this route in the navbar */
21
+ show: boolean;
22
+ /** Sort order in navigation (lower = first) */
23
+ order: number;
24
+ /** Translation key for the navigation label (e.g., 'nav.about') */
25
+ label: string;
26
+ };
27
+
16
28
  /**
17
29
  * A route definition maps each locale to its URL slug
30
+ * Optionally includes navigation configuration
18
31
  */
19
- export type RouteDefinition = Record<Locale, string>;
32
+ export type RouteDefinition = Record<Locale, string> & {
33
+ nav?: NavConfig;
34
+ };
20
35
 
21
36
  /**
22
37
  * Route definitions for all static pages
23
38
  *
24
39
  * Keys are internal route IDs (use these in LocalizedLink and getLocalizedPath)
25
- * Values are the URL slugs for each locale
40
+ * Values are the URL slugs for each locale, plus optional nav config
26
41
  *
27
42
  * Rules:
28
43
  * - Use lowercase letters and hyphens only (no underscores, no special chars)
@@ -30,18 +45,33 @@ export type RouteDefinition = Record<Locale, string>;
30
45
  * - Do not include leading or trailing slashes
31
46
  */
32
47
  export const routes = {
33
- // Home page (root)
34
- home: { en: '', es: '', fr: '' },
48
+ // Home page (root) - not shown in nav (logo links there)
49
+ home: {
50
+ en: '', es: '', fr: '',
51
+ nav: { show: false, order: 0, label: 'nav.home' },
52
+ },
35
53
 
36
54
  // Static pages
37
- about: { en: 'about', es: 'sobre-nosotros', fr: 'a-propos' },
38
- contact: { en: 'contact', es: 'contacto', fr: 'contact' },
55
+ about: {
56
+ en: 'about', es: 'sobre-nosotros', fr: 'a-propos',
57
+ nav: { show: true, order: 3, label: 'nav.about' },
58
+ },
59
+ contact: {
60
+ en: 'contact', es: 'contacto', fr: 'contact',
61
+ nav: { show: true, order: 4, label: 'nav.contact' },
62
+ },
39
63
 
40
64
  // Blog section
41
- blog: { en: 'blog', es: 'blog', fr: 'blogue' },
65
+ blog: {
66
+ en: 'blog', es: 'blog', fr: 'blogue',
67
+ nav: { show: true, order: 2, label: 'nav.blog' },
68
+ },
42
69
 
43
70
  // Components showcase
44
- components: { en: 'components', es: 'componentes', fr: 'composants' },
71
+ components: {
72
+ en: 'components', es: 'componentes', fr: 'composants',
73
+ nav: { show: true, order: 1, label: 'nav.components' },
74
+ },
45
75
  } as const satisfies Record<string, RouteDefinition>;
46
76
 
47
77
  /**
@@ -60,3 +90,6 @@ export const routeIds = Object.keys(routes) as RouteId[];
60
90
  export function isValidRouteId(id: string): id is RouteId {
61
91
  return id in routes;
62
92
  }
93
+
94
+ // Export types
95
+ export type { NavConfig };
@@ -0,0 +1,64 @@
1
+ ---
2
+ /**
3
+ * PageLayout (i18n-aware)
4
+ * Layout wrapper for standard content pages (non-landing)
5
+ * Uses Navbar (with LanguageSwitcher) + Footer for consistent navigation
6
+ *
7
+ * This layout differs from LandingLayout in that it:
8
+ * - Uses the standard Footer instead of LandingFooter (no CTA sections)
9
+ * - Adds proper top padding for fixed navbar
10
+ * - Is intended for content pages like About, Contact, etc.
11
+ */
12
+ import BaseLayout from './BaseLayout.astro';
13
+ import Navbar from '@/components/landing/Navbar.astro';
14
+ import Footer from '@/components/layout/Footer.astro';
15
+ import { type Locale, defaultLocale } from '@/i18n/config';
16
+ import type { RouteId } from '@/i18n/routes';
17
+
18
+ interface Props {
19
+ title?: string;
20
+ description?: string;
21
+ image?: string;
22
+ imageAlt?: string;
23
+ noindex?: boolean;
24
+ nofollow?: boolean;
25
+ includeOrgSchema?: boolean;
26
+ lang?: Locale;
27
+ /**
28
+ * Route ID for generating proper hreflang tags with translated URLs.
29
+ */
30
+ routeId?: RouteId;
31
+ }
32
+
33
+ const {
34
+ title,
35
+ description,
36
+ image,
37
+ imageAlt,
38
+ noindex = false,
39
+ nofollow = false,
40
+ includeOrgSchema = false,
41
+ lang = defaultLocale,
42
+ routeId,
43
+ } = Astro.props;
44
+ ---
45
+
46
+ <BaseLayout
47
+ title={title}
48
+ description={description}
49
+ image={image}
50
+ imageAlt={imageAlt}
51
+ noindex={noindex}
52
+ nofollow={nofollow}
53
+ includeOrgSchema={includeOrgSchema}
54
+ lang={lang}
55
+ routeId={routeId}
56
+ >
57
+ <Navbar slot="header" />
58
+
59
+ <main class="min-h-screen pt-16">
60
+ <slot />
61
+ </main>
62
+
63
+ <Footer slot="footer" locale={lang} />
64
+ </BaseLayout>