create-lacspace-app 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +907 -90
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -43,9 +43,12 @@ var pkgJson = (ctx) => JSON.stringify({
43
43
  "@lacspace/ui": "^1.0.0",
44
44
  "@lacspace/form": "^1.0.0",
45
45
  "@lacspace/validate": "^1.0.0",
46
- // React Kit: dark/light theming (no-flash) + essential hooks.
46
+ // React Kit: dark/light theming (no-flash) + essential hooks + global
47
+ // state (announcement bar, mobile nav) + data fetching (live stats).
47
48
  "@lacspace/theme": "^1.0.1",
48
49
  "@lacspace/hooks": "^1.0.0",
50
+ "@lacspace/store": "^1.0.0",
51
+ "@lacspace/query": "^1.0.0",
49
52
  // The blog & docs templates render Markdown with @lacspace/markdown.
50
53
  ...ctx.template.key === "blog" || ctx.template.key === "docs" ? { "@lacspace/markdown": "^1.0.0" } : {}
51
54
  },
@@ -165,6 +168,12 @@ body {
165
168
  }
166
169
  .gradient-bg { background: linear-gradient(120deg, var(--accent-from), var(--accent-to)); }
167
170
 
171
+ /* opaque theme background (for sticky chrome & chips) */
172
+ .bg-app { background: var(--bg); }
173
+
174
+ /* indeterminate progress bar (used by the "under development" pages) */
175
+ @keyframes loadbar { 0% { transform: translateX(-120%); } 100% { transform: translateX(340%); } }
176
+
168
177
  /* soft entrance for content */
169
178
  @keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
170
179
  main > section, main > * { animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; }
@@ -201,12 +210,23 @@ export const site = defineSite({
201
210
  ogImage: "/og", // \u2728 auto social-share images \u2014 see app/og/route.tsx
202
211
  });
203
212
  `;
204
- var layout = (ctx) => `import type { Metadata } from "next";
213
+ var layout = (ctx) => {
214
+ const isDash = ctx.template.key === "dashboard";
215
+ const imports = isDash ? "" : `import { AnnouncementBar } from "@/components/announcement-bar";
216
+ import { SiteHeader } from "@/components/site-header";
217
+ import { SiteFooter } from "@/components/site-footer";
218
+ `;
219
+ const open = isDash ? "" : `<AnnouncementBar />
220
+ <SiteHeader />
221
+ `;
222
+ const close = isDash ? "" : `
223
+ <SiteFooter />`;
224
+ return `import type { Metadata } from "next";
205
225
  import { Inter } from "next/font/google";
206
226
  import { ThemeProvider } from "@lacspace/theme";
207
227
  import { site } from "@/lib/site";
208
228
  import { CommandMenu } from "@/components/command-menu";
209
- import "./globals.css";
229
+ ${imports}import "./globals.css";
210
230
 
211
231
  const inter = Inter({ subsets: ["latin"], display: "swap" });
212
232
 
@@ -220,7 +240,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
220
240
  <ThemeProvider defaultTheme="dark">
221
241
  {/* \u2728 Press \u2318K / Ctrl-K anywhere \u2014 powered by @lacspace/ui */}
222
242
  <CommandMenu />
223
- {children}
243
+ ${open}{children}${close}
224
244
  </ThemeProvider>
225
245
  <script
226
246
  type="application/ld+json"
@@ -231,6 +251,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
231
251
  );
232
252
  }
233
253
  `;
254
+ };
234
255
  var ogRoute = (ctx) => `import { ImageResponse } from "next/og";
235
256
  import { ogCard } from "@lacspace/og";
236
257
  import { site } from "@/lib/site";
@@ -301,6 +322,9 @@ boring-but-essential stuff already done. Here's what's in the box.
301
322
  - **\u2728 Dynamic OG images** \u2014 every page auto-generates a social-share card at \`/og\` (\`@lacspace/og\`, auto-fitting titles). Share a link and see.
302
323
  - **\u2728 A working contact form** \u2014 \`/contact\` is live, typed, validated and spam-protected (honeypot + timing) via \`@lacspace/form\` + \`@lacspace/validate\`. Just point it at your inbox.
303
324
  - **\u2728 A \u2318K command palette** \u2014 press \`\u2318K\` / \`Ctrl-K\` anywhere, powered by \`@lacspace/ui\`. Also try \`<Reveal>\`, \`<Counter>\`, \`<GradientText>\`, \`<TiltCard>\`, \`<Marquee>\`, \`<Typewriter>\`.
325
+ - **\u2728 Dark / light / system theme** \u2014 a no-flash toggle in the header via \`@lacspace/theme\` + \`@lacspace/hooks\`. The whole template is theme-aware.
326
+ - **\u2728 Multiple pages + auto header & footer** \u2014 the nav and a full footer are generated from one page map. The highest-value page for your template ships with **real content** (e.g. a pricing table, a shop with a working cart, a menu, a settings panel). Every other link still resolves to a real, branded page \u2014 ones you haven't filled in yet show a friendly **"under development"** screen instead of a 404. Add content in \`app/<route>/page.tsx\`.
327
+ - **\u2728 Global state + data fetching** \u2014 a dismissible announcement bar and the mobile menu use \`@lacspace/store\` (with \`persist\`); the home page's live "By the numbers" strip fetches \`/api/stats\` with \`@lacspace/query\` (shared cache, revalidate-on-focus).
304
328
  - **\u2728 Per-page SEO** \u2014 see \`app/about\` & \`app/contact\`: one \`site.meta()\` call gives each route its own title, canonical & OG image.
305
329
  - **\u2728 An SEO CI gate** \u2014 \`.github/workflows/seo.yml\` audits every page on each push and fails below grade A, so SEO can never regress.
306
330
  - **Security headers** \u2014 HSTS, CSP, X-Frame-Options and more, via \`next.config.mjs\`.
@@ -378,25 +402,11 @@ Open [http://localhost:3000](http://localhost:3000). Edit \`app/page.tsx\` and \
378
402
 
379
403
  Built with [Lacspace](https://lacspace.com/packages).
380
404
  `;
381
- var NAV = (name, links) => `<header className="sticky top-0 z-40 backdrop-blur border-b border-hairline">
382
- <nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
383
- <span className="text-lg font-black gradient-text">${name}</span>
384
- <div className="flex items-center gap-6">
385
- <div className="hidden gap-8 text-sm text-muted sm:flex">
386
- ${links.map((l) => `<a href="#${l.toLowerCase()}" className="hover:text-fg">${l}</a>`).join("\n ")}
387
- </div>
388
- <ThemeToggle />
389
- </div>
390
- </nav>
391
- </header>`;
392
- var FOOTER = (name) => `<footer className="border-t border-hairline py-10 text-center text-sm text-faint">
393
- \xA9 {new Date().getFullYear()} ${name}. Built with{" "}
394
- <a href="https://lacspace.com/packages" className="text-muted hover:text-fg">Lacspace</a>.
395
- </footer>`;
396
405
  function homePage(ctx) {
397
406
  const n = ctx.template.siteName;
407
+ if (ctx.template.key === "dashboard") return dashboardHome(ctx);
398
408
  const shell = (inner) => `import { site } from "@/lib/site";
399
- import { ThemeToggle } from "@/components/theme-toggle";
409
+ import { LiveStats } from "@/components/live-stats";
400
410
 
401
411
  // \u2728 Self-canonical home page \u2014 one line, full SEO (title, canonical, OG, Twitter).
402
412
  export const metadata = site.meta({ title: ${JSON.stringify(n)}, path: "/" });
@@ -405,13 +415,13 @@ export default function Home() {
405
415
  return (
406
416
  <main className="min-h-screen">
407
417
  ${inner}
418
+ ${builtWithSection(ctx)}
408
419
  </main>
409
420
  );
410
421
  }
411
422
  `;
412
423
  if (ctx.template.key === "personal") {
413
- return shell(`${NAV(n, ["Work", "About", "Contact"])}
414
- <section className="mx-auto max-w-3xl px-6 py-28 text-center">
424
+ return shell(`<section className="mx-auto max-w-3xl px-6 py-28 text-center">
415
425
  <p className="mb-4 text-sm font-semibold uppercase tracking-widest text-faint">Portfolio</p>
416
426
  <h1 className="text-5xl font-black leading-tight sm:text-7xl">Hi, I'm <span className="gradient-text">${n}</span>.</h1>
417
427
  <p className="mx-auto mt-6 max-w-xl text-lg text-muted">${ctx.template.siteDescription}</p>
@@ -431,12 +441,10 @@ export default function Home() {
431
441
  </div>
432
442
  ))}
433
443
  </div>
434
- </section>
435
- ${FOOTER(n)}`);
444
+ </section>`);
436
445
  }
