@helloleo/plugins 0.1.13 → 0.1.15
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/dist/index.js +203 -1220
- package/dist/templates/tanstack-router-2026-06-15/components.json +22 -0
- package/dist/templates/tanstack-router-2026-06-15/index.html +13 -0
- package/dist/templates/tanstack-router-2026-06-15/package.json +94 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/accordion.tsx +51 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/alert-dialog.tsx +115 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/alert.tsx +49 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/aspect-ratio.tsx +5 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/avatar.tsx +47 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/badge.tsx +32 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/breadcrumb.tsx +101 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/button.tsx +49 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/calendar.tsx +177 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/card.tsx +55 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/carousel.tsx +240 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/chart.tsx +331 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/checkbox.tsx +26 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/collapsible.tsx +11 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/command.tsx +143 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/context-menu.tsx +187 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/dialog.tsx +104 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/drawer.tsx +98 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/dropdown-menu.tsx +188 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/form.tsx +171 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/hover-card.tsx +27 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/input-otp.tsx +69 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/input.tsx +22 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/label.tsx +21 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/menubar.tsx +229 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/navigation-menu.tsx +120 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/pagination.tsx +98 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/popover.tsx +31 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/progress.tsx +25 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/radio-group.tsx +36 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/resizable.tsx +37 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/scroll-area.tsx +44 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/select.tsx +152 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/separator.tsx +24 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sheet.tsx +122 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sidebar.tsx +744 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/skeleton.tsx +7 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/slider.tsx +23 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sonner.tsx +23 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/switch.tsx +27 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/table.tsx +94 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/tabs.tsx +53 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/textarea.tsx +21 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/toggle-group.tsx +57 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/toggle.tsx +42 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/tooltip.tsx +32 -0
- package/dist/templates/tanstack-router-2026-06-15/src/hooks/use-mobile.tsx +19 -0
- package/dist/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +83 -0
- package/dist/templates/tanstack-router-2026-06-15/src/lib/utils.ts +6 -0
- package/dist/templates/tanstack-router-2026-06-15/src/main.tsx +33 -0
- package/dist/templates/tanstack-router-2026-06-15/src/routeTree.gen.ts +59 -0
- package/dist/templates/tanstack-router-2026-06-15/src/router.tsx +23 -0
- package/dist/templates/tanstack-router-2026-06-15/src/routes/__root.tsx +16 -0
- package/dist/templates/tanstack-router-2026-06-15/src/routes/index.tsx +14 -0
- package/dist/templates/tanstack-router-2026-06-15/src/styles.css +214 -0
- package/dist/templates/tanstack-router-2026-06-15/tsconfig.json +28 -0
- package/dist/templates/tanstack-router-2026-06-15/tsr.config.json +3 -0
- package/dist/templates/tanstack-router-2026-06-15/vite.config.ts +21 -0
- package/package.json +3 -2
- package/src/index.ts +0 -1
- package/src/template-render.ts +48 -0
- package/src/templates/tanstack-router-2026-06-15/components.json +22 -0
- package/src/templates/tanstack-router-2026-06-15/index.html +13 -0
- package/src/templates/tanstack-router-2026-06-15/package.json +94 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/accordion.tsx +51 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/alert-dialog.tsx +115 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/alert.tsx +49 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/aspect-ratio.tsx +5 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/avatar.tsx +47 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/badge.tsx +32 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/breadcrumb.tsx +101 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/button.tsx +49 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/calendar.tsx +177 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/card.tsx +55 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/carousel.tsx +240 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/chart.tsx +331 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/checkbox.tsx +26 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/collapsible.tsx +11 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/command.tsx +143 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/context-menu.tsx +187 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/dialog.tsx +104 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/drawer.tsx +98 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/dropdown-menu.tsx +188 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/form.tsx +171 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/hover-card.tsx +27 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/input-otp.tsx +69 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/input.tsx +22 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/label.tsx +21 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/menubar.tsx +229 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/navigation-menu.tsx +120 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/pagination.tsx +98 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/popover.tsx +31 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/progress.tsx +25 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/radio-group.tsx +36 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/resizable.tsx +37 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/scroll-area.tsx +44 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/select.tsx +152 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/separator.tsx +24 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/sheet.tsx +122 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/sidebar.tsx +744 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/skeleton.tsx +7 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/slider.tsx +23 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/sonner.tsx +23 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/switch.tsx +27 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/table.tsx +94 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/tabs.tsx +53 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/textarea.tsx +21 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/toggle-group.tsx +57 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/toggle.tsx +42 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/tooltip.tsx +32 -0
- package/src/templates/tanstack-router-2026-06-15/src/hooks/use-mobile.tsx +19 -0
- package/src/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +83 -0
- package/src/templates/tanstack-router-2026-06-15/src/lib/utils.ts +6 -0
- package/src/templates/tanstack-router-2026-06-15/src/main.tsx +33 -0
- package/src/templates/tanstack-router-2026-06-15/src/routeTree.gen.ts +59 -0
- package/src/templates/tanstack-router-2026-06-15/src/router.tsx +23 -0
- package/src/templates/tanstack-router-2026-06-15/src/routes/__root.tsx +16 -0
- package/src/templates/tanstack-router-2026-06-15/src/routes/index.tsx +14 -0
- package/src/templates/tanstack-router-2026-06-15/src/styles.css +214 -0
- package/src/templates/tanstack-router-2026-06-15/tsconfig.json +28 -0
- package/src/templates/tanstack-router-2026-06-15/tsr.config.json +3 -0
- package/src/templates/tanstack-router-2026-06-15/vite.config.ts +21 -0
- package/src/tools/scaffold.ts +62 -1340
- package/src/tools/setup-netsuite.ts +161 -80
- package/src/tools/start-scaffold.ts +0 -1343
package/dist/index.js
CHANGED
|
@@ -12538,1175 +12538,84 @@ var RemovePlugin = async () => {
|
|
|
12538
12538
|
};
|
|
12539
12539
|
// src/tools/scaffold.ts
|
|
12540
12540
|
import { mkdir } from "fs/promises";
|
|
12541
|
-
import
|
|
12542
|
-
function packageJson(name) {
|
|
12543
|
-
return JSON.stringify({
|
|
12544
|
-
name,
|
|
12545
|
-
private: true,
|
|
12546
|
-
version: "0.0.0",
|
|
12547
|
-
type: "module",
|
|
12548
|
-
scripts: {
|
|
12549
|
-
dev: "vite",
|
|
12550
|
-
start: "vite",
|
|
12551
|
-
build: "vite build",
|
|
12552
|
-
preview: "vite preview"
|
|
12553
|
-
},
|
|
12554
|
-
dependencies: {
|
|
12555
|
-
react: "^18.3.1",
|
|
12556
|
-
"react-dom": "^18.3.1",
|
|
12557
|
-
clsx: "^2.1.1",
|
|
12558
|
-
"tailwind-merge": "^2.6.0",
|
|
12559
|
-
"lucide-react": "^1.14.0"
|
|
12560
|
-
},
|
|
12561
|
-
devDependencies: {
|
|
12562
|
-
"@types/react": "^18.3.12",
|
|
12563
|
-
"@types/react-dom": "^18.3.1",
|
|
12564
|
-
"@vitejs/plugin-react": "^4.3.4",
|
|
12565
|
-
autoprefixer: "^10.4.20",
|
|
12566
|
-
postcss: "^8.4.49",
|
|
12567
|
-
tailwindcss: "^3.4.17",
|
|
12568
|
-
typescript: "^5.6.3",
|
|
12569
|
-
vite: "^6.0.5"
|
|
12570
|
-
}
|
|
12571
|
-
}, null, 2);
|
|
12572
|
-
}
|
|
12573
|
-
function indexHtml(fontFamily) {
|
|
12574
|
-
const fontParam = fontFamily.replace(/\s+/g, "+");
|
|
12575
|
-
return `<!DOCTYPE html>
|
|
12576
|
-
<html lang="en">
|
|
12577
|
-
<head>
|
|
12578
|
-
<meta charset="UTF-8" />
|
|
12579
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
12580
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
12581
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
12582
|
-
<link href="https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
12583
|
-
<title>App</title>
|
|
12584
|
-
</head>
|
|
12585
|
-
<body>
|
|
12586
|
-
<div id="root"></div>
|
|
12587
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
12588
|
-
</body>
|
|
12589
|
-
</html>`;
|
|
12590
|
-
}
|
|
12591
|
-
function viteConfig() {
|
|
12592
|
-
return `import { defineConfig } from 'vite'
|
|
12593
|
-
import react from '@vitejs/plugin-react'
|
|
12594
|
-
import path from 'path'
|
|
12595
|
-
|
|
12596
|
-
export default defineConfig({
|
|
12597
|
-
plugins: [react()],
|
|
12598
|
-
server: {
|
|
12599
|
-
allowedHosts: true,
|
|
12600
|
-
},
|
|
12601
|
-
resolve: {
|
|
12602
|
-
alias: {
|
|
12603
|
-
'@': path.resolve(__dirname, './src'),
|
|
12604
|
-
},
|
|
12605
|
-
},
|
|
12606
|
-
})`;
|
|
12607
|
-
}
|
|
12608
|
-
function tailwindConfig() {
|
|
12609
|
-
return `export default {
|
|
12610
|
-
darkMode: ["class"],
|
|
12611
|
-
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
|
12612
|
-
theme: {
|
|
12613
|
-
extend: {
|
|
12614
|
-
colors: {
|
|
12615
|
-
background: "hsl(var(--background))",
|
|
12616
|
-
foreground: "hsl(var(--foreground))",
|
|
12617
|
-
card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
|
|
12618
|
-
primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
|
|
12619
|
-
secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
|
|
12620
|
-
muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
|
|
12621
|
-
accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
|
|
12622
|
-
destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
|
|
12623
|
-
success: { DEFAULT: "hsl(var(--success))", foreground: "hsl(var(--success-foreground))" },
|
|
12624
|
-
warning: { DEFAULT: "hsl(var(--warning))", foreground: "hsl(var(--warning-foreground))" },
|
|
12625
|
-
border: "hsl(var(--border))",
|
|
12626
|
-
input: "hsl(var(--input))",
|
|
12627
|
-
ring: "hsl(var(--ring))",
|
|
12628
|
-
},
|
|
12629
|
-
fontFamily: {
|
|
12630
|
-
sans: "var(--font-sans)",
|
|
12631
|
-
heading: "var(--font-heading)",
|
|
12632
|
-
},
|
|
12633
|
-
borderRadius: {
|
|
12634
|
-
lg: "var(--radius)",
|
|
12635
|
-
md: "calc(var(--radius) - 2px)",
|
|
12636
|
-
sm: "calc(var(--radius) - 4px)",
|
|
12637
|
-
},
|
|
12638
|
-
borderWidth: {
|
|
12639
|
-
DEFAULT: "var(--border-width)",
|
|
12640
|
-
},
|
|
12641
|
-
boxShadow: {
|
|
12642
|
-
DEFAULT: "var(--shadow)",
|
|
12643
|
-
lg: "var(--shadow-lg)",
|
|
12644
|
-
},
|
|
12645
|
-
},
|
|
12646
|
-
},
|
|
12647
|
-
plugins: [],
|
|
12648
|
-
}`;
|
|
12649
|
-
}
|
|
12650
|
-
function postcssConfig() {
|
|
12651
|
-
return `export default {
|
|
12652
|
-
plugins: {
|
|
12653
|
-
tailwindcss: {},
|
|
12654
|
-
autoprefixer: {},
|
|
12655
|
-
},
|
|
12656
|
-
}`;
|
|
12657
|
-
}
|
|
12658
|
-
function indexCss(primaryColor, fontFamily, borderRadius) {
|
|
12659
|
-
const fontValue = `'${fontFamily}', ui-sans-serif, system-ui, sans-serif`;
|
|
12660
|
-
return `/*
|
|
12661
|
-
* HelloLeo Design System -- READ BEFORE STYLING.
|
|
12662
|
-
*
|
|
12663
|
-
* The CSS variables below are the single source of truth for the project's
|
|
12664
|
-
* design tokens. They power the no-code Design panel in the HelloLeo webapp:
|
|
12665
|
-
* the user edits colors / fonts / radius / shadows visually, and the panel
|
|
12666
|
-
* writes back to THIS file. Keeping the conventions intact is what unlocks
|
|
12667
|
-
* that round-trip.
|
|
12668
|
-
*
|
|
12669
|
-
* Conventions (do not break):
|
|
12670
|
-
* - All colors stored as raw HSL triplets (no hsl() wrapper, no #hex).
|
|
12671
|
-
* Tailwind classes wrap them via hsl(var(--token)). Never inline #hex
|
|
12672
|
-
* in components -- always use design tokens (bg-primary, text-foreground,
|
|
12673
|
-
* border-border, etc.).
|
|
12674
|
-
* - --primary / --secondary / --accent / --destructive / --success /
|
|
12675
|
-
* --warning each have a paired -foreground for text on that surface.
|
|
12676
|
-
* - --radius drives lg/md/sm via calc() in tailwind.config.js. Edit here,
|
|
12677
|
-
* not in component classes.
|
|
12678
|
-
* - --font-sans for body, --font-heading for headings. The webfont is loaded
|
|
12679
|
-
* via a Google Fonts <link> \u2014 in index.html (SPA) or the root route head
|
|
12680
|
-
* (SSR). Keep the family here in sync with that <link>.
|
|
12681
|
-
* - .dark variants mirror :root one-for-one. When adding a token, add it
|
|
12682
|
-
* to BOTH blocks or dark mode breaks.
|
|
12683
|
-
*
|
|
12684
|
-
* UI primitives (Button, Card, Input, Modal, Table, Badge, Alert, Spinner)
|
|
12685
|
-
* MUST live in src/components/ui/ and consume these tokens via Tailwind
|
|
12686
|
-
* classes -- never hardcoded colors. One responsibility per file, <50 lines
|
|
12687
|
-
* per component.
|
|
12688
|
-
*/
|
|
12689
|
-
|
|
12690
|
-
@tailwind base;
|
|
12691
|
-
@tailwind components;
|
|
12692
|
-
@tailwind utilities;
|
|
12693
|
-
|
|
12694
|
-
@layer base {
|
|
12695
|
-
:root {
|
|
12696
|
-
--background: 0 0% 100%;
|
|
12697
|
-
--foreground: 222.2 84% 4.9%;
|
|
12698
|
-
--card: 0 0% 100%;
|
|
12699
|
-
--card-foreground: 222.2 84% 4.9%;
|
|
12700
|
-
--primary: ${primaryColor};
|
|
12701
|
-
--primary-foreground: 210 40% 98%;
|
|
12702
|
-
--secondary: 210 40% 96.1%;
|
|
12703
|
-
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
12704
|
-
--muted: 210 40% 96.1%;
|
|
12705
|
-
--muted-foreground: 215.4 16.3% 46.9%;
|
|
12706
|
-
--accent: 210 40% 96.1%;
|
|
12707
|
-
--accent-foreground: 222.2 47.4% 11.2%;
|
|
12708
|
-
--destructive: 0 84.2% 60.2%;
|
|
12709
|
-
--destructive-foreground: 210 40% 98%;
|
|
12710
|
-
--success: 142 76% 36%;
|
|
12711
|
-
--success-foreground: 210 40% 98%;
|
|
12712
|
-
--warning: 38 92% 50%;
|
|
12713
|
-
--warning-foreground: 222.2 47.4% 11.2%;
|
|
12714
|
-
--border: 214.3 31.8% 91.4%;
|
|
12715
|
-
--input: 214.3 31.8% 91.4%;
|
|
12716
|
-
--ring: 221.2 83.2% 53.3%;
|
|
12717
|
-
--radius: ${borderRadius};
|
|
12718
|
-
--border-width: 1px;
|
|
12719
|
-
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
12720
|
-
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
12721
|
-
--font-sans: ${fontValue};
|
|
12722
|
-
--font-heading: ${fontValue};
|
|
12723
|
-
}
|
|
12724
|
-
|
|
12725
|
-
.dark {
|
|
12726
|
-
--background: 222.2 84% 4.9%;
|
|
12727
|
-
--foreground: 210 40% 98%;
|
|
12728
|
-
--card: 222.2 84% 4.9%;
|
|
12729
|
-
--card-foreground: 210 40% 98%;
|
|
12730
|
-
--primary: 217.2 91.2% 59.8%;
|
|
12731
|
-
--primary-foreground: 222.2 47.4% 11.2%;
|
|
12732
|
-
--secondary: 217.2 32.6% 17.5%;
|
|
12733
|
-
--secondary-foreground: 210 40% 98%;
|
|
12734
|
-
--muted: 217.2 32.6% 17.5%;
|
|
12735
|
-
--muted-foreground: 215 20.2% 65.1%;
|
|
12736
|
-
--accent: 217.2 32.6% 17.5%;
|
|
12737
|
-
--accent-foreground: 210 40% 98%;
|
|
12738
|
-
--destructive: 0 62.8% 30.6%;
|
|
12739
|
-
--destructive-foreground: 210 40% 98%;
|
|
12740
|
-
--success: 142 76% 36%;
|
|
12741
|
-
--success-foreground: 210 40% 98%;
|
|
12742
|
-
--warning: 38 92% 50%;
|
|
12743
|
-
--warning-foreground: 222.2 47.4% 11.2%;
|
|
12744
|
-
--border: 217.2 32.6% 17.5%;
|
|
12745
|
-
--input: 217.2 32.6% 17.5%;
|
|
12746
|
-
--ring: 224.3 76.3% 48%;
|
|
12747
|
-
--border-width: 1px;
|
|
12748
|
-
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
|
|
12749
|
-
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
|
12750
|
-
--font-sans: ${fontValue};
|
|
12751
|
-
--font-heading: ${fontValue};
|
|
12752
|
-
}
|
|
12753
|
-
}
|
|
12754
|
-
|
|
12755
|
-
body {
|
|
12756
|
-
@apply bg-background text-foreground font-sans;
|
|
12757
|
-
}`;
|
|
12758
|
-
}
|
|
12759
|
-
function utilsTs() {
|
|
12760
|
-
return `import { clsx, type ClassValue } from 'clsx'
|
|
12761
|
-
import { twMerge } from 'tailwind-merge'
|
|
12762
|
-
|
|
12763
|
-
export function cn(...inputs: ClassValue[]) {
|
|
12764
|
-
return twMerge(clsx(inputs))
|
|
12765
|
-
}`;
|
|
12766
|
-
}
|
|
12767
|
-
function proxyTs() {
|
|
12768
|
-
return `/*
|
|
12769
|
-
* HelloLeo Integration Proxy -- runtime helper for connected integrations.
|
|
12770
|
-
*
|
|
12771
|
-
* All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
|
|
12772
|
-
* Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
|
|
12773
|
-
* this single proxy. The proxy handles auth and CORS using credentials the
|
|
12774
|
-
* user already configured in Project Settings -- you NEVER pass an API key
|
|
12775
|
-
* from frontend code, and you NEVER call third-party hosts directly.
|
|
12776
|
-
*
|
|
12777
|
-
* Two modes:
|
|
12778
|
-
* A) Frontend-only project -> import callIntegration() from this file.
|
|
12779
|
-
* B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
|
|
12780
|
-
* Use Cloud whenever the project needs backend logic, automations,
|
|
12781
|
-
* webhooks, scheduled jobs, or AI agents. Keep secrets server-side.
|
|
12782
|
-
*
|
|
12783
|
-
* Integration slugs:
|
|
12784
|
-
* Use the exact slug from the Enabled MCP list with the "pipedream-"
|
|
12785
|
-
* prefix removed. e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth".
|
|
12786
|
-
* Native integrations (odoo, airtable) keep their plain name.
|
|
12787
|
-
*
|
|
12788
|
-
* Response shape (always wrapped):
|
|
12789
|
-
* { success: boolean, status: number, data: <provider payload> }
|
|
12790
|
-
*
|
|
12791
|
-
* Unwrap "data" by provider:
|
|
12792
|
-
* odoo -> data.result
|
|
12793
|
-
* airtable -> data.records
|
|
12794
|
-
* notion -> data.results
|
|
12795
|
-
* microsoft 365 -> data.value
|
|
12796
|
-
* klaviyo -> data.data
|
|
12797
|
-
* supabase / dimomaint -> data
|
|
12798
|
-
*
|
|
12799
|
-
* Pagination: put query params directly in the endpoint string,
|
|
12800
|
-
* e.g. endpoint: '/api/tickets?cursor=xyz&per_page=100'.
|
|
12801
|
-
* Read pagination metadata from the response to build the next call.
|
|
12802
|
-
* Never .filter() large lists in the frontend -- paginate server-side.
|
|
12803
|
-
*
|
|
12804
|
-
* Secrets for non-integration APIs (e.g. raw OpenAI): store them in
|
|
12805
|
-
* HelloLeo Cloud -> Settings -> Cloud -> Secrets, and read via env.SECRET_NAME
|
|
12806
|
-
* from a Worker. Never embed secrets in frontend code.
|
|
12807
|
-
*/
|
|
12808
|
-
|
|
12809
|
-
const PROJECT_UUID = import.meta.env.VITE_PROJECT_UUID ?? '';
|
|
12810
|
-
export const INTEGRATION_PROXY_URL =
|
|
12811
|
-
import.meta.env.VITE_INTEGRATION_PROXY_URL ??
|
|
12812
|
-
\`https://api.app.helloleo.dev/api/integration-proxy/\${PROJECT_UUID}\`;
|
|
12813
|
-
|
|
12814
|
-
export interface ProxyRequest {
|
|
12815
|
-
integration: string;
|
|
12816
|
-
endpoint: string;
|
|
12817
|
-
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
12818
|
-
body?: unknown;
|
|
12819
|
-
headers?: Record<string, string>;
|
|
12820
|
-
}
|
|
12821
|
-
|
|
12822
|
-
export interface ProxyResponse<T = unknown> {
|
|
12823
|
-
success: boolean;
|
|
12824
|
-
status: number;
|
|
12825
|
-
data: T;
|
|
12826
|
-
}
|
|
12827
|
-
|
|
12828
|
-
export async function callIntegration<T = unknown>(
|
|
12829
|
-
req: ProxyRequest,
|
|
12830
|
-
): Promise<ProxyResponse<T>> {
|
|
12831
|
-
const res = await fetch(INTEGRATION_PROXY_URL, {
|
|
12832
|
-
method: 'POST',
|
|
12833
|
-
headers: { 'Content-Type': 'application/json' },
|
|
12834
|
-
body: JSON.stringify({ method: 'POST', ...req }),
|
|
12835
|
-
});
|
|
12836
|
-
return res.json();
|
|
12837
|
-
}
|
|
12541
|
+
import path3 from "path";
|
|
12838
12542
|
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
if (!/^[0-9a-fA-F]{6}$/.test(h))
|
|
12861
|
-
return "";
|
|
12862
|
-
const r = Number.parseInt(h.slice(0, 2), 16) / 255;
|
|
12863
|
-
const g = Number.parseInt(h.slice(2, 4), 16) / 255;
|
|
12864
|
-
const b = Number.parseInt(h.slice(4, 6), 16) / 255;
|
|
12865
|
-
const max = Math.max(r, g, b);
|
|
12866
|
-
const min = Math.min(r, g, b);
|
|
12867
|
-
const l = (max + min) / 2;
|
|
12868
|
-
let s = 0;
|
|
12869
|
-
let hue = 0;
|
|
12870
|
-
if (max !== min) {
|
|
12871
|
-
const d = max - min;
|
|
12872
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
12873
|
-
switch (max) {
|
|
12874
|
-
case r:
|
|
12875
|
-
hue = (g - b) / d + (g < b ? 6 : 0);
|
|
12876
|
-
break;
|
|
12877
|
-
case g:
|
|
12878
|
-
hue = (b - r) / d + 2;
|
|
12879
|
-
break;
|
|
12880
|
-
default:
|
|
12881
|
-
hue = (r - g) / d + 4;
|
|
12882
|
-
}
|
|
12883
|
-
hue *= 60;
|
|
12884
|
-
}
|
|
12885
|
-
return `${Math.round(hue)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
12886
|
-
}
|
|
12887
|
-
function normalizePrimaryColor(value) {
|
|
12888
|
-
const trimmed = value.trim();
|
|
12889
|
-
if (/^(#|[0-9a-fA-F]{3}$|[0-9a-fA-F]{6}$)/.test(trimmed)) {
|
|
12890
|
-
const hsl = hexToHslTriplet(trimmed);
|
|
12891
|
-
if (hsl)
|
|
12892
|
-
return hsl;
|
|
12543
|
+
// src/template-render.ts
|
|
12544
|
+
import path2 from "path";
|
|
12545
|
+
var {Glob } = globalThis.Bun;
|
|
12546
|
+
var TEMPLATES_ROOT = path2.join(import.meta.dir, "templates");
|
|
12547
|
+
var TOKEN_RE = /\{\{(LEO_[A-Z0-9_]+)\}\}/g;
|
|
12548
|
+
var SKIP = /(^|\/)(node_modules|dist|dist-ssr|\.tanstack|\.output|bun\.lock|bun\.lockb|package-lock\.json|yarn\.lock|pnpm-lock\.yaml)(\/|$)/;
|
|
12549
|
+
async function renderTemplate(templateId, vars, destDir) {
|
|
12550
|
+
const root = path2.join(TEMPLATES_ROOT, templateId);
|
|
12551
|
+
const glob = new Glob("**/*");
|
|
12552
|
+
const created = [];
|
|
12553
|
+
for await (const rel of glob.scan({
|
|
12554
|
+
cwd: root,
|
|
12555
|
+
dot: true,
|
|
12556
|
+
onlyFiles: true
|
|
12557
|
+
})) {
|
|
12558
|
+
if (SKIP.test(rel))
|
|
12559
|
+
continue;
|
|
12560
|
+
const raw = await Bun.file(path2.join(root, rel)).text();
|
|
12561
|
+
const content = raw.replace(TOKEN_RE, (_m, key) => vars[key] ?? "");
|
|
12562
|
+
await Bun.write(path2.join(destDir, rel), content);
|
|
12563
|
+
created.push(rel);
|
|
12893
12564
|
}
|
|
12894
|
-
return
|
|
12565
|
+
return created.sort();
|
|
12895
12566
|
}
|
|
12896
|
-
function mainTsx() {
|
|
12897
|
-
return `import React from 'react'
|
|
12898
|
-
import ReactDOM from 'react-dom/client'
|
|
12899
|
-
import App from './App'
|
|
12900
|
-
import './index.css'
|
|
12901
12567
|
|
|
12902
|
-
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
)`;
|
|
12568
|
+
// src/tools/scaffold.ts
|
|
12569
|
+
var SCAFFOLD_VERSION = "tanstack-router-2026-06-15";
|
|
12570
|
+
function projectUuidFromDir(dir) {
|
|
12571
|
+
return path3.basename(dir);
|
|
12907
12572
|
}
|
|
12908
12573
|
async function writeBaseScaffold(opts, dir) {
|
|
12909
12574
|
const name = opts.name ?? "leo-project";
|
|
12910
|
-
const
|
|
12911
|
-
|
|
12912
|
-
const
|
|
12913
|
-
await
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
12918
|
-
["tailwind.config.js", tailwindConfig()],
|
|
12919
|
-
["postcss.config.js", postcssConfig()],
|
|
12920
|
-
[path2.join("src", "index.css"), indexCss(primary, font, radius)],
|
|
12921
|
-
[path2.join("src", "lib", "utils.ts"), utilsTs()],
|
|
12922
|
-
[path2.join("src", "lib", "proxy.ts"), proxyTs()],
|
|
12923
|
-
[path2.join("src", "main.tsx"), mainTsx()]
|
|
12924
|
-
];
|
|
12925
|
-
const created = [];
|
|
12926
|
-
for (const [filePath, content] of files) {
|
|
12927
|
-
await Bun.write(path2.join(dir, filePath), content);
|
|
12928
|
-
created.push(filePath);
|
|
12929
|
-
}
|
|
12930
|
-
return created;
|
|
12931
|
-
}
|
|
12932
|
-
var REACT = "^19.0.0";
|
|
12933
|
-
var VITE = "^6.0.5";
|
|
12934
|
-
var TS = "^5.6.3";
|
|
12935
|
-
var TANSTACK = "1.131.50";
|
|
12936
|
-
var BIOME = "2.4.8";
|
|
12937
|
-
function tsconfig() {
|
|
12938
|
-
return JSON.stringify({
|
|
12939
|
-
compilerOptions: {
|
|
12940
|
-
target: "ES2022",
|
|
12941
|
-
module: "ESNext",
|
|
12942
|
-
moduleResolution: "Bundler",
|
|
12943
|
-
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
12944
|
-
jsx: "react-jsx",
|
|
12945
|
-
strict: true,
|
|
12946
|
-
noUncheckedIndexedAccess: true,
|
|
12947
|
-
noUnusedLocals: true,
|
|
12948
|
-
noUnusedParameters: true,
|
|
12949
|
-
verbatimModuleSyntax: true,
|
|
12950
|
-
skipLibCheck: true,
|
|
12951
|
-
noEmit: true,
|
|
12952
|
-
baseUrl: ".",
|
|
12953
|
-
paths: { "@/*": ["./src/*"] },
|
|
12954
|
-
types: ["vite/client"]
|
|
12955
|
-
},
|
|
12956
|
-
include: ["src", "*.config.ts"]
|
|
12957
|
-
}, null, 2);
|
|
12958
|
-
}
|
|
12959
|
-
function biomeJson() {
|
|
12960
|
-
return JSON.stringify({
|
|
12961
|
-
$schema: `https://biomejs.dev/schemas/${BIOME}/schema.json`,
|
|
12962
|
-
vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
|
|
12963
|
-
files: { ignoreUnknown: true },
|
|
12964
|
-
formatter: { enabled: true, indentStyle: "tab", lineWidth: 80 },
|
|
12965
|
-
linter: { enabled: true, rules: { recommended: true } },
|
|
12966
|
-
javascript: { formatter: { quoteStyle: "double" } },
|
|
12967
|
-
assist: { actions: { source: { organizeImports: "on" } } }
|
|
12968
|
-
}, null, 2);
|
|
12969
|
-
}
|
|
12970
|
-
function componentsJson() {
|
|
12971
|
-
return JSON.stringify({
|
|
12972
|
-
$schema: "https://ui.shadcn.com/schema.json",
|
|
12973
|
-
style: "new-york",
|
|
12974
|
-
rsc: false,
|
|
12975
|
-
tsx: true,
|
|
12976
|
-
tailwind: {
|
|
12977
|
-
config: "tailwind.config.js",
|
|
12978
|
-
css: "src/styles/app.css",
|
|
12979
|
-
baseColor: "slate",
|
|
12980
|
-
cssVariables: true,
|
|
12981
|
-
prefix: ""
|
|
12982
|
-
},
|
|
12983
|
-
aliases: {
|
|
12984
|
-
components: "@/components",
|
|
12985
|
-
utils: "@/lib/utils",
|
|
12986
|
-
ui: "@/components/ui",
|
|
12987
|
-
lib: "@/lib",
|
|
12988
|
-
hooks: "@/hooks"
|
|
12989
|
-
},
|
|
12990
|
-
iconLibrary: "lucide"
|
|
12991
|
-
}, null, 2);
|
|
12992
|
-
}
|
|
12993
|
-
function uiButton() {
|
|
12994
|
-
return `import * as React from "react"
|
|
12995
|
-
import { Slot } from "@radix-ui/react-slot"
|
|
12996
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
12997
|
-
import { cn } from "@/lib/utils"
|
|
12998
|
-
|
|
12999
|
-
const buttonVariants = cva(
|
|
13000
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
|
|
13001
|
-
{
|
|
13002
|
-
variants: {
|
|
13003
|
-
variant: {
|
|
13004
|
-
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
13005
|
-
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
13006
|
-
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
13007
|
-
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
13008
|
-
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
13009
|
-
link: "text-primary underline-offset-4 hover:underline",
|
|
13010
|
-
},
|
|
13011
|
-
size: {
|
|
13012
|
-
default: "h-9 px-4 py-2",
|
|
13013
|
-
sm: "h-8 rounded-md px-3 text-xs",
|
|
13014
|
-
lg: "h-10 rounded-md px-8",
|
|
13015
|
-
icon: "h-9 w-9",
|
|
13016
|
-
},
|
|
13017
|
-
},
|
|
13018
|
-
defaultVariants: { variant: "default", size: "default" },
|
|
13019
|
-
},
|
|
13020
|
-
)
|
|
13021
|
-
|
|
13022
|
-
export interface ButtonProps
|
|
13023
|
-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
13024
|
-
VariantProps<typeof buttonVariants> {
|
|
13025
|
-
asChild?: boolean
|
|
13026
|
-
}
|
|
13027
|
-
|
|
13028
|
-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
13029
|
-
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
13030
|
-
const Comp = asChild ? Slot : "button"
|
|
13031
|
-
return (
|
|
13032
|
-
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
|
13033
|
-
)
|
|
13034
|
-
},
|
|
13035
|
-
)
|
|
13036
|
-
Button.displayName = "Button"
|
|
13037
|
-
|
|
13038
|
-
export { Button, buttonVariants }
|
|
13039
|
-
`;
|
|
13040
|
-
}
|
|
13041
|
-
function uiCard() {
|
|
13042
|
-
return `import * as React from "react"
|
|
13043
|
-
import { cn } from "@/lib/utils"
|
|
13044
|
-
|
|
13045
|
-
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
13046
|
-
({ className, ...props }, ref) => (
|
|
13047
|
-
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow", className)} {...props} />
|
|
13048
|
-
),
|
|
13049
|
-
)
|
|
13050
|
-
Card.displayName = "Card"
|
|
13051
|
-
|
|
13052
|
-
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
13053
|
-
({ className, ...props }, ref) => (
|
|
13054
|
-
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
13055
|
-
),
|
|
13056
|
-
)
|
|
13057
|
-
CardHeader.displayName = "CardHeader"
|
|
13058
|
-
|
|
13059
|
-
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
13060
|
-
({ className, ...props }, ref) => (
|
|
13061
|
-
<div ref={ref} className={cn("font-heading font-semibold leading-none tracking-tight", className)} {...props} />
|
|
13062
|
-
),
|
|
13063
|
-
)
|
|
13064
|
-
CardTitle.displayName = "CardTitle"
|
|
13065
|
-
|
|
13066
|
-
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
13067
|
-
({ className, ...props }, ref) => (
|
|
13068
|
-
<div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
13069
|
-
),
|
|
13070
|
-
)
|
|
13071
|
-
CardDescription.displayName = "CardDescription"
|
|
13072
|
-
|
|
13073
|
-
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
13074
|
-
({ className, ...props }, ref) => (
|
|
13075
|
-
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
13076
|
-
),
|
|
13077
|
-
)
|
|
13078
|
-
CardContent.displayName = "CardContent"
|
|
13079
|
-
|
|
13080
|
-
export { Card, CardHeader, CardTitle, CardDescription, CardContent }
|
|
13081
|
-
`;
|
|
13082
|
-
}
|
|
13083
|
-
function uiInput() {
|
|
13084
|
-
return `import * as React from "react"
|
|
13085
|
-
import { cn } from "@/lib/utils"
|
|
13086
|
-
|
|
13087
|
-
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
|
13088
|
-
({ className, type, ...props }, ref) => (
|
|
13089
|
-
<input
|
|
13090
|
-
type={type}
|
|
13091
|
-
ref={ref}
|
|
13092
|
-
className={cn(
|
|
13093
|
-
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
13094
|
-
className,
|
|
13095
|
-
)}
|
|
13096
|
-
{...props}
|
|
13097
|
-
/>
|
|
13098
|
-
),
|
|
13099
|
-
)
|
|
13100
|
-
Input.displayName = "Input"
|
|
13101
|
-
|
|
13102
|
-
export { Input }
|
|
13103
|
-
`;
|
|
13104
|
-
}
|
|
13105
|
-
function uiTextarea() {
|
|
13106
|
-
return `import * as React from "react"
|
|
13107
|
-
import { cn } from "@/lib/utils"
|
|
13108
|
-
|
|
13109
|
-
const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
|
|
13110
|
-
({ className, ...props }, ref) => (
|
|
13111
|
-
<textarea
|
|
13112
|
-
ref={ref}
|
|
13113
|
-
className={cn(
|
|
13114
|
-
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
13115
|
-
className,
|
|
13116
|
-
)}
|
|
13117
|
-
{...props}
|
|
13118
|
-
/>
|
|
13119
|
-
),
|
|
13120
|
-
)
|
|
13121
|
-
Textarea.displayName = "Textarea"
|
|
13122
|
-
|
|
13123
|
-
export { Textarea }
|
|
13124
|
-
`;
|
|
13125
|
-
}
|
|
13126
|
-
function uiLabel() {
|
|
13127
|
-
return `import * as React from "react"
|
|
13128
|
-
import { cn } from "@/lib/utils"
|
|
13129
|
-
|
|
13130
|
-
const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
|
|
13131
|
-
({ className, ...props }, ref) => (
|
|
13132
|
-
<label
|
|
13133
|
-
ref={ref}
|
|
13134
|
-
className={cn(
|
|
13135
|
-
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
13136
|
-
className,
|
|
13137
|
-
)}
|
|
13138
|
-
{...props}
|
|
13139
|
-
/>
|
|
13140
|
-
),
|
|
13141
|
-
)
|
|
13142
|
-
Label.displayName = "Label"
|
|
13143
|
-
|
|
13144
|
-
export { Label }
|
|
13145
|
-
`;
|
|
13146
|
-
}
|
|
13147
|
-
function uiBadge() {
|
|
13148
|
-
return `import * as React from "react"
|
|
13149
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
13150
|
-
import { cn } from "@/lib/utils"
|
|
13151
|
-
|
|
13152
|
-
const badgeVariants = cva(
|
|
13153
|
-
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
13154
|
-
{
|
|
13155
|
-
variants: {
|
|
13156
|
-
variant: {
|
|
13157
|
-
default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
|
13158
|
-
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
13159
|
-
destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
|
13160
|
-
outline: "text-foreground",
|
|
13161
|
-
},
|
|
13162
|
-
},
|
|
13163
|
-
defaultVariants: { variant: "default" },
|
|
13164
|
-
},
|
|
13165
|
-
)
|
|
13166
|
-
|
|
13167
|
-
export interface BadgeProps
|
|
13168
|
-
extends React.HTMLAttributes<HTMLDivElement>,
|
|
13169
|
-
VariantProps<typeof badgeVariants> {}
|
|
13170
|
-
|
|
13171
|
-
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
13172
|
-
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
13173
|
-
}
|
|
13174
|
-
|
|
13175
|
-
export { Badge, badgeVariants }
|
|
13176
|
-
`;
|
|
13177
|
-
}
|
|
13178
|
-
function spaPackageJson(name) {
|
|
13179
|
-
return JSON.stringify({
|
|
13180
|
-
name,
|
|
13181
|
-
private: true,
|
|
13182
|
-
version: "0.0.0",
|
|
13183
|
-
type: "module",
|
|
13184
|
-
scripts: {
|
|
13185
|
-
dev: "vite dev --host",
|
|
13186
|
-
build: "vite build",
|
|
13187
|
-
preview: "vite preview",
|
|
13188
|
-
lint: "biome check --write ."
|
|
13189
|
-
},
|
|
13190
|
-
dependencies: {
|
|
13191
|
-
react: REACT,
|
|
13192
|
-
"react-dom": REACT,
|
|
13193
|
-
"@tanstack/react-router": TANSTACK,
|
|
13194
|
-
"class-variance-authority": "^0.7.1",
|
|
13195
|
-
clsx: "^2.1.1",
|
|
13196
|
-
"tailwind-merge": "^2.6.0",
|
|
13197
|
-
"lucide-react": "^0.468.0",
|
|
13198
|
-
"@radix-ui/react-slot": "^1.1.1"
|
|
13199
|
-
},
|
|
13200
|
-
devDependencies: {
|
|
13201
|
-
"@tanstack/router-plugin": TANSTACK,
|
|
13202
|
-
"@types/react": "^19.0.0",
|
|
13203
|
-
"@types/react-dom": "^19.0.0",
|
|
13204
|
-
"@vitejs/plugin-react": "^4.3.4",
|
|
13205
|
-
"@biomejs/biome": BIOME,
|
|
13206
|
-
autoprefixer: "^10.4.20",
|
|
13207
|
-
postcss: "^8.4.49",
|
|
13208
|
-
tailwindcss: "^3.4.17",
|
|
13209
|
-
typescript: TS,
|
|
13210
|
-
vite: VITE
|
|
13211
|
-
}
|
|
13212
|
-
}, null, 2);
|
|
13213
|
-
}
|
|
13214
|
-
function spaViteConfig() {
|
|
13215
|
-
return `import { defineConfig } from "vite"
|
|
13216
|
-
import react from "@vitejs/plugin-react"
|
|
13217
|
-
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
|
|
13218
|
-
import path from "node:path"
|
|
13219
|
-
|
|
13220
|
-
export default defineConfig({
|
|
13221
|
-
// File-based routing: routes live in src/routes, routeTree.gen.ts is generated.
|
|
13222
|
-
plugins: [TanStackRouterVite({ target: "react", autoCodeSplitting: true }), react()],
|
|
13223
|
-
// allowedHosts:true is REQUIRED for the HelloLeo preview sandbox (proxied host).
|
|
13224
|
-
server: { allowedHosts: true },
|
|
13225
|
-
resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
|
|
13226
|
-
})
|
|
13227
|
-
`;
|
|
13228
|
-
}
|
|
13229
|
-
function spaIndexHtml(fontFamily) {
|
|
13230
|
-
const fontParam = fontFamily.replace(/\s+/g, "+");
|
|
13231
|
-
return `<!DOCTYPE html>
|
|
13232
|
-
<html lang="en">
|
|
13233
|
-
<head>
|
|
13234
|
-
<meta charset="UTF-8" />
|
|
13235
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
13236
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
13237
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
13238
|
-
<link href="https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
13239
|
-
<title>App</title>
|
|
13240
|
-
</head>
|
|
13241
|
-
<body>
|
|
13242
|
-
<div id="root"></div>
|
|
13243
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
13244
|
-
</body>
|
|
13245
|
-
</html>
|
|
13246
|
-
`;
|
|
13247
|
-
}
|
|
13248
|
-
function spaMainTsx() {
|
|
13249
|
-
return `import React from "react"
|
|
13250
|
-
import ReactDOM from "react-dom/client"
|
|
13251
|
-
import { RouterProvider, createRouter } from "@tanstack/react-router"
|
|
13252
|
-
import { routeTree } from "./routeTree.gen"
|
|
13253
|
-
import "./styles/app.css"
|
|
13254
|
-
|
|
13255
|
-
const router = createRouter({ routeTree, defaultPreload: "intent", scrollRestoration: true })
|
|
13256
|
-
|
|
13257
|
-
declare module "@tanstack/react-router" {
|
|
13258
|
-
interface Register {
|
|
13259
|
-
router: typeof router
|
|
13260
|
-
}
|
|
13261
|
-
}
|
|
13262
|
-
|
|
13263
|
-
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
13264
|
-
<React.StrictMode>
|
|
13265
|
-
<RouterProvider router={router} />
|
|
13266
|
-
</React.StrictMode>,
|
|
13267
|
-
)
|
|
13268
|
-
`;
|
|
13269
|
-
}
|
|
13270
|
-
function spaRootRoute() {
|
|
13271
|
-
return `import { Outlet, createRootRoute } from "@tanstack/react-router"
|
|
13272
|
-
|
|
13273
|
-
export const Route = createRootRoute({
|
|
13274
|
-
errorComponent: ({ error }) => (
|
|
13275
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-3 p-8">
|
|
13276
|
-
<h1 className="text-2xl font-heading font-semibold text-destructive">Something went wrong</h1>
|
|
13277
|
-
<pre className="max-w-full overflow-auto rounded-md bg-muted p-4 text-left text-xs text-muted-foreground">{error instanceof Error ? error.message : String(error)}</pre>
|
|
13278
|
-
</main>
|
|
13279
|
-
),
|
|
13280
|
-
notFoundComponent: () => (
|
|
13281
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-2 p-8">
|
|
13282
|
-
<h1 className="text-2xl font-heading font-semibold">404</h1>
|
|
13283
|
-
<p className="text-muted-foreground">This page could not be found.</p>
|
|
13284
|
-
</main>
|
|
13285
|
-
),
|
|
13286
|
-
component: () => (
|
|
13287
|
-
<div className="min-h-screen bg-background text-foreground">
|
|
13288
|
-
<Outlet />
|
|
13289
|
-
</div>
|
|
13290
|
-
),
|
|
13291
|
-
})
|
|
13292
|
-
`;
|
|
13293
|
-
}
|
|
13294
|
-
function spaIndexRoute() {
|
|
13295
|
-
return `import { createFileRoute } from "@tanstack/react-router"
|
|
13296
|
-
import { Button } from "@/components/ui/button"
|
|
13297
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
13298
|
-
|
|
13299
|
-
export const Route = createFileRoute("/")({ component: Home })
|
|
13300
|
-
|
|
13301
|
-
function Home() {
|
|
13302
|
-
return (
|
|
13303
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-6 p-8">
|
|
13304
|
-
<Card className="w-full">
|
|
13305
|
-
<CardHeader>
|
|
13306
|
-
<CardTitle className="text-2xl">Welcome to your app</CardTitle>
|
|
13307
|
-
<CardDescription>TanStack Router SPA \xB7 Tailwind \xB7 shadcn/ui \xB7 Biome</CardDescription>
|
|
13308
|
-
</CardHeader>
|
|
13309
|
-
<CardContent className="flex gap-3">
|
|
13310
|
-
<Button>Get started</Button>
|
|
13311
|
-
<Button variant="outline">Learn more</Button>
|
|
13312
|
-
</CardContent>
|
|
13313
|
-
</Card>
|
|
13314
|
-
</main>
|
|
13315
|
-
)
|
|
13316
|
-
}
|
|
13317
|
-
`;
|
|
13318
|
-
}
|
|
13319
|
-
function ssrPackageJson(name) {
|
|
13320
|
-
return JSON.stringify({
|
|
13321
|
-
name,
|
|
13322
|
-
private: true,
|
|
13323
|
-
version: "0.0.0",
|
|
13324
|
-
type: "module",
|
|
13325
|
-
scripts: {
|
|
13326
|
-
predev: "drizzle-kit push --force",
|
|
13327
|
-
dev: "vite dev --host",
|
|
13328
|
-
build: "vite build",
|
|
13329
|
-
prepreview: "drizzle-kit push --force",
|
|
13330
|
-
preview: "node .output/server/index.mjs",
|
|
13331
|
-
start: "node .output/server/index.mjs",
|
|
13332
|
-
"db:generate": "drizzle-kit generate",
|
|
13333
|
-
"db:push": "drizzle-kit push --force",
|
|
13334
|
-
deploy: "wrangler deploy",
|
|
13335
|
-
lint: "biome check --write ."
|
|
13336
|
-
},
|
|
13337
|
-
dependencies: {
|
|
13338
|
-
react: REACT,
|
|
13339
|
-
"react-dom": REACT,
|
|
13340
|
-
"@tanstack/react-router": TANSTACK,
|
|
13341
|
-
"@tanstack/react-start": TANSTACK,
|
|
13342
|
-
"drizzle-orm": "^0.36.4",
|
|
13343
|
-
"@libsql/client": "^0.14.0",
|
|
13344
|
-
"class-variance-authority": "^0.7.1",
|
|
13345
|
-
clsx: "^2.1.1",
|
|
13346
|
-
"tailwind-merge": "^2.6.0",
|
|
13347
|
-
"lucide-react": "^0.468.0",
|
|
13348
|
-
"@radix-ui/react-slot": "^1.1.1"
|
|
13349
|
-
},
|
|
13350
|
-
devDependencies: {
|
|
13351
|
-
wrangler: "^4.20.0",
|
|
13352
|
-
"@cloudflare/workers-types": "^4.20250601.0",
|
|
13353
|
-
"drizzle-kit": "^0.28.1",
|
|
13354
|
-
"@types/node": "^22.0.0",
|
|
13355
|
-
"@types/react": "^19.0.0",
|
|
13356
|
-
"@types/react-dom": "^19.0.0",
|
|
13357
|
-
"@vitejs/plugin-react": "^4.3.4",
|
|
13358
|
-
"@biomejs/biome": BIOME,
|
|
13359
|
-
autoprefixer: "^10.4.20",
|
|
13360
|
-
postcss: "^8.4.49",
|
|
13361
|
-
tailwindcss: "^3.4.17",
|
|
13362
|
-
typescript: TS,
|
|
13363
|
-
vite: VITE
|
|
13364
|
-
}
|
|
13365
|
-
}, null, 2);
|
|
13366
|
-
}
|
|
13367
|
-
function ssrViteConfig() {
|
|
13368
|
-
return `import { defineConfig } from "vite"
|
|
13369
|
-
import react from "@vitejs/plugin-react"
|
|
13370
|
-
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
|
|
13371
|
-
import path from "node:path"
|
|
13372
|
-
|
|
13373
|
-
// TanStack Start default (Node server) target -> dist/server/server.js + dist/client.
|
|
13374
|
-
// The CF Sandbox container runs that server (PORT-aware), proven by the POC.
|
|
13375
|
-
export default defineConfig({
|
|
13376
|
-
plugins: [tanstackStart({ customViteReactPlugin: true }), react()],
|
|
13377
|
-
// allowedHosts:true is REQUIRED for the HelloLeo preview sandbox (proxied host).
|
|
13378
|
-
server: { allowedHosts: true },
|
|
13379
|
-
// Pre-bundle shared deps at boot so vite doesn't re-optimize + restart the dev
|
|
13380
|
-
// server mid-request (which 500s the first SSR load with "transport disconnected").
|
|
13381
|
-
optimizeDeps: {
|
|
13382
|
-
include: ["clsx", "tailwind-merge", "class-variance-authority", "lucide-react"],
|
|
13383
|
-
},
|
|
13384
|
-
resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
|
|
13385
|
-
// __WORKERD__ tells src/db which runtime it's building for: true only for the
|
|
13386
|
-
// prod cloudflare-module build (NITRO_PRESET set by the publish flow), false
|
|
13387
|
-
// for dev and the Node preview build. Lets getDb dead-code-eliminate the wrong
|
|
13388
|
-
// DB driver per target.
|
|
13389
|
-
define: {
|
|
13390
|
-
__WORKERD__: JSON.stringify(process.env.NITRO_PRESET === "cloudflare-module"),
|
|
13391
|
-
},
|
|
13392
|
-
// "cloudflare:workers" is a workerd-only module (src/db reads the D1 binding
|
|
13393
|
-
// from it in prod). vite's SSR pass can't resolve it \u2014 externalize so the
|
|
13394
|
-
// workerd build keeps it as an import; on other targets it's DCE'd away.
|
|
13395
|
-
build: { rollupOptions: { external: ["cloudflare:workers"] } },
|
|
13396
|
-
})
|
|
13397
|
-
`;
|
|
13398
|
-
}
|
|
13399
|
-
function ssrWranglerJsonc(name) {
|
|
13400
|
-
return `{
|
|
13401
|
-
// PROD target (Workers for Platforms). Dev/preview runs the Node server in the
|
|
13402
|
-
// CF Sandbox container and does NOT use this file.
|
|
13403
|
-
//
|
|
13404
|
-
// NOTE: HelloLeo's publish flow REGENERATES this file at deploy time \u2014 it
|
|
13405
|
-
// auto-creates the D1 database, fills in database_id, and pins its own
|
|
13406
|
-
// compatibility_date. This file is the local-dev / manual \`wrangler deploy\`
|
|
13407
|
-
// copy; for a manual deploy, run \`wrangler d1 create ${name}\` and paste the
|
|
13408
|
-
// id below first. compatibility_date is kept at/under the runtime baseline so
|
|
13409
|
-
// \`wrangler dev\` works on the pinned workerd binary.
|
|
13410
|
-
"$schema": "node_modules/wrangler/config-schema.json",
|
|
13411
|
-
"name": "${name}",
|
|
13412
|
-
"compatibility_date": "2026-04-01",
|
|
13413
|
-
"compatibility_flags": ["nodejs_compat"],
|
|
13414
|
-
"observability": { "enabled": true },
|
|
13415
|
-
"assets": { "directory": ".output/public", "binding": "ASSETS" },
|
|
13416
|
-
"d1_databases": [
|
|
13417
|
-
{ "binding": "DB", "database_name": "${name}", "database_id": "" }
|
|
13418
|
-
]
|
|
13419
|
-
}
|
|
13420
|
-
`;
|
|
13421
|
-
}
|
|
13422
|
-
function ssrRouter() {
|
|
13423
|
-
return `import { createRouter as createTanStackRouter } from "@tanstack/react-router"
|
|
13424
|
-
import { routeTree } from "./routeTree.gen"
|
|
13425
|
-
|
|
13426
|
-
export function createRouter() {
|
|
13427
|
-
return createTanStackRouter({
|
|
13428
|
-
routeTree,
|
|
13429
|
-
defaultPreload: "intent",
|
|
13430
|
-
scrollRestoration: true,
|
|
13431
|
-
})
|
|
13432
|
-
}
|
|
13433
|
-
|
|
13434
|
-
declare module "@tanstack/react-router" {
|
|
13435
|
-
interface Register {
|
|
13436
|
-
router: ReturnType<typeof createRouter>
|
|
13437
|
-
}
|
|
13438
|
-
}
|
|
13439
|
-
`;
|
|
13440
|
-
}
|
|
13441
|
-
function ssrRootRoute(fontFamily) {
|
|
13442
|
-
const fontParam = fontFamily.replace(/\s+/g, "+");
|
|
13443
|
-
return `import { Outlet, createRootRoute, HeadContent, Scripts } from "@tanstack/react-router"
|
|
13444
|
-
import appCss from "../styles/app.css?url"
|
|
13445
|
-
|
|
13446
|
-
export const Route = createRootRoute({
|
|
13447
|
-
head: () => ({
|
|
13448
|
-
meta: [
|
|
13449
|
-
{ charSet: "utf-8" },
|
|
13450
|
-
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
13451
|
-
{ title: "App" },
|
|
13452
|
-
],
|
|
13453
|
-
// The design-system font is loaded here (SSR has no index.html). Keep the
|
|
13454
|
-
// family in sync with --font-sans / --font-heading in src/styles/app.css.
|
|
13455
|
-
links: [
|
|
13456
|
-
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
|
13457
|
-
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
|
|
13458
|
-
{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" },
|
|
13459
|
-
{ rel: "stylesheet", href: appCss },
|
|
13460
|
-
],
|
|
13461
|
-
}),
|
|
13462
|
-
errorComponent: ({ error }) => (
|
|
13463
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-3 p-8">
|
|
13464
|
-
<h1 className="text-2xl font-heading font-semibold text-destructive">Something went wrong</h1>
|
|
13465
|
-
<pre className="max-w-full overflow-auto rounded-md bg-muted p-4 text-left text-xs text-muted-foreground">{error instanceof Error ? error.message : String(error)}</pre>
|
|
13466
|
-
</main>
|
|
13467
|
-
),
|
|
13468
|
-
notFoundComponent: () => (
|
|
13469
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-2 p-8">
|
|
13470
|
-
<h1 className="text-2xl font-heading font-semibold">404</h1>
|
|
13471
|
-
<p className="text-muted-foreground">This page could not be found.</p>
|
|
13472
|
-
</main>
|
|
13473
|
-
),
|
|
13474
|
-
component: RootComponent,
|
|
13475
|
-
})
|
|
13476
|
-
|
|
13477
|
-
function RootComponent() {
|
|
13478
|
-
return (
|
|
13479
|
-
<html lang="en">
|
|
13480
|
-
<head>
|
|
13481
|
-
<HeadContent />
|
|
13482
|
-
</head>
|
|
13483
|
-
<body className="min-h-screen bg-background text-foreground font-sans">
|
|
13484
|
-
<Outlet />
|
|
13485
|
-
<Scripts />
|
|
13486
|
-
</body>
|
|
13487
|
-
</html>
|
|
13488
|
-
)
|
|
13489
|
-
}
|
|
13490
|
-
`;
|
|
13491
|
-
}
|
|
13492
|
-
function ssrIndexRoute() {
|
|
13493
|
-
return `import { createFileRoute } from "@tanstack/react-router"
|
|
13494
|
-
import { createServerFn } from "@tanstack/react-start"
|
|
13495
|
-
import { Button } from "@/components/ui/button"
|
|
13496
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
13497
|
-
|
|
13498
|
-
// Server functions (TanStack Start 1.131.x API \u2014 IMPORTANT, do not invent
|
|
13499
|
-
// methods):
|
|
13500
|
-
// - No input: createServerFn({ method: "GET" }).handler(async () => {...})
|
|
13501
|
-
// - With input: createServerFn({ method: "POST" })
|
|
13502
|
-
// .validator((d: { title: string }) => d) // NOT .inputType()
|
|
13503
|
-
// .handler(async ({ data }) => { data.title }) // input is { data }
|
|
13504
|
-
// Call it as: addTask({ data: { title } }) // NOT addTask({ title })
|
|
13505
|
-
// - DB access: \`const db = await getDb()\` (async). Resolves D1 in prod,
|
|
13506
|
-
// libsql in dev. Safe in server fns AND route loaders \u2014 just
|
|
13507
|
-
// never at module top. Prefer server functions for mutations.
|
|
13508
|
-
// Example DB-backed server fn (dev=libsql, prod=D1, same code):
|
|
13509
|
-
// const getItems = createServerFn({ method: "GET" }).handler(async () => {
|
|
13510
|
-
// const db = await getDb()
|
|
13511
|
-
// return await db.select().from(items)
|
|
13512
|
-
// })
|
|
13513
|
-
const getServerInfo = createServerFn({ method: "GET" }).handler(async () => {
|
|
13514
|
-
return { renderedAt: new Date().toISOString() }
|
|
13515
|
-
})
|
|
13516
|
-
|
|
13517
|
-
export const Route = createFileRoute("/")({
|
|
13518
|
-
loader: () => getServerInfo(),
|
|
13519
|
-
component: Home,
|
|
13520
|
-
})
|
|
13521
|
-
|
|
13522
|
-
function Home() {
|
|
13523
|
-
const info = Route.useLoaderData()
|
|
13524
|
-
return (
|
|
13525
|
-
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-6 p-8">
|
|
13526
|
-
<Card className="w-full">
|
|
13527
|
-
<CardHeader>
|
|
13528
|
-
<CardTitle className="text-2xl">Welcome to your app</CardTitle>
|
|
13529
|
-
<CardDescription>TanStack Start SSR (workerd) \xB7 Tailwind \xB7 shadcn/ui \xB7 Biome \xB7 Drizzle/D1</CardDescription>
|
|
13530
|
-
</CardHeader>
|
|
13531
|
-
<CardContent className="flex flex-col gap-3">
|
|
13532
|
-
<p className="text-sm text-muted-foreground">Server-rendered at {info.renderedAt}</p>
|
|
13533
|
-
<div className="flex gap-3">
|
|
13534
|
-
<Button>Get started</Button>
|
|
13535
|
-
<Button variant="outline">Learn more</Button>
|
|
13536
|
-
</div>
|
|
13537
|
-
</CardContent>
|
|
13538
|
-
</Card>
|
|
13539
|
-
</main>
|
|
13540
|
-
)
|
|
13541
|
-
}
|
|
13542
|
-
`;
|
|
13543
|
-
}
|
|
13544
|
-
function ssrDbSchema() {
|
|
13545
|
-
return `import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
|
13546
|
-
|
|
13547
|
-
// Example table. dev uses local D1 (miniflare), prod uses the D1 binding "DB".
|
|
13548
|
-
export const items = sqliteTable("items", {
|
|
13549
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
13550
|
-
title: text("title").notNull(),
|
|
13551
|
-
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
|
13552
|
-
})
|
|
13553
|
-
`;
|
|
13554
|
-
}
|
|
13555
|
-
function ssrDbIndex() {
|
|
13556
|
-
return `/// <reference types="@cloudflare/workers-types" />
|
|
13557
|
-
import { drizzle as drizzleD1 } from "drizzle-orm/d1"
|
|
13558
|
-
import * as schema from "./schema"
|
|
13559
|
-
|
|
13560
|
-
// Resolve the DB for the CURRENT request. ASYNC \u2014 always \`await getDb()\`.
|
|
13561
|
-
// Works from ANYWHERE on the server (route loaders, server functions,
|
|
13562
|
-
// beforeLoad) because it does NOT depend on AsyncLocalStorage / getEvent (which
|
|
13563
|
-
// is only populated inside server functions under workerd \u2014 calling getDb in a
|
|
13564
|
-
// loader the old way threw "No HTTPEvent found in AsyncLocalStorage").
|
|
13565
|
-
// - PROD (workerd): the D1 binding read straight from the per-request env.
|
|
13566
|
-
// - DEV (node SSR): a local libsql file, imported lazily and behind the DEV
|
|
13567
|
-
// guard so neither @libsql/client nor node:stream is ever bundled into the
|
|
13568
|
-
// workerd build (which would break it).
|
|
13569
|
-
// Same sqlite schema both ways.
|
|
13570
|
-
// __WORKERD__ is replaced at build time by vite (see vite.config.ts): true only
|
|
13571
|
-
// for the prod cloudflare-module build, false for dev + the Node preview build.
|
|
13572
|
-
// Using it (not import.meta.env.DEV) is what lets the Node preview \u2014 also a
|
|
13573
|
-
// production build \u2014 still take the libsql path. It's a literal, so the unused
|
|
13574
|
-
// branch (and its driver) is dead-code-eliminated from each bundle.
|
|
13575
|
-
declare const __WORKERD__: boolean
|
|
13576
|
-
|
|
13577
|
-
export async function getDb() {
|
|
13578
|
-
if (__WORKERD__) {
|
|
13579
|
-
const { env } = await import("cloudflare:workers")
|
|
13580
|
-
return drizzleD1((env as unknown as { DB: D1Database }).DB, { schema })
|
|
13581
|
-
}
|
|
13582
|
-
const { drizzle } = await import("drizzle-orm/libsql")
|
|
13583
|
-
const { createClient } = await import("@libsql/client")
|
|
13584
|
-
return drizzle(
|
|
13585
|
-
createClient({ url: process.env.DATABASE_URL ?? "file:./local.db" }),
|
|
13586
|
-
{ schema },
|
|
13587
|
-
)
|
|
13588
|
-
}
|
|
13589
|
-
`;
|
|
13590
|
-
}
|
|
13591
|
-
function ssrDrizzleConfig() {
|
|
13592
|
-
return `import { defineConfig } from "drizzle-kit"
|
|
13593
|
-
|
|
13594
|
-
// Dev/preview: push the schema straight into the local libsql file (no D1 creds
|
|
13595
|
-
// needed) \u2014 this is what \`predev\` runs so tables exist before the dev server.
|
|
13596
|
-
// Prod (D1) uses generated migrations applied via wrangler, not this push.
|
|
13597
|
-
export default defineConfig({
|
|
13598
|
-
dialect: "sqlite",
|
|
13599
|
-
schema: "./src/db/schema.ts",
|
|
13600
|
-
out: "./drizzle",
|
|
13601
|
-
dbCredentials: { url: process.env.DATABASE_URL ?? "file:./local.db" },
|
|
13602
|
-
})
|
|
13603
|
-
`;
|
|
13604
|
-
}
|
|
13605
|
-
function gitignore(mode) {
|
|
13606
|
-
const base = `node_modules
|
|
13607
|
-
dist
|
|
13608
|
-
.output
|
|
13609
|
-
.nitro
|
|
13610
|
-
.tanstack
|
|
13611
|
-
.vite
|
|
13612
|
-
local.db
|
|
13613
|
-
*.local
|
|
13614
|
-
.DS_Store
|
|
13615
|
-
|
|
13616
|
-
# Generated by the TanStack Router/Start vite plugin
|
|
13617
|
-
src/routeTree.gen.ts
|
|
13618
|
-
`;
|
|
13619
|
-
return mode === "ssr" ? `${base}.wrangler
|
|
13620
|
-
.dev.vars
|
|
13621
|
-
` : base;
|
|
13622
|
-
}
|
|
13623
|
-
async function writeAppScaffold(opts, dir) {
|
|
13624
|
-
const name = opts.name ?? "leo-app";
|
|
13625
|
-
const mode = opts.mode ?? "spa";
|
|
13626
|
-
const primary = normalizePrimaryColor(opts.primaryColor ?? "221.2 83.2% 53.3%");
|
|
13627
|
-
const font = opts.fontFamily ?? "Inter";
|
|
13628
|
-
const radius = opts.borderRadius ?? "0.5rem";
|
|
13629
|
-
await mkdir(path2.join(dir, "src", "lib"), { recursive: true });
|
|
13630
|
-
await mkdir(path2.join(dir, "src", "components", "ui"), { recursive: true });
|
|
13631
|
-
await mkdir(path2.join(dir, "src", "routes"), { recursive: true });
|
|
13632
|
-
await mkdir(path2.join(dir, "src", "styles"), { recursive: true });
|
|
13633
|
-
const files = [
|
|
13634
|
-
["tsconfig.json", tsconfig()],
|
|
13635
|
-
["biome.json", biomeJson()],
|
|
13636
|
-
["components.json", componentsJson()],
|
|
13637
|
-
["tailwind.config.js", tailwindConfig()],
|
|
13638
|
-
["postcss.config.js", postcssConfig()],
|
|
13639
|
-
[".gitignore", gitignore(mode)],
|
|
13640
|
-
[path2.join("src", "styles", "app.css"), indexCss(primary, font, radius)],
|
|
13641
|
-
[path2.join("src", "lib", "utils.ts"), utilsTs()],
|
|
13642
|
-
[path2.join("src", "lib", "proxy.ts"), proxyTs()],
|
|
13643
|
-
[path2.join("src", "components", "ui", "button.tsx"), uiButton()],
|
|
13644
|
-
[path2.join("src", "components", "ui", "card.tsx"), uiCard()],
|
|
13645
|
-
[path2.join("src", "components", "ui", "input.tsx"), uiInput()],
|
|
13646
|
-
[path2.join("src", "components", "ui", "textarea.tsx"), uiTextarea()],
|
|
13647
|
-
[path2.join("src", "components", "ui", "label.tsx"), uiLabel()],
|
|
13648
|
-
[path2.join("src", "components", "ui", "badge.tsx"), uiBadge()]
|
|
13649
|
-
];
|
|
13650
|
-
if (mode === "spa") {
|
|
13651
|
-
files.push(["package.json", spaPackageJson(name)], ["vite.config.ts", spaViteConfig()], ["index.html", spaIndexHtml(font)], [path2.join("src", "main.tsx"), spaMainTsx()], [path2.join("src", "routes", "__root.tsx"), spaRootRoute()], [path2.join("src", "routes", "index.tsx"), spaIndexRoute()]);
|
|
13652
|
-
} else {
|
|
13653
|
-
await mkdir(path2.join(dir, "src", "db"), { recursive: true });
|
|
13654
|
-
files.push(["package.json", ssrPackageJson(name)], ["vite.config.ts", ssrViteConfig()], ["wrangler.jsonc", ssrWranglerJsonc(name)], ["drizzle.config.ts", ssrDrizzleConfig()], [path2.join("src", "router.tsx"), ssrRouter()], [path2.join("src", "routes", "__root.tsx"), ssrRootRoute(font)], [path2.join("src", "routes", "index.tsx"), ssrIndexRoute()], [path2.join("src", "db", "schema.ts"), ssrDbSchema()], [path2.join("src", "db", "index.ts"), ssrDbIndex()]);
|
|
13655
|
-
}
|
|
13656
|
-
const created = [];
|
|
13657
|
-
for (const [filePath, content] of files) {
|
|
13658
|
-
await Bun.write(path2.join(dir, filePath), content);
|
|
13659
|
-
created.push(filePath);
|
|
13660
|
-
}
|
|
13661
|
-
return created.sort();
|
|
13662
|
-
}
|
|
13663
|
-
var scaffoldApp = tool({
|
|
13664
|
-
description: "Scaffold a modern TanStack app with the HelloLeo design system, ShadcnUI, " + "Biome and strict TypeScript. Two modes: mode='spa' (TanStack Router, " + "client-only -- use for static frontends / dashboards) or mode='ssr' " + "(TanStack Start on Cloudflare workerd -- use when the project needs a " + "backend, SSR, server functions, or a database via D1/Drizzle). " + "Creates package.json, vite.config.ts, tsconfig.json (strict), biome.json, " + "components.json, tailwind.config.js, postcss.config.js, " + "src/styles/app.css (design tokens, light+dark), src/lib/utils.ts + proxy.ts, " + "shadcn primitives in src/components/ui (Button, Card, Input, Textarea, " + "Label, Badge), and TanStack routes in src/routes. " + "Call this FIRST for any new project before writing feature routes/components. " + "For NetSuite projects use `setup_netsuite` instead.",
|
|
12575
|
+
const created = await renderTemplate(SCAFFOLD_VERSION, { LEO_NAME: name, LEO_PROJECT_UUID: projectUuidFromDir(dir) }, dir);
|
|
12576
|
+
await mkdir(path3.join(dir, ".leo"), { recursive: true });
|
|
12577
|
+
const leoPath = path3.join(".leo", "project.json");
|
|
12578
|
+
await Bun.write(path3.join(dir, leoPath), JSON.stringify({ $schema: 1, scaffold: SCAFFOLD_VERSION }, null, 2));
|
|
12579
|
+
return [...created, leoPath];
|
|
12580
|
+
}
|
|
12581
|
+
var scaffold = tool({
|
|
12582
|
+
description: "Scaffold a new React 19 + Vite + Tailwind CSS v4 + TanStack Router project " + "with shadcn/ui baked in. Renders the full template: TanStack file-based " + "routing (src/routes/), the complete shadcn/ui component set under " + "src/components/ui, src/lib/utils.ts (cn helper), src/lib/proxy.ts " + "(HelloLeo integration proxy), src/styles.css (shadcn design tokens, " + "light+dark), plus package.json, vite.config.ts, tsconfig.json, " + "tsr.config.json, components.json, .gitignore and the generated " + "src/routeTree.gen.ts. Call this FIRST for any new project before adding " + "routes and pages. For NetSuite projects (NetSuite MCP in the live list), " + "call `setup_netsuite` INSTEAD -- it bundles base scaffold + OAuth wiring " + "in one call. Do not call both.",
|
|
13665
12583
|
args: {
|
|
13666
|
-
name: tool.schema.string().optional().describe("Project name for package.json (default: leo-
|
|
13667
|
-
mode: tool.schema.enum(["spa", "ssr"]).optional().describe("'spa' = TanStack Router client-only (default). 'ssr' = TanStack Start " + "on Cloudflare workerd with SSR + D1/Drizzle. Pick 'ssr' when the app " + "needs a backend, server functions, auth, or a database."),
|
|
13668
|
-
primaryColor: tool.schema.string().optional().describe('Primary brand color as raw HSL triplet, e.g. "221.2 83.2% 53.3%". ' + 'Hex like "#3b82f6" is auto-converted. Default: blue.'),
|
|
13669
|
-
fontFamily: tool.schema.string().optional().describe('Google Font family, e.g. "Inter", "Poppins" (default: Inter)'),
|
|
13670
|
-
borderRadius: tool.schema.string().optional().describe('Base border radius, e.g. "0.5rem" (default: 0.5rem)')
|
|
12584
|
+
name: tool.schema.string().optional().describe("Project name for package.json (default: leo-project)")
|
|
13671
12585
|
},
|
|
13672
12586
|
async execute(args, context) {
|
|
13673
|
-
const
|
|
13674
|
-
const created = await writeAppScaffold(args, context.directory);
|
|
13675
|
-
const next = mode === "ssr" ? [
|
|
13676
|
-
"Mode: SSR (TanStack Start, Node server \u2014 runs in the CF Sandbox container).",
|
|
13677
|
-
"Dev server: `npm run dev`.",
|
|
13678
|
-
"Add routes in src/routes/*.tsx; server logic via createServerFn.",
|
|
13679
|
-
"Database: edit src/db/schema.ts, `npm run db:push`, query via `db` from src/db.",
|
|
13680
|
-
" (dev = local libsql file; prod = D1, see wrangler.jsonc)."
|
|
13681
|
-
] : [
|
|
13682
|
-
"Mode: SPA (TanStack Router, client-only).",
|
|
13683
|
-
"Dev server: `npm run dev`.",
|
|
13684
|
-
"Add routes in src/routes/*.tsx (file-based; routeTree.gen.ts is generated)."
|
|
13685
|
-
];
|
|
12587
|
+
const created = await writeBaseScaffold(args, context.directory);
|
|
13686
12588
|
return [
|
|
13687
|
-
`Scaffolded ${created.length} files
|
|
12589
|
+
`Scaffolded ${created.length} files:`,
|
|
13688
12590
|
...created.map((f) => ` - ${f}`),
|
|
13689
12591
|
"",
|
|
13690
|
-
|
|
13691
|
-
"",
|
|
13692
|
-
"
|
|
13693
|
-
"
|
|
13694
|
-
"
|
|
13695
|
-
"
|
|
13696
|
-
"
|
|
13697
|
-
"
|
|
12592
|
+
"Next: add routes as files under src/routes/ (TanStack file-based",
|
|
12593
|
+
" routing). Edit src/routes/index.tsx for the home page.",
|
|
12594
|
+
"UI: shadcn/ui components are already in src/components/ui -- import and",
|
|
12595
|
+
" use them; do not attempt to re-add via the shadcn CLI.",
|
|
12596
|
+
"Design system: edit tokens in src/styles.css (light + dark blocks).",
|
|
12597
|
+
"Data fetching: React Query is wired (QueryClient on the router context +",
|
|
12598
|
+
" QueryClientProvider). ALWAYS fetch with @tanstack/react-query --",
|
|
12599
|
+
" useQuery / useMutation in components, or a route loader calling",
|
|
12600
|
+
" context.queryClient.ensureQueryData(). Do NOT hand-roll fetches with",
|
|
12601
|
+
" useEffect + useState. Wrap the fetch call (usually callIntegration())",
|
|
12602
|
+
" in the queryFn.",
|
|
12603
|
+
"Integration calls: use callIntegration() from src/lib/proxy.ts -- as the",
|
|
12604
|
+
" queryFn body, never on its own in useEffect. Its header comment",
|
|
12605
|
+
" documents slugs, response unwrapping, pagination, and frontend-only vs",
|
|
12606
|
+
" HelloLeo Cloud routing."
|
|
13698
12607
|
].join(`
|
|
13699
12608
|
`);
|
|
13700
12609
|
}
|
|
13701
12610
|
});
|
|
13702
12611
|
var ScaffoldPlugin = async () => {
|
|
13703
12612
|
return {
|
|
13704
|
-
tool: { scaffold
|
|
12613
|
+
tool: { scaffold }
|
|
13705
12614
|
};
|
|
13706
12615
|
};
|
|
13707
12616
|
// src/tools/setup-netsuite.ts
|
|
13708
12617
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
13709
|
-
import
|
|
12618
|
+
import path4 from "path";
|
|
13710
12619
|
function netsuiteTs() {
|
|
13711
12620
|
return `/*
|
|
13712
12621
|
* NetSuite OAuth client helper (bearer-token-in-fragment).
|
|
@@ -13897,34 +12806,119 @@ export function LoginPage() {
|
|
|
13897
12806
|
}
|
|
13898
12807
|
`;
|
|
13899
12808
|
}
|
|
13900
|
-
function
|
|
13901
|
-
return `import
|
|
12809
|
+
function authTsx() {
|
|
12810
|
+
return `import * as React from 'react';
|
|
12811
|
+
import {
|
|
12812
|
+
getNetSuiteSession,
|
|
12813
|
+
signInWithNetSuite,
|
|
12814
|
+
signOutNetSuite,
|
|
12815
|
+
type NetSuiteSession,
|
|
12816
|
+
} from './netsuite';
|
|
12817
|
+
|
|
12818
|
+
/**
|
|
12819
|
+
* NetSuite auth context. Holds the current session (resolved from the Worker's
|
|
12820
|
+
* /api/me via getNetSuiteSession) and exposes sign-in / sign-out. The only
|
|
12821
|
+
* authoritative auth check is /api/me -- never trust sessionStorage directly.
|
|
12822
|
+
*/
|
|
12823
|
+
export interface NetSuiteAuthContextValue {
|
|
12824
|
+
pending: boolean;
|
|
12825
|
+
isAuthenticated: boolean;
|
|
12826
|
+
session: NetSuiteSession | null;
|
|
12827
|
+
signIn: () => void;
|
|
12828
|
+
signOut: () => Promise<void>;
|
|
12829
|
+
refresh: () => Promise<void>;
|
|
12830
|
+
}
|
|
12831
|
+
|
|
12832
|
+
const NetSuiteAuthContext =
|
|
12833
|
+
React.createContext<NetSuiteAuthContextValue | null>(null);
|
|
12834
|
+
|
|
12835
|
+
export function NetSuiteAuthProvider({
|
|
12836
|
+
children,
|
|
12837
|
+
}: {
|
|
12838
|
+
children: React.ReactNode;
|
|
12839
|
+
}) {
|
|
12840
|
+
const [session, setSession] = React.useState<NetSuiteSession | null>(null);
|
|
12841
|
+
const [pending, setPending] = React.useState(true);
|
|
12842
|
+
|
|
12843
|
+
const refresh = React.useCallback(async () => {
|
|
12844
|
+
setPending(true);
|
|
12845
|
+
try {
|
|
12846
|
+
setSession(await getNetSuiteSession());
|
|
12847
|
+
} finally {
|
|
12848
|
+
setPending(false);
|
|
12849
|
+
}
|
|
12850
|
+
}, []);
|
|
12851
|
+
|
|
12852
|
+
React.useEffect(() => {
|
|
12853
|
+
refresh();
|
|
12854
|
+
}, [refresh]);
|
|
12855
|
+
|
|
12856
|
+
const value: NetSuiteAuthContextValue = {
|
|
12857
|
+
pending,
|
|
12858
|
+
isAuthenticated: Boolean(session?.authenticated),
|
|
12859
|
+
session,
|
|
12860
|
+
signIn: () => signInWithNetSuite(),
|
|
12861
|
+
signOut: signOutNetSuite,
|
|
12862
|
+
refresh,
|
|
12863
|
+
};
|
|
12864
|
+
|
|
12865
|
+
return (
|
|
12866
|
+
<NetSuiteAuthContext.Provider value={value}>
|
|
12867
|
+
{children}
|
|
12868
|
+
</NetSuiteAuthContext.Provider>
|
|
12869
|
+
);
|
|
12870
|
+
}
|
|
12871
|
+
|
|
12872
|
+
export function useNetSuiteAuth(): NetSuiteAuthContextValue {
|
|
12873
|
+
const ctx = React.useContext(NetSuiteAuthContext);
|
|
12874
|
+
if (!ctx) {
|
|
12875
|
+
throw new Error('useNetSuiteAuth must be used within <NetSuiteAuthProvider>');
|
|
12876
|
+
}
|
|
12877
|
+
return ctx;
|
|
12878
|
+
}
|
|
12879
|
+
`;
|
|
12880
|
+
}
|
|
12881
|
+
function authLayoutTsx() {
|
|
12882
|
+
return `import type { ReactNode } from 'react';
|
|
12883
|
+
import { NetSuiteAuthProvider, useNetSuiteAuth } from '../../lib/auth';
|
|
13902
12884
|
import { LoginPage } from './LoginPage';
|
|
13903
|
-
import { getNetSuiteSession, type NetSuiteSession } from '../../lib/netsuite';
|
|
13904
12885
|
|
|
13905
12886
|
/**
|
|
13906
|
-
*
|
|
13907
|
-
*
|
|
13908
|
-
* is the ONLY supported authentication for a NetSuite project (no password
|
|
13909
|
-
* gates, no sessionStorage flags, no shared credential).
|
|
13910
|
-
*
|
|
13911
|
-
* Pattern in src/App.tsx:
|
|
12887
|
+
* NetSuite auth layout. This is the ONLY wiring you need to add by hand --
|
|
12888
|
+
* wrap the root route's <Outlet /> with it:
|
|
13912
12889
|
*
|
|
13913
|
-
*
|
|
13914
|
-
*
|
|
12890
|
+
* // src/routes/__root.tsx
|
|
12891
|
+
* import { AuthLayout } from '../components/auth/AuthLayout';
|
|
12892
|
+
* ...
|
|
12893
|
+
* function RootComponent() {
|
|
13915
12894
|
* return (
|
|
13916
|
-
*
|
|
13917
|
-
* <
|
|
13918
|
-
*
|
|
12895
|
+
* <>
|
|
12896
|
+
* <AuthLayout>
|
|
12897
|
+
* <Outlet />
|
|
12898
|
+
* </AuthLayout>
|
|
12899
|
+
* { ...devtools... }
|
|
12900
|
+
* </>
|
|
13919
12901
|
* );
|
|
13920
12902
|
* }
|
|
12903
|
+
*
|
|
12904
|
+
* It provides NetSuite auth context to the whole tree and gates rendering:
|
|
12905
|
+
* while the session resolves it shows a spinner, unauthenticated users see
|
|
12906
|
+
* <LoginPage />, authenticated users see your routes. Use useNetSuiteAuth()
|
|
12907
|
+
* anywhere below it to read the session or sign out. NEVER swap this for a
|
|
12908
|
+
* password gate -- real OAuth is the only supported login.
|
|
13921
12909
|
*/
|
|
13922
|
-
export function
|
|
13923
|
-
|
|
12910
|
+
export function AuthLayout({ children }: { children: ReactNode }) {
|
|
12911
|
+
return (
|
|
12912
|
+
<NetSuiteAuthProvider>
|
|
12913
|
+
<Gate>{children}</Gate>
|
|
12914
|
+
</NetSuiteAuthProvider>
|
|
12915
|
+
);
|
|
12916
|
+
}
|
|
13924
12917
|
|
|
13925
|
-
|
|
12918
|
+
function Gate({ children }: { children: ReactNode }) {
|
|
12919
|
+
const { pending, isAuthenticated } = useNetSuiteAuth();
|
|
13926
12920
|
|
|
13927
|
-
if (
|
|
12921
|
+
if (pending) {
|
|
13928
12922
|
return (
|
|
13929
12923
|
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
13930
12924
|
<div className="h-8 w-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
|
|
@@ -13932,90 +12926,79 @@ export function AuthGate({ children }: { children: React.ReactNode }) {
|
|
|
13932
12926
|
);
|
|
13933
12927
|
}
|
|
13934
12928
|
|
|
13935
|
-
if (!
|
|
12929
|
+
if (!isAuthenticated) return <LoginPage />;
|
|
13936
12930
|
|
|
13937
12931
|
return <>{children}</>;
|
|
13938
12932
|
}
|
|
13939
12933
|
`;
|
|
13940
12934
|
}
|
|
13941
|
-
function netsuiteAppTsx() {
|
|
13942
|
-
return `import { AuthGate } from './components/auth/AuthGate';
|
|
13943
|
-
|
|
13944
|
-
/**
|
|
13945
|
-
* NetSuite project entry point. KEEP <AuthGate> as the outermost element --
|
|
13946
|
-
* removing it disables real OAuth and exposes NetSuite data to anonymous
|
|
13947
|
-
* users. Build your dashboard / pages inside the gate.
|
|
13948
|
-
*/
|
|
13949
|
-
export default function App() {
|
|
13950
|
-
return (
|
|
13951
|
-
<AuthGate>
|
|
13952
|
-
<div className="min-h-screen bg-background text-foreground p-8">
|
|
13953
|
-
<h1 className="text-2xl font-bold">You are signed in to NetSuite.</h1>
|
|
13954
|
-
<p className="text-muted-foreground mt-2">
|
|
13955
|
-
Replace this placeholder with your dashboard.
|
|
13956
|
-
</p>
|
|
13957
|
-
</div>
|
|
13958
|
-
</AuthGate>
|
|
13959
|
-
);
|
|
13960
|
-
}
|
|
13961
|
-
`;
|
|
13962
|
-
}
|
|
13963
12935
|
var setupNetsuite = tool({
|
|
13964
|
-
description: "Bootstrap a NetSuite-backed React + Vite + Tailwind project in one call.
|
|
12936
|
+
description: "Bootstrap a NetSuite-backed React + Vite + Tailwind v4 + TanStack Router " + "project in one call. Writes the full HelloLeo base scaffold PLUS the " + "NetSuite OAuth wiring, all ADDITIVE -- it never overwrites a scaffold " + "file. Adds src/lib/netsuite.ts (OAuth client), src/lib/auth.tsx " + "(NetSuiteAuthProvider + useNetSuiteAuth), and " + "src/components/auth/{AuthLayout,LoginPage,SignInWithNetSuite}.tsx. " + "Use this INSTEAD of `scaffold` for any project that needs NetSuite. " + "After it runs there is exactly ONE manual step: wrap the root route's " + "<Outlet /> with <AuthLayout> in src/routes/__root.tsx (the tool's output " + "shows the snippet). Real OAuth is wired from day one -- never replace " + "<AuthLayout> with a password placeholder.",
|
|
13965
12937
|
args: {},
|
|
13966
12938
|
async execute(_args, context) {
|
|
13967
12939
|
const dir = context.directory;
|
|
13968
12940
|
const base = await writeBaseScaffold({}, dir);
|
|
13969
|
-
await mkdir2(
|
|
12941
|
+
await mkdir2(path4.join(dir, "src", "components", "auth"), {
|
|
13970
12942
|
recursive: true
|
|
13971
12943
|
});
|
|
13972
12944
|
const nsFiles = [
|
|
13973
|
-
[
|
|
12945
|
+
[path4.join("src", "lib", "netsuite.ts"), netsuiteTs()],
|
|
12946
|
+
[path4.join("src", "lib", "auth.tsx"), authTsx()],
|
|
13974
12947
|
[
|
|
13975
|
-
|
|
13976
|
-
|
|
12948
|
+
path4.join("src", "components", "auth", "AuthLayout.tsx"),
|
|
12949
|
+
authLayoutTsx()
|
|
13977
12950
|
],
|
|
13978
12951
|
[
|
|
13979
|
-
|
|
12952
|
+
path4.join("src", "components", "auth", "LoginPage.tsx"),
|
|
13980
12953
|
netsuiteLoginPageTsx()
|
|
13981
12954
|
],
|
|
13982
12955
|
[
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
]
|
|
13986
|
-
[path3.join("src", "App.tsx"), netsuiteAppTsx()]
|
|
12956
|
+
path4.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
|
|
12957
|
+
signInButtonTsx()
|
|
12958
|
+
]
|
|
13987
12959
|
];
|
|
13988
|
-
const nsCreated = [];
|
|
13989
12960
|
for (const [filePath, content] of nsFiles) {
|
|
13990
|
-
await Bun.write(
|
|
13991
|
-
nsCreated.push(filePath);
|
|
12961
|
+
await Bun.write(path4.join(dir, filePath), content);
|
|
13992
12962
|
}
|
|
13993
|
-
const all = [...base, ...
|
|
12963
|
+
const all = [...base, ...nsFiles.map(([f]) => f)];
|
|
13994
12964
|
return [
|
|
13995
|
-
`
|
|
13996
|
-
|
|
12965
|
+
`Generated ${all.length} files (base scaffold + NetSuite OAuth, all additive -- nothing overwritten).`,
|
|
12966
|
+
"",
|
|
12967
|
+
"All you need to do is wrap the root layout with the auth layout.",
|
|
12968
|
+
"In src/routes/__root.tsx, import AuthLayout and wrap <Outlet />:",
|
|
12969
|
+
"",
|
|
12970
|
+
" import { AuthLayout } from '../components/auth/AuthLayout'",
|
|
12971
|
+
"",
|
|
12972
|
+
" function RootComponent() {",
|
|
12973
|
+
" return (",
|
|
12974
|
+
" <>",
|
|
12975
|
+
" <AuthLayout>",
|
|
12976
|
+
" <Outlet />",
|
|
12977
|
+
" </AuthLayout>",
|
|
12978
|
+
" {/* keep the existing <TanStackDevtools ... /> here */}",
|
|
12979
|
+
" </>",
|
|
12980
|
+
" )",
|
|
12981
|
+
" }",
|
|
12982
|
+
"",
|
|
12983
|
+
"That single wrap gates every route: <LoginPage /> until the user signs",
|
|
12984
|
+
"in with NetSuite, your routes after. Read the session anywhere below it",
|
|
12985
|
+
"with useNetSuiteAuth() from src/lib/auth.tsx. NEVER swap <AuthLayout>",
|
|
12986
|
+
"for a password gate -- real OAuth is the only supported login.",
|
|
13997
12987
|
"",
|
|
13998
|
-
"
|
|
13999
|
-
"
|
|
14000
|
-
"
|
|
12988
|
+
"NetSuite OAuth files written:",
|
|
12989
|
+
" - src/lib/netsuite.ts OAuth client (signIn / session / workerFetch)",
|
|
12990
|
+
" - src/lib/auth.tsx NetSuiteAuthProvider + useNetSuiteAuth",
|
|
12991
|
+
" - src/components/auth/AuthLayout.tsx provider + gate (wrap root with this)",
|
|
12992
|
+
" - src/components/auth/LoginPage.tsx unauthenticated view",
|
|
12993
|
+
" - src/components/auth/SignInWithNetSuite.tsx sign-in button",
|
|
14001
12994
|
"",
|
|
14002
|
-
"
|
|
14003
|
-
" - App.tsx wraps everything in <AuthGate>. NEVER remove the gate",
|
|
14004
|
-
" or build a password placeholder -- real OAuth is the only login.",
|
|
14005
|
-
" - LoginPage.tsx + SignInWithNetSuite.tsx are the unauthenticated",
|
|
14006
|
-
" view. Customise the copy / imagery, keep <SignInWithNetSuite/>",
|
|
14007
|
-
" as the call-to-action.",
|
|
14008
|
-
" - signInWithNetSuite() in src/lib/netsuite.ts hits the Worker at",
|
|
14009
|
-
" /auth/netsuite/start. Deploy the Worker via cloudflare-backend",
|
|
14010
|
-
" MCP with routes:",
|
|
12995
|
+
"Worker (deploy via cloudflare-backend MCP) must serve:",
|
|
14011
12996
|
" GET /auth/netsuite/start -- redirect to NetSuite OAuth",
|
|
14012
12997
|
" GET /auth/netsuite/callback -- code -> token exchange",
|
|
14013
|
-
" GET /api/me
|
|
12998
|
+
" GET /api/me -- current session",
|
|
14014
12999
|
" POST /auth/netsuite/logout -- clear session",
|
|
14015
|
-
"
|
|
14016
|
-
"
|
|
14017
|
-
" - VITE_WORKER_URL must be set so the frontend knows where to",
|
|
14018
|
-
" send users. The HelloLeo Cloud provisioner seeds this."
|
|
13000
|
+
" Reads env.NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET.",
|
|
13001
|
+
" VITE_WORKER_URL must be set (HelloLeo Cloud provisioner seeds it)."
|
|
14019
13002
|
].join(`
|
|
14020
13003
|
`);
|
|
14021
13004
|
}
|