create-lacspace-app 2.11.0 → 2.12.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/README.md CHANGED
@@ -20,6 +20,10 @@ npx create-lacspace-app my-app --template saas --fullstack
20
20
 
21
21
  You choose the *kind* of site you're building. It writes a **real Next.js 15 + React 19 + Tailwind v4 app** — not a hello-world, but a genuinely **polished, modern site**: a fluid `clamp()` type scale, tight display headings, a refined light **and** dark palette, glass chrome, soft layered shadows, a smooth logo marquee, animated counters, scroll reveals and a shimmering primary CTA — every page filled in, an SEO stack wired end-to-end, and a **26-component UI kit** you can drop in anywhere.
22
22
 
23
+ > **New in v2.12 — real-time & privacy (16 add-ons).** Two more keyless, no-vendor add-ons:
24
+ > - **`realtime`** — live server→browser updates over **Server-Sent Events** (`@lacspace/sse`): a channel hub + a `/live` stream + a React `useSSE` feed. No WebSocket server. Full-stack.
25
+ > - **`consent`** — **GDPR-friendly cookie consent** (`@lacspace/consent`): a drop-in `<ConsentBanner/>`, per-category choices, cookie persistence and `whenConsent()` script-gating. Frontend.
26
+ >
23
27
  > **New in v2.11 — the Web Engagement Kit (14 add-ons).** Three keyless, no-vendor add-ons on brand-new `@lacspace` packages:
24
28
  > - **`push`** — real **browser push notifications** (`@lacspace/web-push`): keyless VAPID, no Firebase/FCM. Generates a service worker, a subscribe button, and a backend that stores subscriptions and sends. Full-stack.
25
29
  > - **`captcha`** — a **privacy-friendly proof-of-work CAPTCHA** (`@lacspace/captcha`): no Google/Cloudflare, no keys, no tracking. A drop-in widget + a backend verify route. Full-stack.
package/dist/index.js CHANGED
@@ -2402,6 +2402,38 @@ var FEATURES = [
2402
2402
  "Push works on https (or localhost); the generated public/sw.js receives the notifications."
2403
2403
  ],
2404
2404
  learn: "https://developer.lacspace.com/packages/web-push"
2405
+ },
2406
+ {
2407
+ key: "consent",
2408
+ label: "Cookie consent",
2409
+ description: "GDPR-friendly cookie consent (@lacspace/consent) \u2014 a drop-in <ConsentBanner/>, per-category choices, cookie persistence and whenConsent() script-gating. Frontend, any template.",
2410
+ deps: { "@lacspace/consent": "^1.0.0" },
2411
+ files: () => ({ "components/consent.tsx": consentComponent() }),
2412
+ nextSteps: [
2413
+ "Render <ConsentBanner/> once in app/layout.tsx: import { ConsentBanner } from '@/components/consent'.",
2414
+ "Gate analytics/marketing scripts: whenConsent(consent, 'analytics', () => { /* load tag */ }).",
2415
+ "The choice is stored in a cookie \u2014 gate server-side too with parseConsentCookie()."
2416
+ ],
2417
+ learn: "https://developer.lacspace.com/packages/consent"
2418
+ },
2419
+ {
2420
+ key: "realtime",
2421
+ label: "Real-time (SSE)",
2422
+ description: "Live server-to-browser updates over Server-Sent Events (@lacspace/sse) \u2014 a channel hub + a /live stream + a React useSSE feed. No WebSocket server. Full-stack.",
2423
+ requiresBackend: true,
2424
+ deps: { "@lacspace/sse": "^1.0.0" },
2425
+ files: () => ({ "app/live/page.tsx": realtimeLivePage() }),
2426
+ backend: () => ({
2427
+ deps: { "@lacspace/sse": "^1.0.0" },
2428
+ files: { "src/routes/live.ts": realtimeRoutesBackend() },
2429
+ routes: [{ path: "/live", handler: "liveRoutes", auth: false, importLine: 'import liveRoutes from "./live.js";' }]
2430
+ }),
2431
+ nextSteps: [
2432
+ "Open http://localhost:3000/live in two tabs; click 'Broadcast' in one \u2014 the other updates instantly.",
2433
+ "Push from anywhere on the server: hub.broadcast({ event: 'message', data }) in src/routes/live.ts.",
2434
+ "SSE auto-reconnects and needs no WebSocket server \u2014 great for notifications and live feeds."
2435
+ ],
2436
+ learn: "https://developer.lacspace.com/packages/sse"
2405
2437
  }
