create-lacspace-app 1.6.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 +179 -93
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -43,6 +43,9 @@ var pkgJson = (ctx) => JSON.stringify({
43
43
  "@lacspace/ui": "^1.0.0",
44
44
  "@lacspace/form": "^1.0.0",
45
45
  "@lacspace/validate": "^1.0.0",
46
+ // React Kit: dark/light theming (no-flash) + essential hooks.
47
+ "@lacspace/theme": "^1.0.1",
48
+ "@lacspace/hooks": "^1.0.0",
46
49
  // The blog & docs templates render Markdown with @lacspace/markdown.
47
50
  ...ctx.template.key === "blog" || ctx.template.key === "docs" ? { "@lacspace/markdown": "^1.0.0" } : {}
48
51
  },
@@ -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 {
@@ -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";
@@ -721,8 +802,8 @@ import { useActionState } from "react";
721
802
  import { honeypotProps, timestampValue } from "@lacspace/form";
722
803
  import { submitContact, type ContactState } from "@/app/actions";
723
804
 
724
- const field = "w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 outline-none focus:border-white/30";
725
- 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";
726
807
  const errCls = "mt-1 text-sm text-red-400";
727
808
 
728
809
  export function ContactForm() {
@@ -730,7 +811,7 @@ export function ContactForm() {
730
811
 
731
812
  if (state?.ok) {
732
813
  return (
733
- <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">
734
815
  Thanks \u2014 we&rsquo;ll be in touch! \u2705
735
816
  </p>
736
817
  );
@@ -786,7 +867,7 @@ export default function ContactPage() {
786
867
  return (
787
868
  <main className="mx-auto max-w-2xl px-6 py-24">
788
869
  <h1 className="text-4xl font-black gradient-text sm:text-5xl">Get in touch</h1>
789
- <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>
790
871
  <div className="mt-10">
791
872
  <ContactForm />
792
873
  </div>
@@ -885,7 +966,7 @@ export function DocsSidebar({ nav }: { nav: { group: string; items: DocMeta[] }[
885
966
  <nav className="flex flex-col gap-6 text-sm">
886
967
  {nav.map((group) => (
887
968
  <div key={group.group}>
888
- <p className="mb-2 text-xs font-semibold uppercase tracking-widest text-white/40">{group.group}</p>
969
+ <p className="mb-2 text-xs font-semibold uppercase tracking-widest text-faint">{group.group}</p>
889
970
  <ul className="flex flex-col gap-1">
890
971
  {group.items.map((d) => {
891
972
  const href = \`/docs/\${d.slug}\`;
@@ -894,7 +975,7 @@ export function DocsSidebar({ nav }: { nav: { group: string; items: DocMeta[] }[
894
975
  <li key={d.slug}>
895
976
  <Link
896
977
  href={href}
897
- className={\`block rounded-lg px-3 py-1.5 transition \${active ? "gradient-bg font-semibold text-black" : "text-white/70 hover:bg-white/5 hover:text-white"}\`}
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"}\`}
898
979
  >
899
980
  {d.title}
900
981
  </Link>
@@ -911,13 +992,17 @@ export function DocsSidebar({ nav }: { nav: { group: string; items: DocMeta[] }[
911
992
  var docsLayout = () => `import Link from "next/link";
912
993
  import { getDocNav } from "@/lib/docs";
913
994
  import { DocsSidebar } from "@/components/docs-sidebar";
995
+ import { ThemeToggle } from "@/components/theme-toggle";
914
996
 
915
997
  export default function DocsLayout({ children }: { children: React.ReactNode }) {
916
998
  const nav = getDocNav();
917
999
  return (
918
1000
  <div className="mx-auto grid max-w-6xl gap-10 px-6 py-16 md:grid-cols-[220px_1fr]">
919
1001
  <aside className="md:sticky md:top-24 md:h-fit">
920
- <Link href="/docs" className="mb-6 block text-lg font-black gradient-text">Docs</Link>
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>
921
1006
  <DocsSidebar nav={nav} />
922
1007
  </aside>
923
1008
  <div className="min-w-0">{children}</div>
@@ -940,13 +1025,13 @@ export default function DocsIndex() {
940
1025
  return (
941
1026
  <div>
942
1027
  <h1 className="text-4xl font-black gradient-text">Documentation</h1>
943
- <p className="mt-4 text-white/60">Everything you need to get started and go deep.</p>
1028
+ <p className="mt-4 text-muted">Everything you need to get started and go deep.</p>
944
1029
  <div className="mt-10 grid gap-4 sm:grid-cols-2">
945
1030
  {docs.map((d) => (
946
- <Link key={d.slug} href={\`/docs/\${d.slug}\`} className="rounded-2xl border border-white/10 bg-white/5 p-5 transition hover:border-white/25">
947
- <span className="text-xs font-semibold uppercase tracking-widest text-white/40">{d.group}</span>
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>
948
1033
  <h2 className="mt-1 text-lg font-bold">{d.title}</h2>
949
- {d.description && <p className="mt-1 text-sm text-white/60">{d.description}</p>}
1034
+ {d.description && <p className="mt-1 text-sm text-muted">{d.description}</p>}
950
1035
  </Link>
951
1036
  ))}
952
1037
  </div>
@@ -978,17 +1063,17 @@ export default async function DocPage({ params }: { params: Promise<{ slug: stri
978
1063
 
979
1064
  return (
980
1065
  <article>
981
- <span className="text-xs font-semibold uppercase tracking-widest text-white/40">{doc.group}</span>
1066
+ <span className="text-xs font-semibold uppercase tracking-widest text-faint">{doc.group}</span>
982
1067
  <h1 className="mt-1 text-4xl font-black leading-tight">{doc.title}</h1>
983
- {doc.description && <p className="mt-3 text-lg text-white/60">{doc.description}</p>}
1068
+ {doc.description && <p className="mt-3 text-lg text-muted">{doc.description}</p>}
984
1069
 
985
1070
  {doc.toc.length > 2 && (
986
- <div className="mt-8 rounded-xl border border-white/10 bg-white/5 p-4">
987
- <p className="mb-2 text-xs font-semibold uppercase tracking-widest text-white/40">On this page</p>
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>
988
1073
  <ul className="flex flex-col gap-1 text-sm">
989
1074
  {doc.toc.map((h) => (
990
1075
  <li key={h.id} style={{ paddingLeft: (h.level - 1) * 12 }}>
991
- <a href={\`#\${h.id}\`} className="text-white/60 hover:text-white">{h.text}</a>
1076
+ <a href={\`#\${h.id}\`} className="text-muted hover:text-fg">{h.text}</a>
992
1077
  </li>
993
1078
  ))}
994
1079
  </ul>
@@ -997,9 +1082,9 @@ export default async function DocPage({ params }: { params: Promise<{ slug: stri
997
1082
 
998
1083
  <div className="prose mt-10" dangerouslySetInnerHTML={{ __html: doc.html }} />
999
1084
 
1000
- <nav className="mt-14 flex justify-between gap-4 border-t border-white/10 pt-8 text-sm">
1001
- {prev ? <Link href={\`/docs/\${prev.slug}\`} className="text-white/70 hover:text-white">&larr; {prev.title}</Link> : <span />}
1002
- {next ? <Link href={\`/docs/\${next.slug}\`} className="text-right text-white/70 hover:text-white">{next.title} &rarr;</Link> : <span />}
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 />}
1003
1088
  </nav>
1004
1089
  </article>
1005
1090
  );
@@ -1208,17 +1293,17 @@ export default function Blog() {
1208
1293
  return (
1209
1294
  <main className="mx-auto max-w-3xl px-6 py-24">
1210
1295
  <h1 className="text-4xl font-black gradient-text sm:text-5xl">Blog</h1>
1211
- <p className="mt-4 text-white/60">Thoughts, notes and updates.</p>
1296
+ <p className="mt-4 text-muted">Thoughts, notes and updates.</p>
1212
1297
  <div className="mt-12 flex flex-col gap-8">
1213
1298
  {posts.map((post) => (
1214
- <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">
1215
- {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>}
1216
1301
  <h2 className="mt-1 text-2xl font-bold group-hover:gradient-text">{post.title}</h2>
1217
- <p className="mt-2 text-white/60">{post.excerpt}</p>
1218
- {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>}
1219
1304
  </Link>
1220
1305
  ))}
1221
- {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>}
1222
1307
  </div>
1223
1308
  </main>
1224
1309
  );
@@ -1265,10 +1350,10 @@ export default async function PostPage({ params }: { params: Promise<{ slug: str
1265
1350
 
1266
1351
  return (
1267
1352
  <main className="mx-auto max-w-2xl px-6 py-24">
1268
- <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>
1269
1354
  <article className="mt-6">
1270
1355
  <h1 className="text-4xl font-black leading-tight sm:text-5xl">{post.title}</h1>
1271
- {post.date && <time className="mt-4 block text-sm text-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>}
1272
1357
  <div className="prose mt-10" dangerouslySetInnerHTML={{ __html: post.html }} />
1273
1358
  </article>
1274
1359
  <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
@@ -1376,6 +1461,7 @@ function buildFiles(ctx) {
1376
1461
  "app/actions.ts": actionsTs(),
1377
1462
  "components/command-menu.tsx": commandMenu(ctx),
1378
1463
  "components/contact-form.tsx": contactForm(),
1464
+ "components/theme-toggle.tsx": themeToggle(),
1379
1465
  ".github/workflows/seo.yml": seoWorkflow(),
1380
1466
  ".env.example": envExample(),
1381
1467
  "WELCOME.md": welcomeMd(ctx)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lacspace-app",
3
- "version": "1.6.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": {