create-lacspace-app 2.13.1 → 2.14.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,12 @@ 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.14 — the UI kit (18 add-ons).** The scaffold now comes with a real component layer, not just a page:
24
+ > - **`ui`** — the **96-component `@lacspace/components` library** wired into your template: a working settings page (form validation, a confirm dialog that waits for its promise, toasts), a `<UIProvider>` for app-wide use, and a **theme bridge** that maps your template's accent, radius and font onto the kit's `--lac-*` variables — so the components match your brand and follow your existing dark-mode toggle out of the box. Frontend, any template.
25
+ > - **`dataviz`** — a working `/insights` page from **`@lacspace/charts` + `@lacspace/table` + `@lacspace/date`**: stat cards, an SVG line chart, a date-range filter and a sortable, searchable, CSV-exportable table. No canvas, no D3, no grid licence. Frontend, any template.
26
+ >
27
+ > `npx create-lacspace-app my-app --template saas --with ui,dataviz`
28
+
23
29
  > **New in v2.12 — real-time & privacy (16 add-ons).** Two more keyless, no-vendor add-ons:
24
30
  > - **`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
31
  > - **`consent`** — **GDPR-friendly cookie consent** (`@lacspace/consent`): a drop-in `<ConsentBanner/>`, per-category choices, cookie persistence and `whenConsent()` script-gating. Frontend.
@@ -159,6 +165,8 @@ In the interactive flow, after you pick a template you're offered the same list
159
165
  | --- | --- | --- |
160
166
  | **`ai-chat`** | A streaming chat route `app/api/chat/route.ts` + a chat UI `app/chat/page.tsx`. Reads provider config from env, runs input through a prompt-injection guard, and streams the reply. | `@lacspace/ai` `@lacspace/prompt` `@lacspace/stream` `@lacspace/providers` `@lacspace/memory` `@lacspace/moderation` |
161
167
  | **`rag`** | "Chat with your docs": `content/welcome.md`, an indexer `scripts/index-content.mjs` (+ `rag:index` script), a retrieval+rerank answer route `app/api/ask/route.ts`, and an ask UI `app/ask/page.tsx`. | `@lacspace/rag` `@lacspace/embeddings` `@lacspace/vector` `@lacspace/chunk` `@lacspace/rerank` `@lacspace/providers` `@lacspace/ai` |
168
+ | **`ui`** | The component library, themed to your template: `app/ui/page.tsx` (a real form, a confirm dialog and toasts), `app/ui/layout.tsx`, a `<UIProvider>`, and `app/lacspace-ui.css` — the bridge that maps your tokens onto the kit's. | `@lacspace/components` |
169
+ | **`dataviz`** | A working `/insights` page: stat cards, an SVG line chart, a date-range filter, and a sortable/searchable/exportable data table. | `@lacspace/charts` `@lacspace/table` `@lacspace/date` `@lacspace/components` |
162
170
 
163
171
  ### 🆓 Free & local by default — no API key
164
172
 
package/dist/index.js CHANGED
@@ -2434,8 +2434,364 @@ var FEATURES = [
2434
2434
  "SSE auto-reconnects and needs no WebSocket server \u2014 great for notifications and live feeds."
2435
2435
  ],
2436
2436
  learn: "https://developer.lacspace.com/packages/sse"
2437
+ },
2438
+ {
2439
+ key: "ui",
2440
+ label: "UI components",
2441
+ description: "The @lacspace/components library wired into this template \u2014 96 accessible components, a theme bridge so they inherit your accent and dark mode, and a working settings page. Frontend, any template.",
2442
+ deps: { "@lacspace/components": "^1.0.1" },
2443
+ files: () => ({
2444
+ "app/lacspace-ui.css": uiThemeBridge(),
2445
+ "app/ui/layout.tsx": uiRouteLayout(),
2446
+ "app/ui/page.tsx": uiShowcasePage(),
2447
+ "components/ui-provider.tsx": uiProviderComponent()
2448
+ }),
2449
+ nextSteps: [
2450
+ "Run `npm run dev` and open http://localhost:3000/ui \u2014 a real form, a confirm dialog and toasts, already themed.",
2451
+ 'Use the kit anywhere: import "@lacspace/components/styles.css" and "./lacspace-ui.css" in app/layout.tsx, then wrap <body> with <UIProvider> from @/components/ui-provider.',
2452
+ "Rebrand everything from app/lacspace-ui.css \u2014 it maps this template's accent, radius and font onto the kit's --lac-* variables, so dark mode follows your theme toggle."
2453
+ ],
2454
+ learn: "https://developer.lacspace.com/components"
2455
+ },
2456
+ {
2457
+ key: "dataviz",
2458
+ label: "Charts, tables & dates",
2459
+ description: "A working insights page from @lacspace/charts, @lacspace/table and @lacspace/date \u2014 SVG charts, a sortable/exportable data table and a date-range filter. No canvas, no D3, no grid licence. Frontend, any template.",
2460
+ deps: {
2461
+ "@lacspace/components": "^1.0.1",
2462
+ "@lacspace/charts": "^1.0.0",
2463
+ "@lacspace/table": "^1.0.0",
2464
+ "@lacspace/date": "^1.0.0"
2465
+ },
2466
+ files: () => ({
2467
+ "app/lacspace-ui.css": uiThemeBridge(),
2468
+ "app/insights/layout.tsx": dataVizRouteLayout(),
2469
+ "app/insights/page.tsx": dataVizPage()
2470
+ }),
2471
+ nextSteps: [
2472
+ "Run `npm run dev` and open http://localhost:3000/insights \u2014 stats, a line chart, a date-range filter and an exportable table.",
2473
+ "Swap the sample arrays at the top of app/insights/page.tsx for your own data; everything else is already wired.",
2474
+ "The CSV export neutralises leading =, +, - and @, so an exported cell can never run as a formula in Excel."
2475
+ ],
2476
+ learn: "https://developer.lacspace.com/components#charts"
2437
2477
  }
2438
2478
  ];
2479
+ var uiThemeBridge = () => `/* Map this app's tokens onto @lacspace/components.
2480
+ *
2481
+ * The kit's own dark palette lives behind \`prefers-color-scheme\` and a
2482
+ * \`data-theme\` attribute, but this app switches themes with a \`.dark\` class on
2483
+ * <html>. The repeated \`:root\` below is deliberate: it raises specificity just
2484
+ * enough to win against the kit's own theme blocks, whatever the OS setting is,
2485
+ * so the components follow YOUR toggle and never fight it.
2486
+ *
2487
+ * Every line here is a suggestion \u2014 change any of them and the whole kit
2488
+ * follows. The full token list is at the top of @lacspace/components/styles.css.
2489
+ */
2490
+ :root:root:root {
2491
+ /* Brand */
2492
+ --lac-accent: var(--accent-to);
2493
+ --lac-accent-hover: var(--accent-from);
2494
+ --lac-accent-active: var(--accent-from);
2495
+ --lac-accent-fg: var(--on-accent);
2496
+ --lac-accent-soft: color-mix(in oklab, var(--accent-to) 14%, transparent);
2497
+ --lac-accent-ring: color-mix(in oklab, var(--accent-to) 38%, transparent);
2498
+
2499
+ /* Surfaces & text \u2014 these already swap with the .dark class, so dark mode
2500
+ comes along for free. */
2501
+ --lac-bg: var(--bg);
2502
+ --lac-bg-subtle: var(--surface);
2503
+ --lac-bg-muted: var(--panel);
2504
+ --lac-surface: var(--surface);
2505
+ --lac-surface-raised: var(--panel);
2506
+ --lac-fg: var(--fg);
2507
+ --lac-fg-muted: var(--muted);
2508
+ --lac-fg-faint: var(--faint);
2509
+ --lac-fg-on-accent: var(--on-accent);
2510
+
2511
+ /* Lines & depth */
2512
+ --lac-border: var(--hairline);
2513
+ --lac-border-strong: color-mix(in oklab, var(--fg) 22%, transparent);
2514
+ --lac-shadow-sm: var(--shadow-sm);
2515
+ --lac-shadow: var(--shadow);
2516
+ --lac-shadow-lg: var(--shadow-lg);
2517
+
2518
+ /* Shape & type */
2519
+ --lac-radius: var(--radius);
2520
+ --lac-radius-lg: var(--radius-lg);
2521
+ --lac-font: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
2522
+ }
2523
+ `;
2524
+ var uiRouteLayout = () => `import "@lacspace/components/styles.css";
2525
+ import "../lacspace-ui.css";
2526
+ import { ToastProvider } from "@lacspace/components";
2527
+
2528
+ // Scoped to this route so the add-on needs no edits to app/layout.tsx. To use
2529
+ // the kit across the whole app, move these two imports there and wrap <body>
2530
+ // with <UIProvider> from @/components/ui-provider instead.
2531
+ export default function UILayout({ children }: { children: React.ReactNode }) {
2532
+ return <ToastProvider>{children}</ToastProvider>;
2533
+ }
2534
+ `;
2535
+ var uiProviderComponent = () => `"use client";
2536
+ import { ToastProvider } from "@lacspace/components";
2537
+
2538
+ /**
2539
+ * Wrap <body> with this once to use the component kit across the whole app,
2540
+ * and import the two stylesheets in app/layout.tsx:
2541
+ *
2542
+ * import "@lacspace/components/styles.css";
2543
+ * import "./lacspace-ui.css";
2544
+ */
2545
+ export function UIProvider({ children }: { children: React.ReactNode }) {
2546
+ return <ToastProvider position="bottom-right">{children}</ToastProvider>;
2547
+ }
2548
+ `;
2549
+ var uiShowcasePage = () => `"use client";
2550
+ import { useState } from "react";
2551
+ import {
2552
+ Alert,
2553
+ Badge,
2554
+ Button,
2555
+ Card,
2556
+ CardBody,
2557
+ CardDescription,
2558
+ CardHeader,
2559
+ CardTitle,
2560
+ ConfirmDialog,
2561
+ Field,
2562
+ Input,
2563
+ PasswordInput,
2564
+ Select,
2565
+ Stack,
2566
+ Switch,
2567
+ useToast,
2568
+ } from "@lacspace/components";
2569
+
2570
+ // Everything below is real: a form that validates, a dialog that waits for its
2571
+ // promise, and a toast queue \u2014 none of it styled with utility classes. The look
2572
+ // comes from app/lacspace-ui.css, which maps this template's tokens onto the kit.
2573
+ export default function UIPage() {
2574
+ const { toast } = useToast();
2575
+ const [confirming, setConfirming] = useState(false);
2576
+ const [updates, setUpdates] = useState(true);
2577
+ const [errors, setErrors] = useState<{ email?: string }>({});
2578
+
2579
+ function save(form: FormData) {
2580
+ const email = String(form.get("email") ?? "");
2581
+ if (!email.includes("@")) {
2582
+ setErrors({ email: "That doesn't look like an email address." });
2583
+ return;
2584
+ }
2585
+ setErrors({});
2586
+ toast({ title: "Settings saved", tone: "success" });
2587
+ }
2588
+
2589
+ return (
2590
+ <main className="mx-auto max-w-2xl px-6 py-16">
2591
+ <Stack gap={4}>
2592
+ <Card>
2593
+ <CardHeader>
2594
+ <CardTitle>Account settings</CardTitle>
2595
+ <CardDescription>
2596
+ 96 components from @lacspace/components \u2014 accessible, server-render safe, and
2597
+ themed by CSS variables instead of utility classes.
2598
+ </CardDescription>
2599
+ </CardHeader>
2600
+ <CardBody>
2601
+ <form action={save}>
2602
+ <Stack gap={3}>
2603
+ <Field label="Work email" hint="We never share it." error={errors.email}>
2604
+ {({ id, describedBy, invalid }) => (
2605
+ <Input
2606
+ id={id}
2607
+ name="email"
2608
+ type="email"
2609
+ defaultValue="you@company.com"
2610
+ aria-describedby={describedBy}
2611
+ invalid={invalid}
2612
+ />
2613
+ )}
2614
+ </Field>
2615
+
2616
+ <Field label="New password" hint="At least 12 characters.">
2617
+ {({ id, describedBy }) => (
2618
+ <PasswordInput id={id} name="password" aria-describedby={describedBy} strength />
2619
+ )}
2620
+ </Field>
2621
+
2622
+ <Field label="Plan">
2623
+ {({ id }) => (
2624
+ <Select
2625
+ id={id}
2626
+ name="plan"
2627
+ defaultValue="pro"
2628
+ options={[
2629
+ { value: "free", label: "Free" },
2630
+ { value: "pro", label: "Pro" },
2631
+ { value: "team", label: "Team" },
2632
+ ]}
2633
+ />
2634
+ )}
2635
+ </Field>
2636
+
2637
+ <Switch checked={updates} onChange={setUpdates} label="Email me about product updates" />
2638
+
2639
+ <Button type="submit" full>
2640
+ Save changes
2641
+ </Button>
2642
+ </Stack>
2643
+ </form>
2644
+ </CardBody>
2645
+ </Card>
2646
+
2647
+ <Alert tone="info" title="Rebrand the whole kit from one file">
2648
+ Open <code>app/lacspace-ui.css</code>. It maps this template&rsquo;s accent, radius and font
2649
+ onto the kit&rsquo;s variables, so the components already match \u2014 and they follow your
2650
+ existing dark-mode toggle.
2651
+ </Alert>
2652
+
2653
+ <Card>
2654
+ <CardBody>
2655
+ <Stack direction="row" gap={2} align="center" wrap>
2656
+ <Badge tone="success" dot>
2657
+ Live
2658
+ </Badge>
2659
+ <Button variant="soft" onClick={() => toast("Saved to drafts")}>
2660
+ Show a toast
2661
+ </Button>
2662
+ <Button tone="danger" variant="soft" onClick={() => setConfirming(true)}>
2663
+ Delete account
2664
+ </Button>
2665
+ </Stack>
2666
+ </CardBody>
2667
+ </Card>
2668
+ </Stack>
2669
+
2670
+ <ConfirmDialog
2671
+ open={confirming}
2672
+ onOpenChange={setConfirming}
2673
+ tone="danger"
2674
+ title="Delete this account?"
2675
+ message="Everything in it goes with it. This cannot be undone."
2676
+ confirmLabel="Delete account"
2677
+ onConfirm={async () => {
2678
+ // Return a promise and the button spins, blocks repeat clicks, and
2679
+ // keeps the dialog open if it rejects \u2014 a failed delete never looks
2680
+ // like a successful one.
2681
+ await new Promise((resolve) => setTimeout(resolve, 700));
2682
+ toast({ title: "Account deleted", tone: "success" });
2683
+ }}
2684
+ />
2685
+ </main>
2686
+ );
2687
+ }
2688
+ `;
2689
+ var dataVizRouteLayout = () => `import "@lacspace/components/styles.css";
2690
+ import "@lacspace/charts/styles.css";
2691
+ import "@lacspace/table/styles.css";
2692
+ import "@lacspace/date/styles.css";
2693
+ import "../lacspace-ui.css";
2694
+
2695
+ // Scoped to this route; move these imports to app/layout.tsx to use the kit
2696
+ // everywhere. Order matters: lacspace-ui.css maps this app's tokens onto the
2697
+ // kit, so it comes last.
2698
+ export default function InsightsLayout({ children }: { children: React.ReactNode }) {
2699
+ return <>{children}</>;
2700
+ }
2701
+ `;
2702
+ var dataVizPage = () => `"use client";
2703
+ import { useState } from "react";
2704
+ import { Heading, Grid, Stack, Stat, Text } from "@lacspace/components";
2705
+ import { LineChart } from "@lacspace/charts";
2706
+ import { DataTable, textColumn, badgeColumn, currencyColumn, numberColumn } from "@lacspace/table";
2707
+ import { DateRangePicker, defaultPresets } from "@lacspace/date";
2708
+
2709
+ // Swap these three arrays for your own data \u2014 nothing else needs to change.
2710
+ const MONTHS = ["Apr", "May", "Jun", "Jul", "Aug", "Sep"];
2711
+ const REVENUE = [18, 21, 25, 24, 31, 38];
2712
+ const TARGET = [20, 22, 24, 26, 28, 30];
2713
+
2714
+ type Account = {
2715
+ id: string;
2716
+ account: string;
2717
+ plan: string;
2718
+ seats: number;
2719
+ mrr: number;
2720
+ };
2721
+
2722
+ const ACCOUNTS: Account[] = [
2723
+ { id: "1", account: "Northwind", plan: "team", seats: 42, mrr: 1890 },
2724
+ { id: "2", account: "Umbrella", plan: "pro", seats: 12, mrr: 540 },
2725
+ { id: "3", account: "Initech", plan: "team", seats: 68, mrr: 3060 },
2726
+ { id: "4", account: "Hooli", plan: "free", seats: 3, mrr: 0 },
2727
+ { id: "5", account: "Stark Industries", plan: "pro", seats: 19, mrr: 855 },
2728
+ { id: "6", account: "Wayne Enterprises", plan: "team", seats: 51, mrr: 2295 },
2729
+ ];
2730
+
2731
+ export default function InsightsPage() {
2732
+ const [range, setRange] = useState<unknown>(null);
2733
+
2734
+ return (
2735
+ <main className="mx-auto max-w-5xl px-6 py-16">
2736
+ <Stack gap={5}>
2737
+ <Stack direction="row" justify="between" align="center" gap={3} wrap>
2738
+ <div>
2739
+ <Heading level={1}>Insights</Heading>
2740
+ <Text tone="muted">Charts, a data table and a date filter \u2014 all zero-dependency.</Text>
2741
+ </div>
2742
+ <DateRangePicker
2743
+ numberOfMonths={2}
2744
+ presets={defaultPresets()}
2745
+ separator=" \u2013 "
2746
+ onChange={setRange}
2747
+ />
2748
+ </Stack>
2749
+
2750
+ <Grid columns={{ base: 1, md: 3 }} gap={3}>
2751
+ <Stat label="MRR" value="$8,640" delta={12.4} comparison="vs last month" />
2752
+ <Stat label="Seats" value="195" delta={6.1} comparison="vs last month" />
2753
+ <Stat label="Churn" value="1.8%" delta={-0.3} invertDelta comparison="vs last month" />
2754
+ </Grid>
2755
+
2756
+ <LineChart
2757
+ labels={MONTHS}
2758
+ series={[
2759
+ { name: "Revenue", data: REVENUE, area: true },
2760
+ { name: "Target", data: TARGET, dashed: true },
2761
+ ]}
2762
+ curve
2763
+ dots
2764
+ tooltip
2765
+ responsive
2766
+ dataTable
2767
+ formatValue={(n) => "$" + n + "k"}
2768
+ />
2769
+
2770
+ <DataTable<Account>
2771
+ caption="Accounts"
2772
+ data={ACCOUNTS}
2773
+ getRowId={(row) => row.id}
2774
+ columns={[
2775
+ textColumn<Account>("account", { header: "Account", key: "account", pinned: "left" }),
2776
+ badgeColumn<Account>("plan", {
2777
+ header: "Plan",
2778
+ key: "plan",
2779
+ tones: { team: "success", pro: "info", free: "default" },
2780
+ }),
2781
+ numberColumn<Account>("seats", { header: "Seats", key: "seats", aggregate: "sum" }),
2782
+ currencyColumn<Account>("mrr", { header: "MRR", key: "mrr", currency: "USD", aggregate: "sum" }),
2783
+ ]}
2784
+ defaultSort={[{ id: "mrr", direction: "desc" }]}
2785
+ searchable
2786
+ selectable
2787
+ exportable
2788
+ stickyHeader
2789
+ />
2790
+ </Stack>
2791
+ </main>
2792
+ );
2793
+ }
2794
+ `;
2439
2795
  var aiChatRoute = (_ctx) => `import { resolveConfig } from "@lacspace/providers";
