create-lacspace-app 1.6.0 → 1.8.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 +590 -158
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,6 +43,12 @@ var pkgJson = (ctx) => JSON.stringify({
|
|
|
43
43
|
"@lacspace/ui": "^1.0.0",
|
|
44
44
|
"@lacspace/form": "^1.0.0",
|
|
45
45
|
"@lacspace/validate": "^1.0.0",
|
|
46
|
+
// React Kit: dark/light theming (no-flash) + essential hooks + global
|
|
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",
|
|
46
52
|
// The blog & docs templates render Markdown with @lacspace/markdown.
|
|
47
53
|
...ctx.template.key === "blog" || ctx.template.key === "docs" ? { "@lacspace/markdown": "^1.0.0" } : {}
|
|
48
54
|
},
|
|
@@ -103,25 +109,57 @@ out
|
|
|
103
109
|
`;
|
|
104
110
|
var globalsCss = (ctx) => `@import "tailwindcss";
|
|
105
111
|
|
|
112
|
+
/* Class-based dark mode \u2014 toggled by @lacspace/theme's <ThemeProvider>. */
|
|
113
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
114
|
+
|
|
115
|
+
/* Semantic color tokens \u2192 Tailwind utilities (text-fg, text-muted, bg-surface,
|
|
116
|
+
border-hairline, \u2026). They resolve to the CSS vars below, which swap per theme. */
|
|
117
|
+
@theme inline {
|
|
118
|
+
--color-fg: var(--fg);
|
|
119
|
+
--color-muted: var(--muted);
|
|
120
|
+
--color-faint: var(--faint);
|
|
121
|
+
--color-surface: var(--surface);
|
|
122
|
+
--color-panel: var(--panel);
|
|
123
|
+
--color-hairline: var(--hairline);
|
|
124
|
+
}
|
|
125
|
+
|
|
106
126
|
:root {
|
|
107
127
|
--accent-from: ${ctx.template.accent[0]};
|
|
108
128
|
--accent-to: ${ctx.template.accent[1]};
|
|
129
|
+
/* Light theme */
|
|
130
|
+
--bg: #ffffff;
|
|
131
|
+
--fg: #0a0a0f;
|
|
132
|
+
--muted: #3f3f46;
|
|
133
|
+
--faint: #71717a;
|
|
134
|
+
--surface: rgb(0 0 0 / 0.03);
|
|
135
|
+
--panel: rgb(0 0 0 / 0.06);
|
|
136
|
+
--hairline: rgb(0 0 0 / 0.10);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.dark {
|
|
140
|
+
/* Dark theme (default) */
|
|
109
141
|
--bg: #0a0a0f;
|
|
110
|
-
--fg: #
|
|
142
|
+
--fg: #f5f5f7;
|
|
143
|
+
--muted: rgb(255 255 255 / 0.62);
|
|
144
|
+
--faint: rgb(255 255 255 / 0.42);
|
|
145
|
+
--surface: rgb(255 255 255 / 0.04);
|
|
146
|
+
--panel: rgb(255 255 255 / 0.08);
|
|
147
|
+
--hairline: rgb(255 255 255 / 0.10);
|
|
111
148
|
}
|
|
112
149
|
|
|
113
|
-
* { border-color:
|
|
150
|
+
* { border-color: var(--hairline); }
|
|
114
151
|
html { scroll-behavior: smooth; }
|
|
115
152
|
body {
|
|
116
153
|
background: var(--bg);
|
|
117
154
|
color: var(--fg);
|
|
118
155
|
-webkit-font-smoothing: antialiased;
|
|
119
156
|
text-rendering: optimizeLegibility;
|
|
157
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
120
158
|
}
|
|
121
159
|
|
|
122
|
-
::selection { background: var(--accent-to); color: #
|
|
160
|
+
::selection { background: var(--accent-to); color: #fff; }
|
|
123
161
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
124
|
-
::-webkit-scrollbar-thumb { background:
|
|
162
|
+
::-webkit-scrollbar-thumb { background: var(--panel); border-radius: 8px; }
|
|
125
163
|
:focus-visible { outline: 2px solid var(--accent-to); outline-offset: 2px; border-radius: 4px; }
|
|
126
164
|
|
|
127
165
|
.gradient-text {
|
|
@@ -130,6 +168,12 @@ body {
|
|
|
130
168
|
}
|
|
131
169
|
.gradient-bg { background: linear-gradient(120deg, var(--accent-from), var(--accent-to)); }
|
|
132
170
|
|
|
171
|
+
/* opaque theme background (for sticky chrome & chips) */
|
|
172
|
+
.bg-app { background: var(--bg); }
|
|
173
|
+
|
|
174
|
+
/* indeterminate progress bar (used by the "under development" pages) */
|
|
175
|
+
@keyframes loadbar { 0% { transform: translateX(-120%); } 100% { transform: translateX(340%); } }
|
|
176
|
+
|
|
133
177
|
/* soft entrance for content */
|
|
134
178
|
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
|
|
135
179
|
main > section, main > * { animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; }
|
|
@@ -166,11 +210,23 @@ export const site = defineSite({
|
|
|
166
210
|
ogImage: "/og", // \u2728 auto social-share images \u2014 see app/og/route.tsx
|
|
167
211
|
});
|
|
168
212
|
`;
|
|
169
|
-
var layout = (ctx) =>
|
|
213
|
+
var layout = (ctx) => {
|
|
214
|
+
const isDash = ctx.template.key === "dashboard";
|
|
215
|
+
const imports = isDash ? "" : `import { AnnouncementBar } from "@/components/announcement-bar";
|
|
216
|
+
import { SiteHeader } from "@/components/site-header";
|
|
217
|
+
import { SiteFooter } from "@/components/site-footer";
|
|
218
|
+
`;
|
|
219
|
+
const open = isDash ? "" : `<AnnouncementBar />
|
|
220
|
+
<SiteHeader />
|
|
221
|
+
`;
|
|
222
|
+
const close = isDash ? "" : `
|
|
223
|
+
<SiteFooter />`;
|
|
224
|
+
return `import type { Metadata } from "next";
|
|
170
225
|
import { Inter } from "next/font/google";
|
|
226
|
+
import { ThemeProvider } from "@lacspace/theme";
|
|
171
227
|
import { site } from "@/lib/site";
|
|
172
228
|
import { CommandMenu } from "@/components/command-menu";
|
|
173
|
-
import "./globals.css";
|
|
229
|
+
${imports}import "./globals.css";
|
|
174
230
|
|
|
175
231
|
const inter = Inter({ subsets: ["latin"], display: "swap" });
|
|
176
232
|
|
|
@@ -178,11 +234,14 @@ export const metadata: Metadata = site.meta({ title: ${JSON.stringify(ctx.templa
|
|
|
178
234
|
|
|
179
235
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
180
236
|
return (
|
|
181
|
-
<html lang="en" className={inter.className}>
|
|
237
|
+
<html lang="en" className={inter.className} suppressHydrationWarning>
|
|
182
238
|
<body className="antialiased">
|
|
183
|
-
{/* \u2728
|
|
184
|
-
<
|
|
185
|
-
|
|
239
|
+
{/* \u2728 Dark / light / system theming with a built-in no-flash script \u2014 @lacspace/theme */}
|
|
240
|
+
<ThemeProvider defaultTheme="dark">
|
|
241
|
+
{/* \u2728 Press \u2318K / Ctrl-K anywhere \u2014 powered by @lacspace/ui */}
|
|
242
|
+
<CommandMenu />
|
|
243
|
+
${open}{children}${close}
|
|
244
|
+
</ThemeProvider>
|
|
186
245
|
<script
|
|
187
246
|
type="application/ld+json"
|
|
188
247
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(site.rootJsonLd()) }}
|
|
@@ -192,6 +251,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
192
251
|
);
|
|
193
252
|
}
|
|
194
253
|
`;
|
|
254
|
+
};
|
|
195
255
|
var ogRoute = (ctx) => `import { ImageResponse } from "next/og";
|
|
196
256
|
import { ogCard } from "@lacspace/og";
|
|
197
257
|
import { site } from "@/lib/site";
|
|
@@ -226,7 +286,7 @@ export default function NotFound() {
|
|
|
226
286
|
return (
|
|
227
287
|
<main className="flex min-h-screen flex-col items-center justify-center gap-6 px-6 text-center">
|
|
228
288
|
<div className="text-8xl font-black gradient-text">404</div>
|
|
229
|
-
<p className="text-lg text-
|
|
289
|
+
<p className="text-lg text-muted">This page wandered off.</p>
|
|
230
290
|
<Link href="/" className="gradient-bg rounded-full px-6 py-3 font-semibold text-black">Back home</Link>
|
|
231
291
|
</main>
|
|
232
292
|
);
|
|
@@ -262,6 +322,9 @@ boring-but-essential stuff already done. Here's what's in the box.
|
|
|
262
322
|
- **\u2728 Dynamic OG images** \u2014 every page auto-generates a social-share card at \`/og\` (\`@lacspace/og\`, auto-fitting titles). Share a link and see.
|
|
263
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.
|
|
264
324
|
- **\u2728 A \u2318K command palette** \u2014 press \`\u2318K\` / \`Ctrl-K\` anywhere, powered by \`@lacspace/ui\`. Also try \`<Reveal>\`, \`<Counter>\`, \`<GradientText>\`, \`<TiltCard>\`, \`<Marquee>\`, \`<Typewriter>\`.
|
|
325
|
+
- **\u2728 Dark / light / system theme** \u2014 a no-flash toggle in the header via \`@lacspace/theme\` + \`@lacspace/hooks\`. The whole template is theme-aware.
|
|
326
|
+
- **\u2728 Multiple pages + auto header & footer** \u2014 the nav and a full footer are generated from one page map. 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\`.
|
|
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).
|
|
265
328
|
- **\u2728 Per-page SEO** \u2014 see \`app/about\` & \`app/contact\`: one \`site.meta()\` call gives each route its own title, canonical & OG image.
|
|
266
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.
|
|
267
330
|
- **Security headers** \u2014 HSTS, CSP, X-Frame-Options and more, via \`next.config.mjs\`.
|
|
@@ -339,21 +402,11 @@ Open [http://localhost:3000](http://localhost:3000). Edit \`app/page.tsx\` and \
|
|
|
339
402
|
|
|
340
403
|
Built with [Lacspace](https://lacspace.com/packages).
|
|
341
404
|
`;
|
|
342
|
-
var NAV = (name, links) => `<header className="sticky top-0 z-40 backdrop-blur border-b border-white/10">
|
|
343
|
-
<nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
|
344
|
-
<span className="text-lg font-black gradient-text">${name}</span>
|
|
345
|
-
<div className="hidden gap-8 text-sm text-white/60 sm:flex">
|
|
346
|
-
${links.map((l) => `<a href="#${l.toLowerCase()}" className="hover:text-white">${l}</a>`).join("\n ")}
|
|
347
|
-
</div>
|
|
348
|
-
</nav>
|
|
349
|
-
</header>`;
|
|
350
|
-
var FOOTER = (name) => `<footer className="border-t border-white/10 py-10 text-center text-sm text-white/40">
|
|
351
|
-
\xA9 {new Date().getFullYear()} ${name}. Built with{" "}
|
|
352
|
-
<a href="https://lacspace.com/packages" className="text-white/70 hover:text-white">Lacspace</a>.
|
|
353
|
-
</footer>`;
|
|
354
405
|
function homePage(ctx) {
|
|
355
406
|
const n = ctx.template.siteName;
|
|
407
|
+
if (ctx.template.key === "dashboard") return dashboardHome(ctx);
|
|
356
408
|
const shell = (inner) => `import { site } from "@/lib/site";
|
|
409
|
+
import { LiveStats } from "@/components/live-stats";
|
|
357
410
|
|
|
358
411
|
// \u2728 Self-canonical home page \u2014 one line, full SEO (title, canonical, OG, Twitter).
|
|
359
412
|
export const metadata = site.meta({ title: ${JSON.stringify(n)}, path: "/" });
|
|
@@ -362,40 +415,38 @@ export default function Home() {
|
|
|
362
415
|
return (
|
|
363
416
|
<main className="min-h-screen">
|
|
364
417
|
${inner}
|
|
418
|
+
${builtWithSection(ctx)}
|
|
365
419
|
</main>
|
|
366
420
|
);
|
|
367
421
|
}
|
|
368
422
|
`;
|
|
369
423
|
if (ctx.template.key === "personal") {
|
|
370
|
-
return shell(
|
|
371
|
-
|
|
372
|
-
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-white/40">Portfolio</p>
|
|
424
|
+
return shell(`<section className="mx-auto max-w-3xl px-6 py-28 text-center">
|
|
425
|
+
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-faint">Portfolio</p>
|
|
373
426
|
<h1 className="text-5xl font-black leading-tight sm:text-7xl">Hi, I'm <span className="gradient-text">${n}</span>.</h1>
|
|
374
|
-
<p className="mx-auto mt-6 max-w-xl text-lg text-
|
|
427
|
+
<p className="mx-auto mt-6 max-w-xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
375
428
|
<div className="mt-10 flex justify-center gap-4">
|
|
376
429
|
<a href="#work" className="gradient-bg rounded-full px-6 py-3 font-semibold text-black">View my work</a>
|
|
377
|
-
<a href="#contact" className="rounded-full border border-
|
|
430
|
+
<a href="#contact" className="rounded-full border border-hairline px-6 py-3 font-semibold hover:bg-surface">Get in touch</a>
|
|
378
431
|
</div>
|
|
379
432
|
</section>
|
|
380
433
|
<section id="work" className="mx-auto max-w-6xl px-6 py-16">
|
|
381
434
|
<h2 className="mb-10 text-3xl font-bold">Selected work</h2>
|
|
382
435
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
383
436
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
|
384
|
-
<div key={i} className="group rounded-2xl border border-
|
|
437
|
+
<div key={i} className="group rounded-2xl border border-hairline bg-surface p-6 transition hover:-translate-y-1 hover:border-hairline">
|
|
385
438
|
<div className="mb-4 h-32 rounded-xl gradient-bg opacity-80" />
|
|
386
439
|
<h3 className="font-semibold">Project {i}</h3>
|
|
387
|
-
<p className="mt-1 text-sm text-
|
|
440
|
+
<p className="mt-1 text-sm text-muted">A short description of what you built and the impact it had.</p>
|
|
388
441
|
</div>
|
|
389
442
|
))}
|
|
390
443
|
</div>
|
|
391
|
-
</section
|
|
392
|
-
${FOOTER(n)}`);
|
|
444
|
+
</section>`);
|
|
393
445
|
}
|
|
394
446
|
if (ctx.template.key === "business") {
|
|
395
|
-
return shell(
|
|
396
|
-
<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
447
|
+
return shell(`<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
397
448
|
<h1 className="text-5xl font-black leading-tight sm:text-6xl">We build products <span className="gradient-text">that grow businesses</span>.</h1>
|
|
398
|
-
<p className="mx-auto mt-6 max-w-2xl text-lg text-
|
|
449
|
+
<p className="mx-auto mt-6 max-w-2xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
399
450
|
<a href="#contact" className="mt-10 inline-block gradient-bg rounded-full px-8 py-3 font-semibold text-black">Start a project</a>
|
|
400
451
|
</section>
|
|
401
452
|
<section id="services" className="mx-auto max-w-6xl px-6 py-16">
|
|
@@ -406,25 +457,23 @@ export default function Home() {
|
|
|
406
457
|
{ t: "Design", d: "Brand and product design that people remember." },
|
|
407
458
|
{ t: "Engineering", d: "Fast, reliable software built to last." },
|
|
408
459
|
].map((s) => (
|
|
409
|
-
<div key={s.t} className="rounded-2xl border border-
|
|
460
|
+
<div key={s.t} className="rounded-2xl border border-hairline bg-surface p-8">
|
|
410
461
|
<h3 className="text-xl font-bold gradient-text">{s.t}</h3>
|
|
411
|
-
<p className="mt-2 text-
|
|
462
|
+
<p className="mt-2 text-muted">{s.d}</p>
|
|
412
463
|
</div>
|
|
413
464
|
))}
|
|
414
465
|
</div>
|
|
415
466
|
</section>
|
|
416
467
|
<section id="contact" className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
417
468
|
<h2 className="text-3xl font-bold">Let's work together</h2>
|
|
418
|
-
<p className="mt-3 text-
|
|
419
|
-
<a href="mailto:hello@example.com" className="mt-8 inline-block rounded-full border border-
|
|
420
|
-
</section
|
|
421
|
-
${FOOTER(n)}`);
|
|
469
|
+
<p className="mt-3 text-muted">Tell us about your project and we'll get back within a day.</p>
|
|
470
|
+
<a href="mailto:hello@example.com" className="mt-8 inline-block rounded-full border border-hairline px-8 py-3 font-semibold hover:bg-surface">hello@example.com</a>
|
|
471
|
+
</section>`);
|
|
422
472
|
}
|
|
423
473
|
if (ctx.template.key === "ecommerce") {
|
|
424
|
-
return shell(
|
|
425
|
-
<section className="mx-auto max-w-5xl px-6 py-24 text-center">
|
|
474
|
+
return shell(`<section className="mx-auto max-w-5xl px-6 py-24 text-center">
|
|
426
475
|
<h1 className="text-5xl font-black leading-tight sm:text-6xl"><span className="gradient-text">${n}</span></h1>
|
|
427
|
-
<p className="mx-auto mt-6 max-w-xl text-lg text-
|
|
476
|
+
<p className="mx-auto mt-6 max-w-xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
428
477
|
<a href="#shop" className="mt-10 inline-block gradient-bg rounded-full px-8 py-3 font-semibold text-black">Shop the collection</a>
|
|
429
478
|
</section>
|
|
430
479
|
<section id="shop" className="mx-auto max-w-6xl px-6 py-16">
|
|
@@ -434,32 +483,30 @@ export default function Home() {
|
|
|
434
483
|
{ n: "Aurora Lamp", p: "$89" }, { n: "Terra Mug", p: "$24" },
|
|
435
484
|
{ n: "Linen Throw", p: "$65" }, { n: "Oak Stand", p: "$120" },
|
|
436
485
|
].map((prod) => (
|
|
437
|
-
<div key={prod.n} className="group rounded-2xl border border-
|
|
486
|
+
<div key={prod.n} className="group rounded-2xl border border-hairline bg-surface p-4 transition hover:-translate-y-1">
|
|
438
487
|
<div className="mb-4 aspect-square rounded-xl gradient-bg opacity-80" />
|
|
439
488
|
<div className="flex items-center justify-between">
|
|
440
489
|
<h3 className="font-semibold">{prod.n}</h3>
|
|
441
|
-
<span className="text-
|
|
490
|
+
<span className="text-muted">{prod.p}</span>
|
|
442
491
|
</div>
|
|
443
|
-
<button className="mt-3 w-full rounded-lg border border-
|
|
492
|
+
<button className="mt-3 w-full rounded-lg border border-hairline py-2 text-sm font-medium hover:bg-surface">Add to cart</button>
|
|
444
493
|
</div>
|
|
445
494
|
))}
|
|
446
495
|
</div>
|
|
447
|
-
</section
|
|
448
|
-
${FOOTER(n)}`);
|
|
496
|
+
</section>`);
|
|
449
497
|
}
|
|
450
498
|
if (ctx.template.key === "blog") {
|
|
451
|
-
return shell(
|
|
452
|
-
|
|
453
|
-
<p className="mb-3 text-sm font-semibold uppercase tracking-widest text-white/40">The Journal</p>
|
|
499
|
+
return shell(`<section className="mx-auto max-w-3xl px-6 py-24">
|
|
500
|
+
<p className="mb-3 text-sm font-semibold uppercase tracking-widest text-faint">The Journal</p>
|
|
454
501
|
<h1 className="text-5xl font-black leading-tight sm:text-6xl gradient-text">${n}</h1>
|
|
455
|
-
<p className="mt-6 text-lg text-
|
|
502
|
+
<p className="mt-6 text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
456
503
|
</section>
|
|
457
504
|
<section id="latest" className="mx-auto max-w-5xl px-6 pb-8">
|
|
458
|
-
<a href="#" className="group block rounded-3xl border border-
|
|
505
|
+
<a href="#" className="group block rounded-3xl border border-hairline bg-surface p-8 transition hover:border-hairline">
|
|
459
506
|
<div className="mb-6 h-56 rounded-2xl gradient-bg opacity-80" />
|
|
460
|
-
<span className="text-xs font-semibold uppercase tracking-wider text-
|
|
507
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-faint">Featured</span>
|
|
461
508
|
<h2 className="mt-2 text-3xl font-bold group-hover:opacity-90">The one thing every product needs before launch</h2>
|
|
462
|
-
<p className="mt-2 text-
|
|
509
|
+
<p className="mt-2 text-muted">A short, punchy dek that pulls the reader into the piece and makes them want more.</p>
|
|
463
510
|
</a>
|
|
464
511
|
</section>
|
|
465
512
|
<section className="mx-auto max-w-5xl px-6 py-12">
|
|
@@ -467,22 +514,20 @@ export default function Home() {
|
|
|
467
514
|
{[1, 2, 3, 4].map((i) => (
|
|
468
515
|
<a key={i} href="#" className="group">
|
|
469
516
|
<div className="mb-4 h-40 rounded-xl gradient-bg opacity-70" />
|
|
470
|
-
<span className="text-xs uppercase tracking-wider text-
|
|
517
|
+
<span className="text-xs uppercase tracking-wider text-faint">Essay</span>
|
|
471
518
|
<h3 className="mt-1 text-xl font-semibold group-hover:opacity-90">A thoughtful headline for post {i}</h3>
|
|
472
|
-
<p className="mt-1 text-sm text-
|
|
519
|
+
<p className="mt-1 text-sm text-muted">Two lines of supporting copy to set the scene for the reader.</p>
|
|
473
520
|
</a>
|
|
474
521
|
))}
|
|
475
522
|
</div>
|
|
476
|
-
</section
|
|
477
|
-
${FOOTER(n)}`);
|
|
523
|
+
</section>`);
|
|
478
524
|
}
|
|
479
525
|
if (ctx.template.key === "docs") {
|
|
480
|
-
return shell(
|
|
481
|
-
<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
526
|
+
return shell(`<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
482
527
|
<h1 className="text-5xl font-black leading-tight sm:text-6xl"><span className="gradient-text">${n}</span></h1>
|
|
483
|
-
<p className="mx-auto mt-6 max-w-2xl text-lg text-
|
|
484
|
-
<div className="mt-8 inline-flex items-center gap-3 rounded-lg border border-
|
|
485
|
-
<span className="text-
|
|
528
|
+
<p className="mx-auto mt-6 max-w-2xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
529
|
+
<div className="mt-8 inline-flex items-center gap-3 rounded-lg border border-hairline bg-panel px-4 py-3 font-mono text-sm text-muted">
|
|
530
|
+
<span className="text-faint">$</span> npm install @acme/sdk
|
|
486
531
|
</div>
|
|
487
532
|
</section>
|
|
488
533
|
<section id="guides" className="mx-auto max-w-6xl px-6 py-12">
|
|
@@ -492,60 +537,19 @@ export default function Home() {
|
|
|
492
537
|
{ t: "Guides", d: "Task-focused walkthroughs for the common paths." },
|
|
493
538
|
{ t: "API reference", d: "Every endpoint, typed, with copy-paste examples." },
|
|
494
539
|
].map((c) => (
|
|
495
|
-
<a key={c.t} href="#" className="rounded-2xl border border-
|
|
540
|
+
<a key={c.t} href="#" className="rounded-2xl border border-hairline bg-surface p-8 transition hover:border-hairline">
|
|
496
541
|
<h3 className="text-xl font-bold gradient-text">{c.t}</h3>
|
|
497
|
-
<p className="mt-2 text-
|
|
542
|
+
<p className="mt-2 text-muted">{c.d}</p>
|
|
498
543
|
</a>
|
|
499
544
|
))}
|
|
500
545
|
</div>
|
|
501
|
-
</section
|
|
502
|
-
${FOOTER(n)}`);
|
|
503
|
-
}
|
|
504
|
-
if (ctx.template.key === "dashboard") {
|
|
505
|
-
return shell(`<div className="flex min-h-screen">
|
|
506
|
-
<aside className="hidden w-56 shrink-0 border-r border-white/10 p-6 md:block">
|
|
507
|
-
<div className="mb-8 text-lg font-black gradient-text">${n}</div>
|
|
508
|
-
<nav className="space-y-1 text-sm text-white/60">
|
|
509
|
-
{["Overview", "Analytics", "Customers", "Settings"].map((l) => (
|
|
510
|
-
<a key={l} href="#" className="block rounded-lg px-3 py-2 hover:bg-white/5 hover:text-white">{l}</a>
|
|
511
|
-
))}
|
|
512
|
-
</nav>
|
|
513
|
-
</aside>
|
|
514
|
-
<main className="flex-1 p-6 md:p-10">
|
|
515
|
-
<h1 className="text-2xl font-bold">Overview</h1>
|
|
516
|
-
<p className="mt-1 text-white/50">${ctx.template.siteDescription}</p>
|
|
517
|
-
<div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
518
|
-
{[
|
|
519
|
-
{ l: "Revenue", v: "$48.2k", c: "+12%" }, { l: "Users", v: "12,480", c: "+3.4%" },
|
|
520
|
-
{ l: "Orders", v: "1,204", c: "+8%" }, { l: "Churn", v: "1.2%", c: "-0.3%" },
|
|
521
|
-
].map((s) => (
|
|
522
|
-
<div key={s.l} className="rounded-2xl border border-white/10 bg-white/[0.02] p-5">
|
|
523
|
-
<div className="text-sm text-white/50">{s.l}</div>
|
|
524
|
-
<div className="mt-1 text-2xl font-bold">{s.v}</div>
|
|
525
|
-
<div className="mt-1 text-xs text-emerald-400">{s.c}</div>
|
|
526
|
-
</div>
|
|
527
|
-
))}
|
|
528
|
-
</div>
|
|
529
|
-
<div className="mt-6 rounded-2xl border border-white/10 bg-white/[0.02] p-6">
|
|
530
|
-
<div className="mb-4 font-semibold">Recent activity</div>
|
|
531
|
-
<div className="space-y-3">
|
|
532
|
-
{[1, 2, 3, 4].map((i) => (
|
|
533
|
-
<div key={i} className="flex items-center justify-between border-b border-white/5 pb-3 text-sm">
|
|
534
|
-
<span className="text-white/70">Event #{i}</span>
|
|
535
|
-
<span className="text-white/40">just now</span>
|
|
536
|
-
</div>
|
|
537
|
-
))}
|
|
538
|
-
</div>
|
|
539
|
-
</div>
|
|
540
|
-
</main>
|
|
541
|
-
</div>`);
|
|
546
|
+
</section>`);
|
|
542
547
|
}
|
|
543
548
|
if (ctx.template.key === "restaurant") {
|
|
544
|
-
return shell(
|
|
545
|
-
|
|
546
|
-
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-white/40">Est. 2026</p>
|
|
549
|
+
return shell(`<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
550
|
+
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-faint">Est. 2026</p>
|
|
547
551
|
<h1 className="text-5xl font-black leading-tight sm:text-7xl gradient-text">${n}</h1>
|
|
548
|
-
<p className="mx-auto mt-6 max-w-xl text-lg text-
|
|
552
|
+
<p className="mx-auto mt-6 max-w-xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
549
553
|
<a href="#reserve" className="mt-10 inline-block gradient-bg rounded-full px-8 py-3 font-semibold text-black">Reserve a table</a>
|
|
550
554
|
</section>
|
|
551
555
|
<section id="menu" className="mx-auto max-w-4xl px-6 py-16">
|
|
@@ -555,10 +559,10 @@ export default function Home() {
|
|
|
555
559
|
{ n: "Charred leeks", p: "$14" }, { n: "Handmade tagliatelle", p: "$22" },
|
|
556
560
|
{ n: "Wood-fired trout", p: "$28" }, { n: "Olive oil cake", p: "$11" },
|
|
557
561
|
].map((d) => (
|
|
558
|
-
<div key={d.n} className="flex items-baseline justify-between gap-4 border-b border-
|
|
562
|
+
<div key={d.n} className="flex items-baseline justify-between gap-4 border-b border-hairline pb-4">
|
|
559
563
|
<div>
|
|
560
564
|
<h3 className="text-lg font-semibold">{d.n}</h3>
|
|
561
|
-
<p className="text-sm text-
|
|
565
|
+
<p className="text-sm text-muted">A short, mouth-watering description of the dish.</p>
|
|
562
566
|
</div>
|
|
563
567
|
<span className="shrink-0 gradient-text font-bold">{d.p}</span>
|
|
564
568
|
</div>
|
|
@@ -567,19 +571,17 @@ export default function Home() {
|
|
|
567
571
|
</section>
|
|
568
572
|
<section id="reserve" className="mx-auto max-w-2xl px-6 py-24 text-center">
|
|
569
573
|
<h2 className="text-3xl font-bold">Join us</h2>
|
|
570
|
-
<p className="mt-3 text-
|
|
571
|
-
<a href="tel:+10000000000" className="mt-8 inline-block rounded-full border border-
|
|
572
|
-
</section
|
|
573
|
-
${FOOTER(n)}`);
|
|
574
|
+
<p className="mt-3 text-muted">Open Wed\u2013Sun, 5pm till late. Walk-ins welcome; bookings recommended.</p>
|
|
575
|
+
<a href="tel:+10000000000" className="mt-8 inline-block rounded-full border border-hairline px-8 py-3 font-semibold hover:bg-surface">Call to book</a>
|
|
576
|
+
</section>`);
|
|
574
577
|
}
|
|
575
|
-
return shell(
|
|
576
|
-
|
|
577
|
-
<p className="mb-4 inline-block rounded-full border border-white/15 px-4 py-1 text-xs font-semibold text-white/60">New \xB7 v1.0</p>
|
|
578
|
+
return shell(`<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
579
|
+
<p className="mb-4 inline-block rounded-full border border-hairline px-4 py-1 text-xs font-semibold text-muted">New \xB7 v1.0</p>
|
|
578
580
|
<h1 className="text-5xl font-black leading-tight sm:text-6xl">Ship faster with <span className="gradient-text">${n}</span></h1>
|
|
579
|
-
<p className="mx-auto mt-6 max-w-2xl text-lg text-
|
|
581
|
+
<p className="mx-auto mt-6 max-w-2xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
580
582
|
<div className="mt-10 flex justify-center gap-4">
|
|
581
583
|
<a href="#pricing" className="gradient-bg rounded-full px-8 py-3 font-semibold text-black">Start free</a>
|
|
582
|
-
<a href="#features" className="rounded-full border border-
|
|
584
|
+
<a href="#features" className="rounded-full border border-hairline px-8 py-3 font-semibold hover:bg-surface">See features</a>
|
|
583
585
|
</div>
|
|
584
586
|
</section>
|
|
585
587
|
<section id="features" className="mx-auto max-w-6xl px-6 py-16">
|
|
@@ -589,21 +591,20 @@ export default function Home() {
|
|
|
589
591
|
{ t: "Secure", d: "Hardened headers and auth out of the box." },
|
|
590
592
|
{ t: "Scalable", d: "From your first user to your millionth." },
|
|
591
593
|
].map((f) => (
|
|
592
|
-
<div key={f.t} className="rounded-2xl border border-
|
|
594
|
+
<div key={f.t} className="rounded-2xl border border-hairline bg-surface p-8">
|
|
593
595
|
<h3 className="text-xl font-bold gradient-text">{f.t}</h3>
|
|
594
|
-
<p className="mt-2 text-
|
|
596
|
+
<p className="mt-2 text-muted">{f.d}</p>
|
|
595
597
|
</div>
|
|
596
598
|
))}
|
|
597
599
|
</div>
|
|
598
600
|
</section>
|
|
599
601
|
<section id="pricing" className="mx-auto max-w-md px-6 py-24 text-center">
|
|
600
|
-
<div className="rounded-3xl border border-
|
|
602
|
+
<div className="rounded-3xl border border-hairline bg-surface p-10">
|
|
601
603
|
<h2 className="text-3xl font-bold">Pro</h2>
|
|
602
|
-
<p className="mt-2 text-5xl font-black gradient-text">$29<span className="text-lg text-
|
|
604
|
+
<p className="mt-2 text-5xl font-black gradient-text">$29<span className="text-lg text-faint">/mo</span></p>
|
|
603
605
|
<a href="#" className="mt-8 inline-block w-full gradient-bg rounded-full px-8 py-3 font-semibold text-black">Get started</a>
|
|
604
606
|
</div>
|
|
605
|
-
</section
|
|
606
|
-
${FOOTER(n)}`);
|
|
607
|
+
</section>`);
|
|
607
608
|
}
|
|
608
609
|
var glyph = (ctx) => (ctx.template.siteName.trim()[0] ?? "A").toUpperCase();
|
|
609
610
|
var iconTsx = (ctx) => `import { ImageResponse } from "next/og";
|
|
@@ -655,9 +656,9 @@ export const metadata: Metadata = site.meta({
|
|
|
655
656
|
export default function AboutPage() {
|
|
656
657
|
return (
|
|
657
658
|
<main className="mx-auto max-w-3xl px-6 py-28">
|
|
658
|
-
<Link href="/" className="text-sm text-
|
|
659
|
+
<Link href="/" className="text-sm text-muted hover:text-fg">\u2190 Back home</Link>
|
|
659
660
|
<h1 className="mt-6 text-4xl font-black sm:text-5xl gradient-text">About</h1>
|
|
660
|
-
<p className="mt-6 text-lg leading-relaxed text-
|
|
661
|
+
<p className="mt-6 text-lg leading-relaxed text-muted">
|
|
661
662
|
This page already has its own SEO \u2014 a unique title, canonical URL, Open Graph tags and a
|
|
662
663
|
generated social image \u2014 from a single <code>site.meta()</code> call. Duplicate this file for
|
|
663
664
|
any new route and it just works.
|
|
@@ -666,6 +667,41 @@ export default function AboutPage() {
|
|
|
666
667
|
);
|
|
667
668
|
}
|
|
668
669
|
`;
|
|
670
|
+
var themeToggle = () => `"use client";
|
|
671
|
+
|
|
672
|
+
import { useTheme } from "@lacspace/theme";
|
|
673
|
+
import { useIsMounted } from "@lacspace/hooks";
|
|
674
|
+
|
|
675
|
+
/** A sun/moon button that flips between light and dark. */
|
|
676
|
+
export function ThemeToggle() {
|
|
677
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
678
|
+
const isMounted = useIsMounted();
|
|
679
|
+
const dark = resolvedTheme === "dark";
|
|
680
|
+
|
|
681
|
+
return (
|
|
682
|
+
<button
|
|
683
|
+
type="button"
|
|
684
|
+
aria-label="Toggle theme"
|
|
685
|
+
onClick={() => setTheme(dark ? "light" : "dark")}
|
|
686
|
+
className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-hairline text-muted transition hover:text-fg"
|
|
687
|
+
>
|
|
688
|
+
{/* Render a neutral placeholder until mounted to avoid a hydration mismatch. */}
|
|
689
|
+
{!isMounted() ? (
|
|
690
|
+
<span className="block h-4 w-4" />
|
|
691
|
+
) : dark ? (
|
|
692
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
693
|
+
<circle cx="12" cy="12" r="4" />
|
|
694
|
+
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
|
695
|
+
</svg>
|
|
696
|
+
) : (
|
|
697
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
698
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
699
|
+
</svg>
|
|
700
|
+
)}
|
|
701
|
+
</button>
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
`;
|
|
669
705
|
var commandMenu = (ctx) => `"use client";
|
|
670
706
|
import { useRouter } from "next/navigation";
|
|
671
707
|
import { CommandPalette } from "@lacspace/ui";
|
|
@@ -721,8 +757,8 @@ import { useActionState } from "react";
|
|
|
721
757
|
import { honeypotProps, timestampValue } from "@lacspace/form";
|
|
722
758
|
import { submitContact, type ContactState } from "@/app/actions";
|
|
723
759
|
|
|
724
|
-
const field = "w-full rounded-xl border border-
|
|
725
|
-
const label = "mb-1 block text-sm text-
|
|
760
|
+
const field = "w-full rounded-xl border border-hairline bg-surface px-4 py-3 outline-none focus:border-hairline";
|
|
761
|
+
const label = "mb-1 block text-sm text-muted";
|
|
726
762
|
const errCls = "mt-1 text-sm text-red-400";
|
|
727
763
|
|
|
728
764
|
export function ContactForm() {
|
|
@@ -730,7 +766,7 @@ export function ContactForm() {
|
|
|
730
766
|
|
|
731
767
|
if (state?.ok) {
|
|
732
768
|
return (
|
|
733
|
-
<p className="rounded-2xl border border-
|
|
769
|
+
<p className="rounded-2xl border border-hairline bg-surface p-8 text-center text-lg">
|
|
734
770
|
Thanks \u2014 we’ll be in touch! \u2705
|
|
735
771
|
</p>
|
|
736
772
|
);
|
|
@@ -786,7 +822,7 @@ export default function ContactPage() {
|
|
|
786
822
|
return (
|
|
787
823
|
<main className="mx-auto max-w-2xl px-6 py-24">
|
|
788
824
|
<h1 className="text-4xl font-black gradient-text sm:text-5xl">Get in touch</h1>
|
|
789
|
-
<p className="mt-4 text-
|
|
825
|
+
<p className="mt-4 text-muted">Have a question or a project in mind? Drop a message below.</p>
|
|
790
826
|
<div className="mt-10">
|
|
791
827
|
<ContactForm />
|
|
792
828
|
</div>
|
|
@@ -885,7 +921,7 @@ export function DocsSidebar({ nav }: { nav: { group: string; items: DocMeta[] }[
|
|
|
885
921
|
<nav className="flex flex-col gap-6 text-sm">
|
|
886
922
|
{nav.map((group) => (
|
|
887
923
|
<div key={group.group}>
|
|
888
|
-
<p className="mb-2 text-xs font-semibold uppercase tracking-widest text-
|
|
924
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-widest text-faint">{group.group}</p>
|
|
889
925
|
<ul className="flex flex-col gap-1">
|
|
890
926
|
{group.items.map((d) => {
|
|
891
927
|
const href = \`/docs/\${d.slug}\`;
|
|
@@ -894,7 +930,7 @@ export function DocsSidebar({ nav }: { nav: { group: string; items: DocMeta[] }[
|
|
|
894
930
|
<li key={d.slug}>
|
|
895
931
|
<Link
|
|
896
932
|
href={href}
|
|
897
|
-
className={\`block rounded-lg px-3 py-1.5 transition \${active ? "gradient-bg font-semibold text-black" : "text-
|
|
933
|
+
className={\`block rounded-lg px-3 py-1.5 transition \${active ? "gradient-bg font-semibold text-black" : "text-muted hover:bg-surface hover:text-fg"}\`}
|
|
898
934
|
>
|
|
899
935
|
{d.title}
|
|
900
936
|
</Link>
|
|
@@ -911,13 +947,17 @@ export function DocsSidebar({ nav }: { nav: { group: string; items: DocMeta[] }[
|
|
|
911
947
|
var docsLayout = () => `import Link from "next/link";
|
|
912
948
|
import { getDocNav } from "@/lib/docs";
|
|
913
949
|
import { DocsSidebar } from "@/components/docs-sidebar";
|
|
950
|
+
import { ThemeToggle } from "@/components/theme-toggle";
|
|
914
951
|
|
|
915
952
|
export default function DocsLayout({ children }: { children: React.ReactNode }) {
|
|
916
953
|
const nav = getDocNav();
|
|
917
954
|
return (
|
|
918
955
|
<div className="mx-auto grid max-w-6xl gap-10 px-6 py-16 md:grid-cols-[220px_1fr]">
|
|
919
956
|
<aside className="md:sticky md:top-24 md:h-fit">
|
|
920
|
-
<
|
|
957
|
+
<div className="mb-6 flex items-center justify-between">
|
|
958
|
+
<Link href="/docs" className="block text-lg font-black gradient-text">Docs</Link>
|
|
959
|
+
<ThemeToggle />
|
|
960
|
+
</div>
|
|
921
961
|
<DocsSidebar nav={nav} />
|
|
922
962
|
</aside>
|
|
923
963
|
<div className="min-w-0">{children}</div>
|
|
@@ -940,13 +980,13 @@ export default function DocsIndex() {
|
|
|
940
980
|
return (
|
|
941
981
|
<div>
|
|
942
982
|
<h1 className="text-4xl font-black gradient-text">Documentation</h1>
|
|
943
|
-
<p className="mt-4 text-
|
|
983
|
+
<p className="mt-4 text-muted">Everything you need to get started and go deep.</p>
|
|
944
984
|
<div className="mt-10 grid gap-4 sm:grid-cols-2">
|
|
945
985
|
{docs.map((d) => (
|
|
946
|
-
<Link key={d.slug} href={\`/docs/\${d.slug}\`} className="rounded-2xl border border-
|
|
947
|
-
<span className="text-xs font-semibold uppercase tracking-widest text-
|
|
986
|
+
<Link key={d.slug} href={\`/docs/\${d.slug}\`} className="rounded-2xl border border-hairline bg-surface p-5 transition hover:border-hairline">
|
|
987
|
+
<span className="text-xs font-semibold uppercase tracking-widest text-faint">{d.group}</span>
|
|
948
988
|
<h2 className="mt-1 text-lg font-bold">{d.title}</h2>
|
|
949
|
-
{d.description && <p className="mt-1 text-sm text-
|
|
989
|
+
{d.description && <p className="mt-1 text-sm text-muted">{d.description}</p>}
|
|
950
990
|
</Link>
|
|
951
991
|
))}
|
|
952
992
|
</div>
|
|
@@ -978,17 +1018,17 @@ export default async function DocPage({ params }: { params: Promise<{ slug: stri
|
|
|
978
1018
|
|
|
979
1019
|
return (
|
|
980
1020
|
<article>
|
|
981
|
-
<span className="text-xs font-semibold uppercase tracking-widest text-
|
|
1021
|
+
<span className="text-xs font-semibold uppercase tracking-widest text-faint">{doc.group}</span>
|
|
982
1022
|
<h1 className="mt-1 text-4xl font-black leading-tight">{doc.title}</h1>
|
|
983
|
-
{doc.description && <p className="mt-3 text-lg text-
|
|
1023
|
+
{doc.description && <p className="mt-3 text-lg text-muted">{doc.description}</p>}
|
|
984
1024
|
|
|
985
1025
|
{doc.toc.length > 2 && (
|
|
986
|
-
<div className="mt-8 rounded-xl border border-
|
|
987
|
-
<p className="mb-2 text-xs font-semibold uppercase tracking-widest text-
|
|
1026
|
+
<div className="mt-8 rounded-xl border border-hairline bg-surface p-4">
|
|
1027
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-widest text-faint">On this page</p>
|
|
988
1028
|
<ul className="flex flex-col gap-1 text-sm">
|
|
989
1029
|
{doc.toc.map((h) => (
|
|
990
1030
|
<li key={h.id} style={{ paddingLeft: (h.level - 1) * 12 }}>
|
|
991
|
-
<a href={\`#\${h.id}\`} className="text-
|
|
1031
|
+
<a href={\`#\${h.id}\`} className="text-muted hover:text-fg">{h.text}</a>
|
|
992
1032
|
</li>
|
|
993
1033
|
))}
|
|
994
1034
|
</ul>
|
|
@@ -997,9 +1037,9 @@ export default async function DocPage({ params }: { params: Promise<{ slug: stri
|
|
|
997
1037
|
|
|
998
1038
|
<div className="prose mt-10" dangerouslySetInnerHTML={{ __html: doc.html }} />
|
|
999
1039
|
|
|
1000
|
-
<nav className="mt-14 flex justify-between gap-4 border-t border-
|
|
1001
|
-
{prev ? <Link href={\`/docs/\${prev.slug}\`} className="text-
|
|
1002
|
-
{next ? <Link href={\`/docs/\${next.slug}\`} className="text-right text-
|
|
1040
|
+
<nav className="mt-14 flex justify-between gap-4 border-t border-hairline pt-8 text-sm">
|
|
1041
|
+
{prev ? <Link href={\`/docs/\${prev.slug}\`} className="text-muted hover:text-fg">← {prev.title}</Link> : <span />}
|
|
1042
|
+
{next ? <Link href={\`/docs/\${next.slug}\`} className="text-right text-muted hover:text-fg">{next.title} →</Link> : <span />}
|
|
1003
1043
|
</nav>
|
|
1004
1044
|
</article>
|
|
1005
1045
|
);
|
|
@@ -1208,17 +1248,17 @@ export default function Blog() {
|
|
|
1208
1248
|
return (
|
|
1209
1249
|
<main className="mx-auto max-w-3xl px-6 py-24">
|
|
1210
1250
|
<h1 className="text-4xl font-black gradient-text sm:text-5xl">Blog</h1>
|
|
1211
|
-
<p className="mt-4 text-
|
|
1251
|
+
<p className="mt-4 text-muted">Thoughts, notes and updates.</p>
|
|
1212
1252
|
<div className="mt-12 flex flex-col gap-8">
|
|
1213
1253
|
{posts.map((post) => (
|
|
1214
|
-
<Link key={post.slug} href={\`/blog/\${post.slug}\`} className="group rounded-2xl border border-
|
|
1215
|
-
{post.tag && <span className="text-xs font-semibold uppercase tracking-widest text-
|
|
1254
|
+
<Link key={post.slug} href={\`/blog/\${post.slug}\`} className="group rounded-2xl border border-hairline bg-surface p-6 transition hover:border-hairline">
|
|
1255
|
+
{post.tag && <span className="text-xs font-semibold uppercase tracking-widest text-faint">{post.tag}</span>}
|
|
1216
1256
|
<h2 className="mt-1 text-2xl font-bold group-hover:gradient-text">{post.title}</h2>
|
|
1217
|
-
<p className="mt-2 text-
|
|
1218
|
-
{post.date && <time className="mt-3 block text-sm text-
|
|
1257
|
+
<p className="mt-2 text-muted">{post.excerpt}</p>
|
|
1258
|
+
{post.date && <time className="mt-3 block text-sm text-faint">{new Date(post.date).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })}</time>}
|
|
1219
1259
|
</Link>
|
|
1220
1260
|
))}
|
|
1221
|
-
{posts.length === 0 && <p className="text-
|
|
1261
|
+
{posts.length === 0 && <p className="text-muted">No posts yet \u2014 add a Markdown file in <code>content/posts/</code>.</p>}
|
|
1222
1262
|
</div>
|
|
1223
1263
|
</main>
|
|
1224
1264
|
);
|
|
@@ -1265,10 +1305,10 @@ export default async function PostPage({ params }: { params: Promise<{ slug: str
|
|
|
1265
1305
|
|
|
1266
1306
|
return (
|
|
1267
1307
|
<main className="mx-auto max-w-2xl px-6 py-24">
|
|
1268
|
-
<Link href="/blog" className="text-sm text-
|
|
1308
|
+
<Link href="/blog" className="text-sm text-muted hover:text-fg">← All posts</Link>
|
|
1269
1309
|
<article className="mt-6">
|
|
1270
1310
|
<h1 className="text-4xl font-black leading-tight sm:text-5xl">{post.title}</h1>
|
|
1271
|
-
{post.date && <time className="mt-4 block text-sm text-
|
|
1311
|
+
{post.date && <time className="mt-4 block text-sm text-faint">{new Date(post.date).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })}</time>}
|
|
1272
1312
|
<div className="prose mt-10" dangerouslySetInnerHTML={{ __html: post.html }} />
|
|
1273
1313
|
</article>
|
|
1274
1314
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
|
@@ -1376,10 +1416,27 @@ function buildFiles(ctx) {
|
|
|
1376
1416
|
"app/actions.ts": actionsTs(),
|
|
1377
1417
|
"components/command-menu.tsx": commandMenu(ctx),
|
|
1378
1418
|
"components/contact-form.tsx": contactForm(),
|
|
1419
|
+
"components/theme-toggle.tsx": themeToggle(),
|
|
1420
|
+
// ✨ React Kit: global state (@lacspace/store) + data fetching (@lacspace/query).
|
|
1421
|
+
"lib/store.ts": uiStore(),
|
|
1422
|
+
"components/under-development.tsx": underDevelopmentComponent(),
|
|
1423
|
+
"components/live-stats.tsx": liveStats(),
|
|
1424
|
+
"app/api/stats/route.ts": statsRoute(),
|
|
1379
1425
|
".github/workflows/seo.yml": seoWorkflow(),
|
|
1380
1426
|
".env.example": envExample(),
|
|
1381
1427
|
"WELCOME.md": welcomeMd(ctx)
|
|
1382
1428
|
};
|
|
1429
|
+
const isDash = ctx.template.key === "dashboard";
|
|
1430
|
+
if (isDash) {
|
|
1431
|
+
files["components/dashboard-shell.tsx"] = dashboardShell(ctx);
|
|
1432
|
+
} else {
|
|
1433
|
+
files["components/site-header.tsx"] = siteHeader(ctx);
|
|
1434
|
+
files["components/site-footer.tsx"] = siteFooter(ctx);
|
|
1435
|
+
files["components/announcement-bar.tsx"] = announcementBar();
|
|
1436
|
+
}
|
|
1437
|
+
for (const page of stubPages(ctx)) {
|
|
1438
|
+
files[`app${page.path}/page.tsx`] = isDash ? dashboardStubPage(page) : underDevPage(page);
|
|
1439
|
+
}
|
|
1383
1440
|
if (isBlog) {
|
|
1384
1441
|
files["lib/posts.ts"] = postsLib();
|
|
1385
1442
|
files["app/blog/page.tsx"] = blogListPage(ctx);
|
|
@@ -1399,6 +1456,381 @@ function buildFiles(ctx) {
|
|
|
1399
1456
|
}
|
|
1400
1457
|
return files;
|
|
1401
1458
|
}
|
|
1459
|
+
function pagesFor(ctx) {
|
|
1460
|
+
const k = ctx.template.key;
|
|
1461
|
+
if (k === "dashboard") {
|
|
1462
|
+
return [
|
|
1463
|
+
{ path: "/analytics", label: "Analytics", nav: true, group: "Product" },
|
|
1464
|
+
{ path: "/customers", label: "Customers", nav: true, group: "Product" },
|
|
1465
|
+
{ path: "/billing", label: "Billing", nav: true, group: "Product" },
|
|
1466
|
+
{ path: "/settings", label: "Settings", nav: true, group: "Product" },
|
|
1467
|
+
{ path: "/help", label: "Help", group: "Resources" }
|
|
1468
|
+
];
|
|
1469
|
+
}
|
|
1470
|
+
const common = [
|
|
1471
|
+
{ path: "/about", label: "About", nav: true, group: "Company", real: true },
|
|
1472
|
+
{ path: "/contact", label: "Contact", nav: true, group: "Company", real: true },
|
|
1473
|
+
{ path: "/careers", label: "Careers", group: "Company" },
|
|
1474
|
+
{ path: "/privacy", label: "Privacy", group: "Resources" },
|
|
1475
|
+
{ path: "/terms", label: "Terms", group: "Resources" }
|
|
1476
|
+
];
|
|
1477
|
+
const specific = {
|
|
1478
|
+
personal: [
|
|
1479
|
+
{ path: "/work", label: "Work", nav: true, group: "Product" },
|
|
1480
|
+
{ path: "/blog", label: "Blog", nav: true, group: "Product" },
|
|
1481
|
+
{ path: "/uses", label: "Uses", group: "Resources" }
|
|
1482
|
+
],
|
|
1483
|
+
business: [
|
|
1484
|
+
{ path: "/services", label: "Services", nav: true, group: "Product" },
|
|
1485
|
+
{ path: "/work", label: "Work", nav: true, group: "Product" },
|
|
1486
|
+
{ path: "/pricing", label: "Pricing", nav: true, group: "Product" },
|
|
1487
|
+
{ path: "/faq", label: "FAQ", group: "Resources" }
|
|
1488
|
+
],
|
|
1489
|
+
ecommerce: [
|
|
1490
|
+
{ path: "/shop", label: "Shop", nav: true, group: "Product" },
|
|
1491
|
+
{ path: "/collections", label: "Collections", nav: true, group: "Product" },
|
|
1492
|
+
{ path: "/shipping", label: "Shipping", group: "Resources" },
|
|
1493
|
+
{ path: "/returns", label: "Returns", group: "Resources" }
|
|
1494
|
+
],
|
|
1495
|
+
saas: [
|
|
1496
|
+
{ path: "/features", label: "Features", nav: true, group: "Product" },
|
|
1497
|
+
{ path: "/pricing", label: "Pricing", nav: true, group: "Product" },
|
|
1498
|
+
{ path: "/integrations", label: "Integrations", group: "Product" },
|
|
1499
|
+
{ path: "/changelog", label: "Changelog", group: "Resources" }
|
|
1500
|
+
],
|
|
1501
|
+
blog: [
|
|
1502
|
+
{ path: "/blog", label: "Articles", nav: true, group: "Product", real: true },
|
|
1503
|
+
{ path: "/topics", label: "Topics", nav: true, group: "Product" },
|
|
1504
|
+
{ path: "/newsletter", label: "Newsletter", group: "Resources" }
|
|
1505
|
+
],
|
|
1506
|
+
docs: [
|
|
1507
|
+
{ path: "/docs", label: "Docs", nav: true, group: "Product", real: true },
|
|
1508
|
+
{ path: "/guides", label: "Guides", nav: true, group: "Product" },
|
|
1509
|
+
{ path: "/api-reference", label: "API", nav: true, group: "Product" },
|
|
1510
|
+
{ path: "/changelog", label: "Changelog", group: "Resources" }
|
|
1511
|
+
],
|
|
1512
|
+
restaurant: [
|
|
1513
|
+
{ path: "/menu", label: "Menu", nav: true, group: "Product" },
|
|
1514
|
+
{ path: "/reservations", label: "Reservations", nav: true, group: "Product" },
|
|
1515
|
+
{ path: "/gallery", label: "Gallery", nav: true, group: "Product" },
|
|
1516
|
+
{ path: "/events", label: "Private events", group: "Resources" }
|
|
1517
|
+
]
|
|
1518
|
+
};
|
|
1519
|
+
return [...specific[k] ?? [], ...common];
|
|
1520
|
+
}
|
|
1521
|
+
function stubPages(ctx) {
|
|
1522
|
+
return pagesFor(ctx).filter((p) => !p.real);
|
|
1523
|
+
}
|
|
1524
|
+
var linkLiteral = (pages) => pages.map((l) => `{ href: ${JSON.stringify(l.path)}, label: ${JSON.stringify(l.label)} }`).join(", ");
|
|
1525
|
+
var siteHeader = (ctx) => `"use client";
|
|
1526
|
+
|
|
1527
|
+
import Link from "next/link";
|
|
1528
|
+
import { useUI } from "@/lib/store";
|
|
1529
|
+
import { ThemeToggle } from "./theme-toggle";
|
|
1530
|
+
|
|
1531
|
+
const LINKS = [${linkLiteral(pagesFor(ctx).filter((p) => p.nav))}];
|
|
1532
|
+
|
|
1533
|
+
export function SiteHeader() {
|
|
1534
|
+
const open = useUI((s) => s.navOpen);
|
|
1535
|
+
const toggle = useUI((s) => s.toggleNav);
|
|
1536
|
+
const setOpen = useUI((s) => s.setNavOpen);
|
|
1537
|
+
return (
|
|
1538
|
+
<header className="sticky top-0 z-40 border-b border-hairline bg-app/90 backdrop-blur">
|
|
1539
|
+
<nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
|
1540
|
+
<Link href="/" className="text-lg font-black gradient-text">${ctx.template.siteName}</Link>
|
|
1541
|
+
<div className="hidden items-center gap-8 md:flex">
|
|
1542
|
+
{LINKS.map((l) => (
|
|
1543
|
+
<Link key={l.href} href={l.href} className="text-sm text-muted transition hover:text-fg">{l.label}</Link>
|
|
1544
|
+
))}
|
|
1545
|
+
<ThemeToggle />
|
|
1546
|
+
</div>
|
|
1547
|
+
<div className="flex items-center gap-2 md:hidden">
|
|
1548
|
+
<ThemeToggle />
|
|
1549
|
+
<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
|
+
<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
|
+
</button>
|
|
1552
|
+
</div>
|
|
1553
|
+
</nav>
|
|
1554
|
+
{open ? (
|
|
1555
|
+
<div className="border-t border-hairline md:hidden">
|
|
1556
|
+
<div className="mx-auto flex max-w-6xl flex-col gap-1 px-6 py-3">
|
|
1557
|
+
{LINKS.map((l) => (
|
|
1558
|
+
<Link key={l.href} href={l.href} onClick={() => setOpen(false)} className="rounded-lg px-3 py-2 text-muted transition hover:bg-surface hover:text-fg">{l.label}</Link>
|
|
1559
|
+
))}
|
|
1560
|
+
</div>
|
|
1561
|
+
</div>
|
|
1562
|
+
) : null}
|
|
1563
|
+
</header>
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
`;
|
|
1567
|
+
var siteFooter = (ctx) => {
|
|
1568
|
+
const pages = pagesFor(ctx);
|
|
1569
|
+
const groups = ["Product", "Company", "Resources"].map((title) => ({ title, items: pages.filter((p) => p.group === title) })).filter((g) => g.items.length > 0);
|
|
1570
|
+
const groupsLiteral = groups.map((g) => `{ title: ${JSON.stringify(g.title)}, links: [${linkLiteral(g.items)}] }`).join(", ");
|
|
1571
|
+
return `import Link from "next/link";
|
|
1572
|
+
|
|
1573
|
+
const GROUPS = [${groupsLiteral}];
|
|
1574
|
+
|
|
1575
|
+
export function SiteFooter() {
|
|
1576
|
+
return (
|
|
1577
|
+
<footer className="border-t border-hairline">
|
|
1578
|
+
<div className="mx-auto max-w-6xl px-6 py-14">
|
|
1579
|
+
<div className="grid gap-10 md:grid-cols-4">
|
|
1580
|
+
<div>
|
|
1581
|
+
<div className="text-lg font-black gradient-text">${ctx.template.siteName}</div>
|
|
1582
|
+
<p className="mt-3 max-w-xs text-sm text-muted">${ctx.template.siteDescription}</p>
|
|
1583
|
+
</div>
|
|
1584
|
+
{GROUPS.map((g) => (
|
|
1585
|
+
<div key={g.title}>
|
|
1586
|
+
<div className="text-sm font-semibold">{g.title}</div>
|
|
1587
|
+
<ul className="mt-3 space-y-2 text-sm text-muted">
|
|
1588
|
+
{g.links.map((l) => (
|
|
1589
|
+
<li key={l.href}><Link href={l.href} className="transition hover:text-fg">{l.label}</Link></li>
|
|
1590
|
+
))}
|
|
1591
|
+
</ul>
|
|
1592
|
+
</div>
|
|
1593
|
+
))}
|
|
1594
|
+
</div>
|
|
1595
|
+
<div className="mt-12 flex flex-col gap-3 border-t border-hairline pt-6 text-sm text-faint sm:flex-row sm:items-center sm:justify-between">
|
|
1596
|
+
<span>\xA9 {new Date().getFullYear()} ${ctx.template.siteName}. All rights reserved.</span>
|
|
1597
|
+
<span>Built with <a href="https://lacspace.com/packages" className="text-muted transition hover:text-fg">the Lacspace React Kit</a>.</span>
|
|
1598
|
+
</div>
|
|
1599
|
+
</div>
|
|
1600
|
+
</footer>
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
`;
|
|
1604
|
+
};
|
|
1605
|
+
var announcementBar = () => `"use client";
|
|
1606
|
+
|
|
1607
|
+
import { useAnnouncement } from "@/lib/store";
|
|
1608
|
+
import { useIsMounted } from "@lacspace/hooks";
|
|
1609
|
+
|
|
1610
|
+
export function AnnouncementBar() {
|
|
1611
|
+
const dismissed = useAnnouncement((s) => s.dismissed);
|
|
1612
|
+
const dismiss = useAnnouncement((s) => s.dismiss);
|
|
1613
|
+
const mounted = useIsMounted();
|
|
1614
|
+
|
|
1615
|
+
// Persisted state is client-only \u2014 wait for mount to avoid a hydration flash.
|
|
1616
|
+
if (!mounted() || dismissed) return null;
|
|
1617
|
+
|
|
1618
|
+
return (
|
|
1619
|
+
<div className="relative gradient-bg px-10 py-2 text-center text-sm font-medium text-black">
|
|
1620
|
+
\u2728 Built with the Lacspace React Kit \u2014{" "}
|
|
1621
|
+
<a href="https://lacspace.com/packages" className="underline underline-offset-2">explore the packages</a>
|
|
1622
|
+
<button type="button" aria-label="Dismiss" onClick={dismiss} className="absolute right-3 top-1/2 -translate-y-1/2 text-lg leading-none text-black/60 hover:text-black">\xD7</button>
|
|
1623
|
+
</div>
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
`;
|
|
1627
|
+
var underDevelopmentComponent = () => `"use client";
|
|
1628
|
+
|
|
1629
|
+
import Link from "next/link";
|
|
1630
|
+
|
|
1631
|
+
/** A branded placeholder for pages that don't have content yet. */
|
|
1632
|
+
export function UnderDevelopment({ title = "This page", path }: { title?: string; path?: string }) {
|
|
1633
|
+
return (
|
|
1634
|
+
<div className="mx-auto flex min-h-[70vh] max-w-2xl flex-col items-center justify-center px-6 py-20 text-center">
|
|
1635
|
+
<div className="mb-6 inline-flex h-16 w-16 items-center justify-center rounded-2xl gradient-bg text-3xl">\u{1F6A7}</div>
|
|
1636
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-muted">Under development</p>
|
|
1637
|
+
<h1 className="mt-3 text-4xl font-bold tracking-tight"><span className="gradient-text">{title}</span> is coming soon</h1>
|
|
1638
|
+
<p className="mt-4 max-w-md text-muted">We're putting the finishing touches on this page. Check back shortly \u2014 or head back home in the meantime.</p>
|
|
1639
|
+
<div className="mt-8 h-1.5 w-full max-w-xs overflow-hidden rounded-full bg-surface">
|
|
1640
|
+
<div className="h-full w-1/3 gradient-bg" style={{ animation: "loadbar 1.8s ease-in-out infinite" }} />
|
|
1641
|
+
</div>
|
|
1642
|
+
<div className="mt-10 flex flex-wrap items-center justify-center gap-3">
|
|
1643
|
+
<Link href="/" className="gradient-bg rounded-full px-6 py-3 font-semibold text-black">Back home</Link>
|
|
1644
|
+
<Link href="/contact" className="rounded-full border border-hairline px-6 py-3 font-semibold transition hover:bg-surface">Get in touch</Link>
|
|
1645
|
+
</div>
|
|
1646
|
+
{path ? (
|
|
1647
|
+
<p className="mt-8 text-xs text-faint">Add your content in <code className="rounded bg-surface px-1.5 py-0.5">app{path}/page.tsx</code>.</p>
|
|
1648
|
+
) : null}
|
|
1649
|
+
</div>
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
`;
|
|
1653
|
+
var underDevPage = (page) => `import type { Metadata } from "next";
|
|
1654
|
+
import { site } from "@/lib/site";
|
|
1655
|
+
import { UnderDevelopment } from "@/components/under-development";
|
|
1656
|
+
|
|
1657
|
+
export const metadata: Metadata = site.meta({ title: ${JSON.stringify(page.label)}, path: ${JSON.stringify(page.path)}, description: ${JSON.stringify(page.label + " \u2014 coming soon. We're building this page.")} });
|
|
1658
|
+
|
|
1659
|
+
export default function Page() {
|
|
1660
|
+
return (
|
|
1661
|
+
<main className="min-h-screen">
|
|
1662
|
+
<UnderDevelopment title={${JSON.stringify(page.label)}} path={${JSON.stringify(page.path)}} />
|
|
1663
|
+
</main>
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
`;
|
|
1667
|
+
var dashboardStubPage = (page) => `import type { Metadata } from "next";
|
|
1668
|
+
import { site } from "@/lib/site";
|
|
1669
|
+
import { DashboardShell } from "@/components/dashboard-shell";
|
|
1670
|
+
import { UnderDevelopment } from "@/components/under-development";
|
|
1671
|
+
|
|
1672
|
+
export const metadata: Metadata = site.meta({ title: ${JSON.stringify(page.label)}, path: ${JSON.stringify(page.path)}, description: ${JSON.stringify(page.label + " \u2014 coming soon.")} });
|
|
1673
|
+
|
|
1674
|
+
export default function Page() {
|
|
1675
|
+
return (
|
|
1676
|
+
<DashboardShell title={${JSON.stringify(page.label)}}>
|
|
1677
|
+
<UnderDevelopment title={${JSON.stringify(page.label)}} path={${JSON.stringify(page.path)}} />
|
|
1678
|
+
</DashboardShell>
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
`;
|
|
1682
|
+
var uiStore = () => `import { create, persist } from "@lacspace/store";
|
|
1683
|
+
|
|
1684
|
+
/** Ephemeral UI state \u2014 e.g. the mobile nav. Not persisted. */
|
|
1685
|
+
interface UIState {
|
|
1686
|
+
navOpen: boolean;
|
|
1687
|
+
setNavOpen: (open: boolean) => void;
|
|
1688
|
+
toggleNav: () => void;
|
|
1689
|
+
}
|
|
1690
|
+
export const useUI = create<UIState>((set) => ({
|
|
1691
|
+
navOpen: false,
|
|
1692
|
+
setNavOpen: (navOpen) => set({ navOpen }),
|
|
1693
|
+
toggleNav: () => set((s) => ({ navOpen: !s.navOpen })),
|
|
1694
|
+
}));
|
|
1695
|
+
|
|
1696
|
+
/** Whether the announcement bar was dismissed \u2014 persisted to localStorage. */
|
|
1697
|
+
interface AnnouncementState {
|
|
1698
|
+
dismissed: boolean;
|
|
1699
|
+
dismiss: () => void;
|
|
1700
|
+
}
|
|
1701
|
+
export const useAnnouncement = create<AnnouncementState>(
|
|
1702
|
+
persist(
|
|
1703
|
+
(set) => ({
|
|
1704
|
+
dismissed: false,
|
|
1705
|
+
dismiss: () => set({ dismissed: true }),
|
|
1706
|
+
}),
|
|
1707
|
+
{ name: "announcement" },
|
|
1708
|
+
),
|
|
1709
|
+
);
|
|
1710
|
+
`;
|
|
1711
|
+
var statsRoute = () => `import { NextResponse } from "next/server";
|
|
1712
|
+
|
|
1713
|
+
// Demo endpoint powering <LiveStats/> (@lacspace/query). Swap in your real data.
|
|
1714
|
+
export const dynamic = "force-dynamic";
|
|
1715
|
+
|
|
1716
|
+
export function GET() {
|
|
1717
|
+
const jitter = (n: number) => n + Math.floor(Math.random() * n * 0.04);
|
|
1718
|
+
return NextResponse.json({
|
|
1719
|
+
users: jitter(12480),
|
|
1720
|
+
uptime: 99.98,
|
|
1721
|
+
requests: jitter(1_840_000),
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
`;
|
|
1725
|
+
var liveStats = () => `"use client";
|
|
1726
|
+
|
|
1727
|
+
import { useQuery } from "@lacspace/query";
|
|
1728
|
+
|
|
1729
|
+
interface Stats { users: number; uptime: number; requests: number; }
|
|
1730
|
+
|
|
1731
|
+
export function LiveStats() {
|
|
1732
|
+
// Shared cache, de-duped requests, and revalidation on window focus.
|
|
1733
|
+
const { data, isLoading } = useQuery("stats", async () => {
|
|
1734
|
+
const res = await fetch("/api/stats");
|
|
1735
|
+
if (!res.ok) throw new Error("Failed to load stats");
|
|
1736
|
+
return (await res.json()) as Stats;
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
const items = [
|
|
1740
|
+
{ label: "Active users", value: data ? data.users.toLocaleString() : "\u2014" },
|
|
1741
|
+
{ label: "Uptime", value: data ? data.uptime + "%" : "\u2014" },
|
|
1742
|
+
{ label: "Requests / mo", value: data ? new Intl.NumberFormat("en", { notation: "compact" }).format(data.requests) : "\u2014" },
|
|
1743
|
+
];
|
|
1744
|
+
|
|
1745
|
+
return (
|
|
1746
|
+
<div className="grid gap-4 sm:grid-cols-3">
|
|
1747
|
+
{items.map((s) => (
|
|
1748
|
+
<div key={s.label} className="rounded-2xl border border-hairline bg-app p-6 text-center">
|
|
1749
|
+
<div className={"text-3xl font-bold tabular-nums " + (isLoading ? "animate-pulse text-muted" : "")}>{s.value}</div>
|
|
1750
|
+
<div className="mt-1 text-sm text-muted">{s.label}</div>
|
|
1751
|
+
</div>
|
|
1752
|
+
))}
|
|
1753
|
+
</div>
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
`;
|
|
1757
|
+
var builtWithSection = (ctx) => {
|
|
1758
|
+
const count = pagesFor(ctx).length + 1;
|
|
1759
|
+
return `<section className="mx-auto max-w-6xl px-6 py-20">
|
|
1760
|
+
<div className="rounded-3xl border border-hairline bg-surface p-8 sm:p-12">
|
|
1761
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-muted">Live \xB7 @lacspace/query</p>
|
|
1762
|
+
<h2 className="mt-3 text-3xl font-bold">By the numbers</h2>
|
|
1763
|
+
<p className="mt-2 max-w-xl text-muted">Fetched from an internal API route and revalidated on window focus \u2014 a tiny demo of the data layer wired into this starter.</p>
|
|
1764
|
+
<div className="mt-8"><LiveStats /></div>
|
|
1765
|
+
<div className="mt-12 border-t border-hairline pt-8">
|
|
1766
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-muted">Built with the Lacspace React Kit</p>
|
|
1767
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
1768
|
+
{["@lacspace/theme", "@lacspace/hooks", "@lacspace/store", "@lacspace/query", "@lacspace/ui", "@lacspace/seo", "@lacspace/og", "@lacspace/form"].map((p) => (
|
|
1769
|
+
<span key={p} className="rounded-full border border-hairline bg-app px-3 py-1 font-mono text-xs text-muted">{p}</span>
|
|
1770
|
+
))}
|
|
1771
|
+
</div>
|
|
1772
|
+
<p className="mt-5 max-w-2xl text-sm text-muted">Dark mode with no flash, a \u2318K palette, SEO + dynamic OG images, a validated contact form, and ${count}+ pages wired up \u2014 all scaffolded, all yours. <a href="https://lacspace.com/packages" className="text-fg underline underline-offset-4">Explore the packages \u2192</a></p>
|
|
1773
|
+
</div>
|
|
1774
|
+
</div>
|
|
1775
|
+
</section>`;
|
|
1776
|
+
};
|
|
1777
|
+
var dashboardShell = (ctx) => {
|
|
1778
|
+
const nav = [{ path: "/", label: "Overview" }, ...pagesFor(ctx).filter((p) => p.nav)];
|
|
1779
|
+
return `import Link from "next/link";
|
|
1780
|
+
import type { ReactNode } from "react";
|
|
1781
|
+
import { ThemeToggle } from "./theme-toggle";
|
|
1782
|
+
|
|
1783
|
+
const NAV = [${linkLiteral(nav)}];
|
|
1784
|
+
|
|
1785
|
+
export function DashboardShell({ title, subtitle, children }: { title: string; subtitle?: string; children: ReactNode }) {
|
|
1786
|
+
return (
|
|
1787
|
+
<div className="flex min-h-screen">
|
|
1788
|
+
<aside className="hidden w-56 shrink-0 border-r border-hairline p-6 md:block">
|
|
1789
|
+
<div className="mb-8 flex items-center justify-between">
|
|
1790
|
+
<Link href="/" className="text-lg font-black gradient-text">${ctx.template.siteName}</Link>
|
|
1791
|
+
<ThemeToggle />
|
|
1792
|
+
</div>
|
|
1793
|
+
<nav className="space-y-1 text-sm text-muted">
|
|
1794
|
+
{NAV.map((l) => (
|
|
1795
|
+
<Link key={l.href} href={l.href} className="block rounded-lg px-3 py-2 transition hover:bg-surface hover:text-fg">{l.label}</Link>
|
|
1796
|
+
))}
|
|
1797
|
+
</nav>
|
|
1798
|
+
</aside>
|
|
1799
|
+
<main className="flex-1 p-6 md:p-10">
|
|
1800
|
+
<h1 className="text-2xl font-bold">{title}</h1>
|
|
1801
|
+
{subtitle ? <p className="mt-1 text-muted">{subtitle}</p> : null}
|
|
1802
|
+
<div className="mt-8">{children}</div>
|
|
1803
|
+
</main>
|
|
1804
|
+
</div>
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
`;
|
|
1808
|
+
};
|
|
1809
|
+
var dashboardHome = (ctx) => `import { site } from "@/lib/site";
|
|
1810
|
+
import { DashboardShell } from "@/components/dashboard-shell";
|
|
1811
|
+
import { LiveStats } from "@/components/live-stats";
|
|
1812
|
+
|
|
1813
|
+
export const metadata = site.meta({ title: "Overview", path: "/" });
|
|
1814
|
+
|
|
1815
|
+
export default function Home() {
|
|
1816
|
+
return (
|
|
1817
|
+
<DashboardShell title="Overview" subtitle=${JSON.stringify(ctx.template.siteDescription)}>
|
|
1818
|
+
<LiveStats />
|
|
1819
|
+
<div className="mt-6 rounded-2xl border border-hairline bg-surface p-6">
|
|
1820
|
+
<div className="mb-4 font-semibold">Recent activity</div>
|
|
1821
|
+
<div className="space-y-3">
|
|
1822
|
+
{[1, 2, 3, 4].map((i) => (
|
|
1823
|
+
<div key={i} className="flex items-center justify-between border-b border-hairline pb-3 text-sm">
|
|
1824
|
+
<span className="text-muted">Event #{i}</span>
|
|
1825
|
+
<span className="text-faint">just now</span>
|
|
1826
|
+
</div>
|
|
1827
|
+
))}
|
|
1828
|
+
</div>
|
|
1829
|
+
</div>
|
|
1830
|
+
</DashboardShell>
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
`;
|
|
1402
1834
|
function parseArgs(list) {
|
|
1403
1835
|
const a = { yes: false, install: true, git: true, pm: "npm", help: false };
|
|
1404
1836
|
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.8.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": {
|