create-lacspace-app 2.13.0 → 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 +10 -0
- package/dist/index.js +401 -7
- package/dist/lib.cjs +397 -3
- package/dist/lib.js +401 -7
- package/package.json +1 -1
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
|
|
|
@@ -202,6 +210,8 @@ Every template is Next.js 15 App Router + React 19 + Tailwind v4 — dark, moder
|
|
|
202
210
|
| `--no-install` | skip installing dependencies |
|
|
203
211
|
| `--no-git` | skip git init |
|
|
204
212
|
| `-y, --yes` | accept defaults (needs a project name) |
|
|
213
|
+
| `-h, --help` | show every flag |
|
|
214
|
+
| `-v, --version` | print the installed version |
|
|
205
215
|
|
|
206
216
|
### 🎨 Themes
|
|
207
217
|
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readdirSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
|
-
import {
|
|
2
|
+
import { realpathSync, existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
3
|
+
import { basename, resolve, join, dirname } from 'path';
|
|
4
4
|
import { createInterface } from 'readline/promises';
|
|
5
|
-
import {
|
|
5
|
+
import { stdout, exit, argv, stdin, cwd } from 'process';
|
|
6
6
|
import { spawnSync } from 'child_process';
|
|
7
|
-
import { pathToFileURL } from 'url';
|
|
7
|
+
import { pathToFileURL, fileURLToPath } from 'url';
|
|
8
8
|
|
|
9
9
|
var C = {
|
|
10
10
|
reset: "\x1B[0m",
|
|
@@ -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"
|
|
2477
|
+
}
|
|
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" });
|
|
2437
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’s accent, radius and font
|
|
2649
|
+
onto the kit’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 },
|
|
2438
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";
|
|
@@ -8014,9 +8370,24 @@ function backendFiles(ctx) {
|
|
|
8014
8370
|
}
|
|
8015
8371
|
return files;
|
|
8016
8372
|
}
|
|
8373
|
+
function cliVersion() {
|
|
8374
|
+
try {
|
|
8375
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
8376
|
+
for (const rel of ["../package.json", "./package.json", "../../package.json"]) {
|
|
8377
|
+
const file = resolve(here, rel);
|
|
8378
|
+
if (!existsSync(file)) continue;
|
|
8379
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
8380
|
+
const version = parsed.version;
|
|
8381
|
+
const name = parsed.name;
|
|
8382
|
+
if (typeof version === "string" && (name === "create-lacspace-app" || rel !== "../../package.json")) return version;
|
|
8383
|
+
}
|
|
8384
|
+
} catch {
|
|
8385
|
+
}
|
|
8386
|
+
return "unknown";
|
|
8387
|
+
}
|
|
8017
8388
|
var splitList = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
8018
8389
|
function parseArgs(list) {
|
|
8019
|
-
const a = { features: [], yes: false, install: true, git: true, pm: "npm", help: false, dryRun: false };
|
|
8390
|
+
const a = { features: [], yes: false, install: true, git: true, pm: "npm", help: false, version: false, dryRun: false, unknown: [] };
|
|
8020
8391
|
for (let i = 0; i < list.length; i++) {
|
|
8021
8392
|
const arg = list[i];
|
|
8022
8393
|
const next = () => list[++i] ?? "";
|
|
@@ -8039,6 +8410,7 @@ function parseArgs(list) {
|
|
|
8039
8410
|
} else if (arg === "--recipe") a.recipe = next();
|
|
8040
8411
|
else if (arg.startsWith("--recipe=")) a.recipe = arg.slice(9);
|
|
8041
8412
|
else if (arg === "-h" || arg === "--help") a.help = true;
|
|
8413
|
+
else if (arg === "-v" || arg === "-V" || arg === "--version") a.version = true;
|
|
8042
8414
|
else if (arg.startsWith("--template=")) a.template = arg.slice(11);
|
|
8043
8415
|
else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
|
|
8044
8416
|
else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
|
|
@@ -8046,6 +8418,7 @@ function parseArgs(list) {
|
|
|
8046
8418
|
else if (arg.startsWith("--with=")) a.features.push(...splitList(arg.slice(7)));
|
|
8047
8419
|
else if (arg.startsWith("--features=")) a.features.push(...splitList(arg.slice(11)));
|
|
8048
8420
|
else if (!arg.startsWith("-") && !a.name) a.name = arg;
|
|
8421
|
+
else if (arg.startsWith("-")) a.unknown.push(arg);
|
|
8049
8422
|
}
|
|
8050
8423
|
return a;
|
|
8051
8424
|
}
|
|
@@ -8153,6 +8526,7 @@ ${c("bold", "Options")}
|
|
|
8153
8526
|
--no-git Skip git init
|
|
8154
8527
|
-y, --yes Accept defaults (needs <name>)
|
|
8155
8528
|
-h, --help Show this help
|
|
8529
|
+
-v, --version Print the version and exit
|
|
8156
8530
|
|
|
8157
8531
|
${c("bold", "Feature add-ons")} ${c("dim", "(--with) \u2014 free & keyless, local by default")}
|
|
8158
8532
|
${FEATURES.map((f) => ` ${f.key.padEnd(11)} ${f.description}`).join("\n")}
|
|
@@ -8626,6 +9000,16 @@ async function main() {
|
|
|
8626
9000
|
stdout.write(HELP + "\n");
|
|
8627
9001
|
return;
|
|
8628
9002
|
}
|
|
9003
|
+
if (args.version) {
|
|
9004
|
+
stdout.write(`create-lacspace-app ${cliVersion()}
|
|
9005
|
+
`);
|
|
9006
|
+
return;
|
|
9007
|
+
}
|
|
9008
|
+
if (args.unknown.length) {
|
|
9009
|
+
stdout.write(c("yellow", `
|
|
9010
|
+
! Unknown option${args.unknown.length > 1 ? "s" : ""}: ${args.unknown.join(", ")} \u2014 ignoring. Run with --help to see every flag.
|
|
9011
|
+
`));
|
|
9012
|
+
}
|
|
8629
9013
|
stdout.write(`
|
|
8630
9014
|
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
|
|
8631
9015
|
|
|
@@ -8824,8 +9208,18 @@ ${c("bold", "Your feature add-ons")} ${c("dim", "\u2014 details in LEARN.md")}
|
|
|
8824
9208
|
|
|
8825
9209
|
`);
|
|
8826
9210
|
}
|
|
8827
|
-
|
|
8828
|
-
|
|
9211
|
+
function isDirectRun() {
|
|
9212
|
+
const entry = argv[1];
|
|
9213
|
+
if (!entry) return false;
|
|
9214
|
+
const here = import.meta.url;
|
|
9215
|
+
if (here === pathToFileURL(entry).href) return true;
|
|
9216
|
+
try {
|
|
9217
|
+
if (here === pathToFileURL(realpathSync(entry)).href) return true;
|
|
9218
|
+
} catch {
|
|
9219
|
+
}
|
|
9220
|
+
return basename(entry).replace(/\.[cm]?js$/, "") === "create-lacspace-app";
|
|
9221
|
+
}
|
|
9222
|
+
if (isDirectRun()) {
|
|
8829
9223
|
main().catch((err) => {
|
|
8830
9224
|
stdout.write(c("red", `
|
|
8831
9225
|
\u2717 ${err instanceof Error ? err.message : String(err)}
|