create-lacspace-app 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +703 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -174,6 +174,23 @@ body {
|
|
|
174
174
|
/* indeterminate progress bar (used by the "under development" pages) */
|
|
175
175
|
@keyframes loadbar { 0% { transform: translateX(-120%); } 100% { transform: translateX(340%); } }
|
|
176
176
|
|
|
177
|
+
/* ambient motion for illustrations & the aurora background */
|
|
178
|
+
@keyframes floaty { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-16px); } }
|
|
179
|
+
@keyframes spin-slow { to { transform: rotate(360deg); } }
|
|
180
|
+
@keyframes gradient-pan { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }
|
|
181
|
+
@keyframes marquee-x { from { transform: translateX(0); } to { transform: translateX(-50%); } }
|
|
182
|
+
.animate-floaty { animation: floaty 6s ease-in-out infinite; }
|
|
183
|
+
.animate-floaty-slow { animation: floaty 9s ease-in-out infinite; }
|
|
184
|
+
.animate-spin-slow { animation: spin-slow 26s linear infinite; }
|
|
185
|
+
.gradient-text-animated {
|
|
186
|
+
background: linear-gradient(120deg, var(--accent-from), var(--accent-to), var(--accent-from));
|
|
187
|
+
background-size: 200% 200%;
|
|
188
|
+
-webkit-background-clip: text; background-clip: text; color: transparent;
|
|
189
|
+
animation: gradient-pan 6s ease infinite;
|
|
190
|
+
}
|
|
191
|
+
.marquee-track { display: flex; width: max-content; animation: marquee-x 32s linear infinite; }
|
|
192
|
+
.marquee-mask { -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
|
|
193
|
+
|
|
177
194
|
/* soft entrance for content */
|
|
178
195
|
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
|
|
179
196
|
main > section, main > * { animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; }
|
|
@@ -323,7 +340,7 @@ boring-but-essential stuff already done. Here's what's in the box.
|
|
|
323
340
|
- **\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
341
|
- **\u2728 A \u2318K command palette** \u2014 press \`\u2318K\` / \`Ctrl-K\` anywhere, powered by \`@lacspace/ui\`. Also try \`<Reveal>\`, \`<Counter>\`, \`<GradientText>\`, \`<TiltCard>\`, \`<Marquee>\`, \`<Typewriter>\`.
|
|
325
342
|
- **\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
|
|
343
|
+
- **\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
344
|
- **\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
345
|
- **\u2728 Per-page SEO** \u2014 see \`app/about\` & \`app/contact\`: one \`site.meta()\` call gives each route its own title, canonical & OG image.
|
|
329
346
|
- **\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.
|
|
@@ -407,14 +424,21 @@ function homePage(ctx) {
|
|
|
407
424
|
if (ctx.template.key === "dashboard") return dashboardHome(ctx);
|
|
408
425
|
const shell = (inner) => `import { site } from "@/lib/site";
|
|
409
426
|
import { LiveStats } from "@/components/live-stats";
|
|
427
|
+
import { Aurora } from "@/components/aurora";
|
|
428
|
+
import { HeroArt } from "@/components/hero-art";
|
|
410
429
|
|
|
411
430
|
// \u2728 Self-canonical home page \u2014 one line, full SEO (title, canonical, OG, Twitter).
|
|
412
431
|
export const metadata = site.meta({ title: ${JSON.stringify(n)}, path: "/" });
|
|
413
432
|
|
|
414
433
|
export default function Home() {
|
|
415
434
|
return (
|
|
416
|
-
<main className="min-h-screen">
|
|
435
|
+
<main className="relative min-h-screen overflow-hidden">
|
|
436
|
+
{/* \u2728 Animated, theme-aware gradient backdrop */}
|
|
437
|
+
<Aurora />
|
|
417
438
|
${inner}
|
|
439
|
+
${showcaseSection(ctx)}
|
|
440
|
+
${marqueeSection(ctx)}
|
|
441
|
+
${faqSection(ctx)}
|
|
418
442
|
${builtWithSection(ctx)}
|
|
419
443
|
</main>
|
|
420
444
|
);
|
|
@@ -1433,10 +1457,13 @@ function buildFiles(ctx) {
|
|
|
1433
1457
|
files["components/site-header.tsx"] = siteHeader(ctx);
|
|
1434
1458
|
files["components/site-footer.tsx"] = siteFooter(ctx);
|
|
1435
1459
|
files["components/announcement-bar.tsx"] = announcementBar();
|
|
1460
|
+
files["components/aurora.tsx"] = aurora();
|
|
1461
|
+
files["components/hero-art.tsx"] = heroArt(ctx);
|
|
1436
1462
|
}
|
|
1437
1463
|
for (const page of stubPages(ctx)) {
|
|
1438
1464
|
files[`app${page.path}/page.tsx`] = isDash ? dashboardStubPage(page) : underDevPage(page);
|
|
1439
1465
|
}
|
|
1466
|
+
Object.assign(files, realPageFiles(ctx));
|
|
1440
1467
|
if (isBlog) {
|
|
1441
1468
|
files["lib/posts.ts"] = postsLib();
|
|
1442
1469
|
files["app/blog/page.tsx"] = blogListPage(ctx);
|
|
@@ -1463,7 +1490,7 @@ function pagesFor(ctx) {
|
|
|
1463
1490
|
{ path: "/analytics", label: "Analytics", nav: true, group: "Product" },
|
|
1464
1491
|
{ path: "/customers", label: "Customers", nav: true, group: "Product" },
|
|
1465
1492
|
{ path: "/billing", label: "Billing", nav: true, group: "Product" },
|
|
1466
|
-
{ path: "/settings", label: "Settings", nav: true, group: "Product" },
|
|
1493
|
+
{ path: "/settings", label: "Settings", nav: true, group: "Product", real: true },
|
|
1467
1494
|
{ path: "/help", label: "Help", group: "Resources" }
|
|
1468
1495
|
];
|
|
1469
1496
|
}
|
|
@@ -1476,41 +1503,42 @@ function pagesFor(ctx) {
|
|
|
1476
1503
|
];
|
|
1477
1504
|
const specific = {
|
|
1478
1505
|
personal: [
|
|
1479
|
-
{ path: "/work", label: "Work", nav: true, group: "Product" },
|
|
1506
|
+
{ path: "/work", label: "Work", nav: true, group: "Product", real: true },
|
|
1480
1507
|
{ path: "/blog", label: "Blog", nav: true, group: "Product" },
|
|
1481
1508
|
{ path: "/uses", label: "Uses", group: "Resources" }
|
|
1482
1509
|
],
|
|
1483
1510
|
business: [
|
|
1484
|
-
{ path: "/services", label: "Services", nav: true, group: "Product" },
|
|
1511
|
+
{ path: "/services", label: "Services", nav: true, group: "Product", real: true },
|
|
1485
1512
|
{ path: "/work", label: "Work", nav: true, group: "Product" },
|
|
1486
|
-
{ path: "/pricing", label: "Pricing", nav: true, group: "Product" },
|
|
1513
|
+
{ path: "/pricing", label: "Pricing", nav: true, group: "Product", real: true },
|
|
1487
1514
|
{ path: "/faq", label: "FAQ", group: "Resources" }
|
|
1488
1515
|
],
|
|
1489
1516
|
ecommerce: [
|
|
1490
|
-
{ path: "/shop", label: "Shop", nav: true, group: "Product" },
|
|
1517
|
+
{ path: "/shop", label: "Shop", nav: true, group: "Product", real: true },
|
|
1491
1518
|
{ path: "/collections", label: "Collections", nav: true, group: "Product" },
|
|
1519
|
+
{ path: "/cart", label: "Cart", group: "Product", real: true },
|
|
1492
1520
|
{ path: "/shipping", label: "Shipping", group: "Resources" },
|
|
1493
1521
|
{ path: "/returns", label: "Returns", group: "Resources" }
|
|
1494
1522
|
],
|
|
1495
1523
|
saas: [
|
|
1496
|
-
{ path: "/features", label: "Features", nav: true, group: "Product" },
|
|
1497
|
-
{ path: "/pricing", label: "Pricing", nav: true, group: "Product" },
|
|
1524
|
+
{ path: "/features", label: "Features", nav: true, group: "Product", real: true },
|
|
1525
|
+
{ path: "/pricing", label: "Pricing", nav: true, group: "Product", real: true },
|
|
1498
1526
|
{ path: "/integrations", label: "Integrations", group: "Product" },
|
|
1499
1527
|
{ path: "/changelog", label: "Changelog", group: "Resources" }
|
|
1500
1528
|
],
|
|
1501
1529
|
blog: [
|
|
1502
1530
|
{ path: "/blog", label: "Articles", nav: true, group: "Product", real: true },
|
|
1503
|
-
{ path: "/topics", label: "Topics", nav: true, group: "Product" },
|
|
1531
|
+
{ path: "/topics", label: "Topics", nav: true, group: "Product", real: true },
|
|
1504
1532
|
{ path: "/newsletter", label: "Newsletter", group: "Resources" }
|
|
1505
1533
|
],
|
|
1506
1534
|
docs: [
|
|
1507
1535
|
{ path: "/docs", label: "Docs", nav: true, group: "Product", real: true },
|
|
1508
|
-
{ path: "/guides", label: "Guides", nav: true, group: "Product" },
|
|
1536
|
+
{ path: "/guides", label: "Guides", nav: true, group: "Product", real: true },
|
|
1509
1537
|
{ path: "/api-reference", label: "API", nav: true, group: "Product" },
|
|
1510
1538
|
{ path: "/changelog", label: "Changelog", group: "Resources" }
|
|
1511
1539
|
],
|
|
1512
1540
|
restaurant: [
|
|
1513
|
-
{ path: "/menu", label: "Menu", nav: true, group: "Product" },
|
|
1541
|
+
{ path: "/menu", label: "Menu", nav: true, group: "Product", real: true },
|
|
1514
1542
|
{ path: "/reservations", label: "Reservations", nav: true, group: "Product" },
|
|
1515
1543
|
{ path: "/gallery", label: "Gallery", nav: true, group: "Product" },
|
|
1516
1544
|
{ path: "/events", label: "Private events", group: "Resources" }
|
|
@@ -1522,30 +1550,45 @@ function stubPages(ctx) {
|
|
|
1522
1550
|
return pagesFor(ctx).filter((p) => !p.real);
|
|
1523
1551
|
}
|
|
1524
1552
|
var linkLiteral = (pages) => pages.map((l) => `{ href: ${JSON.stringify(l.path)}, label: ${JSON.stringify(l.label)} }`).join(", ");
|
|
1525
|
-
var siteHeader = (ctx) =>
|
|
1553
|
+
var siteHeader = (ctx) => {
|
|
1554
|
+
const isEcom = ctx.template.key === "ecommerce";
|
|
1555
|
+
const cartImport = isEcom ? `
|
|
1556
|
+
import { useCart } from "@/lib/store";
|
|
1557
|
+
import { useIsMounted } from "@lacspace/hooks";` : "";
|
|
1558
|
+
const cartHook = isEcom ? `
|
|
1559
|
+
const count = useCart((s) => s.items.length);
|
|
1560
|
+
const mounted = useIsMounted();` : "";
|
|
1561
|
+
const cartBtn = isEcom ? `
|
|
1562
|
+
<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">
|
|
1563
|
+
<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>
|
|
1564
|
+
{mounted() && count > 0 ? (
|
|
1565
|
+
<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>
|
|
1566
|
+
) : null}
|
|
1567
|
+
</Link>` : "";
|
|
1568
|
+
return `"use client";
|
|
1526
1569
|
|
|
1527
1570
|
import Link from "next/link";
|
|
1528
1571
|
import { useUI } from "@/lib/store";
|
|
1529
|
-
import { ThemeToggle } from "./theme-toggle"
|
|
1572
|
+
import { ThemeToggle } from "./theme-toggle";${cartImport}
|
|
1530
1573
|
|
|
1531
1574
|
const LINKS = [${linkLiteral(pagesFor(ctx).filter((p) => p.nav))}];
|
|
1532
1575
|
|
|
1533
1576
|
export function SiteHeader() {
|
|
1534
1577
|
const open = useUI((s) => s.navOpen);
|
|
1535
1578
|
const toggle = useUI((s) => s.toggleNav);
|
|
1536
|
-
const setOpen = useUI((s) => s.setNavOpen)
|
|
1579
|
+
const setOpen = useUI((s) => s.setNavOpen);${cartHook}
|
|
1537
1580
|
return (
|
|
1538
1581
|
<header className="sticky top-0 z-40 border-b border-hairline bg-app/90 backdrop-blur">
|
|
1539
1582
|
<nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
|
1540
1583
|
<Link href="/" className="text-lg font-black gradient-text">${ctx.template.siteName}</Link>
|
|
1541
|
-
<div className="hidden items-center gap-
|
|
1584
|
+
<div className="hidden items-center gap-6 md:flex">
|
|
1542
1585
|
{LINKS.map((l) => (
|
|
1543
1586
|
<Link key={l.href} href={l.href} className="text-sm text-muted transition hover:text-fg">{l.label}</Link>
|
|
1544
1587
|
))}
|
|
1545
|
-
<ThemeToggle
|
|
1588
|
+
<ThemeToggle />${cartBtn}
|
|
1546
1589
|
</div>
|
|
1547
1590
|
<div className="flex items-center gap-2 md:hidden">
|
|
1548
|
-
<ThemeToggle
|
|
1591
|
+
<ThemeToggle />${cartBtn}
|
|
1549
1592
|
<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
1593
|
<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
1594
|
</button>
|
|
@@ -1564,6 +1607,7 @@ export function SiteHeader() {
|
|
|
1564
1607
|
);
|
|
1565
1608
|
}
|
|
1566
1609
|
`;
|
|
1610
|
+
};
|
|
1567
1611
|
var siteFooter = (ctx) => {
|
|
1568
1612
|
const pages = pagesFor(ctx);
|
|
1569
1613
|
const groups = ["Product", "Company", "Resources"].map((title) => ({ title, items: pages.filter((p) => p.group === title) })).filter((g) => g.items.length > 0);
|
|
@@ -1707,6 +1751,32 @@ export const useAnnouncement = create<AnnouncementState>(
|
|
|
1707
1751
|
{ name: "announcement" },
|
|
1708
1752
|
),
|
|
1709
1753
|
);
|
|
1754
|
+
|
|
1755
|
+
/** A tiny shopping cart \u2014 persisted to localStorage. */
|
|
1756
|
+
export interface CartItem { id: string; name: string; price: number; }
|
|
1757
|
+
interface CartState {
|
|
1758
|
+
items: CartItem[];
|
|
1759
|
+
add: (item: CartItem) => void;
|
|
1760
|
+
remove: (id: string) => void;
|
|
1761
|
+
clear: () => void;
|
|
1762
|
+
}
|
|
1763
|
+
export const useCart = create<CartState>(
|
|
1764
|
+
persist(
|
|
1765
|
+
(set) => ({
|
|
1766
|
+
items: [],
|
|
1767
|
+
add: (item) => set((s) => ({ items: [...s.items, item] })),
|
|
1768
|
+
remove: (id) => set((s) => {
|
|
1769
|
+
const i = s.items.findIndex((x) => x.id === id);
|
|
1770
|
+
if (i === -1) return {};
|
|
1771
|
+
const items = s.items.slice();
|
|
1772
|
+
items.splice(i, 1);
|
|
1773
|
+
return { items };
|
|
1774
|
+
}),
|
|
1775
|
+
clear: () => set({ items: [] }),
|
|
1776
|
+
}),
|
|
1777
|
+
{ name: "cart" },
|
|
1778
|
+
),
|
|
1779
|
+
);
|
|
1710
1780
|
`;
|
|
1711
1781
|
var statsRoute = () => `import { NextResponse } from "next/server";
|
|
1712
1782
|
|
|
@@ -1831,6 +1901,621 @@ export default function Home() {
|
|
|
1831
1901
|
);
|
|
1832
1902
|
}
|
|
1833
1903
|
`;
|
|
1904
|
+
var pageFile = (o) => `import type { Metadata } from "next";
|
|
1905
|
+
import Link from "next/link";
|
|
1906
|
+
import { site } from "@/lib/site";
|
|
1907
|
+
|
|
1908
|
+
export const metadata: Metadata = site.meta({ title: ${JSON.stringify(o.title)}, path: ${JSON.stringify(o.path)}, description: ${JSON.stringify(o.description)} });
|
|
1909
|
+
|
|
1910
|
+
export default function Page() {
|
|
1911
|
+
return (
|
|
1912
|
+
<main className="min-h-screen">
|
|
1913
|
+
${o.body}
|
|
1914
|
+
</main>
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
`;
|
|
1918
|
+
var pricingPage = (ctx) => pageFile({
|
|
1919
|
+
title: "Pricing",
|
|
1920
|
+
path: "/pricing",
|
|
1921
|
+
description: `Simple, transparent pricing for ${ctx.template.siteName}. Start free and upgrade anytime.`,
|
|
1922
|
+
body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
1923
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-faint">Pricing</p>
|
|
1924
|
+
<h1 className="mt-3 text-4xl font-bold sm:text-5xl">Simple, transparent <span className="gradient-text">pricing</span></h1>
|
|
1925
|
+
<p className="mx-auto mt-4 max-w-xl text-lg text-muted">Start free. Upgrade when you're ready. Cancel anytime.</p>
|
|
1926
|
+
</section>
|
|
1927
|
+
<section className="mx-auto max-w-6xl px-6 pb-24">
|
|
1928
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
1929
|
+
{[
|
|
1930
|
+
{ name: "Starter", price: "$0", period: "/mo", tagline: "For side projects", features: ["1 project", "Community support", "Basic analytics"], cta: "Get started", highlight: false },
|
|
1931
|
+
{ 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 },
|
|
1932
|
+
{ name: "Scale", price: "Custom", period: "", tagline: "For organizations", features: ["SSO & SAML", "Dedicated support", "SLA & audit logs", "Guided onboarding"], cta: "Contact sales", highlight: false },
|
|
1933
|
+
].map((tier) => (
|
|
1934
|
+
<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")}>
|
|
1935
|
+
{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}
|
|
1936
|
+
<h2 className="text-lg font-semibold">{tier.name}</h2>
|
|
1937
|
+
<p className="mt-1 text-sm text-muted">{tier.tagline}</p>
|
|
1938
|
+
<div className="mt-6 flex items-baseline gap-1">
|
|
1939
|
+
<span className="text-4xl font-black">{tier.price}</span>
|
|
1940
|
+
<span className="text-muted">{tier.period}</span>
|
|
1941
|
+
</div>
|
|
1942
|
+
<ul className="mt-6 flex-1 space-y-3 text-sm">
|
|
1943
|
+
{tier.features.map((f) => (
|
|
1944
|
+
<li key={f} className="flex items-center gap-2 text-muted"><span className="text-[color:var(--accent-to)]">\u2713</span> {f}</li>
|
|
1945
|
+
))}
|
|
1946
|
+
</ul>
|
|
1947
|
+
<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>
|
|
1948
|
+
</div>
|
|
1949
|
+
))}
|
|
1950
|
+
</div>
|
|
1951
|
+
<p className="mt-10 text-center text-sm text-faint">All plans include SSL, unlimited bandwidth and a 14-day money-back guarantee.</p>
|
|
1952
|
+
</section>`
|
|
1953
|
+
});
|
|
1954
|
+
var servicesPage = (ctx) => pageFile({
|
|
1955
|
+
title: "Services",
|
|
1956
|
+
path: "/services",
|
|
1957
|
+
description: `What ${ctx.template.siteName} can do for you \u2014 from strategy to launch.`,
|
|
1958
|
+
body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
1959
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-faint">Services</p>
|
|
1960
|
+
<h1 className="mt-3 text-4xl font-bold sm:text-5xl">What we <span className="gradient-text">do</span></h1>
|
|
1961
|
+
<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>
|
|
1962
|
+
</section>
|
|
1963
|
+
<section className="mx-auto max-w-6xl px-6 pb-16">
|
|
1964
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
1965
|
+
{[
|
|
1966
|
+
{ icon: "\u{1F3AF}", title: "Strategy", desc: "Positioning, roadmaps and the plan to get there." },
|
|
1967
|
+
{ icon: "\u{1F3A8}", title: "Design", desc: "Brand, UX and interfaces people enjoy using." },
|
|
1968
|
+
{ icon: "\u{1F6E0}\uFE0F", title: "Development", desc: "Fast, accessible, maintainable web and mobile apps." },
|
|
1969
|
+
{ icon: "\u{1F680}", title: "Launch", desc: "Ship with confidence \u2014 SEO, analytics and monitoring." },
|
|
1970
|
+
{ icon: "\u{1F4C8}", title: "Growth", desc: "Experiments and optimization that move the numbers." },
|
|
1971
|
+
{ icon: "\u{1F91D}", title: "Support", desc: "Ongoing care so your product keeps getting better." },
|
|
1972
|
+
].map((s) => (
|
|
1973
|
+
<div key={s.title} className="rounded-2xl border border-hairline bg-surface p-6 transition hover:-translate-y-1">
|
|
1974
|
+
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl gradient-bg text-2xl">{s.icon}</div>
|
|
1975
|
+
<h3 className="font-semibold">{s.title}</h3>
|
|
1976
|
+
<p className="mt-1 text-sm text-muted">{s.desc}</p>
|
|
1977
|
+
</div>
|
|
1978
|
+
))}
|
|
1979
|
+
</div>
|
|
1980
|
+
</section>
|
|
1981
|
+
<section className="mx-auto max-w-3xl px-6 pb-24 text-center">
|
|
1982
|
+
<h2 className="text-2xl font-bold">Have a project in mind?</h2>
|
|
1983
|
+
<Link href="/contact" className="mt-6 inline-block rounded-full gradient-bg px-8 py-3 font-semibold text-black">Start a conversation</Link>
|
|
1984
|
+
</section>`
|
|
1985
|
+
});
|
|
1986
|
+
var featuresPage = (ctx) => pageFile({
|
|
1987
|
+
title: "Features",
|
|
1988
|
+
path: "/features",
|
|
1989
|
+
description: `Everything ${ctx.template.siteName} gives your team, in one place.`,
|
|
1990
|
+
body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
1991
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-faint">Features</p>
|
|
1992
|
+
<h1 className="mt-3 text-4xl font-bold sm:text-5xl">Built to <span className="gradient-text">ship faster</span></h1>
|
|
1993
|
+
<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>
|
|
1994
|
+
</section>
|
|
1995
|
+
<section className="mx-auto max-w-6xl px-6 pb-24">
|
|
1996
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
1997
|
+
{[
|
|
1998
|
+
{ icon: "\u26A1", title: "Fast by default", desc: "Edge-rendered and cached \u2014 instant everywhere." },
|
|
1999
|
+
{ icon: "\u{1F512}", title: "Secure", desc: "Hardened headers, auth and audit logs out of the box." },
|
|
2000
|
+
{ icon: "\u{1F4CA}", title: "Analytics", desc: "Understand usage without bolting on a dozen tools." },
|
|
2001
|
+
{ icon: "\u{1F50C}", title: "Integrations", desc: "Connect the tools you already use in a click." },
|
|
2002
|
+
{ icon: "\u{1F9E9}", title: "Extensible", desc: "A clean API and webhooks for anything custom." },
|
|
2003
|
+
{ icon: "\u{1F317}", title: "Delightful UX", desc: "Dark mode, a \u2318K palette and thoughtful details." },
|
|
2004
|
+
].map((f) => (
|
|
2005
|
+
<div key={f.title} className="rounded-2xl border border-hairline bg-surface p-6 transition hover:-translate-y-1">
|
|
2006
|
+
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl gradient-bg text-2xl">{f.icon}</div>
|
|
2007
|
+
<h3 className="font-semibold">{f.title}</h3>
|
|
2008
|
+
<p className="mt-1 text-sm text-muted">{f.desc}</p>
|
|
2009
|
+
</div>
|
|
2010
|
+
))}
|
|
2011
|
+
</div>
|
|
2012
|
+
<div className="mt-12 text-center">
|
|
2013
|
+
<Link href="/pricing" className="inline-block rounded-full gradient-bg px-8 py-3 font-semibold text-black">See pricing</Link>
|
|
2014
|
+
</div>
|
|
2015
|
+
</section>`
|
|
2016
|
+
});
|
|
2017
|
+
var workPage = (ctx) => pageFile({
|
|
2018
|
+
title: "Work",
|
|
2019
|
+
path: "/work",
|
|
2020
|
+
description: `Selected projects by ${ctx.template.siteName}.`,
|
|
2021
|
+
body: ` <section className="mx-auto max-w-3xl px-6 py-24">
|
|
2022
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-faint">Work</p>
|
|
2023
|
+
<h1 className="mt-3 text-4xl font-bold sm:text-5xl">Selected <span className="gradient-text">work</span></h1>
|
|
2024
|
+
<p className="mt-4 text-lg text-muted">A few things I've designed and built recently.</p>
|
|
2025
|
+
</section>
|
|
2026
|
+
<section className="mx-auto max-w-6xl px-6 pb-24">
|
|
2027
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
2028
|
+
{[
|
|
2029
|
+
{ title: "Aurora", tag: "Product design", year: "2026" },
|
|
2030
|
+
{ title: "Northwind", tag: "Web app", year: "2025" },
|
|
2031
|
+
{ title: "Lumen", tag: "Branding", year: "2025" },
|
|
2032
|
+
{ title: "Harbor", tag: "Mobile app", year: "2024" },
|
|
2033
|
+
{ title: "Cadence", tag: "Design system", year: "2024" },
|
|
2034
|
+
{ title: "Meadow", tag: "Marketing site", year: "2023" },
|
|
2035
|
+
].map((p) => (
|
|
2036
|
+
<div key={p.title} className="group rounded-2xl border border-hairline bg-surface p-6 transition hover:-translate-y-1">
|
|
2037
|
+
<div className="mb-4 aspect-video rounded-xl gradient-bg opacity-80 transition group-hover:opacity-100" />
|
|
2038
|
+
<div className="flex items-center justify-between">
|
|
2039
|
+
<h3 className="font-semibold">{p.title}</h3>
|
|
2040
|
+
<span className="text-xs text-faint">{p.year}</span>
|
|
2041
|
+
</div>
|
|
2042
|
+
<p className="mt-1 text-sm text-muted">{p.tag}</p>
|
|
2043
|
+
</div>
|
|
2044
|
+
))}
|
|
2045
|
+
</div>
|
|
2046
|
+
</section>`
|
|
2047
|
+
});
|
|
2048
|
+
var menuPage = (ctx) => pageFile({
|
|
2049
|
+
title: "Menu",
|
|
2050
|
+
path: "/menu",
|
|
2051
|
+
description: `The menu at ${ctx.template.siteName} \u2014 seasonal plates and natural wine.`,
|
|
2052
|
+
body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
2053
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-faint">Menu</p>
|
|
2054
|
+
<h1 className="mt-3 text-4xl font-bold sm:text-5xl gradient-text">Tonight's menu</h1>
|
|
2055
|
+
<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>
|
|
2056
|
+
</section>
|
|
2057
|
+
<section className="mx-auto max-w-3xl px-6 pb-24">
|
|
2058
|
+
{[
|
|
2059
|
+
{ section: "Starters", items: [ { n: "Charred leeks, hazelnut", p: "$14" }, { n: "Burrata, heirloom tomato", p: "$16" }, { n: "Wood-fired sourdough", p: "$7" } ] },
|
|
2060
|
+
{ section: "Mains", items: [ { n: "Handmade tagliatelle", p: "$22" }, { n: "Wood-fired trout", p: "$28" }, { n: "Dry-aged sirloin", p: "$34" } ] },
|
|
2061
|
+
{ section: "Dessert", items: [ { n: "Olive oil cake", p: "$11" }, { n: "Dark chocolate tart", p: "$12" } ] },
|
|
2062
|
+
].map((group) => (
|
|
2063
|
+
<div key={group.section} className="mb-12">
|
|
2064
|
+
<h2 className="mb-6 text-2xl font-bold">{group.section}</h2>
|
|
2065
|
+
<div className="space-y-4">
|
|
2066
|
+
{group.items.map((d) => (
|
|
2067
|
+
<div key={d.n} className="flex items-baseline justify-between gap-4 border-b border-hairline pb-3">
|
|
2068
|
+
<span className="font-medium">{d.n}</span>
|
|
2069
|
+
<span className="shrink-0 gradient-text font-bold">{d.p}</span>
|
|
2070
|
+
</div>
|
|
2071
|
+
))}
|
|
2072
|
+
</div>
|
|
2073
|
+
</div>
|
|
2074
|
+
))}
|
|
2075
|
+
<div className="text-center">
|
|
2076
|
+
<Link href="/reservations" className="inline-block rounded-full gradient-bg px-8 py-3 font-semibold text-black">Book a table</Link>
|
|
2077
|
+
</div>
|
|
2078
|
+
</section>`
|
|
2079
|
+
});
|
|
2080
|
+
var cardGridPage = (o) => pageFile({
|
|
2081
|
+
title: o.title,
|
|
2082
|
+
path: o.path,
|
|
2083
|
+
description: o.lead,
|
|
2084
|
+
body: ` <section className="mx-auto max-w-3xl px-6 py-24">
|
|
2085
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-faint">${o.eyebrow}</p>
|
|
2086
|
+
<h1 className="mt-3 text-4xl font-bold sm:text-5xl">${o.heading}</h1>
|
|
2087
|
+
<p className="mt-4 text-lg text-muted">${o.lead}</p>
|
|
2088
|
+
</section>
|
|
2089
|
+
<section className="mx-auto max-w-6xl px-6 pb-24">
|
|
2090
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
2091
|
+
{${JSON.stringify(o.items)}.map((it) => (
|
|
2092
|
+
<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)]">
|
|
2093
|
+
<h3 className="font-semibold">{it.title}</h3>
|
|
2094
|
+
<p className="mt-2 text-sm text-muted">{it.desc}</p>
|
|
2095
|
+
</div>
|
|
2096
|
+
))}
|
|
2097
|
+
</div>
|
|
2098
|
+
</section>`
|
|
2099
|
+
});
|
|
2100
|
+
var topicsPage = (ctx) => cardGridPage({
|
|
2101
|
+
title: "Topics",
|
|
2102
|
+
path: "/topics",
|
|
2103
|
+
eyebrow: "Topics",
|
|
2104
|
+
heading: "Browse by topic",
|
|
2105
|
+
lead: "Find what you care about \u2014 from deep dives to quick notes.",
|
|
2106
|
+
items: [
|
|
2107
|
+
{ title: "Engineering", desc: "How we build and the decisions behind it." },
|
|
2108
|
+
{ title: "Design", desc: "Craft, systems and the details that matter." },
|
|
2109
|
+
{ title: "Product", desc: "What we're shipping and why." },
|
|
2110
|
+
{ title: "Culture", desc: "How we work and what we believe." },
|
|
2111
|
+
{ title: "Tutorials", desc: "Step-by-step, hands-on guides." },
|
|
2112
|
+
{ title: "Announcements", desc: "News and releases." }
|
|
2113
|
+
]
|
|
2114
|
+
});
|
|
2115
|
+
var guidesPage = (ctx) => cardGridPage({
|
|
2116
|
+
title: "Guides",
|
|
2117
|
+
path: "/guides",
|
|
2118
|
+
eyebrow: "Guides",
|
|
2119
|
+
heading: "Guides & tutorials",
|
|
2120
|
+
lead: "Task-focused walkthroughs to get you productive fast.",
|
|
2121
|
+
items: [
|
|
2122
|
+
{ title: "Getting started", desc: "Install, configure and run your first build." },
|
|
2123
|
+
{ title: "Authentication", desc: "Add sign-in, sessions and protected routes." },
|
|
2124
|
+
{ title: "Deployment", desc: "Ship to production the right way." },
|
|
2125
|
+
{ title: "Best practices", desc: "Patterns that scale as your app grows." },
|
|
2126
|
+
{ title: "Migrations", desc: "Upgrade safely between versions." },
|
|
2127
|
+
{ title: "Troubleshooting", desc: "Fix the most common issues quickly." }
|
|
2128
|
+
]
|
|
2129
|
+
});
|
|
2130
|
+
var shopPage = (ctx) => `import type { Metadata } from "next";
|
|
2131
|
+
import { site } from "@/lib/site";
|
|
2132
|
+
import { ProductGrid } from "@/components/product-grid";
|
|
2133
|
+
|
|
2134
|
+
export const metadata: Metadata = site.meta({ title: "Shop", path: "/shop", description: ${JSON.stringify(`Shop everything at ${ctx.template.siteName}.`)} });
|
|
2135
|
+
|
|
2136
|
+
export default function Page() {
|
|
2137
|
+
return (
|
|
2138
|
+
<main className="mx-auto min-h-screen max-w-6xl px-6 py-24">
|
|
2139
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-faint">Shop</p>
|
|
2140
|
+
<h1 className="mt-3 text-4xl font-bold">Everything in the <span className="gradient-text">store</span></h1>
|
|
2141
|
+
<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>
|
|
2142
|
+
<div className="mt-12"><ProductGrid /></div>
|
|
2143
|
+
</main>
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
`;
|
|
2147
|
+
var cartPage = () => `import type { Metadata } from "next";
|
|
2148
|
+
import { site } from "@/lib/site";
|
|
2149
|
+
import { CartView } from "@/components/cart-view";
|
|
2150
|
+
|
|
2151
|
+
export const metadata: Metadata = site.meta({ title: "Your bag", path: "/cart", description: "Review the items in your bag." });
|
|
2152
|
+
|
|
2153
|
+
export default function Page() {
|
|
2154
|
+
return (
|
|
2155
|
+
<main className="mx-auto min-h-screen max-w-6xl px-6 py-24">
|
|
2156
|
+
<h1 className="text-4xl font-bold">Your <span className="gradient-text">bag</span></h1>
|
|
2157
|
+
<div className="mt-12"><CartView /></div>
|
|
2158
|
+
</main>
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
`;
|
|
2162
|
+
var productGrid = () => `"use client";
|
|
2163
|
+
|
|
2164
|
+
import { useCart } from "@/lib/store";
|
|
2165
|
+
|
|
2166
|
+
const PRODUCTS = [
|
|
2167
|
+
{ id: "p1", name: "Aurora Mug", price: 18, emoji: "\u2615" },
|
|
2168
|
+
{ id: "p2", name: "Field Notebook", price: 12, emoji: "\u{1F4D3}" },
|
|
2169
|
+
{ id: "p3", name: "Canvas Tote", price: 24, emoji: "\u{1F45C}" },
|
|
2170
|
+
{ id: "p4", name: "Enamel Pin", price: 8, emoji: "\u{1F4CC}" },
|
|
2171
|
+
{ id: "p5", name: "Cotton Tee", price: 28, emoji: "\u{1F455}" },
|
|
2172
|
+
{ id: "p6", name: "Sticker Pack", price: 6, emoji: "\u2728" },
|
|
2173
|
+
{ id: "p7", name: "Ceramic Vase", price: 42, emoji: "\u{1F3FA}" },
|
|
2174
|
+
{ id: "p8", name: "Linen Apron", price: 34, emoji: "\u{1F9F5}" },
|
|
2175
|
+
];
|
|
2176
|
+
|
|
2177
|
+
export function ProductGrid() {
|
|
2178
|
+
const add = useCart((s) => s.add);
|
|
2179
|
+
return (
|
|
2180
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
2181
|
+
{PRODUCTS.map((p) => (
|
|
2182
|
+
<div key={p.id} className="flex flex-col rounded-2xl border border-hairline bg-surface p-5">
|
|
2183
|
+
<div className="mb-4 flex aspect-square items-center justify-center rounded-xl gradient-bg text-5xl">{p.emoji}</div>
|
|
2184
|
+
<h3 className="font-semibold">{p.name}</h3>
|
|
2185
|
+
<div className="mt-1 text-muted">{"$" + p.price.toFixed(2)}</div>
|
|
2186
|
+
<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>
|
|
2187
|
+
</div>
|
|
2188
|
+
))}
|
|
2189
|
+
</div>
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2192
|
+
`;
|
|
2193
|
+
var cartView = () => `"use client";
|
|
2194
|
+
|
|
2195
|
+
import Link from "next/link";
|
|
2196
|
+
import { useCart } from "@/lib/store";
|
|
2197
|
+
import { useIsMounted } from "@lacspace/hooks";
|
|
2198
|
+
|
|
2199
|
+
export function CartView() {
|
|
2200
|
+
const items = useCart((s) => s.items);
|
|
2201
|
+
const remove = useCart((s) => s.remove);
|
|
2202
|
+
const clear = useCart((s) => s.clear);
|
|
2203
|
+
const mounted = useIsMounted();
|
|
2204
|
+
|
|
2205
|
+
if (!mounted()) return <div className="py-16 text-center text-muted">Loading your bag\u2026</div>;
|
|
2206
|
+
|
|
2207
|
+
if (items.length === 0) {
|
|
2208
|
+
return (
|
|
2209
|
+
<div className="mx-auto max-w-md py-16 text-center">
|
|
2210
|
+
<p className="text-lg text-muted">Your bag is empty.</p>
|
|
2211
|
+
<Link href="/shop" className="mt-6 inline-block rounded-full gradient-bg px-6 py-3 font-semibold text-black">Browse the shop</Link>
|
|
2212
|
+
</div>
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
const total = items.reduce((sum, i) => sum + i.price, 0);
|
|
2217
|
+
|
|
2218
|
+
return (
|
|
2219
|
+
<div className="mx-auto max-w-2xl">
|
|
2220
|
+
<ul className="divide-y divide-hairline">
|
|
2221
|
+
{items.map((i, idx) => (
|
|
2222
|
+
<li key={i.id + "-" + idx} className="flex items-center justify-between py-4">
|
|
2223
|
+
<span className="font-medium">{i.name}</span>
|
|
2224
|
+
<span className="flex items-center gap-4">
|
|
2225
|
+
<span className="tabular-nums text-muted">{"$" + i.price.toFixed(2)}</span>
|
|
2226
|
+
<button type="button" onClick={() => remove(i.id)} aria-label="Remove" className="text-faint transition hover:text-fg">\u2715</button>
|
|
2227
|
+
</span>
|
|
2228
|
+
</li>
|
|
2229
|
+
))}
|
|
2230
|
+
</ul>
|
|
2231
|
+
<div className="mt-6 flex items-center justify-between border-t border-hairline pt-6">
|
|
2232
|
+
<button type="button" onClick={clear} className="text-sm text-muted transition hover:text-fg">Clear bag</button>
|
|
2233
|
+
<span className="text-lg font-bold">Total {"$" + total.toFixed(2)}</span>
|
|
2234
|
+
</div>
|
|
2235
|
+
<button type="button" className="mt-8 w-full rounded-full gradient-bg px-6 py-3 font-semibold text-black">Checkout</button>
|
|
2236
|
+
</div>
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
`;
|
|
2240
|
+
var settingsPage = (ctx) => `import type { Metadata } from "next";
|
|
2241
|
+
import { site } from "@/lib/site";
|
|
2242
|
+
import { DashboardShell } from "@/components/dashboard-shell";
|
|
2243
|
+
import { SettingsPanel } from "@/components/settings-panel";
|
|
2244
|
+
|
|
2245
|
+
export const metadata: Metadata = site.meta({ title: "Settings", path: "/settings", description: ${JSON.stringify(`Manage your ${ctx.template.siteName} preferences.`)} });
|
|
2246
|
+
|
|
2247
|
+
export default function Page() {
|
|
2248
|
+
return (
|
|
2249
|
+
<DashboardShell title="Settings" subtitle="Manage your preferences.">
|
|
2250
|
+
<SettingsPanel />
|
|
2251
|
+
</DashboardShell>
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
`;
|
|
2255
|
+
var settingsPanel = () => `"use client";
|
|
2256
|
+
|
|
2257
|
+
import { useLocalStorage } from "@lacspace/hooks";
|
|
2258
|
+
import { useTheme } from "@lacspace/theme";
|
|
2259
|
+
|
|
2260
|
+
export function SettingsPanel() {
|
|
2261
|
+
// Persisted to the browser with @lacspace/hooks.
|
|
2262
|
+
const [emailNotifs, setEmailNotifs] = useLocalStorage("settings:emailNotifs", true);
|
|
2263
|
+
const [weekly, setWeekly] = useLocalStorage("settings:weeklyDigest", false);
|
|
2264
|
+
const { theme, setTheme } = useTheme();
|
|
2265
|
+
|
|
2266
|
+
const toggle = (on: boolean) =>
|
|
2267
|
+
"relative h-6 w-11 rounded-full transition " + (on ? "gradient-bg" : "bg-panel");
|
|
2268
|
+
const knob = (on: boolean) =>
|
|
2269
|
+
"absolute top-0.5 h-5 w-5 rounded-full bg-white transition-all " + (on ? "left-[1.375rem]" : "left-0.5");
|
|
2270
|
+
|
|
2271
|
+
return (
|
|
2272
|
+
<div className="max-w-xl space-y-8">
|
|
2273
|
+
<section className="rounded-2xl border border-hairline bg-surface p-6">
|
|
2274
|
+
<h2 className="font-semibold">Notifications</h2>
|
|
2275
|
+
<p className="mt-1 text-sm text-muted">Saved to your browser with @lacspace/hooks (useLocalStorage).</p>
|
|
2276
|
+
<div className="mt-5 space-y-4">
|
|
2277
|
+
<label className="flex items-center justify-between gap-4">
|
|
2278
|
+
<span className="text-sm">Email notifications</span>
|
|
2279
|
+
<button type="button" role="switch" aria-checked={emailNotifs} onClick={() => setEmailNotifs((v) => !v)} className={toggle(emailNotifs)}>
|
|
2280
|
+
<span className={knob(emailNotifs)} />
|
|
2281
|
+
</button>
|
|
2282
|
+
</label>
|
|
2283
|
+
<label className="flex items-center justify-between gap-4">
|
|
2284
|
+
<span className="text-sm">Weekly digest</span>
|
|
2285
|
+
<button type="button" role="switch" aria-checked={weekly} onClick={() => setWeekly((v) => !v)} className={toggle(weekly)}>
|
|
2286
|
+
<span className={knob(weekly)} />
|
|
2287
|
+
</button>
|
|
2288
|
+
</label>
|
|
2289
|
+
</div>
|
|
2290
|
+
</section>
|
|
2291
|
+
<section className="rounded-2xl border border-hairline bg-surface p-6">
|
|
2292
|
+
<h2 className="font-semibold">Appearance</h2>
|
|
2293
|
+
<p className="mt-1 text-sm text-muted">Theme via @lacspace/theme.</p>
|
|
2294
|
+
<div className="mt-4 inline-flex rounded-full border border-hairline p-1">
|
|
2295
|
+
{["light", "dark", "system"].map((t) => (
|
|
2296
|
+
<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>
|
|
2297
|
+
))}
|
|
2298
|
+
</div>
|
|
2299
|
+
</section>
|
|
2300
|
+
</div>
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
`;
|
|
2304
|
+
function realPageFiles(ctx) {
|
|
2305
|
+
const k = ctx.template.key;
|
|
2306
|
+
const f = {};
|
|
2307
|
+
if (k === "personal") f["app/work/page.tsx"] = workPage(ctx);
|
|
2308
|
+
if (k === "business") {
|
|
2309
|
+
f["app/services/page.tsx"] = servicesPage(ctx);
|
|
2310
|
+
f["app/pricing/page.tsx"] = pricingPage(ctx);
|
|
2311
|
+
}
|
|
2312
|
+
if (k === "saas") {
|
|
2313
|
+
f["app/features/page.tsx"] = featuresPage(ctx);
|
|
2314
|
+
f["app/pricing/page.tsx"] = pricingPage(ctx);
|
|
2315
|
+
}
|
|
2316
|
+
if (k === "ecommerce") {
|
|
2317
|
+
f["app/shop/page.tsx"] = shopPage(ctx);
|
|
2318
|
+
f["app/cart/page.tsx"] = cartPage();
|
|
2319
|
+
f["components/product-grid.tsx"] = productGrid();
|
|
2320
|
+
f["components/cart-view.tsx"] = cartView();
|
|
2321
|
+
}
|
|
2322
|
+
if (k === "blog") f["app/topics/page.tsx"] = topicsPage();
|
|
2323
|
+
if (k === "docs") f["app/guides/page.tsx"] = guidesPage();
|
|
2324
|
+
if (k === "restaurant") f["app/menu/page.tsx"] = menuPage(ctx);
|
|
2325
|
+
if (k === "dashboard") {
|
|
2326
|
+
f["app/settings/page.tsx"] = settingsPage(ctx);
|
|
2327
|
+
f["components/settings-panel.tsx"] = settingsPanel();
|
|
2328
|
+
}
|
|
2329
|
+
return f;
|
|
2330
|
+
}
|
|
2331
|
+
function creativeData(ctx) {
|
|
2332
|
+
const n = ctx.template.siteName;
|
|
2333
|
+
const map = {
|
|
2334
|
+
personal: {
|
|
2335
|
+
emoji: "\u{1F44B}",
|
|
2336
|
+
chips: ["Design", "Code", "Ship"],
|
|
2337
|
+
showcaseTitle: "Design & build, end to end",
|
|
2338
|
+
showcaseLead: `${n} turns ideas into polished products \u2014 from the first sketch to the last pixel.`,
|
|
2339
|
+
bullets: ["10+ years shipping web & mobile", "Design systems that scale", "Obsessed with the details"],
|
|
2340
|
+
marqueeLabel: "Worked with",
|
|
2341
|
+
marquee: ["Aurora", "Northwind", "Lumen", "Harbor", "Cadence", "Meadow"],
|
|
2342
|
+
faq: [
|
|
2343
|
+
{ q: "Are you available for freelance?", a: "Yes \u2014 I take on a couple of projects each quarter. Reach out via the contact page." },
|
|
2344
|
+
{ q: "What do you work with?", a: "React, Next.js and TypeScript, with a lot of care for UX and performance." },
|
|
2345
|
+
{ q: "Do you do both design and development?", a: "Both \u2014 I can take a project from concept all the way to production." }
|
|
2346
|
+
]
|
|
2347
|
+
},
|
|
2348
|
+
business: {
|
|
2349
|
+
emoji: "\u{1F3E2}",
|
|
2350
|
+
chips: ["Strategy", "Design", "Launch"],
|
|
2351
|
+
showcaseTitle: "Everything you need to grow",
|
|
2352
|
+
showcaseLead: `${n} partners with you from strategy to launch \u2014 one senior team, accountable for outcomes.`,
|
|
2353
|
+
bullets: ["Senior team, no hand-offs", "Fixed timelines & clear pricing", "We sweat the details"],
|
|
2354
|
+
marqueeLabel: "Trusted by teams at",
|
|
2355
|
+
marquee: ["Northwind", "Globex", "Initech", "Umbrella", "Soylent", "Hooli"],
|
|
2356
|
+
faq: [
|
|
2357
|
+
{ q: "How do engagements work?", a: "We scope a clear plan, agree a fixed timeline, and ship in weekly increments." },
|
|
2358
|
+
{ q: "What does it cost?", a: "See our pricing page for plans, or contact us for a custom quote." },
|
|
2359
|
+
{ q: "How soon can we start?", a: "Usually within two weeks. Get in touch to check availability." }
|
|
2360
|
+
]
|
|
2361
|
+
},
|
|
2362
|
+
ecommerce: {
|
|
2363
|
+
emoji: "\u{1F6CD}\uFE0F",
|
|
2364
|
+
chips: ["Shop", "Bag", "Checkout"],
|
|
2365
|
+
showcaseTitle: "Thoughtfully made, delivered fast",
|
|
2366
|
+
showcaseLead: `${n} curates beautiful things and ships them worldwide \u2014 with easy returns, always.`,
|
|
2367
|
+
bullets: ["Free worldwide shipping", "30-day easy returns", "Sustainably sourced"],
|
|
2368
|
+
marqueeLabel: "Why shop with us",
|
|
2369
|
+
marquee: ["Free shipping", "Easy returns", "Secure checkout", "Ethically made", "Gift wrapping", "Loved by 10k+"],
|
|
2370
|
+
faq: [
|
|
2371
|
+
{ q: "How long is shipping?", a: "Most orders arrive in 3\u20135 business days with free worldwide shipping." },
|
|
2372
|
+
{ q: "What's your return policy?", a: "30 days, no questions asked \u2014 see the returns page for details." },
|
|
2373
|
+
{ q: "Is checkout secure?", a: "Yes \u2014 payments are encrypted and we never store your card details." }
|
|
2374
|
+
]
|
|
2375
|
+
},
|
|
2376
|
+
saas: {
|
|
2377
|
+
emoji: "\u26A1",
|
|
2378
|
+
chips: ["Analytics", "Billing", "API"],
|
|
2379
|
+
showcaseTitle: "The platform your team will love",
|
|
2380
|
+
showcaseLead: `${n} brings analytics, billing and automation into one fast, secure place.`,
|
|
2381
|
+
bullets: ["Set up in minutes", "SOC 2-ready security", "Scales with you"],
|
|
2382
|
+
marqueeLabel: "Powering teams at",
|
|
2383
|
+
marquee: ["Globex", "Initech", "Hooli", "Pied Piper", "Umbrella", "Stark"],
|
|
2384
|
+
faq: [
|
|
2385
|
+
{ q: "Is there a free plan?", a: "Yes \u2014 start free and upgrade when you're ready. See the pricing page." },
|
|
2386
|
+
{ q: "How is my data secured?", a: "Encryption in transit and at rest, audit logs, and SSO on higher plans." },
|
|
2387
|
+
{ q: "Can I self-host?", a: "Contact sales \u2014 we offer flexible deployment options." }
|
|
2388
|
+
]
|
|
2389
|
+
},
|
|
2390
|
+
blog: {
|
|
2391
|
+
emoji: "\u270D\uFE0F",
|
|
2392
|
+
chips: ["Essays", "Notes", "Stories"],
|
|
2393
|
+
showcaseTitle: "Words worth your time",
|
|
2394
|
+
showcaseLead: `${n} publishes essays and notes on building things that matter \u2014 no fluff.`,
|
|
2395
|
+
bullets: ["New pieces most weeks", "No clickbait, ever", "Written by practitioners"],
|
|
2396
|
+
marqueeLabel: "Topics we cover",
|
|
2397
|
+
marquee: ["Engineering", "Design", "Product", "Culture", "Tutorials", "Announcements"],
|
|
2398
|
+
faq: [
|
|
2399
|
+
{ q: "How often do you publish?", a: "A new piece most weeks \u2014 subscribe so you never miss one." },
|
|
2400
|
+
{ q: "Can I contribute?", a: "We love guest posts. Pitch us via the contact page." },
|
|
2401
|
+
{ q: "Is there a newsletter?", a: "Yes \u2014 head to the newsletter page to join." }
|
|
2402
|
+
]
|
|
2403
|
+
},
|
|
2404
|
+
docs: {
|
|
2405
|
+
emoji: "\u{1F4DA}",
|
|
2406
|
+
chips: ["Guides", "API", "Examples"],
|
|
2407
|
+
showcaseTitle: "Everything you need to build",
|
|
2408
|
+
showcaseLead: `${n} gives you guides, API references and copy-paste examples \u2014 all in one place.`,
|
|
2409
|
+
bullets: ["Quickstart in 5 minutes", "Searchable & versioned", "Real, runnable examples"],
|
|
2410
|
+
marqueeLabel: "What you'll find",
|
|
2411
|
+
marquee: ["Quickstart", "Guides", "API reference", "Examples", "Migrations", "Troubleshooting"],
|
|
2412
|
+
faq: [
|
|
2413
|
+
{ q: "Where do I start?", a: "Head to the guides for a 5-minute quickstart." },
|
|
2414
|
+
{ q: "Is there an API reference?", a: "Yes \u2014 the API page has the full reference." },
|
|
2415
|
+
{ q: "How do I report a docs issue?", a: "Use the contact page \u2014 we fix docs fast." }
|
|
2416
|
+
]
|
|
2417
|
+
},
|
|
2418
|
+
restaurant: {
|
|
2419
|
+
emoji: "\u{1F377}",
|
|
2420
|
+
chips: ["Menu", "Wine", "Reserve"],
|
|
2421
|
+
showcaseTitle: "Seasonal plates, natural wine",
|
|
2422
|
+
showcaseLead: `${n} serves ingredient-led plates and natural wine in a warm, low-lit room.`,
|
|
2423
|
+
bullets: ["Menu changes with the seasons", "Natural, low-intervention wine", "Walk-ins welcome"],
|
|
2424
|
+
marqueeLabel: "On the pass",
|
|
2425
|
+
marquee: ["Wood-fired", "Local produce", "Natural wine", "House-made pasta", "Seasonal", "Fresh sourdough"],
|
|
2426
|
+
faq: [
|
|
2427
|
+
{ q: "Do you take reservations?", a: "Yes \u2014 book via the reservations page. Walk-ins are welcome too." },
|
|
2428
|
+
{ q: "Any dietary options?", a: "Plenty \u2014 vegetarian and vegan plates change weekly. Just ask about allergies." },
|
|
2429
|
+
{ q: "When are you open?", a: "Wednesday to Sunday, 5pm till late." }
|
|
2430
|
+
]
|
|
2431
|
+
}
|
|
2432
|
+
};
|
|
2433
|
+
return map[ctx.template.key] ?? map.business;
|
|
2434
|
+
}
|
|
2435
|
+
var aurora = () => `export function Aurora() {
|
|
2436
|
+
return (
|
|
2437
|
+
<div aria-hidden className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[720px] overflow-hidden">
|
|
2438
|
+
<div className="absolute -left-20 -top-16 h-96 w-96 rounded-full opacity-30 blur-3xl animate-floaty" style={{ background: "radial-gradient(circle, var(--accent-from), transparent 70%)" }} />
|
|
2439
|
+
<div className="absolute -right-10 top-40 h-80 w-80 rounded-full opacity-25 blur-3xl animate-floaty-slow" style={{ background: "radial-gradient(circle, var(--accent-to), transparent 70%)" }} />
|
|
2440
|
+
<div className="absolute left-1/3 top-72 h-72 w-72 rounded-full opacity-20 blur-3xl animate-floaty" style={{ background: "radial-gradient(circle, var(--accent-to), transparent 70%)" }} />
|
|
2441
|
+
</div>
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
`;
|
|
2445
|
+
var heroArt = (ctx) => {
|
|
2446
|
+
const d = creativeData(ctx);
|
|
2447
|
+
const initials = ctx.template.siteName.trim().split(/\s+/).slice(0, 2).map((w) => (w[0] ?? "").toUpperCase()).join("") || "A";
|
|
2448
|
+
return `export function HeroArt() {
|
|
2449
|
+
return (
|
|
2450
|
+
<div className="relative mx-auto aspect-square w-full max-w-sm select-none">
|
|
2451
|
+
{/* orbiting rings */}
|
|
2452
|
+
<div className="absolute inset-0 rounded-full border border-hairline animate-spin-slow" />
|
|
2453
|
+
<div className="absolute inset-10 rounded-full border border-hairline animate-spin-slow" style={{ animationDirection: "reverse" }} />
|
|
2454
|
+
<div className="absolute inset-0 rounded-full opacity-40 blur-2xl gradient-bg" />
|
|
2455
|
+
{/* center badge with the platform initials */}
|
|
2456
|
+
<div className="absolute left-1/2 top-1/2 flex h-28 w-28 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-3xl gradient-bg text-4xl font-black text-black shadow-2xl animate-floaty">
|
|
2457
|
+
${JSON.stringify(initials)}
|
|
2458
|
+
</div>
|
|
2459
|
+
{/* floating labels */}
|
|
2460
|
+
<div className="absolute left-0 top-10 flex items-center gap-2 rounded-xl border border-hairline bg-app px-3 py-2 text-xs font-medium shadow-lg animate-floaty">
|
|
2461
|
+
<span>${d.emoji}</span> ${JSON.stringify(d.chips[0])}
|
|
2462
|
+
</div>
|
|
2463
|
+
<div className="absolute right-0 top-28 rounded-xl border border-hairline bg-app px-3 py-2 text-xs font-medium shadow-lg animate-floaty-slow">
|
|
2464
|
+
${JSON.stringify(d.chips[1])}
|
|
2465
|
+
</div>
|
|
2466
|
+
<div className="absolute bottom-8 left-6 rounded-xl border border-hairline bg-app px-3 py-2 text-xs font-medium shadow-lg animate-floaty" style={{ animationDelay: "1.2s" }}>
|
|
2467
|
+
${JSON.stringify(d.chips[2])}
|
|
2468
|
+
</div>
|
|
2469
|
+
</div>
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
`;
|
|
2473
|
+
};
|
|
2474
|
+
var showcaseSection = (ctx) => {
|
|
2475
|
+
const d = creativeData(ctx);
|
|
2476
|
+
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 ");
|
|
2477
|
+
return `<section className="mx-auto grid max-w-6xl items-center gap-12 px-6 py-20 md:grid-cols-2">
|
|
2478
|
+
<div>
|
|
2479
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-faint">${ctx.template.siteName}</p>
|
|
2480
|
+
<h2 className="mt-3 text-3xl font-bold sm:text-4xl">${d.showcaseTitle}</h2>
|
|
2481
|
+
<p className="mt-4 max-w-md text-lg text-muted">${d.showcaseLead}</p>
|
|
2482
|
+
<ul className="mt-6 space-y-3 text-sm">
|
|
2483
|
+
${bullets}
|
|
2484
|
+
</ul>
|
|
2485
|
+
</div>
|
|
2486
|
+
<HeroArt />
|
|
2487
|
+
</section>`;
|
|
2488
|
+
};
|
|
2489
|
+
var marqueeSection = (ctx) => {
|
|
2490
|
+
const d = creativeData(ctx);
|
|
2491
|
+
const chip = (t) => `<span className="rounded-full border border-hairline bg-surface px-5 py-2 text-sm font-medium text-muted">${t}</span>`;
|
|
2492
|
+
const track = d.marquee.map(chip).join("\n ");
|
|
2493
|
+
return `<section className="border-y border-hairline py-10">
|
|
2494
|
+
<p className="mb-6 text-center text-xs font-semibold uppercase tracking-widest text-faint">${d.marqueeLabel}</p>
|
|
2495
|
+
<div className="marquee-mask overflow-hidden">
|
|
2496
|
+
<div className="marquee-track gap-4">
|
|
2497
|
+
${track}
|
|
2498
|
+
${track}
|
|
2499
|
+
</div>
|
|
2500
|
+
</div>
|
|
2501
|
+
</section>`;
|
|
2502
|
+
};
|
|
2503
|
+
var faqSection = (ctx) => {
|
|
2504
|
+
const d = creativeData(ctx);
|
|
2505
|
+
const items = d.faq.map((f) => `<details className="group rounded-2xl border border-hairline bg-surface p-5">
|
|
2506
|
+
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 font-medium">
|
|
2507
|
+
${f.q}
|
|
2508
|
+
<span className="text-muted transition group-open:rotate-45">+</span>
|
|
2509
|
+
</summary>
|
|
2510
|
+
<p className="mt-3 text-sm leading-relaxed text-muted">${f.a}</p>
|
|
2511
|
+
</details>`).join("\n ");
|
|
2512
|
+
return `<section className="mx-auto max-w-3xl px-6 py-20">
|
|
2513
|
+
<h2 className="text-center text-3xl font-bold">Frequently asked</h2>
|
|
2514
|
+
<div className="mt-10 space-y-3">
|
|
2515
|
+
${items}
|
|
2516
|
+
</div>
|
|
2517
|
+
</section>`;
|
|
2518
|
+
};
|
|
1834
2519
|
function parseArgs(list) {
|
|
1835
2520
|
const a = { yes: false, install: true, git: true, pm: "npm", help: false };
|
|
1836
2521
|
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.
|
|
3
|
+
"version": "1.10.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": {
|