create-lacspace-app 1.5.0 → 1.7.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 +487 -83
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,8 +43,11 @@ 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
|
-
//
|
|
47
|
-
|
|
46
|
+
// React Kit: dark/light theming (no-flash) + essential hooks.
|
|
47
|
+
"@lacspace/theme": "^1.0.1",
|
|
48
|
+
"@lacspace/hooks": "^1.0.0",
|
|
49
|
+
// The blog & docs templates render Markdown with @lacspace/markdown.
|
|
50
|
+
...ctx.template.key === "blog" || ctx.template.key === "docs" ? { "@lacspace/markdown": "^1.0.0" } : {}
|
|
48
51
|
},
|
|
49
52
|
devDependencies: {
|
|
50
53
|
typescript: "^5.7.0",
|
|
@@ -103,25 +106,57 @@ out
|
|
|
103
106
|
`;
|
|
104
107
|
var globalsCss = (ctx) => `@import "tailwindcss";
|
|
105
108
|
|
|
109
|
+
/* Class-based dark mode \u2014 toggled by @lacspace/theme's <ThemeProvider>. */
|
|
110
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
111
|
+
|
|
112
|
+
/* Semantic color tokens \u2192 Tailwind utilities (text-fg, text-muted, bg-surface,
|
|
113
|
+
border-hairline, \u2026). They resolve to the CSS vars below, which swap per theme. */
|
|
114
|
+
@theme inline {
|
|
115
|
+
--color-fg: var(--fg);
|
|
116
|
+
--color-muted: var(--muted);
|
|
117
|
+
--color-faint: var(--faint);
|
|
118
|
+
--color-surface: var(--surface);
|
|
119
|
+
--color-panel: var(--panel);
|
|
120
|
+
--color-hairline: var(--hairline);
|
|
121
|
+
}
|
|
122
|
+
|
|
106
123
|
:root {
|
|
107
124
|
--accent-from: ${ctx.template.accent[0]};
|
|
108
125
|
--accent-to: ${ctx.template.accent[1]};
|
|
126
|
+
/* Light theme */
|
|
127
|
+
--bg: #ffffff;
|
|
128
|
+
--fg: #0a0a0f;
|
|
129
|
+
--muted: #3f3f46;
|
|
130
|
+
--faint: #71717a;
|
|
131
|
+
--surface: rgb(0 0 0 / 0.03);
|
|
132
|
+
--panel: rgb(0 0 0 / 0.06);
|
|
133
|
+
--hairline: rgb(0 0 0 / 0.10);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.dark {
|
|
137
|
+
/* Dark theme (default) */
|
|
109
138
|
--bg: #0a0a0f;
|
|
110
|
-
--fg: #
|
|
139
|
+
--fg: #f5f5f7;
|
|
140
|
+
--muted: rgb(255 255 255 / 0.62);
|
|
141
|
+
--faint: rgb(255 255 255 / 0.42);
|
|
142
|
+
--surface: rgb(255 255 255 / 0.04);
|
|
143
|
+
--panel: rgb(255 255 255 / 0.08);
|
|
144
|
+
--hairline: rgb(255 255 255 / 0.10);
|
|
111
145
|
}
|
|
112
146
|
|
|
113
|
-
* { border-color:
|
|
147
|
+
* { border-color: var(--hairline); }
|
|
114
148
|
html { scroll-behavior: smooth; }
|
|
115
149
|
body {
|
|
116
150
|
background: var(--bg);
|
|
117
151
|
color: var(--fg);
|
|
118
152
|
-webkit-font-smoothing: antialiased;
|
|
119
153
|
text-rendering: optimizeLegibility;
|
|
154
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
120
155
|
}
|
|
121
156
|
|
|
122
|
-
::selection { background: var(--accent-to); color: #
|
|
157
|
+
::selection { background: var(--accent-to); color: #fff; }
|
|
123
158
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
124
|
-
::-webkit-scrollbar-thumb { background:
|
|
159
|
+
::-webkit-scrollbar-thumb { background: var(--panel); border-radius: 8px; }
|
|
125
160
|
:focus-visible { outline: 2px solid var(--accent-to); outline-offset: 2px; border-radius: 4px; }
|
|
126
161
|
|
|
127
162
|
.gradient-text {
|
|
@@ -134,7 +169,7 @@ body {
|
|
|
134
169
|
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
|
|
135
170
|
main > section, main > * { animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; }
|
|
136
171
|
@media (prefers-reduced-motion: reduce) { *, ::before, ::after { animation: none !important; scroll-behavior: auto; } }
|
|
137
|
-
${ctx.template.key === "blog" ? BLOG_PROSE_CSS : ""}`;
|
|
172
|
+
${ctx.template.key === "blog" || ctx.template.key === "docs" ? BLOG_PROSE_CSS : ""}`;
|
|
138
173
|
var BLOG_PROSE_CSS = `
|
|
139
174
|
/* article typography for Markdown posts */
|
|
140
175
|
.prose { line-height: 1.75; color: rgb(229 231 235 / 0.9); }
|
|
@@ -168,6 +203,7 @@ export const site = defineSite({
|
|
|
168
203
|
`;
|
|
169
204
|
var layout = (ctx) => `import type { Metadata } from "next";
|
|
170
205
|
import { Inter } from "next/font/google";
|
|
206
|
+
import { ThemeProvider } from "@lacspace/theme";
|
|
171
207
|
import { site } from "@/lib/site";
|
|
172
208
|
import { CommandMenu } from "@/components/command-menu";
|
|
173
209
|
import "./globals.css";
|
|
@@ -178,11 +214,14 @@ export const metadata: Metadata = site.meta({ title: ${JSON.stringify(ctx.templa
|
|
|
178
214
|
|
|
179
215
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
180
216
|
return (
|
|
181
|
-
<html lang="en" className={inter.className}>
|
|
217
|
+
<html lang="en" className={inter.className} suppressHydrationWarning>
|
|
182
218
|
<body className="antialiased">
|
|
183
|
-
{/* \u2728
|
|
184
|
-
<
|
|
185
|
-
|
|
219
|
+
{/* \u2728 Dark / light / system theming with a built-in no-flash script \u2014 @lacspace/theme */}
|
|
220
|
+
<ThemeProvider defaultTheme="dark">
|
|
221
|
+
{/* \u2728 Press \u2318K / Ctrl-K anywhere \u2014 powered by @lacspace/ui */}
|
|
222
|
+
<CommandMenu />
|
|
223
|
+
{children}
|
|
224
|
+
</ThemeProvider>
|
|
186
225
|
<script
|
|
187
226
|
type="application/ld+json"
|
|
188
227
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(site.rootJsonLd()) }}
|
|
@@ -226,7 +265,7 @@ export default function NotFound() {
|
|
|
226
265
|
return (
|
|
227
266
|
<main className="flex min-h-screen flex-col items-center justify-center gap-6 px-6 text-center">
|
|
228
267
|
<div className="text-8xl font-black gradient-text">404</div>
|
|
229
|
-
<p className="text-lg text-
|
|
268
|
+
<p className="text-lg text-muted">This page wandered off.</p>
|
|
230
269
|
<Link href="/" className="gradient-bg rounded-full px-6 py-3 font-semibold text-black">Back home</Link>
|
|
231
270
|
</main>
|
|
232
271
|
);
|
|
@@ -339,21 +378,25 @@ Open [http://localhost:3000](http://localhost:3000). Edit \`app/page.tsx\` and \
|
|
|
339
378
|
|
|
340
379
|
Built with [Lacspace](https://lacspace.com/packages).
|
|
341
380
|
`;
|
|
342
|
-
var NAV = (name, links) => `<header className="sticky top-0 z-40 backdrop-blur border-b border-
|
|
381
|
+
var NAV = (name, links) => `<header className="sticky top-0 z-40 backdrop-blur border-b border-hairline">
|
|
343
382
|
<nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
|
344
383
|
<span className="text-lg font-black gradient-text">${name}</span>
|
|
345
|
-
<div className="
|
|
346
|
-
|
|
384
|
+
<div className="flex items-center gap-6">
|
|
385
|
+
<div className="hidden gap-8 text-sm text-muted sm:flex">
|
|
386
|
+
${links.map((l) => `<a href="#${l.toLowerCase()}" className="hover:text-fg">${l}</a>`).join("\n ")}
|
|
387
|
+
</div>
|
|
388
|
+
<ThemeToggle />
|
|
347
389
|
</div>
|
|
348
390
|
</nav>
|
|
349
391
|
</header>`;
|
|
350
|
-
var FOOTER = (name) => `<footer className="border-t border-
|
|
392
|
+
var FOOTER = (name) => `<footer className="border-t border-hairline py-10 text-center text-sm text-faint">
|
|
351
393
|
\xA9 {new Date().getFullYear()} ${name}. Built with{" "}
|
|
352
|
-
<a href="https://lacspace.com/packages" className="text-
|
|
394
|
+
<a href="https://lacspace.com/packages" className="text-muted hover:text-fg">Lacspace</a>.
|
|
353
395
|
</footer>`;
|
|
354
396
|
function homePage(ctx) {
|
|
355
397
|
const n = ctx.template.siteName;
|
|
356
398
|
const shell = (inner) => `import { site } from "@/lib/site";
|
|
399
|
+
import { ThemeToggle } from "@/components/theme-toggle";
|
|
357
400
|
|
|
358
401
|
// \u2728 Self-canonical home page \u2014 one line, full SEO (title, canonical, OG, Twitter).
|
|
359
402
|
export const metadata = site.meta({ title: ${JSON.stringify(n)}, path: "/" });
|
|
@@ -369,22 +412,22 @@ export default function Home() {
|
|
|
369
412
|
if (ctx.template.key === "personal") {
|
|
370
413
|
return shell(`${NAV(n, ["Work", "About", "Contact"])}
|
|
371
414
|
<section className="mx-auto max-w-3xl px-6 py-28 text-center">
|
|
372
|
-
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-
|
|
415
|
+
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-faint">Portfolio</p>
|
|
373
416
|
<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-
|
|
417
|
+
<p className="mx-auto mt-6 max-w-xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
375
418
|
<div className="mt-10 flex justify-center gap-4">
|
|
376
419
|
<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-
|
|
420
|
+
<a href="#contact" className="rounded-full border border-hairline px-6 py-3 font-semibold hover:bg-surface">Get in touch</a>
|
|
378
421
|
</div>
|
|
379
422
|
</section>
|
|
380
423
|
<section id="work" className="mx-auto max-w-6xl px-6 py-16">
|
|
381
424
|
<h2 className="mb-10 text-3xl font-bold">Selected work</h2>
|
|
382
425
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
383
426
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
|
384
|
-
<div key={i} className="group rounded-2xl border border-
|
|
427
|
+
<div key={i} className="group rounded-2xl border border-hairline bg-surface p-6 transition hover:-translate-y-1 hover:border-hairline">
|
|
385
428
|
<div className="mb-4 h-32 rounded-xl gradient-bg opacity-80" />
|
|
386
429
|
<h3 className="font-semibold">Project {i}</h3>
|
|
387
|
-
<p className="mt-1 text-sm text-
|
|
430
|
+
<p className="mt-1 text-sm text-muted">A short description of what you built and the impact it had.</p>
|
|
388
431
|
</div>
|
|
389
432
|
))}
|
|
390
433
|
</div>
|
|
@@ -395,7 +438,7 @@ export default function Home() {
|
|
|
395
438
|
return shell(`${NAV(n, ["Services", "Work", "Contact"])}
|
|
396
439
|
<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
397
440
|
<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-
|
|
441
|
+
<p className="mx-auto mt-6 max-w-2xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
399
442
|
<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
443
|
</section>
|
|
401
444
|
<section id="services" className="mx-auto max-w-6xl px-6 py-16">
|
|
@@ -406,17 +449,17 @@ export default function Home() {
|
|
|
406
449
|
{ t: "Design", d: "Brand and product design that people remember." },
|
|
407
450
|
{ t: "Engineering", d: "Fast, reliable software built to last." },
|
|
408
451
|
].map((s) => (
|
|
409
|
-
<div key={s.t} className="rounded-2xl border border-
|
|
452
|
+
<div key={s.t} className="rounded-2xl border border-hairline bg-surface p-8">
|
|
410
453
|
<h3 className="text-xl font-bold gradient-text">{s.t}</h3>
|
|
411
|
-
<p className="mt-2 text-
|
|
454
|
+
<p className="mt-2 text-muted">{s.d}</p>
|
|
412
455
|
</div>
|
|
413
456
|
))}
|
|
414
457
|
</div>
|
|
415
458
|
</section>
|
|
416
459
|
<section id="contact" className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
417
460
|
<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-
|
|
461
|
+
<p className="mt-3 text-muted">Tell us about your project and we'll get back within a day.</p>
|
|
462
|
+
<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>
|
|
420
463
|
</section>
|
|
421
464
|
${FOOTER(n)}`);
|
|
422
465
|
}
|
|
@@ -424,7 +467,7 @@ export default function Home() {
|
|
|
424
467
|
return shell(`${NAV(n, ["Shop", "About", "Cart"])}
|
|
425
468
|
<section className="mx-auto max-w-5xl px-6 py-24 text-center">
|
|
426
469
|
<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-
|
|
470
|
+
<p className="mx-auto mt-6 max-w-xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
428
471
|
<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
472
|
</section>
|
|
430
473
|
<section id="shop" className="mx-auto max-w-6xl px-6 py-16">
|
|
@@ -434,13 +477,13 @@ export default function Home() {
|
|
|
434
477
|
{ n: "Aurora Lamp", p: "$89" }, { n: "Terra Mug", p: "$24" },
|
|
435
478
|
{ n: "Linen Throw", p: "$65" }, { n: "Oak Stand", p: "$120" },
|
|
436
479
|
].map((prod) => (
|
|
437
|
-
<div key={prod.n} className="group rounded-2xl border border-
|
|
480
|
+
<div key={prod.n} className="group rounded-2xl border border-hairline bg-surface p-4 transition hover:-translate-y-1">
|
|
438
481
|
<div className="mb-4 aspect-square rounded-xl gradient-bg opacity-80" />
|
|
439
482
|
<div className="flex items-center justify-between">
|
|
440
483
|
<h3 className="font-semibold">{prod.n}</h3>
|
|
441
|
-
<span className="text-
|
|
484
|
+
<span className="text-muted">{prod.p}</span>
|
|
442
485
|
</div>
|
|
443
|
-
<button className="mt-3 w-full rounded-lg border border-
|
|
486
|
+
<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
487
|
</div>
|
|
445
488
|
))}
|
|
446
489
|
</div>
|
|
@@ -450,16 +493,16 @@ export default function Home() {
|
|
|
450
493
|
if (ctx.template.key === "blog") {
|
|
451
494
|
return shell(`${NAV(n, ["Latest", "Topics", "About"])}
|
|
452
495
|
<section className="mx-auto max-w-3xl px-6 py-24">
|
|
453
|
-
<p className="mb-3 text-sm font-semibold uppercase tracking-widest text-
|
|
496
|
+
<p className="mb-3 text-sm font-semibold uppercase tracking-widest text-faint">The Journal</p>
|
|
454
497
|
<h1 className="text-5xl font-black leading-tight sm:text-6xl gradient-text">${n}</h1>
|
|
455
|
-
<p className="mt-6 text-lg text-
|
|
498
|
+
<p className="mt-6 text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
456
499
|
</section>
|
|
457
500
|
<section id="latest" className="mx-auto max-w-5xl px-6 pb-8">
|
|
458
|
-
<a href="#" className="group block rounded-3xl border border-
|
|
501
|
+
<a href="#" className="group block rounded-3xl border border-hairline bg-surface p-8 transition hover:border-hairline">
|
|
459
502
|
<div className="mb-6 h-56 rounded-2xl gradient-bg opacity-80" />
|
|
460
|
-
<span className="text-xs font-semibold uppercase tracking-wider text-
|
|
503
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-faint">Featured</span>
|
|
461
504
|
<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-
|
|
505
|
+
<p className="mt-2 text-muted">A short, punchy dek that pulls the reader into the piece and makes them want more.</p>
|
|
463
506
|
</a>
|
|
464
507
|
</section>
|
|
465
508
|
<section className="mx-auto max-w-5xl px-6 py-12">
|
|
@@ -467,9 +510,9 @@ export default function Home() {
|
|
|
467
510
|
{[1, 2, 3, 4].map((i) => (
|
|
468
511
|
<a key={i} href="#" className="group">
|
|
469
512
|
<div className="mb-4 h-40 rounded-xl gradient-bg opacity-70" />
|
|
470
|
-
<span className="text-xs uppercase tracking-wider text-
|
|
513
|
+
<span className="text-xs uppercase tracking-wider text-faint">Essay</span>
|
|
471
514
|
<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-
|
|
515
|
+
<p className="mt-1 text-sm text-muted">Two lines of supporting copy to set the scene for the reader.</p>
|
|
473
516
|
</a>
|
|
474
517
|
))}
|
|
475
518
|
</div>
|
|
@@ -480,9 +523,9 @@ export default function Home() {
|
|
|
480
523
|
return shell(`${NAV(n, ["Guides", "API", "Examples"])}
|
|
481
524
|
<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
482
525
|
<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-
|
|
526
|
+
<p className="mx-auto mt-6 max-w-2xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
527
|
+
<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">
|
|
528
|
+
<span className="text-faint">$</span> npm install @acme/sdk
|
|
486
529
|
</div>
|
|
487
530
|
</section>
|
|
488
531
|
<section id="guides" className="mx-auto max-w-6xl px-6 py-12">
|
|
@@ -492,9 +535,9 @@ export default function Home() {
|
|
|
492
535
|
{ t: "Guides", d: "Task-focused walkthroughs for the common paths." },
|
|
493
536
|
{ t: "API reference", d: "Every endpoint, typed, with copy-paste examples." },
|
|
494
537
|
].map((c) => (
|
|
495
|
-
<a key={c.t} href="#" className="rounded-2xl border border-
|
|
538
|
+
<a key={c.t} href="#" className="rounded-2xl border border-hairline bg-surface p-8 transition hover:border-hairline">
|
|
496
539
|
<h3 className="text-xl font-bold gradient-text">{c.t}</h3>
|
|
497
|
-
<p className="mt-2 text-
|
|
540
|
+
<p className="mt-2 text-muted">{c.d}</p>
|
|
498
541
|
</a>
|
|
499
542
|
))}
|
|
500
543
|
</div>
|
|
@@ -503,36 +546,39 @@ export default function Home() {
|
|
|
503
546
|
}
|
|
504
547
|
if (ctx.template.key === "dashboard") {
|
|
505
548
|
return shell(`<div className="flex min-h-screen">
|
|
506
|
-
<aside className="hidden w-56 shrink-0 border-r border-
|
|
507
|
-
<div className="mb-8
|
|
508
|
-
|
|
549
|
+
<aside className="hidden w-56 shrink-0 border-r border-hairline p-6 md:block">
|
|
550
|
+
<div className="mb-8 flex items-center justify-between">
|
|
551
|
+
<span className="text-lg font-black gradient-text">${n}</span>
|
|
552
|
+
<ThemeToggle />
|
|
553
|
+
</div>
|
|
554
|
+
<nav className="space-y-1 text-sm text-muted">
|
|
509
555
|
{["Overview", "Analytics", "Customers", "Settings"].map((l) => (
|
|
510
|
-
<a key={l} href="#" className="block rounded-lg px-3 py-2 hover:bg-
|
|
556
|
+
<a key={l} href="#" className="block rounded-lg px-3 py-2 hover:bg-surface hover:text-fg">{l}</a>
|
|
511
557
|
))}
|
|
512
558
|
</nav>
|
|
513
559
|
</aside>
|
|
514
560
|
<main className="flex-1 p-6 md:p-10">
|
|
515
561
|
<h1 className="text-2xl font-bold">Overview</h1>
|
|
516
|
-
<p className="mt-1 text-
|
|
562
|
+
<p className="mt-1 text-muted">${ctx.template.siteDescription}</p>
|
|
517
563
|
<div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
518
564
|
{[
|
|
519
565
|
{ l: "Revenue", v: "$48.2k", c: "+12%" }, { l: "Users", v: "12,480", c: "+3.4%" },
|
|
520
566
|
{ l: "Orders", v: "1,204", c: "+8%" }, { l: "Churn", v: "1.2%", c: "-0.3%" },
|
|
521
567
|
].map((s) => (
|
|
522
|
-
<div key={s.l} className="rounded-2xl border border-
|
|
523
|
-
<div className="text-sm text-
|
|
568
|
+
<div key={s.l} className="rounded-2xl border border-hairline bg-surface p-5">
|
|
569
|
+
<div className="text-sm text-muted">{s.l}</div>
|
|
524
570
|
<div className="mt-1 text-2xl font-bold">{s.v}</div>
|
|
525
571
|
<div className="mt-1 text-xs text-emerald-400">{s.c}</div>
|
|
526
572
|
</div>
|
|
527
573
|
))}
|
|
528
574
|
</div>
|
|
529
|
-
<div className="mt-6 rounded-2xl border border-
|
|
575
|
+
<div className="mt-6 rounded-2xl border border-hairline bg-surface p-6">
|
|
530
576
|
<div className="mb-4 font-semibold">Recent activity</div>
|
|
531
577
|
<div className="space-y-3">
|
|
532
578
|
{[1, 2, 3, 4].map((i) => (
|
|
533
|
-
<div key={i} className="flex items-center justify-between border-b border-
|
|
534
|
-
<span className="text-
|
|
535
|
-
<span className="text-
|
|
579
|
+
<div key={i} className="flex items-center justify-between border-b border-hairline pb-3 text-sm">
|
|
580
|
+
<span className="text-muted">Event #{i}</span>
|
|
581
|
+
<span className="text-faint">just now</span>
|
|
536
582
|
</div>
|
|
537
583
|
))}
|
|
538
584
|
</div>
|
|
@@ -543,9 +589,9 @@ export default function Home() {
|
|
|
543
589
|
if (ctx.template.key === "restaurant") {
|
|
544
590
|
return shell(`${NAV(n, ["Menu", "Story", "Reserve"])}
|
|
545
591
|
<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
546
|
-
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-
|
|
592
|
+
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-faint">Est. 2026</p>
|
|
547
593
|
<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-
|
|
594
|
+
<p className="mx-auto mt-6 max-w-xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
549
595
|
<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
596
|
</section>
|
|
551
597
|
<section id="menu" className="mx-auto max-w-4xl px-6 py-16">
|
|
@@ -555,10 +601,10 @@ export default function Home() {
|
|
|
555
601
|
{ n: "Charred leeks", p: "$14" }, { n: "Handmade tagliatelle", p: "$22" },
|
|
556
602
|
{ n: "Wood-fired trout", p: "$28" }, { n: "Olive oil cake", p: "$11" },
|
|
557
603
|
].map((d) => (
|
|
558
|
-
<div key={d.n} className="flex items-baseline justify-between gap-4 border-b border-
|
|
604
|
+
<div key={d.n} className="flex items-baseline justify-between gap-4 border-b border-hairline pb-4">
|
|
559
605
|
<div>
|
|
560
606
|
<h3 className="text-lg font-semibold">{d.n}</h3>
|
|
561
|
-
<p className="text-sm text-
|
|
607
|
+
<p className="text-sm text-muted">A short, mouth-watering description of the dish.</p>
|
|
562
608
|
</div>
|
|
563
609
|
<span className="shrink-0 gradient-text font-bold">{d.p}</span>
|
|
564
610
|
</div>
|
|
@@ -567,19 +613,19 @@ export default function Home() {
|
|
|
567
613
|
</section>
|
|
568
614
|
<section id="reserve" className="mx-auto max-w-2xl px-6 py-24 text-center">
|
|
569
615
|
<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-
|
|
616
|
+
<p className="mt-3 text-muted">Open Wed\u2013Sun, 5pm till late. Walk-ins welcome; bookings recommended.</p>
|
|
617
|
+
<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>
|
|
572
618
|
</section>
|
|
573
619
|
${FOOTER(n)}`);
|
|
574
620
|
}
|
|
575
621
|
return shell(`${NAV(n, ["Features", "Pricing", "Sign in"])}
|
|
576
622
|
<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
577
|
-
<p className="mb-4 inline-block rounded-full border border-
|
|
623
|
+
<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
624
|
<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-
|
|
625
|
+
<p className="mx-auto mt-6 max-w-2xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
580
626
|
<div className="mt-10 flex justify-center gap-4">
|
|
581
627
|
<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-
|
|
628
|
+
<a href="#features" className="rounded-full border border-hairline px-8 py-3 font-semibold hover:bg-surface">See features</a>
|
|
583
629
|
</div>
|
|
584
630
|
</section>
|
|
585
631
|
<section id="features" className="mx-auto max-w-6xl px-6 py-16">
|
|
@@ -589,17 +635,17 @@ export default function Home() {
|
|
|
589
635
|
{ t: "Secure", d: "Hardened headers and auth out of the box." },
|
|
590
636
|
{ t: "Scalable", d: "From your first user to your millionth." },
|
|
591
637
|
].map((f) => (
|
|
592
|
-
<div key={f.t} className="rounded-2xl border border-
|
|
638
|
+
<div key={f.t} className="rounded-2xl border border-hairline bg-surface p-8">
|
|
593
639
|
<h3 className="text-xl font-bold gradient-text">{f.t}</h3>
|
|
594
|
-
<p className="mt-2 text-
|
|
640
|
+
<p className="mt-2 text-muted">{f.d}</p>
|
|
595
641
|
</div>
|
|
596
642
|
))}
|
|
597
643
|
</div>
|
|
598
644
|
</section>
|
|
599
645
|
<section id="pricing" className="mx-auto max-w-md px-6 py-24 text-center">
|
|
600
|
-
<div className="rounded-3xl border border-
|
|
646
|
+
<div className="rounded-3xl border border-hairline bg-surface p-10">
|
|
601
647
|
<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-
|
|
648
|
+
<p className="mt-2 text-5xl font-black gradient-text">$29<span className="text-lg text-faint">/mo</span></p>
|
|
603
649
|
<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
650
|
</div>
|
|
605
651
|
</section>
|
|
@@ -655,9 +701,9 @@ export const metadata: Metadata = site.meta({
|
|
|
655
701
|
export default function AboutPage() {
|
|
656
702
|
return (
|
|
657
703
|
<main className="mx-auto max-w-3xl px-6 py-28">
|
|
658
|
-
<Link href="/" className="text-sm text-
|
|
704
|
+
<Link href="/" className="text-sm text-muted hover:text-fg">\u2190 Back home</Link>
|
|
659
705
|
<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-
|
|
706
|
+
<p className="mt-6 text-lg leading-relaxed text-muted">
|
|
661
707
|
This page already has its own SEO \u2014 a unique title, canonical URL, Open Graph tags and a
|
|
662
708
|
generated social image \u2014 from a single <code>site.meta()</code> call. Duplicate this file for
|
|
663
709
|
any new route and it just works.
|
|
@@ -666,6 +712,41 @@ export default function AboutPage() {
|
|
|
666
712
|
);
|
|
667
713
|
}
|
|
668
714
|
`;
|
|
715
|
+
var themeToggle = () => `"use client";
|
|
716
|
+
|
|
717
|
+
import { useTheme } from "@lacspace/theme";
|
|
718
|
+
import { useIsMounted } from "@lacspace/hooks";
|
|
719
|
+
|
|
720
|
+
/** A sun/moon button that flips between light and dark. */
|
|
721
|
+
export function ThemeToggle() {
|
|
722
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
723
|
+
const isMounted = useIsMounted();
|
|
724
|
+
const dark = resolvedTheme === "dark";
|
|
725
|
+
|
|
726
|
+
return (
|
|
727
|
+
<button
|
|
728
|
+
type="button"
|
|
729
|
+
aria-label="Toggle theme"
|
|
730
|
+
onClick={() => setTheme(dark ? "light" : "dark")}
|
|
731
|
+
className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-hairline text-muted transition hover:text-fg"
|
|
732
|
+
>
|
|
733
|
+
{/* Render a neutral placeholder until mounted to avoid a hydration mismatch. */}
|
|
734
|
+
{!isMounted() ? (
|
|
735
|
+
<span className="block h-4 w-4" />
|
|
736
|
+
) : dark ? (
|
|
737
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
738
|
+
<circle cx="12" cy="12" r="4" />
|
|
739
|
+
<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" />
|
|
740
|
+
</svg>
|
|
741
|
+
) : (
|
|
742
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
743
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
744
|
+
</svg>
|
|
745
|
+
)}
|
|
746
|
+
</button>
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
`;
|
|
669
750
|
var commandMenu = (ctx) => `"use client";
|
|
670
751
|
import { useRouter } from "next/navigation";
|
|
671
752
|
import { CommandPalette } from "@lacspace/ui";
|
|
@@ -678,7 +759,8 @@ export function CommandMenu() {
|
|
|
678
759
|
items={[
|
|
679
760
|
{ id: "home", label: "Home", group: "Navigate", shortcut: "G H", onSelect: () => router.push("/") },
|
|
680
761
|
{ id: "about", label: "About", group: "Navigate", onSelect: () => router.push("/about") },${ctx.template.key === "blog" ? `
|
|
681
|
-
{ id: "blog", label: "Blog", group: "Navigate", onSelect: () => router.push("/blog") },` : ""}
|
|
762
|
+
{ id: "blog", label: "Blog", group: "Navigate", onSelect: () => router.push("/blog") },` : ""}${ctx.template.key === "docs" ? `
|
|
763
|
+
{ id: "docs", label: "Docs", group: "Navigate", onSelect: () => router.push("/docs") },` : ""}
|
|
682
764
|
{ id: "contact", label: "Contact", group: "Navigate", onSelect: () => router.push("/contact") },
|
|
683
765
|
{ id: "packages", label: "Lacspace packages", group: "Links", onSelect: () => window.open("https://lacspace.com/packages", "_blank") },
|
|
684
766
|
]}
|
|
@@ -720,8 +802,8 @@ import { useActionState } from "react";
|
|
|
720
802
|
import { honeypotProps, timestampValue } from "@lacspace/form";
|
|
721
803
|
import { submitContact, type ContactState } from "@/app/actions";
|
|
722
804
|
|
|
723
|
-
const field = "w-full rounded-xl border border-
|
|
724
|
-
const label = "mb-1 block text-sm text-
|
|
805
|
+
const field = "w-full rounded-xl border border-hairline bg-surface px-4 py-3 outline-none focus:border-hairline";
|
|
806
|
+
const label = "mb-1 block text-sm text-muted";
|
|
725
807
|
const errCls = "mt-1 text-sm text-red-400";
|
|
726
808
|
|
|
727
809
|
export function ContactForm() {
|
|
@@ -729,7 +811,7 @@ export function ContactForm() {
|
|
|
729
811
|
|
|
730
812
|
if (state?.ok) {
|
|
731
813
|
return (
|
|
732
|
-
<p className="rounded-2xl border border-
|
|
814
|
+
<p className="rounded-2xl border border-hairline bg-surface p-8 text-center text-lg">
|
|
733
815
|
Thanks \u2014 we’ll be in touch! \u2705
|
|
734
816
|
</p>
|
|
735
817
|
);
|
|
@@ -785,7 +867,7 @@ export default function ContactPage() {
|
|
|
785
867
|
return (
|
|
786
868
|
<main className="mx-auto max-w-2xl px-6 py-24">
|
|
787
869
|
<h1 className="text-4xl font-black gradient-text sm:text-5xl">Get in touch</h1>
|
|
788
|
-
<p className="mt-4 text-
|
|
870
|
+
<p className="mt-4 text-muted">Have a question or a project in mind? Drop a message below.</p>
|
|
789
871
|
<div className="mt-10">
|
|
790
872
|
<ContactForm />
|
|
791
873
|
</div>
|
|
@@ -793,6 +875,316 @@ export default function ContactPage() {
|
|
|
793
875
|
);
|
|
794
876
|
}
|
|
795
877
|
`;
|
|
878
|
+
var docsLib = () => `import fs from "node:fs";
|
|
879
|
+
import path from "node:path";
|
|
880
|
+
import { markdownToHtml, extractHeadings, type Heading } from "@lacspace/markdown";
|
|
881
|
+
|
|
882
|
+
const DIR = path.join(process.cwd(), "content/docs");
|
|
883
|
+
|
|
884
|
+
export interface DocMeta {
|
|
885
|
+
slug: string;
|
|
886
|
+
title: string;
|
|
887
|
+
group: string;
|
|
888
|
+
order: number;
|
|
889
|
+
description: string;
|
|
890
|
+
}
|
|
891
|
+
export interface Doc extends DocMeta {
|
|
892
|
+
html: string;
|
|
893
|
+
toc: Heading[];
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function parse(raw: string): { data: Record<string, string>; body: string } {
|
|
897
|
+
const m = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/.exec(raw);
|
|
898
|
+
if (!m) return { data: {}, body: raw };
|
|
899
|
+
const data: Record<string, string> = {};
|
|
900
|
+
for (const line of m[1].split(/\\r?\\n/)) {
|
|
901
|
+
const i = line.indexOf(":");
|
|
902
|
+
if (i === -1) continue;
|
|
903
|
+
data[line.slice(0, i).trim()] = line.slice(i + 1).trim().replace(/^["']|["']$/g, "");
|
|
904
|
+
}
|
|
905
|
+
return { data, body: m[2] };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export function getAllDocs(): DocMeta[] {
|
|
909
|
+
if (!fs.existsSync(DIR)) return [];
|
|
910
|
+
return fs
|
|
911
|
+
.readdirSync(DIR)
|
|
912
|
+
.filter((f) => f.endsWith(".md"))
|
|
913
|
+
.map((f) => {
|
|
914
|
+
const { data } = parse(fs.readFileSync(path.join(DIR, f), "utf8"));
|
|
915
|
+
return {
|
|
916
|
+
slug: f.replace(/\\.md$/, ""),
|
|
917
|
+
title: data.title ?? f,
|
|
918
|
+
group: data.group ?? "Docs",
|
|
919
|
+
order: Number(data.order ?? "99"),
|
|
920
|
+
description: data.description ?? "",
|
|
921
|
+
};
|
|
922
|
+
})
|
|
923
|
+
.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export function getDoc(slug: string): Doc | null {
|
|
927
|
+
const file = path.join(DIR, \`\${slug}.md\`);
|
|
928
|
+
if (!fs.existsSync(file)) return null;
|
|
929
|
+
const { data, body } = parse(fs.readFileSync(file, "utf8"));
|
|
930
|
+
return {
|
|
931
|
+
slug,
|
|
932
|
+
title: data.title ?? slug,
|
|
933
|
+
group: data.group ?? "Docs",
|
|
934
|
+
order: Number(data.order ?? "99"),
|
|
935
|
+
description: data.description ?? "",
|
|
936
|
+
html: markdownToHtml(body, { headingOffset: 1 }),
|
|
937
|
+
toc: extractHeadings(body),
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
export function getDocNav(): { group: string; items: DocMeta[] }[] {
|
|
942
|
+
const groups: { group: string; items: DocMeta[] }[] = [];
|
|
943
|
+
for (const doc of getAllDocs()) {
|
|
944
|
+
let g = groups.find((x) => x.group === doc.group);
|
|
945
|
+
if (!g) { g = { group: doc.group, items: [] }; groups.push(g); }
|
|
946
|
+
g.items.push(doc);
|
|
947
|
+
}
|
|
948
|
+
return groups;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export function adjacentDocs(slug: string): { prev: DocMeta | null; next: DocMeta | null } {
|
|
952
|
+
const all = getAllDocs();
|
|
953
|
+
const i = all.findIndex((d) => d.slug === slug);
|
|
954
|
+
return { prev: i > 0 ? all[i - 1]! : null, next: i >= 0 && i < all.length - 1 ? all[i + 1]! : null };
|
|
955
|
+
}
|
|
956
|
+
`;
|
|
957
|
+
var docsSidebar = () => `"use client";
|
|
958
|
+
import Link from "next/link";
|
|
959
|
+
import { usePathname } from "next/navigation";
|
|
960
|
+
|
|
961
|
+
interface DocMeta { slug: string; title: string; group: string; order: number; description: string; }
|
|
962
|
+
|
|
963
|
+
export function DocsSidebar({ nav }: { nav: { group: string; items: DocMeta[] }[] }) {
|
|
964
|
+
const path = usePathname();
|
|
965
|
+
return (
|
|
966
|
+
<nav className="flex flex-col gap-6 text-sm">
|
|
967
|
+
{nav.map((group) => (
|
|
968
|
+
<div key={group.group}>
|
|
969
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-widest text-faint">{group.group}</p>
|
|
970
|
+
<ul className="flex flex-col gap-1">
|
|
971
|
+
{group.items.map((d) => {
|
|
972
|
+
const href = \`/docs/\${d.slug}\`;
|
|
973
|
+
const active = path === href;
|
|
974
|
+
return (
|
|
975
|
+
<li key={d.slug}>
|
|
976
|
+
<Link
|
|
977
|
+
href={href}
|
|
978
|
+
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"}\`}
|
|
979
|
+
>
|
|
980
|
+
{d.title}
|
|
981
|
+
</Link>
|
|
982
|
+
</li>
|
|
983
|
+
);
|
|
984
|
+
})}
|
|
985
|
+
</ul>
|
|
986
|
+
</div>
|
|
987
|
+
))}
|
|
988
|
+
</nav>
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
`;
|
|
992
|
+
var docsLayout = () => `import Link from "next/link";
|
|
993
|
+
import { getDocNav } from "@/lib/docs";
|
|
994
|
+
import { DocsSidebar } from "@/components/docs-sidebar";
|
|
995
|
+
import { ThemeToggle } from "@/components/theme-toggle";
|
|
996
|
+
|
|
997
|
+
export default function DocsLayout({ children }: { children: React.ReactNode }) {
|
|
998
|
+
const nav = getDocNav();
|
|
999
|
+
return (
|
|
1000
|
+
<div className="mx-auto grid max-w-6xl gap-10 px-6 py-16 md:grid-cols-[220px_1fr]">
|
|
1001
|
+
<aside className="md:sticky md:top-24 md:h-fit">
|
|
1002
|
+
<div className="mb-6 flex items-center justify-between">
|
|
1003
|
+
<Link href="/docs" className="block text-lg font-black gradient-text">Docs</Link>
|
|
1004
|
+
<ThemeToggle />
|
|
1005
|
+
</div>
|
|
1006
|
+
<DocsSidebar nav={nav} />
|
|
1007
|
+
</aside>
|
|
1008
|
+
<div className="min-w-0">{children}</div>
|
|
1009
|
+
</div>
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
`;
|
|
1013
|
+
var docsIndexPage = (ctx) => `import Link from "next/link";
|
|
1014
|
+
import { site } from "@/lib/site";
|
|
1015
|
+
import { getAllDocs } from "@/lib/docs";
|
|
1016
|
+
|
|
1017
|
+
export const metadata = site.meta({
|
|
1018
|
+
title: "Documentation",
|
|
1019
|
+
path: "/docs",
|
|
1020
|
+
description: "Documentation for ${ctx.template.siteName}.",
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
export default function DocsIndex() {
|
|
1024
|
+
const docs = getAllDocs();
|
|
1025
|
+
return (
|
|
1026
|
+
<div>
|
|
1027
|
+
<h1 className="text-4xl font-black gradient-text">Documentation</h1>
|
|
1028
|
+
<p className="mt-4 text-muted">Everything you need to get started and go deep.</p>
|
|
1029
|
+
<div className="mt-10 grid gap-4 sm:grid-cols-2">
|
|
1030
|
+
{docs.map((d) => (
|
|
1031
|
+
<Link key={d.slug} href={\`/docs/\${d.slug}\`} className="rounded-2xl border border-hairline bg-surface p-5 transition hover:border-hairline">
|
|
1032
|
+
<span className="text-xs font-semibold uppercase tracking-widest text-faint">{d.group}</span>
|
|
1033
|
+
<h2 className="mt-1 text-lg font-bold">{d.title}</h2>
|
|
1034
|
+
{d.description && <p className="mt-1 text-sm text-muted">{d.description}</p>}
|
|
1035
|
+
</Link>
|
|
1036
|
+
))}
|
|
1037
|
+
</div>
|
|
1038
|
+
</div>
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
`;
|
|
1042
|
+
var docsPage = () => `import Link from "next/link";
|
|
1043
|
+
import { notFound } from "next/navigation";
|
|
1044
|
+
import { site } from "@/lib/site";
|
|
1045
|
+
import { getAllDocs, getDoc, adjacentDocs } from "@/lib/docs";
|
|
1046
|
+
|
|
1047
|
+
export function generateStaticParams() {
|
|
1048
|
+
return getAllDocs().map((d) => ({ slug: d.slug }));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
|
1052
|
+
const { slug } = await params;
|
|
1053
|
+
const doc = getDoc(slug);
|
|
1054
|
+
if (!doc) return {};
|
|
1055
|
+
return site.meta({ title: doc.title, path: \`/docs/\${slug}\`, description: doc.description });
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export default async function DocPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
1059
|
+
const { slug } = await params;
|
|
1060
|
+
const doc = getDoc(slug);
|
|
1061
|
+
if (!doc) notFound();
|
|
1062
|
+
const { prev, next } = adjacentDocs(slug);
|
|
1063
|
+
|
|
1064
|
+
return (
|
|
1065
|
+
<article>
|
|
1066
|
+
<span className="text-xs font-semibold uppercase tracking-widest text-faint">{doc.group}</span>
|
|
1067
|
+
<h1 className="mt-1 text-4xl font-black leading-tight">{doc.title}</h1>
|
|
1068
|
+
{doc.description && <p className="mt-3 text-lg text-muted">{doc.description}</p>}
|
|
1069
|
+
|
|
1070
|
+
{doc.toc.length > 2 && (
|
|
1071
|
+
<div className="mt-8 rounded-xl border border-hairline bg-surface p-4">
|
|
1072
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-widest text-faint">On this page</p>
|
|
1073
|
+
<ul className="flex flex-col gap-1 text-sm">
|
|
1074
|
+
{doc.toc.map((h) => (
|
|
1075
|
+
<li key={h.id} style={{ paddingLeft: (h.level - 1) * 12 }}>
|
|
1076
|
+
<a href={\`#\${h.id}\`} className="text-muted hover:text-fg">{h.text}</a>
|
|
1077
|
+
</li>
|
|
1078
|
+
))}
|
|
1079
|
+
</ul>
|
|
1080
|
+
</div>
|
|
1081
|
+
)}
|
|
1082
|
+
|
|
1083
|
+
<div className="prose mt-10" dangerouslySetInnerHTML={{ __html: doc.html }} />
|
|
1084
|
+
|
|
1085
|
+
<nav className="mt-14 flex justify-between gap-4 border-t border-hairline pt-8 text-sm">
|
|
1086
|
+
{prev ? <Link href={\`/docs/\${prev.slug}\`} className="text-muted hover:text-fg">← {prev.title}</Link> : <span />}
|
|
1087
|
+
{next ? <Link href={\`/docs/\${next.slug}\`} className="text-right text-muted hover:text-fg">{next.title} →</Link> : <span />}
|
|
1088
|
+
</nav>
|
|
1089
|
+
</article>
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
`;
|
|
1093
|
+
var docsSitemapTs = () => `import { sitemapForSite } from "@lacspace/sitemap";
|
|
1094
|
+
import { site } from "@/lib/site";
|
|
1095
|
+
import { getAllDocs } from "@/lib/docs";
|
|
1096
|
+
|
|
1097
|
+
export function GET() {
|
|
1098
|
+
const paths = ["/", "/about", "/contact", "/docs", ...getAllDocs().map((d) => \`/docs/\${d.slug}\`)];
|
|
1099
|
+
const xml = sitemapForSite(site.config, paths);
|
|
1100
|
+
return new Response(xml, { headers: { "content-type": "application/xml" } });
|
|
1101
|
+
}
|
|
1102
|
+
`;
|
|
1103
|
+
var sampleDocIntro = (ctx) => `---
|
|
1104
|
+
title: Introduction
|
|
1105
|
+
group: Getting Started
|
|
1106
|
+
order: 1
|
|
1107
|
+
description: What ${ctx.template.siteName} is and how these docs work.
|
|
1108
|
+
---
|
|
1109
|
+
|
|
1110
|
+
# Introduction
|
|
1111
|
+
|
|
1112
|
+
Welcome to the **${ctx.template.siteName}** documentation. These pages are plain
|
|
1113
|
+
**Markdown files** in \`content/docs/\`, rendered to static pages with
|
|
1114
|
+
[\`@lacspace/markdown\`](https://www.npmjs.com/package/@lacspace/markdown).
|
|
1115
|
+
|
|
1116
|
+
## How the docs are organised
|
|
1117
|
+
|
|
1118
|
+
- Each \`.md\` file becomes a page at \`/docs/<filename>\`.
|
|
1119
|
+
- Front-matter sets the **title**, **group** (sidebar heading) and **order**.
|
|
1120
|
+
- The sidebar, the on-this-page table of contents and prev/next links are all
|
|
1121
|
+
generated for you.
|
|
1122
|
+
|
|
1123
|
+
> Edit these files, add your own, and the navigation updates automatically.
|
|
1124
|
+
`;
|
|
1125
|
+
var sampleDocInstall = () => `---
|
|
1126
|
+
title: Installation
|
|
1127
|
+
group: Getting Started
|
|
1128
|
+
order: 2
|
|
1129
|
+
description: Add a new documentation page in under a minute.
|
|
1130
|
+
---
|
|
1131
|
+
|
|
1132
|
+
# Installation & setup
|
|
1133
|
+
|
|
1134
|
+
Create a Markdown file in \`content/docs/\` with front-matter at the top:
|
|
1135
|
+
|
|
1136
|
+
\`\`\`md
|
|
1137
|
+
---
|
|
1138
|
+
title: My page
|
|
1139
|
+
group: Guides
|
|
1140
|
+
order: 1
|
|
1141
|
+
description: A short summary for SEO and the index.
|
|
1142
|
+
---
|
|
1143
|
+
|
|
1144
|
+
# My page
|
|
1145
|
+
|
|
1146
|
+
Write **Markdown** here \u2014 headings, lists, tables and code all work.
|
|
1147
|
+
\`\`\`
|
|
1148
|
+
|
|
1149
|
+
That's it \u2014 the page appears at \`/docs/my-page\` and in the sidebar under "Guides".
|
|
1150
|
+
`;
|
|
1151
|
+
var sampleDocWriting = () => `---
|
|
1152
|
+
title: Writing content
|
|
1153
|
+
group: Guides
|
|
1154
|
+
order: 1
|
|
1155
|
+
description: Everything the Markdown renderer supports.
|
|
1156
|
+
---
|
|
1157
|
+
|
|
1158
|
+
# Writing content
|
|
1159
|
+
|
|
1160
|
+
The renderer supports the essentials \u2014 and a bit more.
|
|
1161
|
+
|
|
1162
|
+
## Text & lists
|
|
1163
|
+
|
|
1164
|
+
**Bold**, *italic*, \`inline code\`, and:
|
|
1165
|
+
|
|
1166
|
+
- bullet lists
|
|
1167
|
+
- that nest
|
|
1168
|
+
- [x] task lists
|
|
1169
|
+
|
|
1170
|
+
## Tables
|
|
1171
|
+
|
|
1172
|
+
| Feature | Supported |
|
|
1173
|
+
| ------- | :-------: |
|
|
1174
|
+
| Headings + anchors | \u2705 |
|
|
1175
|
+
| Code blocks | \u2705 |
|
|
1176
|
+
| Tables | \u2705 |
|
|
1177
|
+
|
|
1178
|
+
## Code
|
|
1179
|
+
|
|
1180
|
+
\`\`\`ts
|
|
1181
|
+
export function greet(name: string) {
|
|
1182
|
+
return \`Hello, \${name}!\`;
|
|
1183
|
+
}
|
|
1184
|
+
\`\`\`
|
|
1185
|
+
|
|
1186
|
+
Every heading gets an anchor id, so the on-this-page menu links straight to it.
|
|
1187
|
+
`;
|
|
796
1188
|
var seoWorkflow = () => `name: SEO
|
|
797
1189
|
|
|
798
1190
|
on:
|
|
@@ -901,17 +1293,17 @@ export default function Blog() {
|
|
|
901
1293
|
return (
|
|
902
1294
|
<main className="mx-auto max-w-3xl px-6 py-24">
|
|
903
1295
|
<h1 className="text-4xl font-black gradient-text sm:text-5xl">Blog</h1>
|
|
904
|
-
<p className="mt-4 text-
|
|
1296
|
+
<p className="mt-4 text-muted">Thoughts, notes and updates.</p>
|
|
905
1297
|
<div className="mt-12 flex flex-col gap-8">
|
|
906
1298
|
{posts.map((post) => (
|
|
907
|
-
<Link key={post.slug} href={\`/blog/\${post.slug}\`} className="group rounded-2xl border border-
|
|
908
|
-
{post.tag && <span className="text-xs font-semibold uppercase tracking-widest text-
|
|
1299
|
+
<Link key={post.slug} href={\`/blog/\${post.slug}\`} className="group rounded-2xl border border-hairline bg-surface p-6 transition hover:border-hairline">
|
|
1300
|
+
{post.tag && <span className="text-xs font-semibold uppercase tracking-widest text-faint">{post.tag}</span>}
|
|
909
1301
|
<h2 className="mt-1 text-2xl font-bold group-hover:gradient-text">{post.title}</h2>
|
|
910
|
-
<p className="mt-2 text-
|
|
911
|
-
{post.date && <time className="mt-3 block text-sm text-
|
|
1302
|
+
<p className="mt-2 text-muted">{post.excerpt}</p>
|
|
1303
|
+
{post.date && <time className="mt-3 block text-sm text-faint">{new Date(post.date).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })}</time>}
|
|
912
1304
|
</Link>
|
|
913
1305
|
))}
|
|
914
|
-
{posts.length === 0 && <p className="text-
|
|
1306
|
+
{posts.length === 0 && <p className="text-muted">No posts yet \u2014 add a Markdown file in <code>content/posts/</code>.</p>}
|
|
915
1307
|
</div>
|
|
916
1308
|
</main>
|
|
917
1309
|
);
|
|
@@ -958,10 +1350,10 @@ export default async function PostPage({ params }: { params: Promise<{ slug: str
|
|
|
958
1350
|
|
|
959
1351
|
return (
|
|
960
1352
|
<main className="mx-auto max-w-2xl px-6 py-24">
|
|
961
|
-
<Link href="/blog" className="text-sm text-
|
|
1353
|
+
<Link href="/blog" className="text-sm text-muted hover:text-fg">← All posts</Link>
|
|
962
1354
|
<article className="mt-6">
|
|
963
1355
|
<h1 className="text-4xl font-black leading-tight sm:text-5xl">{post.title}</h1>
|
|
964
|
-
{post.date && <time className="mt-4 block text-sm text-
|
|
1356
|
+
{post.date && <time className="mt-4 block text-sm text-faint">{new Date(post.date).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })}</time>}
|
|
965
1357
|
<div className="prose mt-10" dangerouslySetInnerHTML={{ __html: post.html }} />
|
|
966
1358
|
</article>
|
|
967
1359
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
|
@@ -1045,6 +1437,7 @@ Check the [first post](/blog/welcome) for the full Markdown reference.
|
|
|
1045
1437
|
`;
|
|
1046
1438
|
function buildFiles(ctx) {
|
|
1047
1439
|
const isBlog = ctx.template.key === "blog";
|
|
1440
|
+
const isDocs = ctx.template.key === "docs";
|
|
1048
1441
|
const files = {
|
|
1049
1442
|
"package.json": pkgJson(ctx),
|
|
1050
1443
|
"tsconfig.json": tsconfig(),
|
|
@@ -1063,11 +1456,12 @@ function buildFiles(ctx) {
|
|
|
1063
1456
|
"app/apple-icon.tsx": appleIconTsx(ctx),
|
|
1064
1457
|
"app/manifest.ts": manifestTs(ctx),
|
|
1065
1458
|
"app/robots.txt/route.ts": robotsTs(),
|
|
1066
|
-
"app/sitemap.xml/route.ts": isBlog ? blogSitemapTs() : sitemapTs(),
|
|
1459
|
+
"app/sitemap.xml/route.ts": isBlog ? blogSitemapTs() : isDocs ? docsSitemapTs() : sitemapTs(),
|
|
1067
1460
|
"app/contact/page.tsx": contactPage(ctx),
|
|
1068
1461
|
"app/actions.ts": actionsTs(),
|
|
1069
1462
|
"components/command-menu.tsx": commandMenu(ctx),
|
|
1070
1463
|
"components/contact-form.tsx": contactForm(),
|
|
1464
|
+
"components/theme-toggle.tsx": themeToggle(),
|
|
1071
1465
|
".github/workflows/seo.yml": seoWorkflow(),
|
|
1072
1466
|
".env.example": envExample(),
|
|
1073
1467
|
"WELCOME.md": welcomeMd(ctx)
|
|
@@ -1079,6 +1473,16 @@ function buildFiles(ctx) {
|
|
|
1079
1473
|
files["content/posts/welcome.md"] = samplePostWelcome(ctx);
|
|
1080
1474
|
files["content/posts/building-in-the-open.md"] = samplePostSecond(ctx);
|
|
1081
1475
|
}
|
|
1476
|
+
if (isDocs) {
|
|
1477
|
+
files["lib/docs.ts"] = docsLib();
|
|
1478
|
+
files["components/docs-sidebar.tsx"] = docsSidebar();
|
|
1479
|
+
files["app/docs/layout.tsx"] = docsLayout();
|
|
1480
|
+
files["app/docs/page.tsx"] = docsIndexPage(ctx);
|
|
1481
|
+
files["app/docs/[slug]/page.tsx"] = docsPage();
|
|
1482
|
+
files["content/docs/introduction.md"] = sampleDocIntro(ctx);
|
|
1483
|
+
files["content/docs/installation.md"] = sampleDocInstall();
|
|
1484
|
+
files["content/docs/writing-content.md"] = sampleDocWriting();
|
|
1485
|
+
}
|
|
1082
1486
|
return files;
|
|
1083
1487
|
}
|
|
1084
1488
|
function parseArgs(list) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-lacspace-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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": {
|