2406
2438
  ];
2407
2439
  var aiChatRoute = (_ctx) => `import { resolveConfig } from "@lacspace/providers";
@@ -6920,6 +6952,69 @@ router.post("/send", asyncHandler(async (req, res) => {
6920
6952
  res.json({ sent: results.filter((r) => r.result && !r.result.expired).length, removed: expired.length });
6921
6953
  }));
6922
6954
 
6955
+ export default router;
6956
+ `;
6957
+ var consentComponent = () => `"use client";
6958
+ // how this works: re-export the consent UI + API from one component. Render
6959
+ // <ConsentBanner/> once in app/layout.tsx; gate scripts with whenConsent().
6960
+ export { ConsentBanner, useConsent } from "@lacspace/consent/react";
6961
+ export { consent, whenConsent } from "@lacspace/consent";
6962
+ `;
6963
+ var realtimeLivePage = () => `"use client";
6964
+ import { useState } from "react";
6965
+ import { useSSE } from "@lacspace/sse/react";
6966
+
6967
+ const API = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
6968
+
6969
+ export default function LivePage() {
6970
+ const [log, setLog] = useState<string[]>([]);
6971
+ const { status } = useSSE(API + "/live", {
6972
+ onEvent: { message: (m) => setLog((prev) => [String((m as { text?: string }).text ?? ""), ...prev].slice(0, 50)) },
6973
+ });
6974
+
6975
+ async function broadcast() {
6976
+ await fetch(API + "/live/broadcast", {
6977
+ method: "POST",
6978
+ headers: { "Content-Type": "application/json" },
6979
+ body: JSON.stringify({ text: "Hello at " + new Date().toLocaleTimeString() }),
6980
+ });
6981
+ }
6982
+
6983
+ return (
6984
+ <main className="mx-auto max-w-lg px-6 py-16">
6985
+ <h1 className="text-2xl font-bold">Live feed</h1>
6986
+ <p className="mt-1 text-muted">Server-Sent Events \u2014 status: {status}.</p>
6987
+ <button className="mt-4 rounded-xl border border-hairline px-4 py-2" onClick={broadcast}>Broadcast a message</button>
6988
+ <ul className="mt-6 space-y-2">
6989
+ {log.map((line, i) => (<li key={i} className="rounded-xl border border-hairline p-3">{line}</li>))}
6990
+ </ul>
6991
+ </main>
6992
+ );
6993
+ }
6994
+ `;
6995
+ var realtimeRoutesBackend = () => `import express from "express";
6996
+ import { SSEHub, sseHandler } from "@lacspace/sse";
6997
+ import { asyncHandler } from "../http.js";
6998
+
6999
+ // how this works: one shared hub holds every open SSE connection. GET /live opens
7000
+ // a stream; POST /live/broadcast pushes a "message" event to everyone connected.
7001
+ // Push from anywhere in your app by importing this hub and calling hub.broadcast().
7002
+ const router = express.Router();
7003
+ const hub = new SSEHub();
7004
+ hub.startHeartbeat();
7005
+
7006
+ router.get("/", (_req, res) => {
7007
+ const client = sseHandler(res, { onClose: () => hub.remove(client) });
7008
+ hub.add(client);
7009
+ client.send({ event: "message", data: { text: "Connected to the live feed." } });
7010
+ });
7011
+
7012
+ router.post("/broadcast", asyncHandler(async (req, res) => {
7013
+ const text = typeof req.body?.text === "string" ? req.body.text : "ping";
7014
+ const sent = hub.broadcast({ event: "message", data: { text } });
7015
+ res.json({ sent });
7016
+ }));
7017
+
6923
7018
  export default router;
6924
7019
  `;
6925
7020
  var scope = (ctx) => `@${ctx.name}`;
package/dist/lib.cjs CHANGED
@@ -2406,6 +2406,38 @@ var FEATURES = [
2406
2406
  "Push works on https (or localhost); the generated public/sw.js receives the notifications."
2407
2407
  ],
2408
2408
  learn: "https://developer.lacspace.com/packages/web-push"