437
446
  if (ctx.template.key === "business") {
438
- return shell(`${NAV(n, ["Services", "Work", "Contact"])}
439
- <section className="mx-auto max-w-4xl px-6 py-28 text-center">
447
+ return shell(`<section className="mx-auto max-w-4xl px-6 py-28 text-center">
440
448
  <h1 className="text-5xl font-black leading-tight sm:text-6xl">We build products <span className="gradient-text">that grow businesses</span>.</h1>
441
449
  <p className="mx-auto mt-6 max-w-2xl text-lg text-muted">${ctx.template.siteDescription}</p>
442
450
  <a href="#contact" className="mt-10 inline-block gradient-bg rounded-full px-8 py-3 font-semibold text-black">Start a project</a>
@@ -460,12 +468,10 @@ export default function Home() {
460
468
  <h2 className="text-3xl font-bold">Let's work together</h2>
461
469
  <p className="mt-3 text-muted">Tell us about your project and we'll get back within a day.</p>
462
470
  <a href="mailto:hello@example.com" className="mt-8 inline-block rounded-full border border-hairline px-8 py-3 font-semibold hover:bg-surface">hello@example.com</a>
463
- </section>
464
- ${FOOTER(n)}`);
471
+ </section>`);
465
472
  }
466
473
  if (ctx.template.key === "ecommerce") {
467
- return shell(`${NAV(n, ["Shop", "About", "Cart"])}
468
- <section className="mx-auto max-w-5xl px-6 py-24 text-center">
474
+ return shell(`<section className="mx-auto max-w-5xl px-6 py-24 text-center">
469
475
  <h1 className="text-5xl font-black leading-tight sm:text-6xl"><span className="gradient-text">${n}</span></h1>
470
476
  <p className="mx-auto mt-6 max-w-xl text-lg text-muted">${ctx.template.siteDescription}</p>
471
477
  <a href="#shop" className="mt-10 inline-block gradient-bg rounded-full px-8 py-3 font-semibold text-black">Shop the collection</a>
@@ -487,12 +493,10 @@ export default function Home() {
487
493
  </div>
488
494
  ))}
489
495
  </div>
490
- </section>
491
- ${FOOTER(n)}`);
496
+ </section>`);
492
497
  }
493
498
  if (ctx.template.key === "blog") {
494
- return shell(`${NAV(n, ["Latest", "Topics", "About"])}
495
- <section className="mx-auto max-w-3xl px-6 py-24">
499
+ return shell(`<section className="mx-auto max-w-3xl px-6 py-24">
496
500
  <p className="mb-3 text-sm font-semibold uppercase tracking-widest text-faint">The Journal</p>
497
501
  <h1 className="text-5xl font-black leading-tight sm:text-6xl gradient-text">${n}</h1>
498
502
  <p className="mt-6 text-lg text-muted">${ctx.template.siteDescription}</p>
@@ -516,12 +520,10 @@ export default function Home() {
516
520
  </a>
517
521
  ))}
518
522
  </div>
519
- </section>
520
- ${FOOTER(n)}`);
523
+ </section>`);
521
524
  }
522
525
  if (ctx.template.key === "docs") {
523
- return shell(`${NAV(n, ["Guides", "API", "Examples"])}
524
- <section className="mx-auto max-w-4xl px-6 py-28 text-center">
526
+ return shell(`<section className="mx-auto max-w-4xl px-6 py-28 text-center">
525
527
  <h1 className="text-5xl font-black leading-tight sm:text-6xl"><span className="gradient-text">${n}</span></h1>
526
528
  <p className="mx-auto mt-6 max-w-2xl text-lg text-muted">${ctx.template.siteDescription}</p>
527
529
  <div className="mt-8 inline-flex items-center gap-3 rounded-lg border border-hairline bg-panel px-4 py-3 font-mono text-sm text-muted">
@@ -541,54 +543,10 @@ export default function Home() {
541
543
  </a>
542
544
  ))}
543
545
  </div>
544
- </section>
545
- ${FOOTER(n)}`);
546
- }
547
- if (ctx.template.key === "dashboard") {
548
- return shell(`<div className="flex min-h-screen">
549
- <aside className="hidden w-56 shrink-0 border-r border-hairline p-6 md:block">
550
- <div className="mb-8 flex items-center justify-between">
551
- <span className="text-lg font-black gradient-text">${n}</span>
552
- <ThemeToggle />
553
- </div>
554
- <nav className="space-y-1 text-sm text-muted">
555
- {["Overview", "Analytics", "Customers", "Settings"].map((l) => (
556
- <a key={l} href="#" className="block rounded-lg px-3 py-2 hover:bg-surface hover:text-fg">{l}</a>
557
- ))}
558
- </nav>
559
- </aside>
560
- <main className="flex-1 p-6 md:p-10">
561
- <h1 className="text-2xl font-bold">Overview</h1>
562
- <p className="mt-1 text-muted">${ctx.template.siteDescription}</p>
563
- <div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
564
- {[
565
- { l: "Revenue", v: "$48.2k", c: "+12%" }, { l: "Users", v: "12,480", c: "+3.4%" },
566
- { l: "Orders", v: "1,204", c: "+8%" }, { l: "Churn", v: "1.2%", c: "-0.3%" },
567
- ].map((s) => (
568
- <div key={s.l} className="rounded-2xl border border-hairline bg-surface p-5">
569
- <div className="text-sm text-muted">{s.l}</div>
570
- <div className="mt-1 text-2xl font-bold">{s.v}</div>
571
- <div className="mt-1 text-xs text-emerald-400">{s.c}</div>
572
- </div>
573
- ))}
574
- </div>
575
- <div className="mt-6 rounded-2xl border border-hairline bg-surface p-6">
576
- <div className="mb-4 font-semibold">Recent activity</div>
577
- <div className="space-y-3">
578
- {[1, 2, 3, 4].map((i) => (
579
- <div key={i} className="flex items-center justify-between border-b border-hairline pb-3 text-sm">
580
- <span className="text-muted">Event #{i}</span>
581
- <span className="text-faint">just now</span>
582
- </div>
583
- ))}
584
- </div>
585
- </div>
586
- </main>
587
- </div>`);
546
+ </section>`);
588
547
  }