2440
2796
  import { stream } from "@lacspace/ai";
2441
2797
  import type { Message } from "@lacspace/ai";
package/dist/lib.cjs CHANGED
@@ -2438,8 +2438,364 @@ var FEATURES = [
2438
2438
  "SSE auto-reconnects and needs no WebSocket server \u2014 great for notifications and live feeds."
2439
2439
  ],
2440
2440
  learn: "https://developer.lacspace.com/packages/sse"
2441
+ },
2442
+ {
2443
+ key: "ui",
2444
+ label: "UI components",
2445
+ description: "The @lacspace/components library wired into this template \u2014 96 accessible components, a theme bridge so they inherit your accent and dark mode, and a working settings page. Frontend, any template.",
2446
+ deps: { "@lacspace/components": "^1.0.1" },
2447
+ files: () => ({
2448
+ "app/lacspace-ui.css": uiThemeBridge(),
2449
+ "app/ui/layout.tsx": uiRouteLayout(),
2450
+ "app/ui/page.tsx": uiShowcasePage(),
2451
+ "components/ui-provider.tsx": uiProviderComponent()
2452
+ }),
2453
+ nextSteps: [
2454
+ "Run `npm run dev` and open http://localhost:3000/ui \u2014 a real form, a confirm dialog and toasts, already themed.",
2455
+ 'Use the kit anywhere: import "@lacspace/components/styles.css" and "./lacspace-ui.css" in app/layout.tsx, then wrap <body> with <UIProvider> from @/components/ui-provider.',
2456
+ "Rebrand everything from app/lacspace-ui.css \u2014 it maps this template's accent, radius and font onto the kit's --lac-* variables, so dark mode follows your theme toggle."
2457
+ ],
2458
+ learn: "https://developer.lacspace.com/components"
2459
+ },
2460
+ {
2461
+ key: "dataviz",
2462
+ label: "Charts, tables & dates",
2463
+ description: "A working insights page from @lacspace/charts, @lacspace/table and @lacspace/date \u2014 SVG charts, a sortable/exportable data table and a date-range filter. No canvas, no D3, no grid licence. Frontend, any template.",
2464
+ deps: {
2465
+ "@lacspace/components": "^1.0.1",
2466
+ "@lacspace/charts": "^1.0.0",
2467
+ "@lacspace/table": "^1.0.0",
2468
+ "@lacspace/date": "^1.0.0"
2469
+ },
2470
+ files: () => ({
2471
+ "app/lacspace-ui.css": uiThemeBridge(),
2472
+ "app/insights/layout.tsx": dataVizRouteLayout(),
2473
+ "app/insights/page.tsx": dataVizPage()
2474
+ }),
2475
+ nextSteps: [
2476
+ "Run `npm run dev` and open http://localhost:3000/insights \u2014 stats, a line chart, a date-range filter and an exportable table.",
2477
+ "Swap the sample arrays at the top of app/insights/page.tsx for your own data; everything else is already wired.",
2478
+ "The CSV export neutralises leading =, +, - and @, so an exported cell can never run as a formula in Excel."
2479
+ ],
2480
+ learn: "https://developer.lacspace.com/components#charts"
2441
2481
  }
