create-lacspace-app 1.8.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 +488 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -323,7 +323,7 @@ boring-but-essential stuff already done. Here's what's in the box.
323
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.
324
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
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. Every link resolves to a real, branded page \u2014 pages you haven't filled in yet show a friendly **"under development"** screen instead of a 404. Add content in \`app/<route>/page.tsx\`.
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
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).
328
328
  - **\u2728 Per-page SEO** \u2014 see \`app/about\` & \`app/contact\`: one \`site.meta()\` call gives each route its own title, canonical & OG image.
329
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.
@@ -1437,6 +1437,7 @@ function buildFiles(ctx) {
1437
1437
  for (const page of stubPages(ctx)) {
1438
1438
  files[`app${page.path}/page.tsx`] = isDash ? dashboardStubPage(page) : underDevPage(page);
1439
1439
  }
1440
+ Object.assign(files, realPageFiles(ctx));
1440
1441
  if (isBlog) {
1441
1442
  files["lib/posts.ts"] = postsLib();
1442
1443
  files["app/blog/page.tsx"] = blogListPage(ctx);
@@ -1463,7 +1464,7 @@ function pagesFor(ctx) {
1463
1464
  { path: "/analytics", label: "Analytics", nav: true, group: "Product" },
1464
1465
  { path: "/customers", label: "Customers", nav: true, group: "Product" },
1465
1466
  { path: "/billing", label: "Billing", nav: true, group: "Product" },
1466
- { path: "/settings", label: "Settings", nav: true, group: "Product" },
1467
+ { path: "/settings", label: "Settings", nav: true, group: "Product", real: true },
1467
1468
  { path: "/help", label: "Help", group: "Resources" }
1468
1469
  ];
1469
1470
  }
@@ -1476,41 +1477,42 @@ function pagesFor(ctx) {
1476
1477
  ];
1477
1478
  const specific = {
1478
1479
  personal: [
1479
- { path: "/work", label: "Work", nav: true, group: "Product" },
1480
+ { path: "/work", label: "Work", nav: true, group: "Product", real: true },
1480
1481
  { path: "/blog", label: "Blog", nav: true, group: "Product" },
1481
1482
  { path: "/uses", label: "Uses", group: "Resources" }
1482
1483
  ],
1483
1484
  business: [
1484
- { path: "/services", label: "Services", nav: true, group: "Product" },
1485
+ { path: "/services", label: "Services", nav: true, group: "Product", real: true },
1485
1486
  { path: "/work", label: "Work", nav: true, group: "Product" },
1486
- { path: "/pricing", label: "Pricing", nav: true, group: "Product" },
1487
+ { path: "/pricing", label: "Pricing", nav: true, group: "Product", real: true },
1487
1488
  { path: "/faq", label: "FAQ", group: "Resources" }
1488
1489
  ],
1489
1490
  ecommerce: [
1490
- { path: "/shop", label: "Shop", nav: true, group: "Product" },
1491
+ { path: "/shop", label: "Shop", nav: true, group: "Product", real: true },
1491
1492
  { path: "/collections", label: "Collections", nav: true, group: "Product" },
1493
+ { path: "/cart", label: "Cart", group: "Product", real: true },
1492
1494
  { path: "/shipping", label: "Shipping", group: "Resources" },
1493
1495
  { path: "/returns", label: "Returns", group: "Resources" }
1494
1496
  ],
1495
1497
  saas: [
1496
- { path: "/features", label: "Features", nav: true, group: "Product" },
1497
- { path: "/pricing", label: "Pricing", nav: true, group: "Product" },
1498
+ { path: "/features", label: "Features", nav: true, group: "Product", real: true },
1499
+ { path: "/pricing", label: "Pricing", nav: true, group: "Product", real: true },
1498
1500
  { path: "/integrations", label: "Integrations", group: "Product" },
1499
1501
  { path: "/changelog", label: "Changelog", group: "Resources" }
1500
1502
  ],
1501
1503
  blog: [
1502
1504
  { path: "/blog", label: "Articles", nav: true, group: "Product", real: true },
1503
- { path: "/topics", label: "Topics", nav: true, group: "Product" },
1505
+ { path: "/topics", label: "Topics", nav: true, group: "Product", real: true },
1504
1506
  { path: "/newsletter", label: "Newsletter", group: "Resources" }
1505
1507
  ],
1506
1508
  docs: [
1507
1509
  { path: "/docs", label: "Docs", nav: true, group: "Product", real: true },
1508
- { path: "/guides", label: "Guides", nav: true, group: "Product" },
1510
+ { path: "/guides", label: "Guides", nav: true, group: "Product", real: true },
1509
1511
  { path: "/api-reference", label: "API", nav: true, group: "Product" },
1510
1512
  { path: "/changelog", label: "Changelog", group: "Resources" }
1511
1513
  ],
1512
1514
  restaurant: [
1513
- { path: "/menu", label: "Menu", nav: true, group: "Product" },
1515
+ { path: "/menu", label: "Menu", nav: true, group: "Product", real: true },
1514
1516
  { path: "/reservations", label: "Reservations", nav: true, group: "Product" },
1515
1517
  { path: "/gallery", label: "Gallery", nav: true, group: "Product" },
1516
1518
  { path: "/events", label: "Private events", group: "Resources" }
@@ -1522,30 +1524,45 @@ function stubPages(ctx) {
1522
1524
  return pagesFor(ctx).filter((p) => !p.real);
1523
1525
  }
1524
1526
  var linkLiteral = (pages) => pages.map((l) => `{ href: ${JSON.stringify(l.path)}, label: ${JSON.stringify(l.label)} }`).join(", ");
1525
- var siteHeader = (ctx) => `"use client";
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";
1526
1543
 
