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.
Files changed (2) hide show
  1. package/dist/index.js +487 -83
  2. 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
- // The blog template renders Markdown posts with @lacspace/markdown.
47
- ...ctx.template.key === "blog" ? { "@lacspace/markdown": "^1.0.0" } : {}
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: #e5e7eb;
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: rgb(255 255 255 / 0.08); }
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: #0a0a0f; }
157
+ ::selection { background: var(--accent-to); color: #fff; }
123
158
  ::-webkit-scrollbar { width: 10px; height: 10px; }
124
- ::-webkit-scrollbar-thumb { background: rgb(255 255 255 / 0.12); border-radius: 8px; }
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 Press \u2318K / Ctrl-K anywhere \u2014 powered by @lacspace/ui */}
184
- <CommandMenu />
185
- {children}
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-white/60">This page wandered off.</p>
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-white/10">
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="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 ")}
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-white/10 py-10 text-center text-sm text-white/40">
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-white/70 hover:text-white">Lacspace</a>.
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-white/40">Portfolio</p>
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-white/60">${ctx.template.siteDescription}</p>
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-white/20 px-6 py-3 font-semibold hover:bg-white/5">Get in touch</a>
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-white/10 bg-white/[0.02] p-6 transition hover:-translate-y-1 hover:border-white/20">
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-white/50">A short description of what you built and the impact it had.</p>
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-white/60">${ctx.template.siteDescription}</p>
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-white/10 bg-white/[0.02] p-8">
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-white/60">{s.d}</p>
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-white/60">Tell us about your project and we'll get back within a day.</p>
419
- <a href="mailto:hello@example.com" className="mt-8 inline-block rounded-full border border-white/20 px-8 py-3 font-semibold hover:bg-white/5">hello@example.com</a>
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-white/60">${ctx.template.siteDescription}</p>
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-white/10 bg-white/[0.02] p-4 transition hover:-translate-y-1">
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-white/60">{prod.p}</span>
484
+ <span className="text-muted">{prod.p}</span>
442
485
  </div>
443
- <button className="mt-3 w-full rounded-lg border border-white/15 py-2 text-sm font-medium hover:bg-white/5">Add to cart</button>
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-white/40">The Journal</p>
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-white/60">${ctx.template.siteDescription}</p>
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-white/10 bg-white/[0.02] p-8 transition hover:border-white/20">
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-white/40">Featured</span>
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-white/60">A short, punchy dek that pulls the reader into the piece and makes them want more.</p>
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-white/40">Essay</span>
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-white/50">Two lines of supporting copy to set the scene for the reader.</p>
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-white/60">${ctx.template.siteDescription}</p>
484
- <div className="mt-8 inline-flex items-center gap-3 rounded-lg border border-white/10 bg-black/40 px-4 py-3 font-mono text-sm text-white/80">
485
- <span className="text-white/40">$</span> npm install @acme/sdk
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-white/10 bg-white/[0.02] p-8 transition hover:border-white/20">
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-white/60">{c.d}</p>
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-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">
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-white/5 hover:text-white">{l}</a>
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-white/50">${ctx.template.siteDescription}</p>
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-white/10 bg-white/[0.02] p-5">
523
- <div className="text-sm text-white/50">{s.l}</div>
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-white/10 bg-white/[0.02] p-6">
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-white/5 pb-3 text-sm">
534
- <span className="text-white/70">Event #{i}</span>
535
- <span className="text-white/40">just now</span>
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-white/40">Est. 2026</p>
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-white/60">${ctx.template.siteDescription}</p>
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-white/10 pb-4">
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-white/50">A short, mouth-watering description of the dish.</p>
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-white/60">Open Wed\u2013Sun, 5pm till late. Walk-ins welcome; bookings recommended.</p>
571
- <a href="tel:+10000000000" className="mt-8 inline-block rounded-full border border-white/20 px-8 py-3 font-semibold hover:bg-white/5">Call to book</a>
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-white/15 px-4 py-1 text-xs font-semibold text-white/60">New \xB7 v1.0</p>
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-white/60">${ctx.template.siteDescription}</p>
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-white/20 px-8 py-3 font-semibold hover:bg-white/5">See features</a>
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-white/10 bg-white/[0.02] p-8">
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-white/60">{f.d}</p>
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-white/10 bg-white/[0.02] p-10">
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-white/40">/mo</span></p>
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-white/50 hover:text-white">\u2190 Back home</Link>
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-white/60">
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-white/10 bg-white/5 px-4 py-3 outline-none focus:border-white/30";
724
- const label = "mb-1 block text-sm text-white/60";
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-white/10 bg-white/5 p-8 text-center text-lg">
814
+ <p className="rounded-2xl border border-hairline bg-surface p-8 text-center text-lg">
733
815
  Thanks \u2014 we&rsquo;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-white/60">Have a question or a project in mind? Drop a message below.</p>
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">&larr; {prev.title}</Link> : <span />}
1087
+ {next ? <Link href={\`/docs/\${next.slug}\`} className="text-right text-muted hover:text-fg">{next.title} &rarr;</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-white/60">Thoughts, notes and updates.</p>
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-white/10 bg-white/5 p-6 transition hover:border-white/25">
908
- {post.tag && <span className="text-xs font-semibold uppercase tracking-widest text-white/40">{post.tag}</span>}
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-white/60">{post.excerpt}</p>
911
- {post.date && <time className="mt-3 block text-sm text-white/40">{new Date(post.date).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })}</time>}
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-white/50">No posts yet \u2014 add a Markdown file in <code>content/posts/</code>.</p>}
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-white/50 hover:text-white">&larr; All posts</Link>
1353
+ <Link href="/blog" className="text-sm text-muted hover:text-fg">&larr; 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-white/40">{new Date(post.date).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })}</time>}
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.5.0",
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": {