2409
+ },
2410
+ {
2411
+ key: "consent",
2412
+ label: "Cookie consent",
2413
+ description: "GDPR-friendly cookie consent (@lacspace/consent) \u2014 a drop-in <ConsentBanner/>, per-category choices, cookie persistence and whenConsent() script-gating. Frontend, any template.",
2414
+ deps: { "@lacspace/consent": "^1.0.0" },
2415
+ files: () => ({ "components/consent.tsx": consentComponent() }),
2416
+ nextSteps: [
2417
+ "Render <ConsentBanner/> once in app/layout.tsx: import { ConsentBanner } from '@/components/consent'.",
2418
+ "Gate analytics/marketing scripts: whenConsent(consent, 'analytics', () => { /* load tag */ }).",
2419
+ "The choice is stored in a cookie \u2014 gate server-side too with parseConsentCookie()."
2420
+ ],
2421
+ learn: "https://developer.lacspace.com/packages/consent"
2422
+ },
2423
+ {
2424
+ key: "realtime",
2425
+ label: "Real-time (SSE)",
2426
+ description: "Live server-to-browser updates over Server-Sent Events (@lacspace/sse) \u2014 a channel hub + a /live stream + a React useSSE feed. No WebSocket server. Full-stack.",
2427
+ requiresBackend: true,
2428
+ deps: { "@lacspace/sse": "^1.0.0" },
2429
+ files: () => ({ "app/live/page.tsx": realtimeLivePage() }),
2430
+ backend: () => ({
2431
+ deps: { "@lacspace/sse": "^1.0.0" },
2432
+ files: { "src/routes/live.ts": realtimeRoutesBackend() },
2433
+ routes: [{ path: "/live", handler: "liveRoutes", auth: false, importLine: 'import liveRoutes from "./live.js";' }]
2434
+ }),
2435
+ nextSteps: [
2436
+ "Open http://localhost:3000/live in two tabs; click 'Broadcast' in one \u2014 the other updates instantly.",
2437
+ "Push from anywhere on the server: hub.broadcast({ event: 'message', data }) in src/routes/live.ts.",
2438
+ "SSE auto-reconnects and needs no WebSocket server \u2014 great for notifications and live feeds."
2439
+ ],
2440
+ learn: "https://developer.lacspace.com/packages/sse"
2409
2441
  }
2410
2442
  ];
2411
2443
  var aiChatRoute = (_ctx) => `import { resolveConfig } from "@lacspace/providers";
@@ -6924,6 +6956,69 @@ router.post("/send", asyncHandler(async (req, res) => {
6924
6956
  res.json({ sent: results.filter((r) => r.result && !r.result.expired).length, removed: expired.length });
6925
6957
  }));
6926
6958
 
6959
+ export default router;
6960
+ `;
6961
+ var consentComponent = () => `"use client";
6962
+ // how this works: re-export the consent UI + API from one component. Render
6963
+ // <ConsentBanner/> once in app/layout.tsx; gate scripts with whenConsent().
6964
+ export { ConsentBanner, useConsent } from "@lacspace/consent/react";
6965
+ export { consent, whenConsent } from "@lacspace/consent";
6966
+ `;
6967
+ var realtimeLivePage = () => `"use client";
6968
+ import { useState } from "react";
6969
+ import { useSSE } from "@lacspace/sse/react";
6970
+
6971
+ const API = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
6972
+
6973
+ export default function LivePage() {
6974
+ const [log, setLog] = useState<string[]>([]);
6975
+ const { status } = useSSE(API + "/live", {
6976
+ onEvent: { message: (m) => setLog((prev) => [String((m as { text?: string }).text ?? ""), ...prev].slice(0, 50)) },
6977
+ });
6978
+
6979
+ async function broadcast() {
6980
+ await fetch(API + "/live/broadcast", {
6981
+ method: "POST",
6982
+ headers: { "Content-Type": "application/json" },
6983
+ body: JSON.stringify({ text: "Hello at " + new Date().toLocaleTimeString() }),
6984
+ });
6985
+ }
6986
+
6987
+ return (
6988
+ <main className="mx-auto max-w-lg px-6 py-16">
6989
+ <h1 className="text-2xl font-bold">Live feed</h1>
6990
+ <p className="mt-1 text-muted">Server-Sent Events \u2014 status: {status}.</p>
6991
+ <button className="mt-4 rounded-xl border border-hairline px-4 py-2" onClick={broadcast}>Broadcast a message</button>
6992
+ <ul className="mt-6 space-y-2">
6993
+ {log.map((line, i) => (<li key={i} className="rounded-xl border border-hairline p-3">{line}</li>))}
6994
+ </ul>
6995
+ </main>
6996
+ );
6997
+ }
6998
+ `;
6999
+ var realtimeRoutesBackend = () => `import express from "express";
7000
+ import { SSEHub, sseHandler } from "@lacspace/sse";
7001
+ import { asyncHandler } from "../http.js";
7002
+
7003
+ // how this works: one shared hub holds every open SSE connection. GET /live opens
7004
+ // a stream; POST /live/broadcast pushes a "message" event to everyone connected.
7005
+ // Push from anywhere in your app by importing this hub and calling hub.broadcast().
7006
+ const router = express.Router();
7007
+ const hub = new SSEHub();
7008
+ hub.startHeartbeat();
7009
+
7010
+ router.get("/", (_req, res) => {
7011
+ const client = sseHandler(res, { onClose: () => hub.remove(client) });
7012
+ hub.add(client);
7013
+ client.send({ event: "message", data: { text: "Connected to the live feed." } });
7014
+ });
7015
+
7016
+ router.post("/broadcast", asyncHandler(async (req, res) => {
7017
+ const text = typeof req.body?.text === "string" ? req.body.text : "ping";
7018
+ const sent = hub.broadcast({ event: "message", data: { text } });
7019
+ res.json({ sent });
7020
+ }));
7021
+
6927
7022
  export default router;
6928
7023
  `;
