create-lacspace-app 1.12.1 → 1.13.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 +515 -51
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -35,22 +35,23 @@ var pkgJson = (ctx) => JSON.stringify({
35
35
  next: "^15.1.0",
36
36
  react: "^19.0.0",
37
37
  "react-dom": "^19.0.0",
38
- "@lacspace/seo": "^1.6.0",
39
- "@lacspace/headers": "^1.1.1",
40
- "@lacspace/robots": "^1.2.0",
41
- "@lacspace/sitemap": "^1.1.0",
42
- "@lacspace/og": "^1.0.0",
43
- "@lacspace/ui": "^1.0.0",
44
- "@lacspace/form": "^1.0.0",
45
- "@lacspace/validate": "^1.0.0",
38
+ "@lacspace/seo": "^1.6.2",
39
+ "@lacspace/headers": "^1.1.2",
40
+ "@lacspace/robots": "^1.2.1",
41
+ "@lacspace/sitemap": "^1.1.1",
42
+ "@lacspace/og": "^1.0.1",
43
+ "@lacspace/ui": "^1.0.1",
44
+ "@lacspace/form": "^1.0.1",
45
+ "@lacspace/validate": "^1.0.1",
46
46
  // React Kit: dark/light theming (no-flash) + essential hooks + global
47
47
  // state (announcement bar, mobile nav) + data fetching (live stats).
48
- "@lacspace/theme": "^1.0.1",
49
- "@lacspace/hooks": "^1.0.0",
50
- "@lacspace/store": "^1.0.0",
51
- "@lacspace/query": "^1.0.0",
52
- // The blog & docs templates render Markdown with @lacspace/markdown.
53
- ...ctx.template.key === "blog" || ctx.template.key === "docs" ? { "@lacspace/markdown": "^1.0.0" } : {}
48
+ "@lacspace/theme": "^1.0.2",
49
+ "@lacspace/hooks": "^1.0.1",
50
+ "@lacspace/store": "^1.1.0",
51
+ "@lacspace/query": "^1.0.1",
52
+ // The blog & docs templates render Markdown with @lacspace/markdown
53
+ // (>=1.0.1 includes the URL-scheme XSS hardening keep the floor there).
54
+ ...ctx.template.key === "blog" || ctx.template.key === "docs" ? { "@lacspace/markdown": "^1.0.1" } : {}
54
55
  },
55
56
  devDependencies: {
56
57
  typescript: "^5.7.0",
@@ -429,6 +430,7 @@ function homePage(ctx) {
429
430
  import { LiveStats } from "@/components/live-stats";
430
431
  import { Aurora } from "@/components/aurora";
431
432
  import { HeroArt } from "@/components/hero-art";
433
+ import { StatBand, Steps, Testimonial } from "@/components/ui";
432
434
 
433
435
  // \u2728 Self-canonical home page \u2014 one line, full SEO (title, canonical, OG, Twitter).
434
436
  export const metadata = site.meta({ title: ${JSON.stringify(n)}, path: "/" });
@@ -441,6 +443,7 @@ export default function Home() {
441
443
  ${inner}
442
444
  ${showcaseSection(ctx)}
443
445
  ${marqueeSection(ctx)}
446
+ ${extraHomeSections(ctx)}
444
447
  ${faqSection(ctx)}
445
448
  ${builtWithSection(ctx)}
446
449
  </main>
@@ -668,32 +671,138 @@ export default function AppleIcon() {
668
671
  );
669
672
  }
670
673
  `;
671
- var aboutPage = (ctx) => `import type { Metadata } from "next";
672
- import Link from "next/link";
673
- import { site } from "@/lib/site";
674
-
675
- // \u2728 Per-page SEO, auto-generated: title template, canonical, Open Graph,
676
- // Twitter card and its OWN dynamic OG image \u2014 all from one line.
677
- export const metadata: Metadata = site.meta({
678
- title: "About",
679
- path: "/about",
680
- description: "Learn more about ${ctx.template.siteName}.",
674
+ var aboutPage = (ctx) => {
675
+ const r = richContent(ctx);
676
+ const n = ctx.template.siteName;
677
+ const story = r.process.map((p, i) => ({ date: ["Then", "Next", "Now", "Ahead"][i] ?? "", title: p.title, desc: p.desc }));
678
+ return pageFile({
679
+ title: "About",
680
+ path: "/about",
681
+ description: `The story, the team and the values behind ${n}.`,
682
+ cta: { title: "Let's build something together", subtitle: "We'd love to hear what you're working on.", label: "Get in touch", href: "/contact" },
683
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24">
684
+ <Breadcrumbs items={[{ label: "Home", href: "/" }, { label: "About" }]} />
685
+ <h1 className="mt-6 text-4xl font-bold sm:text-5xl">About <span className="gradient-text">${n}</span></h1>
686
+ <p className="mt-4 text-lg text-muted">${n} exists to help you ship something you're proud of \u2014 faster, and with less fuss. Here's who we are and what we believe.</p>
687
+ </section>
688
+ <Section className="pt-0"><StatBand stats={${JSON.stringify(r.stats)}} /></Section>
689
+ <Section eyebrow="Our story" title="How we got here" className="pt-0">
690
+ <div className="mx-auto max-w-2xl"><Timeline items={${JSON.stringify(story)}} /></div>
691
+ </Section>
692
+ <Section eyebrow="What we value" title="Principles we won't compromise" className="pt-0">
693
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
694
+ {${JSON.stringify(r.values)}.map((v) => <FeatureCard key={v.title} title={v.title} desc={v.desc} />)}
695
+ </div>
696
+ </Section>
697
+ <Section eyebrow="The team" title="The people behind ${n}" className="pt-0">
698
+ <TeamGrid members={${JSON.stringify(r.team)}} />
699
+ </Section>
700
+ <Section title="Frequently asked" className="pt-0"><FAQ items={${JSON.stringify(r.faqs.slice(0, 4))}} /></Section>`
701
+ });
702
+ };
703
+ var faqPage = (ctx) => {
704
+ const r = richContent(ctx);
705
+ const n = ctx.template.siteName;
706
+ return pageFile({
707
+ title: "FAQ",
708
+ path: "/faq",
709
+ description: `Answers to common questions about ${n}.`,
710
+ cta: { title: "Still have questions?", subtitle: "We're happy to help \u2014 reach out any time.", label: "Contact us", href: "/contact" },
711
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
712
+ <Pill>FAQ</Pill>
713
+ <h1 className="mt-4 text-4xl font-bold sm:text-5xl">Frequently asked <span className="gradient-text">questions</span></h1>
714
+ <p className="mx-auto mt-4 max-w-xl text-lg text-muted">Everything you need to know about ${n}. Can't find an answer? Get in touch.</p>
715
+ </section>
716
+ <Section className="pt-0"><FAQ items={${JSON.stringify(r.faqs)}} /></Section>`
717
+ });
718
+ };
719
+ var integrationsPage = (ctx) => pageFile({
720
+ title: "Integrations",
721
+ path: "/integrations",
722
+ description: `Connect ${ctx.template.siteName} to the tools your team already uses.`,
723
+ cta: { title: "Don't see your tool?", subtitle: "We add integrations every month \u2014 tell us what you need.", label: "Request an integration", href: "/contact" },
724
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
725
+ <Pill>Integrations</Pill>
726
+ <h1 className="mt-4 text-4xl font-bold sm:text-5xl">Works with your <span className="gradient-text">stack</span></h1>
727
+ <p className="mx-auto mt-4 max-w-xl text-lg text-muted">Plug ${ctx.template.siteName} into the tools you already love.</p>
728
+ </section>
729
+ <Section className="pt-0"><LogoCloud label="Popular integrations" names={["Slack", "Notion", "GitHub", "Figma", "Linear", "Stripe", "Zapier", "HubSpot"]} /></Section>
730
+ <Section eyebrow="Categories" title="Everything connects" className="pt-0">
731
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
732
+ {[
733
+ { icon: "\u{1F4AC}", title: "Communication", desc: "Slack, Teams and email \u2014 never miss an update." },
734
+ { icon: "\u{1F4CA}", title: "Analytics", desc: "Pipe events to your warehouse or BI tool." },
735
+ { icon: "\u{1F517}", title: "Automation", desc: "Zapier and webhooks to connect anything." },
736
+ { icon: "\u{1F4B3}", title: "Payments", desc: "Stripe and PayPal, wired and ready." },
737
+ { icon: "\u{1F5C2}\uFE0F", title: "Productivity", desc: "Notion, Linear and Jira in two clicks." },
738
+ { icon: "\u{1F510}", title: "Identity", desc: "SSO, SAML and SCIM for teams." },
739
+ ].map((f) => <FeatureCard key={f.title} icon={f.icon} title={f.title} desc={f.desc} />)}
740
+ </div>
741
+ </Section>`
742
+ });
743
+ var changelogPage = (ctx) => pageFile({
744
+ title: "Changelog",
745
+ path: "/changelog",
746
+ description: `New features and improvements shipped to ${ctx.template.siteName}.`,
747
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
748
+ <Pill>Changelog</Pill>
749
+ <h1 className="mt-4 text-4xl font-bold sm:text-5xl">What&rsquo;s <span className="gradient-text">new</span></h1>
750
+ <p className="mx-auto mt-4 max-w-xl text-lg text-muted">Every improvement we ship to ${ctx.template.siteName}, newest first.</p>
751
+ </section>
752
+ <Section className="pt-0">
753
+ <div className="mx-auto max-w-2xl">
754
+ <Timeline items={[
755
+ { date: "This week", title: "Dark mode everywhere", desc: "A no-flash theme system across every page." },
756
+ { date: "Last week", title: "Faster search", desc: "Command palette results now feel instant." },
757
+ { date: "Earlier", title: "Team roles", desc: "Invite teammates with granular permissions." },
758
+ { date: "Launch", title: "Hello, world \u{1F44B}", desc: "The first public release." },
759
+ ]} />
760
+ </div>
761
+ </Section>`
762
+ });
763
+ var galleryPage = (ctx) => pageFile({
764
+ title: "Gallery",
765
+ path: "/gallery",
766
+ description: `A look inside ${ctx.template.siteName}.`,
767
+ cta: { title: "Come see for yourself", subtitle: "Book a table and taste the difference.", label: "Reserve a table", href: "/reservations" },
768
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
769
+ <Pill>Gallery</Pill>
770
+ <h1 className="mt-4 text-4xl font-bold sm:text-5xl">A look <span className="gradient-text">inside</span></h1>
771
+ <p className="mx-auto mt-4 max-w-xl text-lg text-muted">The room, the plates, the little details that make ${ctx.template.siteName}.</p>
772
+ </section>
773
+ <Section className="pt-0">
774
+ <Gallery items={[
775
+ { emoji: "\u{1F37D}\uFE0F", label: "The dining room" },
776
+ { emoji: "\u{1F958}", label: "Signature dish" },
777
+ { emoji: "\u{1F377}", label: "Cellar" },
778
+ { emoji: "\u{1F468}\u200D\u{1F373}", label: "The kitchen" },
779
+ { emoji: "\u{1F370}", label: "Desserts" },
780
+ { emoji: "\u{1F33F}", label: "Fresh daily" },
781
+ ]} />
782
+ </Section>`
783
+ });
784
+ var usesPage = (ctx) => pageFile({
785
+ title: "Uses",
786
+ path: "/uses",
787
+ description: `The gear, apps and tools ${ctx.template.siteName} uses every day.`,
788
+ body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
789
+ <Pill>Uses</Pill>
790
+ <h1 className="mt-4 text-4xl font-bold sm:text-5xl">What I <span className="gradient-text">use</span></h1>
791
+ <p className="mx-auto mt-4 max-w-xl text-lg text-muted">The gear and software behind the work.</p>
792
+ </section>
793
+ <Section title="Editor & tools" className="pt-0">
794
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
795
+ {[
796
+ { icon: "\u{1F5A5}\uFE0F", title: "VS Code", desc: "My editor of choice, tuned to the bone." },
797
+ { icon: "\u26A1", title: "Next.js", desc: "The React framework behind everything I ship." },
798
+ { icon: "\u{1F3A8}", title: "Figma", desc: "Where every design starts." },
799
+ { icon: "\u2328\uFE0F", title: "Raycast", desc: "Launcher, clipboard history and snippets." },
800
+ { icon: "\u{1F9E0}", title: "Obsidian", desc: "Notes and a second brain." },
801
+ { icon: "\u{1F3A7}", title: "Focus playlist", desc: "Lo-fi, always." },
802
+ ].map((f) => <FeatureCard key={f.title} icon={f.icon} title={f.title} desc={f.desc} />)}
803
+ </div>
804
+ </Section>`
681
805
  });
682
-
683
- export default function AboutPage() {
684
- return (
685
- <main className="mx-auto max-w-3xl px-6 py-28">
686
- <Link href="/" className="text-sm text-muted hover:text-fg">\u2190 Back home</Link>
687
- <h1 className="mt-6 text-4xl font-black sm:text-5xl gradient-text">About</h1>
688
- <p className="mt-6 text-lg leading-relaxed text-muted">
689
- This page already has its own SEO \u2014 a unique title, canonical URL, Open Graph tags and a
690
- generated social image \u2014 from a single <code>site.meta()</code> call. Duplicate this file for
691
- any new route and it just works.
692
- </p>
693
- </main>
694
- );
695
- }
696
- `;
697
806
  var themeToggle = () => `"use client";
698
807
 
699
808
  import { useTheme } from "@lacspace/theme";
@@ -1475,13 +1584,13 @@ function buildFiles(ctx) {
1475
1584
  "WELCOME.md": welcomeMd(ctx)
1476
1585
  };
1477
1586
  const isDash = ctx.template.key === "dashboard";
1587
+ files["components/aurora.tsx"] = aurora();
1478
1588
  if (isDash) {
1479
1589
  files["components/dashboard-shell.tsx"] = dashboardShell(ctx);
1480
1590
  } else {
1481
1591
  files["components/site-header.tsx"] = siteHeader(ctx);
1482
1592
  files["components/site-footer.tsx"] = siteFooter(ctx);
1483
1593
  files["components/announcement-bar.tsx"] = announcementBar();
1484
- files["components/aurora.tsx"] = aurora();
1485
1594
  files["components/hero-art.tsx"] = heroArt(ctx);
1486
1595
  }
1487
1596
  for (const page of stubPages(ctx)) {
@@ -1523,6 +1632,7 @@ function pagesFor(ctx) {
1523
1632
  { path: "/about", label: "About", nav: true, group: "Company", real: true },
1524
1633
  { path: "/contact", label: "Contact", nav: true, group: "Company", real: true },
1525
1634
  { path: "/careers", label: "Careers", group: "Company" },
1635
+ { path: "/faq", label: "FAQ", group: "Resources", real: true },
1526
1636
  { path: "/privacy", label: "Privacy", group: "Resources" },
1527
1637
  { path: "/terms", label: "Terms", group: "Resources" }
1528
1638
  ];
@@ -1530,13 +1640,12 @@ function pagesFor(ctx) {
1530
1640
  personal: [
1531
1641
  { path: "/work", label: "Work", nav: true, group: "Product", real: true },
1532
1642
  { path: "/blog", label: "Blog", nav: true, group: "Product" },
1533
- { path: "/uses", label: "Uses", group: "Resources" }
1643
+ { path: "/uses", label: "Uses", group: "Resources", real: true }
1534
1644
  ],
1535
1645
  business: [
1536
1646
  { path: "/services", label: "Services", nav: true, group: "Product", real: true },
1537
1647
  { path: "/work", label: "Work", nav: true, group: "Product" },
1538
- { path: "/pricing", label: "Pricing", nav: true, group: "Product", real: true },
1539
- { path: "/faq", label: "FAQ", group: "Resources" }
1648
+ { path: "/pricing", label: "Pricing", nav: true, group: "Product", real: true }
1540
1649
  ],
1541
1650
  ecommerce: [
1542
1651
  { path: "/shop", label: "Shop", nav: true, group: "Product", real: true },
@@ -1548,8 +1657,8 @@ function pagesFor(ctx) {
1548
1657
  saas: [
1549
1658
  { path: "/features", label: "Features", nav: true, group: "Product", real: true },
1550
1659
  { path: "/pricing", label: "Pricing", nav: true, group: "Product", real: true },
1551
- { path: "/integrations", label: "Integrations", group: "Product" },
1552
- { path: "/changelog", label: "Changelog", group: "Resources" }
1660
+ { path: "/integrations", label: "Integrations", group: "Product", real: true },
1661
+ { path: "/changelog", label: "Changelog", group: "Resources", real: true }
1553
1662
  ],
1554
1663
  blog: [
1555
1664
  { path: "/blog", label: "Articles", nav: true, group: "Product", real: true },
@@ -1560,12 +1669,12 @@ function pagesFor(ctx) {
1560
1669
  { path: "/docs", label: "Docs", nav: true, group: "Product", real: true },
1561
1670
  { path: "/guides", label: "Guides", nav: true, group: "Product", real: true },
1562
1671
  { path: "/api-reference", label: "API", nav: true, group: "Product" },
1563
- { path: "/changelog", label: "Changelog", group: "Resources" }
1672
+ { path: "/changelog", label: "Changelog", group: "Resources", real: true }
1564
1673
  ],
1565
1674
  restaurant: [
1566
1675
  { path: "/menu", label: "Menu", nav: true, group: "Product", real: true },
1567
1676
  { path: "/reservations", label: "Reservations", nav: true, group: "Product" },
1568
- { path: "/gallery", label: "Gallery", nav: true, group: "Product" },
1677
+ { path: "/gallery", label: "Gallery", nav: true, group: "Product", real: true },
1569
1678
  { path: "/events", label: "Private events", group: "Resources" }
1570
1679
  ]
1571
1680
  };
@@ -1945,7 +2054,7 @@ var pageFile = (o) => `import type { Metadata } from "next";
1945
2054
  import Link from "next/link";
1946
2055
  import { site } from "@/lib/site";
1947
2056
  import { Aurora } from "@/components/aurora";
1948
- import { Section, Pill, StatCard, FeatureCard, Testimonial, Steps, CTABand, Bento, AreaChart, Newsletter } from "@/components/ui";
2057
+ import { Section, Pill, StatCard, FeatureCard, Testimonial, Steps, CTABand, Bento, AreaChart, Newsletter, Badge, Callout, Accordion, FAQ, Tabs, Timeline, PricingTable, LogoCloud, StatBand, TeamGrid, FeatureSplit, Gallery, Rating, Progress, Breadcrumbs, Avatar, AvatarGroup } from "@/components/ui";
1949
2058
 
1950
2059
  export const metadata: Metadata = site.meta({ title: ${JSON.stringify(o.title)}, path: ${JSON.stringify(o.path)}, description: ${JSON.stringify(o.description)} });
1951
2060
 
@@ -2559,6 +2668,244 @@ export function Newsletter({ title = "Stay in the loop", subtitle = "Occasional
2559
2668
  </div>
2560
2669
  );
2561
2670
  }
2671
+ `,
2672
+ "components/ui/badge.tsx": `import type { ReactNode } from "react";
2673
+
2674
+ const tones = {
2675
+ default: "border-hairline bg-surface text-muted",
2676
+ accent: "border-transparent gradient-bg text-black",
2677
+ success: "border-emerald-500/30 bg-emerald-500/10 text-emerald-400",
2678
+ warning: "border-amber-500/30 bg-amber-500/10 text-amber-400",
2679
+ danger: "border-red-500/30 bg-red-500/10 text-red-400",
2680
+ } as const;
2681
+
2682
+ export function Badge({ children, tone = "default", className = "" }: { children: ReactNode; tone?: keyof typeof tones; className?: string }) {
2683
+ return <span className={"inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-semibold " + tones[tone] + " " + className}>{children}</span>;
2684
+ }
2685
+ `,
2686
+ "components/ui/callout.tsx": `import type { ReactNode } from "react";
2687
+
2688
+ const tones = {
2689
+ info: { c: "border-sky-500/30 bg-sky-500/10", i: "\u{1F4A1}" },
2690
+ success: { c: "border-emerald-500/30 bg-emerald-500/10", i: "\u2705" },
2691
+ warning: { c: "border-amber-500/30 bg-amber-500/10", i: "\u26A0\uFE0F" },
2692
+ danger: { c: "border-red-500/30 bg-red-500/10", i: "\u26D4" },
2693
+ } as const;
2694
+
2695
+ export function Callout({ children, title, tone = "info" }: { children: ReactNode; title?: string; tone?: keyof typeof tones }) {
2696
+ const t = tones[tone];
2697
+ return (
2698
+ <div className={"flex gap-3 rounded-2xl border p-4 " + t.c}>
2699
+ <div className="text-lg leading-none">{t.i}</div>
2700
+ <div className="text-sm">
2701
+ {title ? <p className="font-semibold">{title}</p> : null}
2702
+ <div className="text-muted">{children}</div>
2703
+ </div>
2704
+ </div>
2705
+ );
2706
+ }
2707
+ `,
2708
+ "components/ui/accordion.tsx": `export function Accordion({ items }: { items: { q: string; a: string }[] }) {
2709
+ return (
2710
+ <div className="overflow-hidden rounded-2xl border border-hairline bg-surface">
2711
+ {items.map((it, i) => (
2712
+ <details key={it.q} className={"group px-6 " + (i > 0 ? "border-t border-hairline" : "")}>
2713
+ <summary className="flex cursor-pointer list-none items-center justify-between py-4 font-medium">
2714
+ {it.q}
2715
+ <span className="ml-4 text-muted transition group-open:rotate-45">+</span>
2716
+ </summary>
2717
+ <p className="pb-5 text-sm leading-relaxed text-muted">{it.a}</p>
2718
+ </details>
2719
+ ))}
2720
+ </div>
2721
+ );
2722
+ }
2723
+ `,
2724
+ "components/ui/faq.tsx": `export function FAQ({ items, title }: { items: { q: string; a: string }[]; title?: string }) {
2725
+ return (
2726
+ <div className="mx-auto max-w-3xl">
2727
+ {title ? <h2 className="mb-8 text-center text-3xl font-bold sm:text-4xl">{title}</h2> : null}
2728
+ <div className="overflow-hidden rounded-2xl border border-hairline bg-surface">
2729
+ {items.map((it, i) => (
2730
+ <details key={it.q} className={"group px-6 " + (i > 0 ? "border-t border-hairline" : "")}>
2731
+ <summary className="flex cursor-pointer list-none items-center justify-between py-4 font-medium">{it.q}<span className="ml-4 text-muted transition group-open:rotate-45">+</span></summary>
2732
+ <p className="pb-5 text-sm leading-relaxed text-muted">{it.a}</p>
2733
+ </details>
2734
+ ))}
2735
+ </div>
2736
+ </div>
2737
+ );
2738
+ }
2739
+ `,
2740
+ "components/ui/tabs.tsx": `"use client";
2741
+
2742
+ import { useState, type ReactNode } from "react";
2743
+
2744
+ export function Tabs({ tabs }: { tabs: { label: string; content: ReactNode }[] }) {
2745
+ const [i, setI] = useState(0);
2746
+ return (
2747
+ <div>
2748
+ <div className="inline-flex flex-wrap gap-1 rounded-full border border-hairline bg-surface p-1">
2749
+ {tabs.map((t, idx) => (
2750
+ <button key={t.label} onClick={() => setI(idx)} className={"rounded-full px-4 py-2 text-sm font-medium transition " + (i === idx ? "gradient-bg text-black" : "text-muted hover:text-fg")}>{t.label}</button>
2751
+ ))}
2752
+ </div>
2753
+ <div className="mt-6">{tabs[i]?.content}</div>
2754
+ </div>
2755
+ );
2756
+ }
2757
+ `,
2758
+ "components/ui/timeline.tsx": `export function Timeline({ items }: { items: { date?: string; title: string; desc?: string }[] }) {
2759
+ return (
2760
+ <ol className="relative ml-3 border-l border-hairline">
2761
+ {items.map((it) => (
2762
+ <li key={it.title} className="mb-8 ml-6">
2763
+ <span aria-hidden className="absolute -left-[7px] mt-1.5 h-3 w-3 rounded-full gradient-bg" />
2764
+ {it.date ? <p className="text-xs font-semibold uppercase tracking-widest text-faint">{it.date}</p> : null}
2765
+ <h3 className="mt-1 font-semibold">{it.title}</h3>
2766
+ {it.desc ? <p className="mt-1 text-sm text-muted">{it.desc}</p> : null}
2767
+ </li>
2768
+ ))}
2769
+ </ol>
2770
+ );
2771
+ }
2772
+ `,
2773
+ "components/ui/pricing-table.tsx": `import Link from "next/link";
2774
+
2775
+ interface Tier { name: string; price: string; period?: string; features: string[]; ctaLabel?: string; ctaHref?: string; featured?: boolean; }
2776
+
2777
+ export function PricingTable({ tiers }: { tiers: Tier[] }) {
2778
+ return (
2779
+ <div className="grid gap-6 md:grid-cols-3">
2780
+ {tiers.map((t) => (
2781
+ <div key={t.name} className={"relative flex flex-col rounded-3xl border p-8 " + (t.featured ? "border-[color:var(--accent-to)] bg-surface shadow-lg" : "border-hairline bg-surface")}>
2782
+ {t.featured ? <span className="absolute -top-3 left-8 rounded-full gradient-bg px-3 py-1 text-xs font-bold text-black">Most popular</span> : null}
2783
+ <h3 className="font-semibold">{t.name}</h3>
2784
+ <div className="mt-3 flex items-end gap-1"><span className="text-4xl font-bold">{t.price}</span>{t.period ? <span className="mb-1 text-sm text-muted">/{t.period}</span> : null}</div>
2785
+ <ul className="mt-6 flex-1 space-y-2 text-sm text-muted">
2786
+ {t.features.map((f) => <li key={f} className="flex gap-2"><span className="gradient-text">\u2713</span>{f}</li>)}
2787
+ </ul>
2788
+ <Link href={t.ctaHref ?? "/contact"} className={"mt-8 rounded-full px-6 py-3 text-center font-semibold transition " + (t.featured ? "gradient-bg text-black hover:opacity-90" : "border border-hairline hover:bg-app")}>{t.ctaLabel ?? "Choose " + t.name}</Link>
2789
+ </div>
2790
+ ))}
2791
+ </div>
2792
+ );
2793
+ }
2794
+ `,
2795
+ "components/ui/logo-cloud.tsx": `export function LogoCloud({ names, label }: { names: string[]; label?: string }) {
2796
+ return (
2797
+ <div className="text-center">
2798
+ {label ? <p className="text-sm font-semibold uppercase tracking-widest text-faint">{label}</p> : null}
2799
+ <div className="mt-6 flex flex-wrap items-center justify-center gap-x-10 gap-y-4">
2800
+ {names.map((n) => <span key={n} className="text-lg font-bold text-muted opacity-70 transition hover:opacity-100">{n}</span>)}
2801
+ </div>
2802
+ </div>
2803
+ );
2804
+ }
2805
+ `,
2806
+ "components/ui/stat-band.tsx": `export function StatBand({ stats }: { stats: { value: string; label: string }[] }) {
2807
+ return (
2808
+ <div className="grid gap-6 rounded-3xl border border-hairline bg-surface p-8 sm:grid-cols-2 lg:grid-cols-4">
2809
+ {stats.map((s) => (
2810
+ <div key={s.label} className="text-center">
2811
+ <div className="gradient-text text-4xl font-bold tabular-nums">{s.value}</div>
2812
+ <div className="mt-1 text-sm text-muted">{s.label}</div>
2813
+ </div>
2814
+ ))}
2815
+ </div>
2816
+ );
2817
+ }
2818
+ `,
2819
+ "components/ui/team-grid.tsx": `export function TeamGrid({ members }: { members: { name: string; role: string; bio?: string }[] }) {
2820
+ return (
2821
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
2822
+ {members.map((m) => (
2823
+ <div key={m.name} className="rounded-2xl border border-hairline bg-surface p-6 text-center transition hover:-translate-y-1">
2824
+ <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full gradient-bg text-xl font-bold text-black">{m.name.split(" ").map((x) => x[0]).join("").slice(0, 2)}</div>
2825
+ <h3 className="mt-4 font-semibold">{m.name}</h3>
2826
+ <p className="gradient-text text-sm">{m.role}</p>
2827
+ {m.bio ? <p className="mt-2 text-sm text-muted">{m.bio}</p> : null}
2828
+ </div>
2829
+ ))}
2830
+ </div>
2831
+ );
2832
+ }
2833
+ `,
2834
+ "components/ui/feature-split.tsx": `import type { ReactNode } from "react";
2835
+
2836
+ export function FeatureSplit({ eyebrow, title, desc, bullets, media, reverse = false }: { eyebrow?: string; title: string; desc?: string; bullets?: string[]; media?: ReactNode; reverse?: boolean }) {
2837
+ return (
2838
+ <div className={"grid items-center gap-10 md:grid-cols-2 " + (reverse ? "md:[&>*:first-child]:order-2" : "")}>
2839
+ <div>
2840
+ {eyebrow ? <p className="text-sm font-semibold uppercase tracking-widest text-faint">{eyebrow}</p> : null}
2841
+ <h2 className="mt-3 text-3xl font-bold sm:text-4xl">{title}</h2>
2842
+ {desc ? <p className="mt-4 text-lg text-muted">{desc}</p> : null}
2843
+ {bullets ? <ul className="mt-6 space-y-2 text-muted">{bullets.map((b) => <li key={b} className="flex gap-2"><span className="gradient-text">\u2713</span>{b}</li>)}</ul> : null}
2844
+ </div>
2845
+ <div className="relative overflow-hidden rounded-3xl border border-hairline bg-surface p-8">
2846
+ <div aria-hidden className="pointer-events-none absolute -right-10 -top-10 h-40 w-40 rounded-full gradient-bg opacity-30 blur-3xl" />
2847
+ <div className="relative">{media ?? <div className="grid h-48 place-items-center text-5xl">\u2728</div>}</div>
2848
+ </div>
2849
+ </div>
2850
+ );
2851
+ }
2852
+ `,
2853
+ "components/ui/gallery.tsx": `export function Gallery({ items }: { items: { label?: string; emoji?: string }[] }) {
2854
+ return (
2855
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
2856
+ {items.map((it, i) => (
2857
+ <div key={i} className="group relative grid aspect-square place-items-center overflow-hidden rounded-2xl border border-hairline bg-surface">
2858
+ <div aria-hidden className="pointer-events-none absolute inset-0 gradient-bg opacity-10 transition group-hover:opacity-20" />
2859
+ <span className="relative text-4xl">{it.emoji ?? "\u{1F5BC}\uFE0F"}</span>
2860
+ {it.label ? <span className="absolute bottom-2 left-3 text-xs text-muted">{it.label}</span> : null}
2861
+ </div>
2862
+ ))}
2863
+ </div>
2864
+ );
2865
+ }
2866
+ `,
2867
+ "components/ui/rating.tsx": `export function Rating({ value = 5, count }: { value?: number; count?: number }) {
2868
+ return (
2869
+ <div className="inline-flex items-center gap-1 text-amber-400" aria-label={value + " out of 5"}>
2870
+ {[0, 1, 2, 3, 4].map((i) => <span key={i}>{i < Math.round(value) ? "\u2605" : "\u2606"}</span>)}
2871
+ {count ? <span className="ml-1 text-sm text-muted">({count})</span> : null}
2872
+ </div>
2873
+ );
2874
+ }
2875
+ `,
2876
+ "components/ui/progress.tsx": `export function Progress({ value, label }: { value: number; label?: string }) {
2877
+ const v = Math.max(0, Math.min(100, value));
2878
+ return (
2879
+ <div>
2880
+ {label ? <div className="mb-1 flex justify-between text-sm"><span>{label}</span><span className="text-muted">{v}%</span></div> : null}
2881
+ <div className="h-2 overflow-hidden rounded-full bg-app"><div className="h-full gradient-bg" style={{ width: v + "%" }} /></div>
2882
+ </div>
2883
+ );
2884
+ }
2885
+ `,
2886
+ "components/ui/breadcrumbs.tsx": `import Link from "next/link";
2887
+
2888
+ export function Breadcrumbs({ items }: { items: { label: string; href?: string }[] }) {
2889
+ return (
2890
+ <nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-2 text-sm text-muted">
2891
+ {items.map((it, i) => (
2892
+ <span key={it.label} className="flex items-center gap-2">
2893
+ {it.href ? <Link href={it.href} className="hover:text-fg">{it.label}</Link> : <span className="text-fg">{it.label}</span>}
2894
+ {i < items.length - 1 ? <span aria-hidden className="text-faint">/</span> : null}
2895
+ </span>
2896
+ ))}
2897
+ </nav>
2898
+ );
2899
+ }
2900
+ `,
2901
+ "components/ui/avatar.tsx": `export function Avatar({ name, size = 40 }: { name: string; size?: number }) {
2902
+ const initials = name.split(" ").map((x) => x[0]).join("").slice(0, 2).toUpperCase();
2903
+ return <span className="inline-flex items-center justify-center rounded-full gradient-bg font-bold text-black" style={{ width: size, height: size, fontSize: size * 0.4 }}>{initials}</span>;
2904
+ }
2905
+
2906
+ export function AvatarGroup({ names }: { names: string[] }) {
2907
+ return <div className="flex -space-x-2">{names.map((n) => <span key={n} className="rounded-full bg-app p-0.5"><Avatar name={n} size={36} /></span>)}</div>;
2908
+ }
2562
2909
  `,
2563
2910
  "components/ui/index.ts": `export * from "./section";
2564
2911
  export * from "./pill";
@@ -2570,12 +2917,32 @@ export * from "./cta-band";
2570
2917
  export * from "./bento";
2571
2918
  export * from "./area-chart";
2572
2919
  export * from "./newsletter";
2920
+ export * from "./badge";
2921
+ export * from "./callout";
2922
+ export * from "./accordion";
2923
+ export * from "./faq";
2924
+ export * from "./tabs";
2925
+ export * from "./timeline";
2926
+ export * from "./pricing-table";
2927
+ export * from "./logo-cloud";
2928
+ export * from "./stat-band";
2929
+ export * from "./team-grid";
2930
+ export * from "./feature-split";
2931
+ export * from "./gallery";
2932
+ export * from "./rating";
2933
+ export * from "./progress";
2934
+ export * from "./breadcrumbs";
2935
+ export * from "./avatar";
2573
2936
  `
2574
2937
  });
2575
2938
  function realPageFiles(ctx) {
2576
2939
  const k = ctx.template.key;
2577
2940
  const f = {};
2578
- if (k === "personal") f["app/work/page.tsx"] = workPage(ctx);
2941
+ if (k !== "dashboard") f["app/faq/page.tsx"] = faqPage(ctx);
2942
+ if (k === "personal") {
2943
+ f["app/work/page.tsx"] = workPage(ctx);
2944
+ f["app/uses/page.tsx"] = usesPage(ctx);
2945
+ }
2579
2946
  if (k === "business") {
2580
2947
  f["app/services/page.tsx"] = servicesPage(ctx);
2581
2948
  f["app/pricing/page.tsx"] = pricingPage(ctx);
@@ -2583,6 +2950,8 @@ function realPageFiles(ctx) {
2583
2950
  if (k === "saas") {
2584
2951
  f["app/features/page.tsx"] = featuresPage(ctx);
2585
2952
  f["app/pricing/page.tsx"] = pricingPage(ctx);
2953
+ f["app/integrations/page.tsx"] = integrationsPage(ctx);
2954
+ f["app/changelog/page.tsx"] = changelogPage(ctx);
2586
2955
  }
2587
2956
  if (k === "ecommerce") {
2588
2957
  f["app/shop/page.tsx"] = shopPage(ctx);
@@ -2594,8 +2963,14 @@ function realPageFiles(ctx) {
2594
2963
  f["app/topics/page.tsx"] = topicsPage();
2595
2964
  f["app/newsletter/page.tsx"] = newsletterPage(ctx);
2596
2965
  }
2597
- if (k === "docs") f["app/guides/page.tsx"] = guidesPage();
2598
- if (k === "restaurant") f["app/menu/page.tsx"] = menuPage(ctx);
2966
+ if (k === "docs") {
2967
+ f["app/guides/page.tsx"] = guidesPage();
2968
+ f["app/changelog/page.tsx"] = changelogPage(ctx);
2969
+ }
2970
+ if (k === "restaurant") {
2971
+ f["app/menu/page.tsx"] = menuPage(ctx);
2972
+ f["app/gallery/page.tsx"] = galleryPage(ctx);
2973
+ }
2599
2974
  if (k === "dashboard") {
2600
2975
  f["app/settings/page.tsx"] = settingsPage(ctx);
2601
2976
  f["components/settings-panel.tsx"] = settingsPanel();
@@ -2746,6 +3121,95 @@ var heroArt = (ctx) => {
2746
3121
  }
2747
3122
  `;
2748
3123
  };
3124
+ function richContent(ctx) {
3125
+ const n = ctx.template.siteName;
3126
+ const base = {
3127
+ stats: [
3128
+ { value: "10k+", label: "Monthly visitors" },
3129
+ { value: "99.9%", label: "Uptime" },
3130
+ { value: "4.9\u2605", label: "Average rating" },
3131
+ { value: "<1s", label: "Load time" }
3132
+ ],
3133
+ process: [
3134
+ { title: "Discover", desc: `We learn what ${n} needs and map the fastest path to value.` },
3135
+ { title: "Design", desc: "We shape the experience and validate it early with real content." },
3136
+ { title: "Build", desc: "We ship in weekly increments you can see and steer." },
3137
+ { title: "Launch", desc: "We go live, measure, and keep improving together." }
3138
+ ],
3139
+ testimonials: [
3140
+ { quote: `Working with ${n} was the smoothest launch we've had \u2014 everything just worked.`, author: "Sam Rivera", role: "Product Lead" },
3141
+ { quote: "Fast, polished and genuinely easy to build on. Highly recommend.", author: "Jordan Ellis", role: "Founder" },
3142
+ { quote: "The attention to detail shows on every single page.", author: "Priya Nair", role: "Design Director" }
3143
+ ],
3144
+ team: [
3145
+ { name: "Alex Morgan", role: "Founder & CEO", bio: "Sets the vision and keeps us honest." },
3146
+ { name: "Riya Sharma", role: "Head of Design", bio: "Makes every pixel earn its place." },
3147
+ { name: "Chris Doyle", role: "Lead Engineer", bio: "Ships fast without breaking things." }
3148
+ ],
3149
+ values: [
3150
+ { title: "Craft", desc: "We care about the details others skip." },
3151
+ { title: "Speed", desc: "We ship early and iterate in the open." },
3152
+ { title: "Honesty", desc: "Clear pricing, clear timelines, no surprises." },
3153
+ { title: "Partnership", desc: "Your goals are our goals." }
3154
+ ],
3155
+ faqs: [
3156
+ { q: `Is ${n} really production-ready?`, a: `Yes \u2014 every ${n} page is server-rendered, SEO-optimized and ships with sensible security headers out of the box.` },
3157
+ { q: "How do I get started?", a: "Clone the project, run npm install and npm run dev \u2014 you'll have a live site in under a minute." },
3158
+ { q: "Can I customize the design?", a: "Completely. Colours, fonts and layout are token-driven, so a few edits reskin the whole site." },
3159
+ { q: "Is it accessible and fast?", a: "Built mobile-first with semantic HTML, keyboard support and a strong Lighthouse baseline." },
3160
+ { q: "Do you support dark mode?", a: "Yes \u2014 light, dark and system themes with a no-flash script, powered by @lacspace/theme." },
3161
+ { q: "How do I deploy?", a: "Deploy to any Node host or Vercel in one click \u2014 sitemap, robots and OG images are all wired up." }
3162
+ ]
3163
+ };
3164
+ const over = {
3165
+ restaurant: {
3166
+ stats: [
3167
+ { value: "20+", label: "Years serving" },
3168
+ { value: "4.8\u2605", label: "Google rating" },
3169
+ { value: "120", label: "Seats" },
3170
+ { value: "Daily", label: "Fresh menu" }
3171
+ ],
3172
+ process: [
3173
+ { title: "Reserve", desc: `Book your table at ${n} online in seconds.` },
3174
+ { title: "Arrive", desc: "Settle in \u2014 we'll take it from here." },
3175
+ { title: "Savour", desc: "Seasonal plates, made from scratch." }
3176
+ ],
3177
+ testimonials: [
3178
+ { quote: `Best meal we've had all year \u2014 ${n} never misses.`, author: "Dana Lopez", role: "Regular" },
3179
+ { quote: "The tasting menu is an experience. Book ahead!", author: "Marco Bianchi", role: "Food writer" },
3180
+ { quote: "Warm service, unforgettable flavours.", author: "Aisha Khan", role: "Guest" }
3181
+ ]
3182
+ },
3183
+ ecommerce: {
3184
+ stats: [
3185
+ { value: "50k+", label: "Orders shipped" },
3186
+ { value: "4.9\u2605", label: "Customer rating" },
3187
+ { value: "48h", label: "Fast delivery" },
3188
+ { value: "30d", label: "Free returns" }
3189
+ ]
3190
+ }
3191
+ };
3192
+ return { ...base, ...over[ctx.template.key] ?? {} };
3193
+ }
3194
+ var extraHomeSections = (ctx) => {
3195
+ const r = richContent(ctx);
3196
+ const tst = r.testimonials.map((t) => `<Testimonial quote={${JSON.stringify(t.quote)}} author={${JSON.stringify(t.author)}} role={${JSON.stringify(t.role ?? "")}} />`).join("\n ");
3197
+ return `<section className="mx-auto max-w-6xl px-6 py-16">
3198
+ <p className="text-center text-sm font-semibold uppercase tracking-widest text-faint">By the numbers</p>
3199
+ <div className="mt-8"><StatBand stats={${JSON.stringify(r.stats)}} /></div>
3200
+ </section>
3201
+ <section className="mx-auto max-w-6xl px-6 py-16">
3202
+ <h2 className="text-center text-3xl font-bold sm:text-4xl">How it works</h2>
3203
+ <p className="mx-auto mt-3 max-w-xl text-center text-muted">A simple, proven path from first hello to launch day.</p>
3204
+ <div className="mt-10"><Steps items={${JSON.stringify(r.process)}} /></div>
3205
+ </section>
3206
+ <section className="mx-auto max-w-6xl px-6 py-16">
3207
+ <h2 className="text-center text-3xl font-bold sm:text-4xl">Loved by teams</h2>
3208
+ <div className="mt-10 grid gap-6 md:grid-cols-3">
3209
+ ${tst}
3210
+ </div>
3211
+ </section>`;
3212
+ };
2749
3213
  var showcaseSection = (ctx) => {
2750
3214
  const d = creativeData(ctx);
2751
3215
  const bullets = d.bullets.map((b) => `<li className="flex items-center gap-2 text-muted"><span className="text-[color:var(--accent-to)]">\u2713</span> ${b}</li>`).join("\n ");
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "create-lacspace-app",
3
- "version": "1.12.1",
3
+ "version": "1.13.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": {
7
- "create-lacspace-app": "./dist/index.js"
7
+ "create-lacspace-app": "dist/index.js"
8
8
  },
9
9
  "files": [
10
10
  "dist"