1527
1544
  import Link from "next/link";
1528
1545
  import { useUI } from "@/lib/store";
1529
- import { ThemeToggle } from "./theme-toggle";
1546
+ import { ThemeToggle } from "./theme-toggle";${cartImport}
1530
1547
 
1531
1548
  const LINKS = [${linkLiteral(pagesFor(ctx).filter((p) => p.nav))}];
1532
1549
 
1533
1550
  export function SiteHeader() {
1534
1551
  const open = useUI((s) => s.navOpen);
1535
1552
  const toggle = useUI((s) => s.toggleNav);
1536
- const setOpen = useUI((s) => s.setNavOpen);
1553
+ const setOpen = useUI((s) => s.setNavOpen);${cartHook}
1537
1554
  return (
1538
1555
  <header className="sticky top-0 z-40 border-b border-hairline bg-app/90 backdrop-blur">
1539
1556
  <nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
1540
1557
  <Link href="/" className="text-lg font-black gradient-text">${ctx.template.siteName}</Link>
1541
- <div className="hidden items-center gap-8 md:flex">
1558
+ <div className="hidden items-center gap-6 md:flex">
1542
1559
  {LINKS.map((l) => (
1543
1560
  <Link key={l.href} href={l.href} className="text-sm text-muted transition hover:text-fg">{l.label}</Link>
1544
1561
  ))}
1545
- <ThemeToggle />
1562
+ <ThemeToggle />${cartBtn}
1546
1563
  </div>
1547
1564
  <div className="flex items-center gap-2 md:hidden">
1548
- <ThemeToggle />
1565
+ <ThemeToggle />${cartBtn}
1549
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">
1550
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>
1551
1568
  </button>
@@ -1564,6 +1581,7 @@ export function SiteHeader() {
1564
1581
  );
1565
1582
  }
1566
1583
  `;
1584
+ };
1567
1585
  var siteFooter = (ctx) => {
1568
1586
  const pages = pagesFor(ctx);
1569
1587
  const groups = ["Product", "Company", "Resources"].map((title) => ({ title, items: pages.filter((p) => p.group === title) })).filter((g) => g.items.length > 0);
@@ -1707,6 +1725,32 @@ export const useAnnouncement = create<AnnouncementState>(
1707
1725
  { name: "announcement" },
1708
1726
  ),
1709
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
+ );
1710
1754
  `;
1711
1755
  var statsRoute = () => `import { NextResponse } from "next/server";
1712
1756
 
@@ -1831,6 +1875,433 @@ export default function Home() {
1831
1875
  );
1832
1876
  }
1833
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
+ }
1834
2305
  function parseArgs(list) {
1835
2306
  const a = { yes: false, install: true, git: true, pm: "npm", help: false };
1836
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.8.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": {