6929
7024
  var scope = (ctx) => `@${ctx.name}`;
package/dist/lib.js CHANGED
@@ -2403,6 +2403,38 @@ var FEATURES = [
2403
2403
  "Push works on https (or localhost); the generated public/sw.js receives the notifications."
2404
2404
  ],
2405
2405
  learn: "https://developer.lacspace.com/packages/web-push"
2406
+ },
2407
+ {
2408
+ key: "consent",
2409
+ label: "Cookie consent",
2410
+ description: "GDPR-friendly cookie consent (@lacspace/consent) \u2014 a drop-in <ConsentBanner/>, per-category choices, cookie persistence and whenConsent() script-gating. Frontend, any template.",
2411
+ deps: { "@lacspace/consent": "^1.0.0" },
2412
+ files: () => ({ "components/consent.tsx": consentComponent() }),
2413
+ nextSteps: [
2414
+ "Render <ConsentBanner/> once in app/layout.tsx: import { ConsentBanner } from '@/components/consent'.",
2415
+ "Gate analytics/marketing scripts: whenConsent(consent, 'analytics', () => { /* load tag */ }).",
2416
+ "The choice is stored in a cookie \u2014 gate server-side too with parseConsentCookie()."
2417
+ ],
2418
+ learn: "https://developer.lacspace.com/packages/consent"
2419
+ },
2420
+ {
2421
+ key: "realtime",
2422
+ label: "Real-time (SSE)",
2423
+ description: "Live server-to-browser updates over Server-Sent Events (@lacspace/sse) \u2014 a channel hub + a /live stream + a React useSSE feed. No WebSocket server. Full-stack.",
2424
+ requiresBackend: true,
2425
+ deps: { "@lacspace/sse": "^1.0.0" },
2426
+ files: () => ({ "app/live/page.tsx": realtimeLivePage() }),
2427
+ backend: () => ({
2428
+ deps: { "@lacspace/sse": "^1.0.0" },
2429
+ files: { "src/routes/live.ts": realtimeRoutesBackend() },
2430
+ routes: [{ path: "/live", handler: "liveRoutes", auth: false, importLine: 'import liveRoutes from "./live.js";' }]
2431
+ }),
2432
+ nextSteps: [
2433
+ "Open http://localhost:3000/live in two tabs; click 'Broadcast' in one \u2014 the other updates instantly.",
2434
+ "Push from anywhere on the server: hub.broadcast({ event: 'message', data }) in src/routes/live.ts.",
2435
+ "SSE auto-reconnects and needs no WebSocket server \u2014 great for notifications and live feeds."
2436
+ ],
2437
+ learn: "https://developer.lacspace.com/packages/sse"
2406
2438
  }