589
548
  if (ctx.template.key === "restaurant") {
590
- return shell(`${NAV(n, ["Menu", "Story", "Reserve"])}
591
- <section className="mx-auto max-w-4xl px-6 py-28 text-center">
549
+ return shell(`<section className="mx-auto max-w-4xl px-6 py-28 text-center">
592
550
  <p className="mb-4 text-sm font-semibold uppercase tracking-widest text-faint">Est. 2026</p>
593
551
  <h1 className="text-5xl font-black leading-tight sm:text-7xl gradient-text">${n}</h1>
594
552
  <p className="mx-auto mt-6 max-w-xl text-lg text-muted">${ctx.template.siteDescription}</p>
@@ -615,11 +573,9 @@ export default function Home() {
615
573
  <h2 className="text-3xl font-bold">Join us</h2>
616
574
  <p className="mt-3 text-muted">Open Wed\u2013Sun, 5pm till late. Walk-ins welcome; bookings recommended.</p>
617
575
  <a href="tel:+10000000000" className="mt-8 inline-block rounded-full border border-hairline px-8 py-3 font-semibold hover:bg-surface">Call to book</a>
618
- </section>
619
- ${FOOTER(n)}`);
576
+ </section>`);
620
577
  }
621
- return shell(`${NAV(n, ["Features", "Pricing", "Sign in"])}
622
- <section className="mx-auto max-w-4xl px-6 py-28 text-center">
578
+ return shell(`<section className="mx-auto max-w-4xl px-6 py-28 text-center">
623
579
  <p className="mb-4 inline-block rounded-full border border-hairline px-4 py-1 text-xs font-semibold text-muted">New \xB7 v1.0</p>
624
580
  <h1 className="text-5xl font-black leading-tight sm:text-6xl">Ship faster with <span className="gradient-text">${n}</span></h1>
625
581
  <p className="mx-auto mt-6 max-w-2xl text-lg text-muted">${ctx.template.siteDescription}</p>
@@ -648,8 +604,7 @@ export default function Home() {
648
604
  <p className="mt-2 text-5xl font-black gradient-text">$29<span className="text-lg text-faint">/mo</span></p>
649
605
  <a href="#" className="mt-8 inline-block w-full gradient-bg rounded-full px-8 py-3 font-semibold text-black">Get started</a>
650
606
  </div>
651
- </section>
652
- ${FOOTER(n)}`);
607
+ </section>`);
653
608
  }
654
609
  var glyph = (ctx) => (ctx.template.siteName.trim()[0] ?? "A").toUpperCase();
655
610
  var iconTsx = (ctx) => `import { ImageResponse } from "next/og";
@@ -1462,10 +1417,27 @@ function buildFiles(ctx) {
1462
1417
  "components/command-menu.tsx": commandMenu(ctx),
1463
1418
  "components/contact-form.tsx": contactForm(),
1464
1419
  "components/theme-toggle.tsx": themeToggle(),
1420
+ // ✨ React Kit: global state (@lacspace/store) + data fetching (@lacspace/query).
1421
+ "lib/store.ts": uiStore(),
1422
+ "components/under-development.tsx": underDevelopmentComponent(),
1423
+ "components/live-stats.tsx": liveStats(),
1424
+ "app/api/stats/route.ts": statsRoute(),
1465
1425
  ".github/workflows/seo.yml": seoWorkflow(),
1466
1426
  ".env.example": envExample(),
1467
1427
  "WELCOME.md": welcomeMd(ctx)
1468
1428
  };
1429
+ const isDash = ctx.template.key === "dashboard";
1430
+ if (isDash) {
1431
+ files["components/dashboard-shell.tsx"] = dashboardShell(ctx);
1432
+ } else {
1433
+ files["components/site-header.tsx"] = siteHeader(ctx);
1434
+ files["components/site-footer.tsx"] = siteFooter(ctx);
1435
+ files["components/announcement-bar.tsx"] = announcementBar();
1436
+ }
1437
+ for (const page of stubPages(ctx)) {
1438
+ files[`app${page.path}/page.tsx`] = isDash ? dashboardStubPage(page) : underDevPage(page);
1439
+ }
1440
+ Object.assign(files, realPageFiles(ctx));
1469
1441
  if (isBlog) {
1470
1442
  files["lib/posts.ts"] = postsLib();
1471
1443
  files["app/blog/page.tsx"] = blogListPage(ctx);
@@ -1485,6 +1457,851 @@ function buildFiles(ctx) {
1485
1457
  }
1486
1458
  return files;
1487
1459
  }
1460
+ function pagesFor(ctx) {
1461
+ const k = ctx.template.key;
1462
+ if (k === "dashboard") {
1463
+ return [
1464
+ { path: "/analytics", label: "Analytics", nav: true, group: "Product" },
1465
+ { path: "/customers", label: "Customers", nav: true, group: "Product" },
1466
+ { path: "/billing", label: "Billing", nav: true, group: "Product" },
1467
+ { path: "/settings", label: "Settings", nav: true, group: "Product", real: true },
1468
+ { path: "/help", label: "Help", group: "Resources" }
1469
+ ];
1470
+ }
1471
+ const common = [
1472
+ { path: "/about", label: "About", nav: true, group: "Company", real: true },
1473
+ { path: "/contact", label: "Contact", nav: true, group: "Company", real: true },
1474
+ { path: "/careers", label: "Careers", group: "Company" },
1475
+ { path: "/privacy", label: "Privacy", group: "Resources" },
1476
+ { path: "/terms", label: "Terms", group: "Resources" }
1477
+ ];
1478
+ const specific = {
1479
+ personal: [
1480
+ { path: "/work", label: "Work", nav: true, group: "Product", real: true },
1481
+ { path: "/blog", label: "Blog", nav: true, group: "Product" },
1482
+ { path: "/uses", label: "Uses", group: "Resources" }
1483
+ ],
1484
+ business: [
1485
+ { path: "/services", label: "Services", nav: true, group: "Product", real: true },
1486
+ { path: "/work", label: "Work", nav: true, group: "Product" },
1487
+ { path: "/pricing", label: "Pricing", nav: true, group: "Product", real: true },
1488
+ { path: "/faq", label: "FAQ", group: "Resources" }
1489
+ ],
1490
+ ecommerce: [
1491
+ { path: "/shop", label: "Shop", nav: true, group: "Product", real: true },
1492
+ { path: "/collections", label: "Collections", nav: true, group: "Product" },
1493
+ { path: "/cart", label: "Cart", group: "Product", real: true },
1494
+ { path: "/shipping", label: "Shipping", group: "Resources" },
1495
+ { path: "/returns", label: "Returns", group: "Resources" }
1496
+ ],
1497
+ saas: [
1498
+ { path: "/features", label: "Features", nav: true, group: "Product", real: true },
1499
+ { path: "/pricing", label: "Pricing", nav: true, group: "Product", real: true },
1500
+ { path: "/integrations", label: "Integrations", group: "Product" },
1501
+ { path: "/changelog", label: "Changelog", group: "Resources" }
1502
+ ],
1503
+ blog: [
1504
+ { path: "/blog", label: "Articles", nav: true, group: "Product", real: true },
1505
+ { path: "/topics", label: "Topics", nav: true, group: "Product", real: true },
1506
+ { path: "/newsletter", label: "Newsletter", group: "Resources" }
1507
+ ],
1508
+ docs: [
1509
+ { path: "/docs", label: "Docs", nav: true, group: "Product", real: true },
1510
+ { path: "/guides", label: "Guides", nav: true, group: "Product", real: true },
1511
+ { path: "/api-reference", label: "API", nav: true, group: "Product" },
1512
+ { path: "/changelog", label: "Changelog", group: "Resources" }
1513
+ ],
1514
+ restaurant: [
1515
+ { path: "/menu", label: "Menu", nav: true, group: "Product", real: true },
1516
+ { path: "/reservations", label: "Reservations", nav: true, group: "Product" },
1517
+ { path: "/gallery", label: "Gallery", nav: true, group: "Product" },
1518
+ { path: "/events", label: "Private events", group: "Resources" }
1519
+ ]
1520
+ };
1521
+ return [...specific[k] ?? [], ...common];
1522
+ }
1523
+ function stubPages(ctx) {
1524
+ return pagesFor(ctx).filter((p) => !p.real);
1525
+ }
1526
+ var linkLiteral = (pages) => pages.map((l) => `{ href: ${JSON.stringify(l.path)}, label: ${JSON.stringify(l.label)} }`).join(", ");
1527
+ var siteHeader = (ctx) => {
1528
+ const isEcom = ctx.template.key === "ecommerce";
1529
+ const cartImport = isEcom ? `
1530
+ import { useCart } from "@/lib/store";
1531
+ import { useIsMounted } from "@lacspace/hooks";` : "";
1532
+ const cartHook = isEcom ? `
1533
+ const count = useCart((s) => s.items.length);
1534
+ const mounted = useIsMounted();` : "";
1535
+ const cartBtn = isEcom ? `
1536
+ <Link href="/cart" aria-label="Cart" className="relative inline-flex h-9 w-9 items-center justify-center rounded-full border border-hairline text-muted transition hover:text-fg">
1537
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden><circle cx="9" cy="21" r="1" /><circle cx="20" cy="21" r="1" /><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" /></svg>
1538
+ {mounted() && count > 0 ? (
1539
+ <span className="absolute -right-1 -top-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full gradient-bg px-1 text-[10px] font-bold text-black">{count}</span>
1540
+ ) : null}
1541
+ </Link>` : "";
1542
+ return `"use client";
1543
+
1544
+ import Link from "next/link";
1545
+ import { useUI } from "@/lib/store";
1546
+ import { ThemeToggle } from "./theme-toggle";${cartImport}
1547
+
1548
+ const LINKS = [${linkLiteral(pagesFor(ctx).filter((p) => p.nav))}];
1549
+
1550
+ export function SiteHeader() {
1551
+ const open = useUI((s) => s.navOpen);
1552
+ const toggle = useUI((s) => s.toggleNav);
1553
+ const setOpen = useUI((s) => s.setNavOpen);${cartHook}
1554
+ return (
1555
+ <header className="sticky top-0 z-40 border-b border-hairline bg-app/90 backdrop-blur">
1556
+ <nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
1557
+ <Link href="/" className="text-lg font-black gradient-text">${ctx.template.siteName}</Link>
1558
+ <div className="hidden items-center gap-6 md:flex">
1559
+ {LINKS.map((l) => (
1560
+ <Link key={l.href} href={l.href} className="text-sm text-muted transition hover:text-fg">{l.label}</Link>
1561
+ ))}
1562
+ <ThemeToggle />${cartBtn}
1563
+ </div>
1564
+ <div className="flex items-center gap-2 md:hidden">
1565
+ <ThemeToggle />${cartBtn}
1566
+ <button type="button" aria-label="Menu" onClick={toggle} className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-hairline text-muted transition hover:text-fg">
1567
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden><path d="M3 6h18M3 12h18M3 18h18" /></svg>
1568
+ </button>
1569
+ </div>
1570
+ </nav>
1571
+ {open ? (
1572
+ <div className="border-t border-hairline md:hidden">
1573
+ <div className="mx-auto flex max-w-6xl flex-col gap-1 px-6 py-3">
1574
+ {LINKS.map((l) => (
1575
+ <Link key={l.href} href={l.href} onClick={() => setOpen(false)} className="rounded-lg px-3 py-2 text-muted transition hover:bg-surface hover:text-fg">{l.label}</Link>
1576
+ ))}
1577
+ </div>
1578
+ </div>
1579
+ ) : null}
1580
+ </header>
1581
+ );
1582
+ }
1583
+ `;
1584
+ };
1585
+ var siteFooter = (ctx) => {
1586
+ const pages = pagesFor(ctx);
1587
+ const groups = ["Product", "Company", "Resources"].map((title) => ({ title, items: pages.filter((p) => p.group === title) })).filter((g) => g.items.length > 0);
1588
+ const groupsLiteral = groups.map((g) => `{ title: ${JSON.stringify(g.title)}, links: [${linkLiteral(g.items)}] }`).join(", ");
1589
+ return `import Link from "next/link";
1590
+
1591
+ const GROUPS = [${groupsLiteral}];
1592
+
1593
+ export function SiteFooter() {
1594
+ return (
1595
+ <footer className="border-t border-hairline">
1596
+ <div className="mx-auto max-w-6xl px-6 py-14">
1597
+ <div className="grid gap-10 md:grid-cols-4">
1598
+ <div>
1599
+ <div className="text-lg font-black gradient-text">${ctx.template.siteName}</div>
1600
+ <p className="mt-3 max-w-xs text-sm text-muted">${ctx.template.siteDescription}</p>
1601
+ </div>
1602
+ {GROUPS.map((g) => (
1603
+ <div key={g.title}>
1604
+ <div className="text-sm font-semibold">{g.title}</div>
1605
+ <ul className="mt-3 space-y-2 text-sm text-muted">
1606
+ {g.links.map((l) => (
1607
+ <li key={l.href}><Link href={l.href} className="transition hover:text-fg">{l.label}</Link></li>
1608
+ ))}
1609
+ </ul>
1610
+ </div>
1611
+ ))}
1612
+ </div>
1613
+ <div className="mt-12 flex flex-col gap-3 border-t border-hairline pt-6 text-sm text-faint sm:flex-row sm:items-center sm:justify-between">
1614
+ <span>\xA9 {new Date().getFullYear()} ${ctx.template.siteName}. All rights reserved.</span>
1615
+ <span>Built with <a href="https://lacspace.com/packages" className="text-muted transition hover:text-fg">the Lacspace React Kit</a>.</span>
1616
+ </div>
1617
+ </div>
1618
+ </footer>
1619
+ );
1620
+ }
1621
+ `;
1622
+ };
1623
+ var announcementBar = () => `"use client";
1624
+
1625
+ import { useAnnouncement } from "@/lib/store";
1626
+ import { useIsMounted } from "@lacspace/hooks";
1627
+
1628
+ export function AnnouncementBar() {
1629
+ const dismissed = useAnnouncement((s) => s.dismissed);
1630
+ const dismiss = useAnnouncement((s) => s.dismiss);
1631
+ const mounted = useIsMounted();
1632
+
1633
+ // Persisted state is client-only \u2014 wait for mount to avoid a hydration flash.
1634
+ if (!mounted() || dismissed) return null;
1635
+
1636
+ return (
1637
+ <div className="relative gradient-bg px-10 py-2 text-center text-sm font-medium text-black">
1638
+ \u2728 Built with the Lacspace React Kit \u2014{" "}
1639
+ <a href="https://lacspace.com/packages" className="underline underline-offset-2">explore the packages</a>
1640
+ <button type="button" aria-label="Dismiss" onClick={dismiss} className="absolute right-3 top-1/2 -translate-y-1/2 text-lg leading-none text-black/60 hover:text-black">\xD7</button>
1641
+ </div>
1642
+ );
1643
+ }
1644
+ `;
1645
+ var underDevelopmentComponent = () => `"use client";
1646
+
1647
+ import Link from "next/link";
1648
+
1649
+ /** A branded placeholder for pages that don't have content yet. */
1650
+ export function UnderDevelopment({ title = "This page", path }: { title?: string; path?: string }) {
1651
+ return (
1652
+ <div className="mx-auto flex min-h-[70vh] max-w-2xl flex-col items-center justify-center px-6 py-20 text-center">
1653
+ <div className="mb-6 inline-flex h-16 w-16 items-center justify-center rounded-2xl gradient-bg text-3xl">\u{1F6A7}</div>
1654
+ <p className="text-sm font-semibold uppercase tracking-widest text-muted">Under development</p>
1655
+ <h1 className="mt-3 text-4xl font-bold tracking-tight"><span className="gradient-text">{title}</span> is coming soon</h1>
1656
+ <p className="mt-4 max-w-md text-muted">We're putting the finishing touches on this page. Check back shortly \u2014 or head back home in the meantime.</p>
1657
+ <div className="mt-8 h-1.5 w-full max-w-xs overflow-hidden rounded-full bg-surface">
1658
+ <div className="h-full w-1/3 gradient-bg" style={{ animation: "loadbar 1.8s ease-in-out infinite" }} />
1659
+ </div>
1660
+ <div className="mt-10 flex flex-wrap items-center justify-center gap-3">
1661
+ <Link href="/" className="gradient-bg rounded-full px-6 py-3 font-semibold text-black">Back home</Link>
1662
+ <Link href="/contact" className="rounded-full border border-hairline px-6 py-3 font-semibold transition hover:bg-surface">Get in touch</Link>
1663
+ </div>
1664
+ {path ? (
1665
+ <p className="mt-8 text-xs text-faint">Add your content in <code className="rounded bg-surface px-1.5 py-0.5">app{path}/page.tsx</code>.</p>
1666
+ ) : null}
1667
+ </div>
1668
+ );
1669
+ }
1670
+ `;
1671
+ var underDevPage = (page) => `import type { Metadata } from "next";
1672
+ import { site } from "@/lib/site";
1673
+ import { UnderDevelopment } from "@/components/under-development";
1674
+
1675
+ export const metadata: Metadata = site.meta({ title: ${JSON.stringify(page.label)}, path: ${JSON.stringify(page.path)}, description: ${JSON.stringify(page.label + " \u2014 coming soon. We're building this page.")} });
1676
+
1677
+ export default function Page() {
1678
+ return (
1679
+ <main className="min-h-screen">
1680
+ <UnderDevelopment title={${JSON.stringify(page.label)}} path={${JSON.stringify(page.path)}} />
1681
+ </main>
1682
+ );
1683
+ }
1684
+ `;
1685
+ var dashboardStubPage = (page) => `import type { Metadata } from "next";
1686
+ import { site } from "@/lib/site";
1687
+ import { DashboardShell } from "@/components/dashboard-shell";
1688
+ import { UnderDevelopment } from "@/components/under-development";
1689
+
1690
+ export const metadata: Metadata = site.meta({ title: ${JSON.stringify(page.label)}, path: ${JSON.stringify(page.path)}, description: ${JSON.stringify(page.label + " \u2014 coming soon.")} });
1691
+
1692
+ export default function Page() {
1693
+ return (
1694
+ <DashboardShell title={${JSON.stringify(page.label)}}>
1695
+ <UnderDevelopment title={${JSON.stringify(page.label)}} path={${JSON.stringify(page.path)}} />
1696
+ </DashboardShell>
1697
+ );
1698
+ }
1699
+ `;
1700
+ var uiStore = () => `import { create, persist } from "@lacspace/store";
1701
+
1702
+ /** Ephemeral UI state \u2014 e.g. the mobile nav. Not persisted. */
1703
+ interface UIState {
1704
+ navOpen: boolean;
1705
+ setNavOpen: (open: boolean) => void;
1706
+ toggleNav: () => void;
1707
+ }
1708
+ export const useUI = create<UIState>((set) => ({
1709
+ navOpen: false,
1710
+ setNavOpen: (navOpen) => set({ navOpen }),
1711
+ toggleNav: () => set((s) => ({ navOpen: !s.navOpen })),
1712
+ }));
1713
+
1714
+ /** Whether the announcement bar was dismissed \u2014 persisted to localStorage. */
1715
+ interface AnnouncementState {
1716
+ dismissed: boolean;
1717
+ dismiss: () => void;
1718
+ }
1719
+ export const useAnnouncement = create<AnnouncementState>(
1720
+ persist(
1721
+ (set) => ({
1722
+ dismissed: false,
1723
+ dismiss: () => set({ dismissed: true }),
1724
+ }),
1725
+ { name: "announcement" },
1726
+ ),
1727
+ );
1728
+
1729
+ /** A tiny shopping cart \u2014 persisted to localStorage. */
1730
+ export interface CartItem { id: string; name: string; price: number; }
1731
+ interface CartState {
1732
+ items: CartItem[];
1733
+ add: (item: CartItem) => void;
1734
+ remove: (id: string) => void;
1735
+ clear: () => void;
1736
+ }
1737
+ export const useCart = create<CartState>(
1738
+ persist(
1739
+ (set) => ({
1740
+ items: [],
1741
+ add: (item) => set((s) => ({ items: [...s.items, item] })),
1742
+ remove: (id) => set((s) => {
1743
+ const i = s.items.findIndex((x) => x.id === id);
1744
+ if (i === -1) return {};
1745
+ const items = s.items.slice();
1746
+ items.splice(i, 1);
1747
+ return { items };
1748
+ }),
1749
+ clear: () => set({ items: [] }),
1750
+ }),
1751
+ { name: "cart" },
1752
+ ),
1753
+ );
1754
+ `;
1755
+ var statsRoute = () => `import { NextResponse } from "next/server";
1756
+
1757
+ // Demo endpoint powering <LiveStats/> (@lacspace/query). Swap in your real data.
1758
+ export const dynamic = "force-dynamic";
1759
+
1760
+ export function GET() {
1761
+ const jitter = (n: number) => n + Math.floor(Math.random() * n * 0.04);
1762
+ return NextResponse.json({
1763
+ users: jitter(12480),
1764
+ uptime: 99.98,
1765
+ requests: jitter(1_840_000),
1766
+ });
1767
+ }
1768
+ `;
1769
+ var liveStats = () => `"use client";
1770
+
1771
+ import { useQuery } from "@lacspace/query";
1772
+
1773
+ interface Stats { users: number; uptime: number; requests: number; }
1774
+
1775
+ export function LiveStats() {
1776
+ // Shared cache, de-duped requests, and revalidation on window focus.
1777
+ const { data, isLoading } = useQuery("stats", async () => {
1778
+ const res = await fetch("/api/stats");
1779
+ if (!res.ok) throw new Error("Failed to load stats");
1780
+ return (await res.json()) as Stats;
1781
+ });
1782
+
1783
+ const items = [
1784
+ { label: "Active users", value: data ? data.users.toLocaleString() : "\u2014" },
1785
+ { label: "Uptime", value: data ? data.uptime + "%" : "\u2014" },
1786
+ { label: "Requests / mo", value: data ? new Intl.NumberFormat("en", { notation: "compact" }).format(data.requests) : "\u2014" },
1787
+ ];
1788
+
1789
+ return (
1790
+ <div className="grid gap-4 sm:grid-cols-3">
1791
+ {items.map((s) => (
1792
+ <div key={s.label} className="rounded-2xl border border-hairline bg-app p-6 text-center">
1793
+ <div className={"text-3xl font-bold tabular-nums " + (isLoading ? "animate-pulse text-muted" : "")}>{s.value}</div>
1794
+ <div className="mt-1 text-sm text-muted">{s.label}</div>
1795
+ </div>
1796
+ ))}
1797
+ </div>
1798
+ );
1799
+ }
1800
+ `;
1801
+ var builtWithSection = (ctx) => {
1802
+ const count = pagesFor(ctx).length + 1;
1803
+ return `<section className="mx-auto max-w-6xl px-6 py-20">
1804
+ <div className="rounded-3xl border border-hairline bg-surface p-8 sm:p-12">
1805
+ <p className="text-sm font-semibold uppercase tracking-widest text-muted">Live \xB7 @lacspace/query</p>
1806
+ <h2 className="mt-3 text-3xl font-bold">By the numbers</h2>
1807
+ <p className="mt-2 max-w-xl text-muted">Fetched from an internal API route and revalidated on window focus \u2014 a tiny demo of the data layer wired into this starter.</p>
1808
+ <div className="mt-8"><LiveStats /></div>
1809
+ <div className="mt-12 border-t border-hairline pt-8">
1810
+ <p className="text-sm font-semibold uppercase tracking-widest text-muted">Built with the Lacspace React Kit</p>
1811
+ <div className="mt-4 flex flex-wrap gap-2">
1812
+ {["@lacspace/theme", "@lacspace/hooks", "@lacspace/store", "@lacspace/query", "@lacspace/ui", "@lacspace/seo", "@lacspace/og", "@lacspace/form"].map((p) => (
1813
+ <span key={p} className="rounded-full border border-hairline bg-app px-3 py-1 font-mono text-xs text-muted">{p}</span>
1814
+ ))}
1815
+ </div>
1816
+ <p className="mt-5 max-w-2xl text-sm text-muted">Dark mode with no flash, a \u2318K palette, SEO + dynamic OG images, a validated contact form, and ${count}+ pages wired up \u2014 all scaffolded, all yours. <a href="https://lacspace.com/packages" className="text-fg underline underline-offset-4">Explore the packages \u2192</a></p>
1817
+ </div>
1818
+ </div>
1819
+ </section>`;
1820
+ };
1821
+ var dashboardShell = (ctx) => {
1822
+ const nav = [{ path: "/", label: "Overview" }, ...pagesFor(ctx).filter((p) => p.nav)];
1823
+ return `import Link from "next/link";
1824
+ import type { ReactNode } from "react";
1825
+ import { ThemeToggle } from "./theme-toggle";
1826
+
1827
+ const NAV = [${linkLiteral(nav)}];
1828
+
1829
+ export function DashboardShell({ title, subtitle, children }: { title: string; subtitle?: string; children: ReactNode }) {
1830
+ return (
1831
+ <div className="flex min-h-screen">
1832
+ <aside className="hidden w-56 shrink-0 border-r border-hairline p-6 md:block">
1833
+ <div className="mb-8 flex items-center justify-between">
1834
+ <Link href="/" className="text-lg font-black gradient-text">${ctx.template.siteName}</Link>
1835
+ <ThemeToggle />
1836
+ </div>
1837
+ <nav className="space-y-1 text-sm text-muted">
1838
+ {NAV.map((l) => (
1839
+ <Link key={l.href} href={l.href} className="block rounded-lg px-3 py-2 transition hover:bg-surface hover:text-fg">{l.label}</Link>
1840
+ ))}
1841
+ </nav>
1842
+ </aside>
1843
+ <main className="flex-1 p-6 md:p-10">
1844
+ <h1 className="text-2xl font-bold">{title}</h1>
1845
+ {subtitle ? <p className="mt-1 text-muted">{subtitle}</p> : null}
1846
+ <div className="mt-8">{children}</div>
1847
+ </main>
1848
+ </div>
1849
+ );
1850
+ }
1851
+ `;
1852
+ };
1853
+ var dashboardHome = (ctx) => `import { site } from "@/lib/site";
1854
+ import { DashboardShell } from "@/components/dashboard-shell";
1855
+ import { LiveStats } from "@/components/live-stats";
1856
+
1857
+ export const metadata = site.meta({ title: "Overview", path: "/" });
1858
+
1859
+ export default function Home() {
1860
+ return (
1861
+ <DashboardShell title="Overview" subtitle=${JSON.stringify(ctx.template.siteDescription)}>
1862
+ <LiveStats />
1863
+ <div className="mt-6 rounded-2xl border border-hairline bg-surface p-6">
1864
+ <div className="mb-4 font-semibold">Recent activity</div>
1865
+ <div className="space-y-3">
1866
+ {[1, 2, 3, 4].map((i) => (
1867
+ <div key={i} className="flex items-center justify-between border-b border-hairline pb-3 text-sm">
1868
+ <span className="text-muted">Event #{i}</span>
1869
+ <span className="text-faint">just now</span>
1870
+ </div>
1871
+ ))}
1872
+ </div>
1873
+ </div>
1874
+ </DashboardShell>
1875
+ );
1876
+ }
1877
+ `;
1878
+ var pageFile = (o) => `import type { Metadata } from "next";
1879
+ import Link from "next/link";
1880
+ import { site } from "@/lib/site";
1881
+
1882
+ export const metadata: Metadata = site.meta({ title: ${JSON.stringify(o.title)}, path: ${JSON.stringify(o.path)}, description: ${JSON.stringify(o.description)} });
1883
+
1884
+ export default function Page() {
1885
+ return (
1886
+ <main className="min-h-screen">
1887
+ ${o.body}
1888
+ </main>
1889
+ );
1890
+ }
1891
+ `;
1892
+ var pricingPage = (ctx) => pageFile({
1893
+ title: "Pricing",
1894
+ path: "/pricing",
1895
+ description: `Simple, transparent pricing for ${ctx.template.siteName}. Start free and upgrade anytime.`,
1896
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
1897
+ <p className="text-sm font-semibold uppercase tracking-widest text-faint">Pricing</p>
1898
+ <h1 className="mt-3 text-4xl font-bold sm:text-5xl">Simple, transparent <span className="gradient-text">pricing</span></h1>
1899
+ <p className="mx-auto mt-4 max-w-xl text-lg text-muted">Start free. Upgrade when you're ready. Cancel anytime.</p>
1900
+ </section>
1901
+ <section className="mx-auto max-w-6xl px-6 pb-24">
1902
+ <div className="grid gap-6 md:grid-cols-3">
1903
+ {[
1904
+ { name: "Starter", price: "$0", period: "/mo", tagline: "For side projects", features: ["1 project", "Community support", "Basic analytics"], cta: "Get started", highlight: false },
1905
+ { name: "Pro", price: "$29", period: "/mo", tagline: "For growing teams", features: ["Unlimited projects", "Priority support", "Advanced analytics", "Custom domain"], cta: "Start free trial", highlight: true },
1906
+ { name: "Scale", price: "Custom", period: "", tagline: "For organizations", features: ["SSO & SAML", "Dedicated support", "SLA & audit logs", "Guided onboarding"], cta: "Contact sales", highlight: false },
1907
+ ].map((tier) => (
1908
+ <div key={tier.name} className={"flex flex-col rounded-3xl border p-8 " + (tier.highlight ? "border-transparent bg-surface ring-2 ring-[color:var(--accent-to)]" : "border-hairline bg-surface")}>
1909
+ {tier.highlight ? <span className="mb-4 inline-block w-fit rounded-full gradient-bg px-3 py-1 text-xs font-bold text-black">Most popular</span> : null}
1910
+ <h2 className="text-lg font-semibold">{tier.name}</h2>
1911
+ <p className="mt-1 text-sm text-muted">{tier.tagline}</p>
1912
+ <div className="mt-6 flex items-baseline gap-1">
1913
+ <span className="text-4xl font-black">{tier.price}</span>
1914
+ <span className="text-muted">{tier.period}</span>
1915
+ </div>
1916
+ <ul className="mt-6 flex-1 space-y-3 text-sm">
1917
+ {tier.features.map((f) => (
1918
+ <li key={f} className="flex items-center gap-2 text-muted"><span className="text-[color:var(--accent-to)]">\u2713</span> {f}</li>
1919
+ ))}
1920
+ </ul>
1921
+ <Link href="/contact" className={"mt-8 rounded-full px-6 py-3 text-center font-semibold transition " + (tier.highlight ? "gradient-bg text-black" : "border border-hairline hover:bg-app")}>{tier.cta}</Link>
1922
+ </div>
1923
+ ))}
1924
+ </div>
1925
+ <p className="mt-10 text-center text-sm text-faint">All plans include SSL, unlimited bandwidth and a 14-day money-back guarantee.</p>
1926
+ </section>`
1927
+ });
1928
+ var servicesPage = (ctx) => pageFile({
1929
+ title: "Services",
1930
+ path: "/services",
1931
+ description: `What ${ctx.template.siteName} can do for you \u2014 from strategy to launch.`,
1932
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
1933
+ <p className="text-sm font-semibold uppercase tracking-widest text-faint">Services</p>
1934
+ <h1 className="mt-3 text-4xl font-bold sm:text-5xl">What we <span className="gradient-text">do</span></h1>
1935
+ <p className="mx-auto mt-4 max-w-xl text-lg text-muted">End-to-end help \u2014 from the first sketch to a product your customers love.</p>
1936
+ </section>
1937
+ <section className="mx-auto max-w-6xl px-6 pb-16">
1938
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
1939
+ {[
1940
+ { icon: "\u{1F3AF}", title: "Strategy", desc: "Positioning, roadmaps and the plan to get there." },
1941
+ { icon: "\u{1F3A8}", title: "Design", desc: "Brand, UX and interfaces people enjoy using." },
1942
+ { icon: "\u{1F6E0}\uFE0F", title: "Development", desc: "Fast, accessible, maintainable web and mobile apps." },
1943
+ { icon: "\u{1F680}", title: "Launch", desc: "Ship with confidence \u2014 SEO, analytics and monitoring." },
1944
+ { icon: "\u{1F4C8}", title: "Growth", desc: "Experiments and optimization that move the numbers." },
1945
+ { icon: "\u{1F91D}", title: "Support", desc: "Ongoing care so your product keeps getting better." },
1946
+ ].map((s) => (
1947
+ <div key={s.title} className="rounded-2xl border border-hairline bg-surface p-6 transition hover:-translate-y-1">
1948
+ <div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl gradient-bg text-2xl">{s.icon}</div>
1949
+ <h3 className="font-semibold">{s.title}</h3>
1950
+ <p className="mt-1 text-sm text-muted">{s.desc}</p>
1951
+ </div>
1952
+ ))}
1953
+ </div>
1954
+ </section>
1955
+ <section className="mx-auto max-w-3xl px-6 pb-24 text-center">
1956
+ <h2 className="text-2xl font-bold">Have a project in mind?</h2>
1957
+ <Link href="/contact" className="mt-6 inline-block rounded-full gradient-bg px-8 py-3 font-semibold text-black">Start a conversation</Link>
1958
+ </section>`
1959
+ });
1960
+ var featuresPage = (ctx) => pageFile({
1961
+ title: "Features",
1962
+ path: "/features",
1963
+ description: `Everything ${ctx.template.siteName} gives your team, in one place.`,
1964
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
1965
+ <p className="text-sm font-semibold uppercase tracking-widest text-faint">Features</p>
1966
+ <h1 className="mt-3 text-4xl font-bold sm:text-5xl">Built to <span className="gradient-text">ship faster</span></h1>
1967
+ <p className="mx-auto mt-4 max-w-xl text-lg text-muted">A focused set of features that do the heavy lifting so your team can move.</p>
1968
+ </section>
1969
+ <section className="mx-auto max-w-6xl px-6 pb-24">
1970
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
1971
+ {[
1972
+ { icon: "\u26A1", title: "Fast by default", desc: "Edge-rendered and cached \u2014 instant everywhere." },
1973
+ { icon: "\u{1F512}", title: "Secure", desc: "Hardened headers, auth and audit logs out of the box." },
1974
+ { icon: "\u{1F4CA}", title: "Analytics", desc: "Understand usage without bolting on a dozen tools." },
1975
+ { icon: "\u{1F50C}", title: "Integrations", desc: "Connect the tools you already use in a click." },
1976
+ { icon: "\u{1F9E9}", title: "Extensible", desc: "A clean API and webhooks for anything custom." },
1977
+ { icon: "\u{1F317}", title: "Delightful UX", desc: "Dark mode, a \u2318K palette and thoughtful details." },
1978
+ ].map((f) => (
1979
+ <div key={f.title} className="rounded-2xl border border-hairline bg-surface p-6 transition hover:-translate-y-1">
1980
+ <div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl gradient-bg text-2xl">{f.icon}</div>
1981
+ <h3 className="font-semibold">{f.title}</h3>
1982
+ <p className="mt-1 text-sm text-muted">{f.desc}</p>
1983
+ </div>
1984
+ ))}
1985
+ </div>
1986
+ <div className="mt-12 text-center">
1987
+ <Link href="/pricing" className="inline-block rounded-full gradient-bg px-8 py-3 font-semibold text-black">See pricing</Link>
1988
+ </div>
1989
+ </section>`
1990
+ });
1991
+ var workPage = (ctx) => pageFile({
1992
+ title: "Work",
1993
+ path: "/work",
1994
+ description: `Selected projects by ${ctx.template.siteName}.`,
1995
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24">
1996
+ <p className="text-sm font-semibold uppercase tracking-widest text-faint">Work</p>
1997
+ <h1 className="mt-3 text-4xl font-bold sm:text-5xl">Selected <span className="gradient-text">work</span></h1>
1998
+ <p className="mt-4 text-lg text-muted">A few things I've designed and built recently.</p>
1999
+ </section>
2000
+ <section className="mx-auto max-w-6xl px-6 pb-24">
2001
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
2002
+ {[
2003
+ { title: "Aurora", tag: "Product design", year: "2026" },
2004
+ { title: "Northwind", tag: "Web app", year: "2025" },
2005
+ { title: "Lumen", tag: "Branding", year: "2025" },
2006
+ { title: "Harbor", tag: "Mobile app", year: "2024" },
2007
+ { title: "Cadence", tag: "Design system", year: "2024" },
2008
+ { title: "Meadow", tag: "Marketing site", year: "2023" },
2009
+ ].map((p) => (
2010
+ <div key={p.title} className="group rounded-2xl border border-hairline bg-surface p-6 transition hover:-translate-y-1">
2011
+ <div className="mb-4 aspect-video rounded-xl gradient-bg opacity-80 transition group-hover:opacity-100" />
2012
+ <div className="flex items-center justify-between">
2013
+ <h3 className="font-semibold">{p.title}</h3>
2014
+ <span className="text-xs text-faint">{p.year}</span>
2015
+ </div>
2016
+ <p className="mt-1 text-sm text-muted">{p.tag}</p>
2017
+ </div>
2018
+ ))}
2019
+ </div>
2020
+ </section>`
2021
+ });
2022
+ var menuPage = (ctx) => pageFile({
2023
+ title: "Menu",
2024
+ path: "/menu",
2025
+ description: `The menu at ${ctx.template.siteName} \u2014 seasonal plates and natural wine.`,
2026
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
2027
+ <p className="text-sm font-semibold uppercase tracking-widest text-faint">Menu</p>
2028
+ <h1 className="mt-3 text-4xl font-bold sm:text-5xl gradient-text">Tonight's menu</h1>
2029
+ <p className="mx-auto mt-4 max-w-xl text-lg text-muted">Seasonal, ingredient-led and always changing. Here's what we're serving now.</p>
2030
+ </section>
2031
+ <section className="mx-auto max-w-3xl px-6 pb-24">
2032
+ {[
2033
+ { section: "Starters", items: [ { n: "Charred leeks, hazelnut", p: "$14" }, { n: "Burrata, heirloom tomato", p: "$16" }, { n: "Wood-fired sourdough", p: "$7" } ] },
2034
+ { section: "Mains", items: [ { n: "Handmade tagliatelle", p: "$22" }, { n: "Wood-fired trout", p: "$28" }, { n: "Dry-aged sirloin", p: "$34" } ] },
2035
+ { section: "Dessert", items: [ { n: "Olive oil cake", p: "$11" }, { n: "Dark chocolate tart", p: "$12" } ] },
2036
+ ].map((group) => (
2037
+ <div key={group.section} className="mb-12">
2038
+ <h2 className="mb-6 text-2xl font-bold">{group.section}</h2>
2039
+ <div className="space-y-4">
2040
+ {group.items.map((d) => (
2041
+ <div key={d.n} className="flex items-baseline justify-between gap-4 border-b border-hairline pb-3">
2042
+ <span className="font-medium">{d.n}</span>
2043
+ <span className="shrink-0 gradient-text font-bold">{d.p}</span>
2044
+ </div>
2045
+ ))}
2046
+ </div>
2047
+ </div>
2048
+ ))}
2049
+ <div className="text-center">
2050
+ <Link href="/reservations" className="inline-block rounded-full gradient-bg px-8 py-3 font-semibold text-black">Book a table</Link>
2051
+ </div>
2052
+ </section>`
2053
+ });
2054
+ var cardGridPage = (o) => pageFile({
2055
+ title: o.title,
2056
+ path: o.path,
2057
+ description: o.lead,
2058
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24">
2059
+ <p className="text-sm font-semibold uppercase tracking-widest text-faint">${o.eyebrow}</p>
2060
+ <h1 className="mt-3 text-4xl font-bold sm:text-5xl">${o.heading}</h1>
2061
+ <p className="mt-4 text-lg text-muted">${o.lead}</p>
2062
+ </section>
2063
+ <section className="mx-auto max-w-6xl px-6 pb-24">
2064
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
2065
+ {${JSON.stringify(o.items)}.map((it) => (
2066
+ <div key={it.title} className="rounded-2xl border border-hairline bg-surface p-6 transition hover:-translate-y-1 hover:border-[color:var(--accent-to)]">
2067
+ <h3 className="font-semibold">{it.title}</h3>
2068
+ <p className="mt-2 text-sm text-muted">{it.desc}</p>
2069
+ </div>
2070
+ ))}
2071
+ </div>
2072
+ </section>`
2073
+ });
2074
+ var topicsPage = (ctx) => cardGridPage({
2075
+ title: "Topics",
2076
+ path: "/topics",
2077
+ eyebrow: "Topics",
2078
+ heading: "Browse by topic",
2079
+ lead: "Find what you care about \u2014 from deep dives to quick notes.",
2080
+ items: [
2081
+ { title: "Engineering", desc: "How we build and the decisions behind it." },
2082
+ { title: "Design", desc: "Craft, systems and the details that matter." },
2083
+ { title: "Product", desc: "What we're shipping and why." },
2084
+ { title: "Culture", desc: "How we work and what we believe." },
2085
+ { title: "Tutorials", desc: "Step-by-step, hands-on guides." },
2086
+ { title: "Announcements", desc: "News and releases." }
2087
+ ]
2088
+ });
2089
+ var guidesPage = (ctx) => cardGridPage({
2090
+ title: "Guides",
2091
+ path: "/guides",
2092
+ eyebrow: "Guides",
2093
+ heading: "Guides & tutorials",
2094
+ lead: "Task-focused walkthroughs to get you productive fast.",
2095
+ items: [
2096
+ { title: "Getting started", desc: "Install, configure and run your first build." },
2097
+ { title: "Authentication", desc: "Add sign-in, sessions and protected routes." },
2098
+ { title: "Deployment", desc: "Ship to production the right way." },
2099
+ { title: "Best practices", desc: "Patterns that scale as your app grows." },
2100
+ { title: "Migrations", desc: "Upgrade safely between versions." },
2101
+ { title: "Troubleshooting", desc: "Fix the most common issues quickly." }
2102
+ ]
2103
+ });
2104
+ var shopPage = (ctx) => `import type { Metadata } from "next";
2105
+ import { site } from "@/lib/site";
2106
+ import { ProductGrid } from "@/components/product-grid";
2107
+
2108
+ export const metadata: Metadata = site.meta({ title: "Shop", path: "/shop", description: ${JSON.stringify(`Shop everything at ${ctx.template.siteName}.`)} });
2109
+
2110
+ export default function Page() {
2111
+ return (
2112
+ <main className="mx-auto min-h-screen max-w-6xl px-6 py-24">
2113
+ <p className="text-sm font-semibold uppercase tracking-widest text-faint">Shop</p>
2114
+ <h1 className="mt-3 text-4xl font-bold">Everything in the <span className="gradient-text">store</span></h1>
2115
+ <p className="mt-4 max-w-xl text-muted">Add items to your bag \u2014 it's saved locally with @lacspace/store, so it survives a refresh.</p>
2116
+ <div className="mt-12"><ProductGrid /></div>
2117
+ </main>
2118
+ );
2119
+ }
2120
+ `;
2121
+ var cartPage = () => `import type { Metadata } from "next";
2122
+ import { site } from "@/lib/site";
2123
+ import { CartView } from "@/components/cart-view";
2124
+
2125
+ export const metadata: Metadata = site.meta({ title: "Your bag", path: "/cart", description: "Review the items in your bag." });
2126
+
2127
+ export default function Page() {
2128
+ return (
2129
+ <main className="mx-auto min-h-screen max-w-6xl px-6 py-24">
2130
+ <h1 className="text-4xl font-bold">Your <span className="gradient-text">bag</span></h1>
2131
+ <div className="mt-12"><CartView /></div>
2132
+ </main>
2133
+ );
2134
+ }
2135
+ `;
2136
+ var productGrid = () => `"use client";
2137
+
2138
+ import { useCart } from "@/lib/store";
2139
+
2140
+ const PRODUCTS = [
2141
+ { id: "p1", name: "Aurora Mug", price: 18, emoji: "\u2615" },
2142
+ { id: "p2", name: "Field Notebook", price: 12, emoji: "\u{1F4D3}" },
2143
+ { id: "p3", name: "Canvas Tote", price: 24, emoji: "\u{1F45C}" },
2144
+ { id: "p4", name: "Enamel Pin", price: 8, emoji: "\u{1F4CC}" },
2145
+ { id: "p5", name: "Cotton Tee", price: 28, emoji: "\u{1F455}" },
2146
+ { id: "p6", name: "Sticker Pack", price: 6, emoji: "\u2728" },
2147
+ { id: "p7", name: "Ceramic Vase", price: 42, emoji: "\u{1F3FA}" },
2148
+ { id: "p8", name: "Linen Apron", price: 34, emoji: "\u{1F9F5}" },
2149
+ ];
2150
+
2151
+ export function ProductGrid() {
2152
+ const add = useCart((s) => s.add);
2153
+ return (
2154
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
2155
+ {PRODUCTS.map((p) => (
2156
+ <div key={p.id} className="flex flex-col rounded-2xl border border-hairline bg-surface p-5">
2157
+ <div className="mb-4 flex aspect-square items-center justify-center rounded-xl gradient-bg text-5xl">{p.emoji}</div>
2158
+ <h3 className="font-semibold">{p.name}</h3>
2159
+ <div className="mt-1 text-muted">{"$" + p.price.toFixed(2)}</div>
2160
+ <button type="button" onClick={() => add({ id: p.id, name: p.name, price: p.price })} className="mt-4 rounded-full gradient-bg px-4 py-2 text-sm font-semibold text-black transition hover:opacity-90">Add to bag</button>
2161
+ </div>
2162
+ ))}
2163
+ </div>
2164
+ );
2165
+ }
2166
+ `;
2167
+ var cartView = () => `"use client";
2168
+
2169
+ import Link from "next/link";
2170
+ import { useCart } from "@/lib/store";
2171
+ import { useIsMounted } from "@lacspace/hooks";
2172
+
2173
+ export function CartView() {
2174
+ const items = useCart((s) => s.items);
2175
+ const remove = useCart((s) => s.remove);
2176
+ const clear = useCart((s) => s.clear);
2177
+ const mounted = useIsMounted();
2178
+
2179
+ if (!mounted()) return <div className="py-16 text-center text-muted">Loading your bag\u2026</div>;
2180
+
2181
+ if (items.length === 0) {
2182
+ return (
2183
+ <div className="mx-auto max-w-md py-16 text-center">
2184
+ <p className="text-lg text-muted">Your bag is empty.</p>
2185
+ <Link href="/shop" className="mt-6 inline-block rounded-full gradient-bg px-6 py-3 font-semibold text-black">Browse the shop</Link>
2186
+ </div>
2187
+ );
2188
+ }
2189
+
2190
+ const total = items.reduce((sum, i) => sum + i.price, 0);
2191
+
2192
+ return (
2193
+ <div className="mx-auto max-w-2xl">
2194
+ <ul className="divide-y divide-hairline">
2195
+ {items.map((i, idx) => (
2196
+ <li key={i.id + "-" + idx} className="flex items-center justify-between py-4">
2197
+ <span className="font-medium">{i.name}</span>
2198
+ <span className="flex items-center gap-4">
2199
+ <span className="tabular-nums text-muted">{"$" + i.price.toFixed(2)}</span>
2200
+ <button type="button" onClick={() => remove(i.id)} aria-label="Remove" className="text-faint transition hover:text-fg">\u2715</button>
2201
+ </span>
2202
+ </li>
2203
+ ))}
2204
+ </ul>
2205
+ <div className="mt-6 flex items-center justify-between border-t border-hairline pt-6">
2206
+ <button type="button" onClick={clear} className="text-sm text-muted transition hover:text-fg">Clear bag</button>
2207
+ <span className="text-lg font-bold">Total {"$" + total.toFixed(2)}</span>
2208
+ </div>
2209
+ <button type="button" className="mt-8 w-full rounded-full gradient-bg px-6 py-3 font-semibold text-black">Checkout</button>
2210
+ </div>
2211
+ );
2212
+ }
2213
+ `;
2214
+ var settingsPage = (ctx) => `import type { Metadata } from "next";
2215
+ import { site } from "@/lib/site";
2216
+ import { DashboardShell } from "@/components/dashboard-shell";
2217
+ import { SettingsPanel } from "@/components/settings-panel";
2218
+
2219
+ export const metadata: Metadata = site.meta({ title: "Settings", path: "/settings", description: ${JSON.stringify(`Manage your ${ctx.template.siteName} preferences.`)} });
2220
+
2221
+ export default function Page() {
2222
+ return (
2223
+ <DashboardShell title="Settings" subtitle="Manage your preferences.">
2224
+ <SettingsPanel />
2225
+ </DashboardShell>
2226
+ );
2227
+ }
2228
+ `;
2229
+ var settingsPanel = () => `"use client";
2230
+
2231
+ import { useLocalStorage } from "@lacspace/hooks";
2232
+ import { useTheme } from "@lacspace/theme";
2233
+
2234
+ export function SettingsPanel() {
2235
+ // Persisted to the browser with @lacspace/hooks.
2236
+ const [emailNotifs, setEmailNotifs] = useLocalStorage("settings:emailNotifs", true);
2237
+ const [weekly, setWeekly] = useLocalStorage("settings:weeklyDigest", false);
2238
+ const { theme, setTheme } = useTheme();
2239
+
2240
+ const toggle = (on: boolean) =>
2241
+ "relative h-6 w-11 rounded-full transition " + (on ? "gradient-bg" : "bg-panel");
2242
+ const knob = (on: boolean) =>
2243
+ "absolute top-0.5 h-5 w-5 rounded-full bg-white transition-all " + (on ? "left-[1.375rem]" : "left-0.5");
2244
+
2245
+ return (
2246
+ <div className="max-w-xl space-y-8">
2247
+ <section className="rounded-2xl border border-hairline bg-surface p-6">
2248
+ <h2 className="font-semibold">Notifications</h2>
2249
+ <p className="mt-1 text-sm text-muted">Saved to your browser with @lacspace/hooks (useLocalStorage).</p>
2250
+ <div className="mt-5 space-y-4">
2251
+ <label className="flex items-center justify-between gap-4">
2252
+ <span className="text-sm">Email notifications</span>
2253
+ <button type="button" role="switch" aria-checked={emailNotifs} onClick={() => setEmailNotifs((v) => !v)} className={toggle(emailNotifs)}>
2254
+ <span className={knob(emailNotifs)} />
2255
+ </button>
2256
+ </label>
2257
+ <label className="flex items-center justify-between gap-4">
2258
+ <span className="text-sm">Weekly digest</span>
2259
+ <button type="button" role="switch" aria-checked={weekly} onClick={() => setWeekly((v) => !v)} className={toggle(weekly)}>
2260
+ <span className={knob(weekly)} />
2261
+ </button>
2262
+ </label>
2263
+ </div>
2264
+ </section>
2265
+ <section className="rounded-2xl border border-hairline bg-surface p-6">
2266
+ <h2 className="font-semibold">Appearance</h2>
2267
+ <p className="mt-1 text-sm text-muted">Theme via @lacspace/theme.</p>
2268
+ <div className="mt-4 inline-flex rounded-full border border-hairline p-1">
2269
+ {["light", "dark", "system"].map((t) => (
2270
+ <button key={t} type="button" onClick={() => setTheme(t)} className={"rounded-full px-4 py-1.5 text-sm capitalize transition " + (theme === t ? "gradient-bg font-semibold text-black" : "text-muted hover:text-fg")}>{t}</button>
2271
+ ))}
2272
+ </div>
2273
+ </section>
2274
+ </div>
2275
+ );
2276
+ }
2277
+ `;
2278
+ function realPageFiles(ctx) {
2279
+ const k = ctx.template.key;
2280
+ const f = {};
2281
+ if (k === "personal") f["app/work/page.tsx"] = workPage(ctx);
2282
+ if (k === "business") {
2283
+ f["app/services/page.tsx"] = servicesPage(ctx);
2284
+ f["app/pricing/page.tsx"] = pricingPage(ctx);
2285
+ }
2286
+ if (k === "saas") {
2287
+ f["app/features/page.tsx"] = featuresPage(ctx);
2288
+ f["app/pricing/page.tsx"] = pricingPage(ctx);
2289
+ }
2290
+ if (k === "ecommerce") {
2291
+ f["app/shop/page.tsx"] = shopPage(ctx);
2292
+ f["app/cart/page.tsx"] = cartPage();
2293
+ f["components/product-grid.tsx"] = productGrid();
2294
+ f["components/cart-view.tsx"] = cartView();
2295
+ }
2296
+ if (k === "blog") f["app/topics/page.tsx"] = topicsPage();
2297
+ if (k === "docs") f["app/guides/page.tsx"] = guidesPage();
2298
+ if (k === "restaurant") f["app/menu/page.tsx"] = menuPage(ctx);
2299
+ if (k === "dashboard") {
2300
+ f["app/settings/page.tsx"] = settingsPage(ctx);
2301
+ f["components/settings-panel.tsx"] = settingsPanel();
2302
+ }
2303
+ return f;
2304
+ }
1488
2305
  function parseArgs(list) {
1489
2306
  const a = { yes: false, install: true, git: true, pm: "npm", help: false };
1490
2307
  for (let i = 0; i < list.length; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lacspace-app",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Scaffold a beautiful, production-ready Next.js app from a Lacspace template — portfolio, business, e-commerce, SaaS, blog, docs, dashboard or restaurant — pre-wired with SEO, security headers, sitemap and robots. Like create-next-app, but you start gorgeous.",
5
5
  "type": "module",
6
6
  "bin": {