2442
2482
  ];
2483
+ var uiThemeBridge = () => `/* Map this app's tokens onto @lacspace/components.
2484
+ *
2485
+ * The kit's own dark palette lives behind \`prefers-color-scheme\` and a
2486
+ * \`data-theme\` attribute, but this app switches themes with a \`.dark\` class on
2487
+ * <html>. The repeated \`:root\` below is deliberate: it raises specificity just
2488
+ * enough to win against the kit's own theme blocks, whatever the OS setting is,
2489
+ * so the components follow YOUR toggle and never fight it.
2490
+ *
2491
+ * Every line here is a suggestion \u2014 change any of them and the whole kit
2492
+ * follows. The full token list is at the top of @lacspace/components/styles.css.
2493
+ */
2494
+ :root:root:root {
2495
+ /* Brand */
2496
+ --lac-accent: var(--accent-to);
2497
+ --lac-accent-hover: var(--accent-from);
2498
+ --lac-accent-active: var(--accent-from);
2499
+ --lac-accent-fg: var(--on-accent);
2500
+ --lac-accent-soft: color-mix(in oklab, var(--accent-to) 14%, transparent);
2501
+ --lac-accent-ring: color-mix(in oklab, var(--accent-to) 38%, transparent);
2502
+
2503
+ /* Surfaces & text \u2014 these already swap with the .dark class, so dark mode
2504
+ comes along for free. */
2505
+ --lac-bg: var(--bg);
2506
+ --lac-bg-subtle: var(--surface);
2507
+ --lac-bg-muted: var(--panel);
2508
+ --lac-surface: var(--surface);
2509
+ --lac-surface-raised: var(--panel);
2510
+ --lac-fg: var(--fg);
2511
+ --lac-fg-muted: var(--muted);
2512
+ --lac-fg-faint: var(--faint);
2513
+ --lac-fg-on-accent: var(--on-accent);
2514
+
2515
+ /* Lines & depth */
2516
+ --lac-border: var(--hairline);
2517
+ --lac-border-strong: color-mix(in oklab, var(--fg) 22%, transparent);
2518
+ --lac-shadow-sm: var(--shadow-sm);
2519
+ --lac-shadow: var(--shadow);
2520
+ --lac-shadow-lg: var(--shadow-lg);
2521
+
2522
+ /* Shape & type */
2523
+ --lac-radius: var(--radius);
2524
+ --lac-radius-lg: var(--radius-lg);
2525
+ --lac-font: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
2526
+ }
2527
+ `;
2528
+ var uiRouteLayout = () => `import "@lacspace/components/styles.css";
2529
+ import "../lacspace-ui.css";
2530
+ import { ToastProvider } from "@lacspace/components";
2531
+
2532
+ // Scoped to this route so the add-on needs no edits to app/layout.tsx. To use
2533
+ // the kit across the whole app, move these two imports there and wrap <body>
2534
+ // with <UIProvider> from @/components/ui-provider instead.
2535
+ export default function UILayout({ children }: { children: React.ReactNode }) {
2536
+ return <ToastProvider>{children}</ToastProvider>;
2537
+ }
2538
+ `;
2539
+ var uiProviderComponent = () => `"use client";
2540
+ import { ToastProvider } from "@lacspace/components";
2541
+
2542
+ /**
2543
+ * Wrap <body> with this once to use the component kit across the whole app,
2544
+ * and import the two stylesheets in app/layout.tsx:
2545
+ *
2546
+ * import "@lacspace/components/styles.css";
2547
+ * import "./lacspace-ui.css";
2548
+ */
2549
+ export function UIProvider({ children }: { children: React.ReactNode }) {
2550
+ return <ToastProvider position="bottom-right">{children}</ToastProvider>;
2551
+ }
2552
+ `;
2553
+ var uiShowcasePage = () => `"use client";
2554
+ import { useState } from "react";
2555
+ import {
2556
+ Alert,
2557
+ Badge,
2558
+ Button,
2559
+ Card,
2560
+ CardBody,
2561
+ CardDescription,
2562
+ CardHeader,
2563
+ CardTitle,
2564
+ ConfirmDialog,
2565
+ Field,
2566
+ Input,
2567
+ PasswordInput,
2568
+ Select,
2569
+ Stack,
2570
+ Switch,
2571
+ useToast,
2572
+ } from "@lacspace/components";
2573
+
2574
+ // Everything below is real: a form that validates, a dialog that waits for its
2575
+ // promise, and a toast queue \u2014 none of it styled with utility classes. The look
2576
+ // comes from app/lacspace-ui.css, which maps this template's tokens onto the kit.
2577
+ export default function UIPage() {
2578
+ const { toast } = useToast();
2579
+ const [confirming, setConfirming] = useState(false);
2580
+ const [updates, setUpdates] = useState(true);
2581
+ const [errors, setErrors] = useState<{ email?: string }>({});
2582
+
2583
+ function save(form: FormData) {
2584
+ const email = String(form.get("email") ?? "");
2585
+ if (!email.includes("@")) {
2586
+ setErrors({ email: "That doesn't look like an email address." });
2587
+ return;
2588
+ }
2589
+ setErrors({});
2590
+ toast({ title: "Settings saved", tone: "success" });
2591
+ }
2592
+
2593
+ return (
2594
+ <main className="mx-auto max-w-2xl px-6 py-16">
2595
+ <Stack gap={4}>
2596
+ <Card>
2597
+ <CardHeader>
2598
+ <CardTitle>Account settings</CardTitle>
2599
+ <CardDescription>
2600
+ 96 components from @lacspace/components \u2014 accessible, server-render safe, and
2601
+ themed by CSS variables instead of utility classes.
2602
+ </CardDescription>
2603
+ </CardHeader>
2604
+ <CardBody>
2605
+ <form action={save}>
2606
+ <Stack gap={3}>
2607
+ <Field label="Work email" hint="We never share it." error={errors.email}>
2608
+ {({ id, describedBy, invalid }) => (
2609
+ <Input
2610
+ id={id}
2611
+ name="email"
2612
+ type="email"
2613
+ defaultValue="you@company.com"
2614
+ aria-describedby={describedBy}
2615
+ invalid={invalid}
2616
+ />
2617
+ )}
2618
+ </Field>
2619
+
2620
+ <Field label="New password" hint="At least 12 characters.">
2621
+ {({ id, describedBy }) => (
2622
+ <PasswordInput id={id} name="password" aria-describedby={describedBy} strength />
2623
+ )}
2624
+ </Field>
2625
+
2626
+ <Field label="Plan">
2627
+ {({ id }) => (
2628
+ <Select
2629
+ id={id}
2630
+ name="plan"
2631
+ defaultValue="pro"
2632
+ options={[
2633
+ { value: "free", label: "Free" },
2634
+ { value: "pro", label: "Pro" },
2635
+ { value: "team", label: "Team" },
2636
+ ]}
2637
+ />
2638
+ )}
2639
+ </Field>
2640
+
2641
+ <Switch checked={updates} onChange={setUpdates} label="Email me about product updates" />
2642
+
2643
+ <Button type="submit" full>
2644
+ Save changes
2645
+ </Button>
2646
+ </Stack>
2647
+ </form>
2648
+ </CardBody>
2649
+ </Card>
2650
+
2651
+ <Alert tone="info" title="Rebrand the whole kit from one file">
2652
+ Open <code>app/lacspace-ui.css</code>. It maps this template&rsquo;s accent, radius and font
2653
+ onto the kit&rsquo;s variables, so the components already match \u2014 and they follow your
2654
+ existing dark-mode toggle.
2655
+ </Alert>
2656
+
2657
+ <Card>
2658
+ <CardBody>
2659
+ <Stack direction="row" gap={2} align="center" wrap>
2660
+ <Badge tone="success" dot>
2661
+ Live
2662
+ </Badge>
2663
+ <Button variant="soft" onClick={() => toast("Saved to drafts")}>
2664
+ Show a toast
2665
+ </Button>
2666
+ <Button tone="danger" variant="soft" onClick={() => setConfirming(true)}>
2667
+ Delete account
2668
+ </Button>
2669
+ </Stack>
2670
+ </CardBody>
2671
+ </Card>
2672
+ </Stack>
2673
+
2674
+ <ConfirmDialog
2675
+ open={confirming}
2676
+ onOpenChange={setConfirming}
2677
+ tone="danger"
2678
+ title="Delete this account?"
2679
+ message="Everything in it goes with it. This cannot be undone."
2680
+ confirmLabel="Delete account"
2681
+ onConfirm={async () => {
2682
+ // Return a promise and the button spins, blocks repeat clicks, and
2683
+ // keeps the dialog open if it rejects \u2014 a failed delete never looks
2684
+ // like a successful one.
2685
+ await new Promise((resolve) => setTimeout(resolve, 700));
2686
+ toast({ title: "Account deleted", tone: "success" });
2687
+ }}
2688
+ />
2689
+ </main>
2690
+ );
2691
+ }
2692
+ `;
2693
+ var dataVizRouteLayout = () => `import "@lacspace/components/styles.css";
2694
+ import "@lacspace/charts/styles.css";
2695
+ import "@lacspace/table/styles.css";
2696
+ import "@lacspace/date/styles.css";
2697
+ import "../lacspace-ui.css";
2698
+
2699
+ // Scoped to this route; move these imports to app/layout.tsx to use the kit
2700
+ // everywhere. Order matters: lacspace-ui.css maps this app's tokens onto the
2701
+ // kit, so it comes last.
2702
+ export default function InsightsLayout({ children }: { children: React.ReactNode }) {
2703
+ return <>{children}</>;
2704
+ }
2705
+ `;
2706
+ var dataVizPage = () => `"use client";
2707
+ import { useState } from "react";
2708
+ import { Heading, Grid, Stack, Stat, Text } from "@lacspace/components";
2709
+ import { LineChart } from "@lacspace/charts";
2710
+ import { DataTable, textColumn, badgeColumn, currencyColumn, numberColumn } from "@lacspace/table";
2711
+ import { DateRangePicker, defaultPresets } from "@lacspace/date";
2712
+
2713
+ // Swap these three arrays for your own data \u2014 nothing else needs to change.
2714
+ const MONTHS = ["Apr", "May", "Jun", "Jul", "Aug", "Sep"];
2715
+ const REVENUE = [18, 21, 25, 24, 31, 38];
2716
+ const TARGET = [20, 22, 24, 26, 28, 30];
2717
+
2718
+ type Account = {
2719
+ id: string;
2720
+ account: string;
2721
+ plan: string;
2722
+ seats: number;
2723
+ mrr: number;
2724
+ };
2725
+
2726
+ const ACCOUNTS: Account[] = [
2727
+ { id: "1", account: "Northwind", plan: "team", seats: 42, mrr: 1890 },
2728
+ { id: "2", account: "Umbrella", plan: "pro", seats: 12, mrr: 540 },
2729
+ { id: "3", account: "Initech", plan: "team", seats: 68, mrr: 3060 },
2730
+ { id: "4", account: "Hooli", plan: "free", seats: 3, mrr: 0 },
2731
+ { id: "5", account: "Stark Industries", plan: "pro", seats: 19, mrr: 855 },
2732
+ { id: "6", account: "Wayne Enterprises", plan: "team", seats: 51, mrr: 2295 },
2733
+ ];
2734
+
2735
+ export default function InsightsPage() {
2736
+ const [range, setRange] = useState<unknown>(null);
2737
+
2738
+ return (
2739
+ <main className="mx-auto max-w-5xl px-6 py-16">
2740
+ <Stack gap={5}>
2741
+ <Stack direction="row" justify="between" align="center" gap={3} wrap>
2742
+ <div>
2743
+ <Heading level={1}>Insights</Heading>
2744
+ <Text tone="muted">Charts, a data table and a date filter \u2014 all zero-dependency.</Text>
2745
+ </div>
2746
+ <DateRangePicker
2747
+ numberOfMonths={2}
2748
+ presets={defaultPresets()}
2749
+ separator=" \u2013 "
2750
+ onChange={setRange}
2751
+ />
2752
+ </Stack>
2753
+
2754
+ <Grid columns={{ base: 1, md: 3 }} gap={3}>
2755
+ <Stat label="MRR" value="$8,640" delta={12.4} comparison="vs last month" />
2756
+ <Stat label="Seats" value="195" delta={6.1} comparison="vs last month" />
2757
+ <Stat label="Churn" value="1.8%" delta={-0.3} invertDelta comparison="vs last month" />
2758
+ </Grid>
2759
+
2760
+ <LineChart
2761
+ labels={MONTHS}
2762
+ series={[
2763
+ { name: "Revenue", data: REVENUE, area: true },
2764
+ { name: "Target", data: TARGET, dashed: true },
2765
+ ]}
2766
+ curve
2767
+ dots
2768
+ tooltip
2769
+ responsive
2770
+ dataTable
2771
+ formatValue={(n) => "$" + n + "k"}
2772
+ />
2773
+
2774
+ <DataTable<Account>
2775
+ caption="Accounts"
2776
+ data={ACCOUNTS}
2777
+ getRowId={(row) => row.id}
2778
+ columns={[
2779
+ textColumn<Account>("account", { header: "Account", key: "account", pinned: "left" }),
2780
+ badgeColumn<Account>("plan", {
2781
+ header: "Plan",
2782
+ key: "plan",
2783
+ tones: { team: "success", pro: "info", free: "default" },
2784
+ }),
2785
+ numberColumn<Account>("seats", { header: "Seats", key: "seats", aggregate: "sum" }),
2786
+ currencyColumn<Account>("mrr", { header: "MRR", key: "mrr", currency: "USD", aggregate: "sum" }),
2787
+ ]}
2788
+ defaultSort={[{ id: "mrr", direction: "desc" }]}
2789
+ searchable
2790
+ selectable
2791
+ exportable
2792
+ stickyHeader
2793
+ />
2794
+ </Stack>
2795
+ </main>
2796
+ );
2797
+ }
2798
+ `;
2443
2799
  var aiChatRoute = (_ctx) => `import { resolveConfig } from "@lacspace/providers";
2444
2800
  import { stream } from "@lacspace/ai";
2445
2801
  import type { Message } from "@lacspace/ai";
package/dist/lib.js CHANGED
@@ -2435,8 +2435,364 @@ var FEATURES = [
2435
2435
  "SSE auto-reconnects and needs no WebSocket server \u2014 great for notifications and live feeds."
2436
2436
  ],
2437
2437
  learn: "https://developer.lacspace.com/packages/sse"
2438
+ },
2439
+ {
2440
+ key: "ui",
2441
+ label: "UI components",
2442
+ description: "The @lacspace/components library wired into this template \u2014 96 accessible components, a theme bridge so they inherit your accent and dark mode, and a working settings page. Frontend, any template.",
2443
+ deps: { "@lacspace/components": "^1.0.1" },
2444
+ files: () => ({
2445
+ "app/lacspace-ui.css": uiThemeBridge(),
2446
+ "app/ui/layout.tsx": uiRouteLayout(),
2447
+ "app/ui/page.tsx": uiShowcasePage(),
2448
+ "components/ui-provider.tsx": uiProviderComponent()
2449
+ }),
2450
+ nextSteps: [
2451
+ "Run `npm run dev` and open http://localhost:3000/ui \u2014 a real form, a confirm dialog and toasts, already themed.",
2452
+ 'Use the kit anywhere: import "@lacspace/components/styles.css" and "./lacspace-ui.css" in app/layout.tsx, then wrap <body> with <UIProvider> from @/components/ui-provider.',
2453
+ "Rebrand everything from app/lacspace-ui.css \u2014 it maps this template's accent, radius and font onto the kit's --lac-* variables, so dark mode follows your theme toggle."
2454
+ ],
2455
+ learn: "https://developer.lacspace.com/components"
2456
+ },
2457
+ {
2458
+ key: "dataviz",
2459
+ label: "Charts, tables & dates",
2460
+ description: "A working insights page from @lacspace/charts, @lacspace/table and @lacspace/date \u2014 SVG charts, a sortable/exportable data table and a date-range filter. No canvas, no D3, no grid licence. Frontend, any template.",
2461
+ deps: {
2462
+ "@lacspace/components": "^1.0.1",
2463
+ "@lacspace/charts": "^1.0.0",
2464
+ "@lacspace/table": "^1.0.0",
2465
+ "@lacspace/date": "^1.0.0"
2466
+ },
2467
+ files: () => ({
2468
+ "app/lacspace-ui.css": uiThemeBridge(),
2469
+ "app/insights/layout.tsx": dataVizRouteLayout(),
2470
+ "app/insights/page.tsx": dataVizPage()
2471
+ }),
2472
+ nextSteps: [
2473
+ "Run `npm run dev` and open http://localhost:3000/insights \u2014 stats, a line chart, a date-range filter and an exportable table.",
2474
+ "Swap the sample arrays at the top of app/insights/page.tsx for your own data; everything else is already wired.",
2475
+ "The CSV export neutralises leading =, +, - and @, so an exported cell can never run as a formula in Excel."
2476
+ ],
2477
+ learn: "https://developer.lacspace.com/components#charts"
2438
2478
  }
2439
2479
  ];
2480
+ var uiThemeBridge = () => `/* Map this app's tokens onto @lacspace/components.
2481
+ *
2482
+ * The kit's own dark palette lives behind \`prefers-color-scheme\` and a
2483
+ * \`data-theme\` attribute, but this app switches themes with a \`.dark\` class on
2484
+ * <html>. The repeated \`:root\` below is deliberate: it raises specificity just
2485
+ * enough to win against the kit's own theme blocks, whatever the OS setting is,
2486
+ * so the components follow YOUR toggle and never fight it.
2487
+ *
2488
+ * Every line here is a suggestion \u2014 change any of them and the whole kit
2489
+ * follows. The full token list is at the top of @lacspace/components/styles.css.
2490
+ */
2491
+ :root:root:root {
2492
+ /* Brand */
2493
+ --lac-accent: var(--accent-to);
2494
+ --lac-accent-hover: var(--accent-from);
2495
+ --lac-accent-active: var(--accent-from);
2496
+ --lac-accent-fg: var(--on-accent);
2497
+ --lac-accent-soft: color-mix(in oklab, var(--accent-to) 14%, transparent);
2498
+ --lac-accent-ring: color-mix(in oklab, var(--accent-to) 38%, transparent);
2499
+
2500
+ /* Surfaces & text \u2014 these already swap with the .dark class, so dark mode
2501
+ comes along for free. */
2502
+ --lac-bg: var(--bg);
2503
+ --lac-bg-subtle: var(--surface);
2504
+ --lac-bg-muted: var(--panel);
2505
+ --lac-surface: var(--surface);
2506
+ --lac-surface-raised: var(--panel);
2507
+ --lac-fg: var(--fg);
2508
+ --lac-fg-muted: var(--muted);
2509
+ --lac-fg-faint: var(--faint);
2510
+ --lac-fg-on-accent: var(--on-accent);
2511
+
2512
+ /* Lines & depth */
2513
+ --lac-border: var(--hairline);
2514
+ --lac-border-strong: color-mix(in oklab, var(--fg) 22%, transparent);
2515
+ --lac-shadow-sm: var(--shadow-sm);
2516
+ --lac-shadow: var(--shadow);
2517
+ --lac-shadow-lg: var(--shadow-lg);
2518
+
2519
+ /* Shape & type */
2520
+ --lac-radius: var(--radius);
2521
+ --lac-radius-lg: var(--radius-lg);
2522
+ --lac-font: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
2523
+ }
2524
+ `;
2525
+ var uiRouteLayout = () => `import "@lacspace/components/styles.css";
2526
+ import "../lacspace-ui.css";
2527
+ import { ToastProvider } from "@lacspace/components";
2528
+
2529
+ // Scoped to this route so the add-on needs no edits to app/layout.tsx. To use
2530
+ // the kit across the whole app, move these two imports there and wrap <body>
2531
+ // with <UIProvider> from @/components/ui-provider instead.
2532
+ export default function UILayout({ children }: { children: React.ReactNode }) {
2533
+ return <ToastProvider>{children}</ToastProvider>;
2534
+ }
2535
+ `;
2536
+ var uiProviderComponent = () => `"use client";
2537
+ import { ToastProvider } from "@lacspace/components";
2538
+
2539
+ /**
2540
+ * Wrap <body> with this once to use the component kit across the whole app,
2541
+ * and import the two stylesheets in app/layout.tsx:
2542
+ *
2543
+ * import "@lacspace/components/styles.css";
2544
+ * import "./lacspace-ui.css";
2545
+ */
2546
+ export function UIProvider({ children }: { children: React.ReactNode }) {
2547
+ return <ToastProvider position="bottom-right">{children}</ToastProvider>;
2548
+ }
2549
+ `;
2550
+ var uiShowcasePage = () => `"use client";
2551
+ import { useState } from "react";
2552
+ import {
2553
+ Alert,
2554
+ Badge,
2555
+ Button,
2556
+ Card,
2557
+ CardBody,
2558
+ CardDescription,
2559
+ CardHeader,
2560
+ CardTitle,
2561
+ ConfirmDialog,
2562
+ Field,
2563
+ Input,
2564
+ PasswordInput,
2565
+ Select,
2566
+ Stack,
2567
+ Switch,
2568
+ useToast,
2569
+ } from "@lacspace/components";
2570
+
2571
+ // Everything below is real: a form that validates, a dialog that waits for its
2572
+ // promise, and a toast queue \u2014 none of it styled with utility classes. The look
2573
+ // comes from app/lacspace-ui.css, which maps this template's tokens onto the kit.
2574
+ export default function UIPage() {
2575
+ const { toast } = useToast();
2576
+ const [confirming, setConfirming] = useState(false);
2577
+ const [updates, setUpdates] = useState(true);
2578
+ const [errors, setErrors] = useState<{ email?: string }>({});
2579
+
2580
+ function save(form: FormData) {
2581
+ const email = String(form.get("email") ?? "");
2582
+ if (!email.includes("@")) {
2583
+ setErrors({ email: "That doesn't look like an email address." });
2584
+ return;
2585
+ }
2586
+ setErrors({});
2587
+ toast({ title: "Settings saved", tone: "success" });
2588
+ }
2589
+
2590
+ return (
2591
+ <main className="mx-auto max-w-2xl px-6 py-16">
2592
+ <Stack gap={4}>
2593
+ <Card>
2594
+ <CardHeader>
2595
+ <CardTitle>Account settings</CardTitle>
2596
+ <CardDescription>
2597
+ 96 components from @lacspace/components \u2014 accessible, server-render safe, and
2598
+ themed by CSS variables instead of utility classes.
2599
+ </CardDescription>
2600
+ </CardHeader>
2601
+ <CardBody>
2602
+ <form action={save}>
2603
+ <Stack gap={3}>
2604
+ <Field label="Work email" hint="We never share it." error={errors.email}>
2605
+ {({ id, describedBy, invalid }) => (
2606
+ <Input
2607
+ id={id}
2608
+ name="email"
2609
+ type="email"
2610
+ defaultValue="you@company.com"
2611
+ aria-describedby={describedBy}
2612
+ invalid={invalid}
2613
+ />
2614
+ )}
2615
+ </Field>
2616
+
2617
+ <Field label="New password" hint="At least 12 characters.">
2618
+ {({ id, describedBy }) => (
2619
+ <PasswordInput id={id} name="password" aria-describedby={describedBy} strength />
2620
+ )}
2621
+ </Field>
2622
+
2623
+ <Field label="Plan">
2624
+ {({ id }) => (
2625
+ <Select
2626
+ id={id}
2627
+ name="plan"
2628
+ defaultValue="pro"
2629
+ options={[
2630
+ { value: "free", label: "Free" },
2631
+ { value: "pro", label: "Pro" },
2632
+ { value: "team", label: "Team" },
2633
+ ]}
2634
+ />
2635
+ )}
2636
+ </Field>
2637
+
2638
+ <Switch checked={updates} onChange={setUpdates} label="Email me about product updates" />
2639
+
2640
+ <Button type="submit" full>
2641
+ Save changes
2642
+ </Button>
2643
+ </Stack>
2644
+ </form>
2645
+ </CardBody>
2646
+ </Card>
2647
+
2648
+ <Alert tone="info" title="Rebrand the whole kit from one file">
2649
+ Open <code>app/lacspace-ui.css</code>. It maps this template&rsquo;s accent, radius and font
2650
+ onto the kit&rsquo;s variables, so the components already match \u2014 and they follow your
2651
+ existing dark-mode toggle.
2652
+ </Alert>
2653
+
2654
+ <Card>
2655
+ <CardBody>
2656
+ <Stack direction="row" gap={2} align="center" wrap>
2657
+ <Badge tone="success" dot>
2658
+ Live
2659
+ </Badge>
2660
+ <Button variant="soft" onClick={() => toast("Saved to drafts")}>
2661
+ Show a toast
2662
+ </Button>
2663
+ <Button tone="danger" variant="soft" onClick={() => setConfirming(true)}>
2664
+ Delete account
2665
+ </Button>
2666
+ </Stack>
2667
+ </CardBody>
2668
+ </Card>
2669
+ </Stack>
2670
+
2671
+ <ConfirmDialog
2672
+ open={confirming}
2673
+ onOpenChange={setConfirming}
2674
+ tone="danger"
2675
+ title="Delete this account?"
2676
+ message="Everything in it goes with it. This cannot be undone."
2677
+ confirmLabel="Delete account"
2678
+ onConfirm={async () => {
2679
+ // Return a promise and the button spins, blocks repeat clicks, and
2680
+ // keeps the dialog open if it rejects \u2014 a failed delete never looks
2681
+ // like a successful one.
2682
+ await new Promise((resolve) => setTimeout(resolve, 700));
2683
+ toast({ title: "Account deleted", tone: "success" });
2684
+ }}
2685
+ />
2686
+ </main>
2687
+ );
2688
+ }
2689
+ `;
2690
+ var dataVizRouteLayout = () => `import "@lacspace/components/styles.css";
2691
+ import "@lacspace/charts/styles.css";
2692
+ import "@lacspace/table/styles.css";
2693
+ import "@lacspace/date/styles.css";
2694
+ import "../lacspace-ui.css";
2695
+
2696
+ // Scoped to this route; move these imports to app/layout.tsx to use the kit
2697
+ // everywhere. Order matters: lacspace-ui.css maps this app's tokens onto the
2698
+ // kit, so it comes last.
2699
+ export default function InsightsLayout({ children }: { children: React.ReactNode }) {
2700
+ return <>{children}</>;
2701
+ }
2702
+ `;
2703
+ var dataVizPage = () => `"use client";
2704
+ import { useState } from "react";
2705
+ import { Heading, Grid, Stack, Stat, Text } from "@lacspace/components";
2706
+ import { LineChart } from "@lacspace/charts";
2707
+ import { DataTable, textColumn, badgeColumn, currencyColumn, numberColumn } from "@lacspace/table";
2708
+ import { DateRangePicker, defaultPresets } from "@lacspace/date";
2709
+
2710
+ // Swap these three arrays for your own data \u2014 nothing else needs to change.
2711
+ const MONTHS = ["Apr", "May", "Jun", "Jul", "Aug", "Sep"];
2712
+ const REVENUE = [18, 21, 25, 24, 31, 38];
2713
+ const TARGET = [20, 22, 24, 26, 28, 30];
2714
+
2715
+ type Account = {
2716
+ id: string;
2717
+ account: string;
2718
+ plan: string;
2719
+ seats: number;
2720
+ mrr: number;
2721
+ };
2722
+
2723
+ const ACCOUNTS: Account[] = [
2724
+ { id: "1", account: "Northwind", plan: "team", seats: 42, mrr: 1890 },
2725
+ { id: "2", account: "Umbrella", plan: "pro", seats: 12, mrr: 540 },
2726
+ { id: "3", account: "Initech", plan: "team", seats: 68, mrr: 3060 },
2727
+ { id: "4", account: "Hooli", plan: "free", seats: 3, mrr: 0 },
2728
+ { id: "5", account: "Stark Industries", plan: "pro", seats: 19, mrr: 855 },
2729
+ { id: "6", account: "Wayne Enterprises", plan: "team", seats: 51, mrr: 2295 },
2730
+ ];
2731
+
2732
+ export default function InsightsPage() {
2733
+ const [range, setRange] = useState<unknown>(null);
2734
+
2735
+ return (
2736
+ <main className="mx-auto max-w-5xl px-6 py-16">
2737
+ <Stack gap={5}>
2738
+ <Stack direction="row" justify="between" align="center" gap={3} wrap>
2739
+ <div>
2740
+ <Heading level={1}>Insights</Heading>
2741
+ <Text tone="muted">Charts, a data table and a date filter \u2014 all zero-dependency.</Text>
2742
+ </div>
2743
+ <DateRangePicker
2744
+ numberOfMonths={2}
2745
+ presets={defaultPresets()}
2746
+ separator=" \u2013 "
2747
+ onChange={setRange}
2748
+ />
2749
+ </Stack>
2750
+
2751
+ <Grid columns={{ base: 1, md: 3 }} gap={3}>
2752
+ <Stat label="MRR" value="$8,640" delta={12.4} comparison="vs last month" />
2753
+ <Stat label="Seats" value="195" delta={6.1} comparison="vs last month" />
2754
+ <Stat label="Churn" value="1.8%" delta={-0.3} invertDelta comparison="vs last month" />
2755
+ </Grid>
2756
+
2757
+ <LineChart
2758
+ labels={MONTHS}
2759
+ series={[
2760
+ { name: "Revenue", data: REVENUE, area: true },
2761
+ { name: "Target", data: TARGET, dashed: true },
2762
+ ]}
2763
+ curve
2764
+ dots
2765
+ tooltip
2766
+ responsive
2767
+ dataTable
2768
+ formatValue={(n) => "$" + n + "k"}
2769
+ />
2770
+
2771
+ <DataTable<Account>
2772
+ caption="Accounts"
2773
+ data={ACCOUNTS}
2774
+ getRowId={(row) => row.id}
2775
+ columns={[
2776
+ textColumn<Account>("account", { header: "Account", key: "account", pinned: "left" }),
2777
+ badgeColumn<Account>("plan", {
2778
+ header: "Plan",
2779
+ key: "plan",
2780
+ tones: { team: "success", pro: "info", free: "default" },
2781
+ }),
2782
+ numberColumn<Account>("seats", { header: "Seats", key: "seats", aggregate: "sum" }),
2783
+ currencyColumn<Account>("mrr", { header: "MRR", key: "mrr", currency: "USD", aggregate: "sum" }),
2784
+ ]}
2785
+ defaultSort={[{ id: "mrr", direction: "desc" }]}
2786
+ searchable
2787
+ selectable
2788
+ exportable
2789
+ stickyHeader
2790
+ />
2791
+ </Stack>
2792
+ </main>
2793
+ );
2794
+ }
2795
+ `;
2440
2796
  var aiChatRoute = (_ctx) => `import { resolveConfig } from "@lacspace/providers";
2441
2797
  import { stream } from "@lacspace/ai";
2442
2798
  import type { Message } from "@lacspace/ai";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lacspace-app",
3
- "version": "2.13.1",
3
+ "version": "2.14.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": {