2407
2439
  ];
2408
2440
  var aiChatRoute = (_ctx) => `import { resolveConfig } from "@lacspace/providers";
@@ -6921,6 +6953,69 @@ router.post("/send", asyncHandler(async (req, res) => {
6921
6953
  res.json({ sent: results.filter((r) => r.result && !r.result.expired).length, removed: expired.length });
6922
6954
  }));
6923
6955
 
6956
+ export default router;
6957
+ `;
6958
+ var consentComponent = () => `"use client";
6959
+ // how this works: re-export the consent UI + API from one component. Render
6960
+ // <ConsentBanner/> once in app/layout.tsx; gate scripts with whenConsent().
6961
+ export { ConsentBanner, useConsent } from "@lacspace/consent/react";
6962
+ export { consent, whenConsent } from "@lacspace/consent";
6963
+ `;
6964
+ var realtimeLivePage = () => `"use client";
6965
+ import { useState } from "react";
6966
+ import { useSSE } from "@lacspace/sse/react";
6967
+
6968
+ const API = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
6969
+
6970
+ export default function LivePage() {
6971
+ const [log, setLog] = useState<string[]>([]);
6972
+ const { status } = useSSE(API + "/live", {
6973
+ onEvent: { message: (m) => setLog((prev) => [String((m as { text?: string }).text ?? ""), ...prev].slice(0, 50)) },
6974
+ });
6975
+
6976
+ async function broadcast() {
6977
+ await fetch(API + "/live/broadcast", {
6978
+ method: "POST",
6979
+ headers: { "Content-Type": "application/json" },
6980
+ body: JSON.stringify({ text: "Hello at " + new Date().toLocaleTimeString() }),
6981
+ });
6982
+ }
6983
+
6984
+ return (
6985
+ <main className="mx-auto max-w-lg px-6 py-16">
6986
+ <h1 className="text-2xl font-bold">Live feed</h1>
6987
+ <p className="mt-1 text-muted">Server-Sent Events \u2014 status: {status}.</p>
6988
+ <button className="mt-4 rounded-xl border border-hairline px-4 py-2" onClick={broadcast}>Broadcast a message</button>
6989
+ <ul className="mt-6 space-y-2">
6990
+ {log.map((line, i) => (<li key={i} className="rounded-xl border border-hairline p-3">{line}</li>))}
6991
+ </ul>
6992
+ </main>
6993
+ );
6994
+ }
6995
+ `;
6996
+ var realtimeRoutesBackend = () => `import express from "express";
6997
+ import { SSEHub, sseHandler } from "@lacspace/sse";
6998
+ import { asyncHandler } from "../http.js";
6999
+
7000
+ // how this works: one shared hub holds every open SSE connection. GET /live opens
7001
+ // a stream; POST /live/broadcast pushes a "message" event to everyone connected.
7002
+ // Push from anywhere in your app by importing this hub and calling hub.broadcast().
7003
+ const router = express.Router();
7004
+ const hub = new SSEHub();
7005
+ hub.startHeartbeat();
7006
+
7007
+ router.get("/", (_req, res) => {
7008
+ const client = sseHandler(res, { onClose: () => hub.remove(client) });
7009
+ hub.add(client);
7010
+ client.send({ event: "message", data: { text: "Connected to the live feed." } });
7011
+ });
7012
+
7013
+ router.post("/broadcast", asyncHandler(async (req, res) => {
7014
+ const text = typeof req.body?.text === "string" ? req.body.text : "ping";
7015
+ const sent = hub.broadcast({ event: "message", data: { text } });
7016
+ res.json({ sent });
7017
+ }));
7018
+
6924
7019
  export default router;
6925
7020
  `;
6926
7021
  var scope = (ctx) => `@${ctx.name}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lacspace-app",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Scaffold a beautiful, production-ready Next.js app from a Lacspace template — portfolio, business, e-commerce, SaaS, blog, docs, dashboard, restaurant or marketplace — pre-wired with SEO, security headers, sitemap and robots. Use it as a CLI (like create-next-app, but you start gorgeous) or import it as a library to generate projects programmatically.",
5
5
  "type": "module",
6
6
  "bin": {