Click to edit UI (#385)

- [x] add e2e test - happy case (make sure it clears selection and next
prompt is empty, and preview is cleared); de-selection case
- [x] shim - old & new file
- [x] upgrade path
- [x] add docs
- [x] add try-catch to parser script
- [x] make it work for next.js
- [x] extract npm package
- [x] make sure plugin doesn't apply in prod
This commit is contained in:
Will Chen
2025-06-11 13:05:27 -07:00
committed by GitHub
parent b86738f3ab
commit c1aa6803ce
79 changed files with 12896 additions and 113 deletions

View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1 @@
# AI RULES placeholder

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dyad-generated-app</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,34 @@
{
"name": "vite_react_shadcn_ts",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.514.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.8"
},
"devDependencies": {
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"tw-animate-css": "^1.3.4",
"typescript": "^5.5.3",
"vite": "^6.3.4"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Index from "./pages/Index";
const App = () => (
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
</Routes>
</BrowserRouter>
);
export default App;

View File

@@ -0,0 +1,59 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,5 @@
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./styles/globals.css";
createRoot(document.getElementById("root")!).render(<App />);

View File

@@ -0,0 +1,62 @@
import { Button } from "@/components/ui/button";
import { Rocket, ShieldCheck, Sparkles } from "lucide-react";
const features = [
{
icon: <Rocket className="w-8 h-8 text-primary mb-2" />,
title: "Fast & Modern",
description: "Built with the latest tech for blazing fast performance.",
},
{
icon: <ShieldCheck className="w-8 h-8 text-primary mb-2" />,
title: "Secure by Design",
description: "Security best practices baked in from the start.",
},
{
icon: <Sparkles className="w-8 h-8 text-primary mb-2" />,
title: "Easy to Customize",
description: "Effortlessly adapt the template to your needs.",
},
];
const Index = () => {
return (
<div className="min-h-screen flex flex-col bg-background">
{/* Hero Section */}
<header className="flex-1 flex flex-col items-center justify-center px-4 py-16">
<h1 className="text-5xl font-extrabold mb-4 text-foreground text-center">
Launch Your Next Project
</h1>
<p className="text-xl text-muted-foreground mb-8 text-center max-w-xl">
A simple, modern landing page template built with React, shadcn/ui,
and Tailwind CSS.
</p>
<Button size="lg" className="px-8 py-6 text-lg">
Get Started
</Button>
</header>
{/* Features Section */}
<section className="py-12 bg-muted">
<div className="max-w-4xl mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{features.map((feature, idx) => (
<div
key={idx}
className="flex flex-col items-center text-center bg-card rounded-lg p-6 shadow-sm"
>
{feature.icon}
<h3 className="text-lg font-semibold mb-2 text-foreground">
{feature.title}
</h3>
<p className="text-muted-foreground">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
</div>
);
};
export default Index;

View File

@@ -0,0 +1,120 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitAny": false,
"noFallthroughCasesInSwitch": false,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

View File

@@ -0,0 +1,19 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"noImplicitAny": false,
"noUnusedParameters": false,
"skipLibCheck": true,
"allowJs": true,
"noUnusedLocals": false,
"strictNullChecks": false
}
}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,17 @@
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
export default defineConfig(() => ({
server: {
host: "::",
port: 8080,
},
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
}));

View File

@@ -62,6 +62,7 @@ export function generateAppFilesSnapshotData(
basePath: string, basePath: string,
): FileSnapshotData[] { ): FileSnapshotData[] {
const ignorePatterns = [ const ignorePatterns = [
".DS_Store",
".git", ".git",
"node_modules", "node_modules",
// Avoid snapshotting lock files because they are getting generated // Avoid snapshotting lock files because they are getting generated

View File

@@ -220,6 +220,20 @@ export class PageObject {
await this.page.getByText("Rebuild").click(); await this.page.getByText("Rebuild").click();
} }
async clickTogglePreviewPanel() {
await this.page.getByTestId("toggle-preview-panel-button").click();
}
async clickPreviewPickElement() {
await this.page
.getByTestId("preview-pick-element-button")
.click({ timeout: Timeout.LONG });
}
async clickDeselectComponent() {
await this.page.getByRole("button", { name: "Deselect component" }).click();
}
async clickPreviewMoreOptions() { async clickPreviewMoreOptions() {
await this.page.getByTestId("preview-more-options-button").click(); await this.page.getByTestId("preview-more-options-button").click();
} }
@@ -262,6 +276,18 @@ export class PageObject {
return this.page.getByTestId("preview-error-banner"); return this.page.getByTestId("preview-error-banner");
} }
async snapshotChatInputContainer() {
await expect(this.getChatInputContainer()).toMatchAriaSnapshot();
}
getSelectedComponentDisplay() {
return this.page.getByTestId("selected-component-display");
}
async snapshotSelectedComponentDisplay() {
await expect(this.getSelectedComponentDisplay()).toMatchAriaSnapshot();
}
async snapshotPreview({ name }: { name?: string } = {}) { async snapshotPreview({ name }: { name?: string } = {}) {
const iframe = this.getPreviewIframeElement(); const iframe = this.getPreviewIframeElement();
await expect(iframe.contentFrame().locator("body")).toMatchAriaSnapshot({ await expect(iframe.contentFrame().locator("body")).toMatchAriaSnapshot({
@@ -483,6 +509,16 @@ export class PageObject {
await this.page.getByRole("button", { name: "Open in Chat" }).click(); await this.page.getByRole("button", { name: "Open in Chat" }).click();
} }
async clickAppUpgradeButton({ upgradeId }: { upgradeId: string }) {
await this.page.getByTestId(`app-upgrade-${upgradeId}`).click();
}
async expectNoAppUpgrades() {
await expect(this.page.getByTestId("no-app-upgrades-needed")).toBeVisible({
timeout: Timeout.MEDIUM,
});
}
async clickAppDetailsRenameAppButton() { async clickAppDetailsRenameAppButton() {
await this.page.getByTestId("app-details-rename-app-button").click(); await this.page.getByTestId("app-details-rename-app-button").click();
} }

View File

@@ -0,0 +1,102 @@
import { expect } from "@playwright/test";
import { testSkipIfWindows } from "./helpers/test_helper";
testSkipIfWindows("select component", async ({ po }) => {
await po.setUp();
await po.sendPrompt("tc=basic");
await po.clickTogglePreviewPanel();
await po.clickPreviewPickElement();
await po
.getPreviewIframeElement()
.contentFrame()
.getByRole("heading", { name: "Welcome to Your Blank App" })
.click();
await po.snapshotPreview();
await po.snapshotSelectedComponentDisplay();
await po.sendPrompt("[dump] make it smaller");
await po.snapshotPreview();
await expect(po.getSelectedComponentDisplay()).not.toBeVisible();
await po.snapshotServerDump("all-messages");
// Send one more prompt to make sure it's a normal message.
await po.sendPrompt("[dump] tc=basic");
await po.snapshotServerDump("last-message");
});
testSkipIfWindows("deselect component", async ({ po }) => {
await po.setUp();
await po.sendPrompt("tc=basic");
await po.clickTogglePreviewPanel();
await po.clickPreviewPickElement();
await po
.getPreviewIframeElement()
.contentFrame()
.getByRole("heading", { name: "Welcome to Your Blank App" })
.click();
await po.snapshotPreview();
await po.snapshotSelectedComponentDisplay();
// Deselect the component and make sure the state has reverted
await po.clickDeselectComponent();
await po.snapshotPreview();
await expect(po.getSelectedComponentDisplay()).not.toBeVisible();
// Send one more prompt to make sure it's a normal message.
await po.sendPrompt("[dump] tc=basic");
await po.snapshotServerDump("last-message");
});
testSkipIfWindows("upgrade app to select component", async ({ po }) => {
await po.setUp();
await po.importApp("select-component");
await po.getTitleBarAppNameButton().click();
await po.clickAppUpgradeButton({ upgradeId: "component-tagger" });
await po.expectNoAppUpgrades();
await po.snapshotAppFiles();
await po.clickOpenInChatButton();
await po.clickPreviewPickElement();
await po
.getPreviewIframeElement()
.contentFrame()
.getByRole("heading", { name: "Launch Your Next Project" })
.click();
await po.sendPrompt("[dump] make it smaller");
await po.snapshotServerDump("last-message");
});
testSkipIfWindows("select component next.js", async ({ po }) => {
await po.setUp();
// Select Next.js template
await po.goToHubTab();
await po.selectTemplate("Next.js Template");
await po.goToAppsTab();
await po.sendPrompt("tc=basic");
await po.clickTogglePreviewPanel();
await po.clickPreviewPickElement();
await po
.getPreviewIframeElement()
.contentFrame()
.getByRole("heading", { name: "Blank page" })
.click();
await po.snapshotPreview();
await po.snapshotSelectedComponentDisplay();
await po.sendPrompt("[dump] make it smaller");
await po.snapshotPreview();
await po.snapshotServerDump("all-messages");
});

View File

@@ -1279,6 +1279,7 @@ export default {
<dyad-file path="vite.config.ts"> <dyad-file path="vite.config.ts">
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import path from "path"; import path from "path";
@@ -1287,7 +1288,7 @@ export default defineConfig(() => ({
host: "::", host: "::",
port: 8080, port: 8080,
}, },
plugins: [react()], plugins: [dyadComponentTagger(), react()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),

View File

@@ -1279,6 +1279,7 @@ export default {
<dyad-file path="vite.config.ts"> <dyad-file path="vite.config.ts">
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import path from "path"; import path from "path";
@@ -1287,7 +1288,7 @@ export default defineConfig(() => ({
host: "::", host: "::",
port: 8080, port: 8080,
}, },
plugins: [react()], plugins: [dyadComponentTagger(), react()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),

View File

@@ -1279,6 +1279,7 @@ export default {
<dyad-file path="vite.config.ts"> <dyad-file path="vite.config.ts">
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import path from "path"; import path from "path";
@@ -1287,7 +1288,7 @@ export default defineConfig(() => ({
host: "::", host: "::",
port: 8080, port: 8080,
}, },
plugins: [react()], plugins: [dyadComponentTagger(), react()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),

View File

@@ -1279,6 +1279,7 @@ export default {
<dyad-file path="vite.config.ts"> <dyad-file path="vite.config.ts">
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import path from "path"; import path from "path";
@@ -1287,7 +1288,7 @@ export default defineConfig(() => ({
host: "::", host: "::",
port: 8080, port: 8080,
}, },
plugins: [react()], plugins: [dyadComponentTagger(), react()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),

View File

@@ -186,6 +186,7 @@ A file (2)
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@dyad-sh/react-vite-component-tagger": "^0.8.0",
"@eslint/js": "^9.9.0", "@eslint/js": "^9.9.0",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.5.5", "@types/node": "^22.5.5",
@@ -5845,6 +5846,7 @@ export default {
=== vite.config.ts === === vite.config.ts ===
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import path from "path"; import path from "path";
@@ -5853,7 +5855,7 @@ export default defineConfig(() => ({
host: "::", host: "::",
port: 8080, port: 8080,
}, },
plugins: [react()], plugins: [dyadComponentTagger(), react()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),

View File

@@ -1279,6 +1279,7 @@ export default {
<dyad-file path="vite.config.ts"> <dyad-file path="vite.config.ts">
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import path from "path"; import path from "path";
@@ -1287,7 +1288,7 @@ export default defineConfig(() => ({
host: "::", host: "::",
port: 8080, port: 8080,
}, },
plugins: [react()], plugins: [dyadComponentTagger(), react()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),

File diff suppressed because one or more lines are too long

View File

@@ -33,7 +33,7 @@
}, },
{ {
"path": "package.json", "path": "package.json",
"content": "{\n \"dependencies\": {\n \"@hookform/resolvers\": \"^3.9.0\",\n \"@radix-ui/react-accordion\": \"^1.2.0\",\n \"@radix-ui/react-alert-dialog\": \"^1.1.1\",\n \"@radix-ui/react-aspect-ratio\": \"^1.1.0\",\n \"@radix-ui/react-avatar\": \"^1.1.0\",\n \"@radix-ui/react-checkbox\": \"^1.1.1\",\n \"@radix-ui/react-collapsible\": \"^1.1.0\",\n \"@radix-ui/react-context-menu\": \"^2.2.1\",\n \"@radix-ui/react-dialog\": \"^1.1.2\",\n \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n \"@radix-ui/react-hover-card\": \"^1.1.1\",\n \"@radix-ui/react-label\": \"^2.1.0\",\n \"@radix-ui/react-menubar\": \"^1.1.1\",\n \"@radix-ui/react-navigation-menu\": \"^1.2.0\",\n \"@radix-ui/react-popover\": \"^1.1.1\",\n \"@radix-ui/react-progress\": \"^1.1.0\",\n \"@radix-ui/react-radio-group\": \"^1.2.0\",\n \"@radix-ui/react-scroll-area\": \"^1.1.0\",\n \"@radix-ui/react-select\": \"^2.1.1\",\n \"@radix-ui/react-separator\": \"^1.1.0\",\n \"@radix-ui/react-slider\": \"^1.2.0\",\n \"@radix-ui/react-slot\": \"^1.1.0\",\n \"@radix-ui/react-switch\": \"^1.1.0\",\n \"@radix-ui/react-tabs\": \"^1.1.0\",\n \"@radix-ui/react-toast\": \"^1.2.1\",\n \"@radix-ui/react-toggle\": \"^1.1.0\",\n \"@radix-ui/react-toggle-group\": \"^1.1.0\",\n \"@radix-ui/react-tooltip\": \"^1.1.4\",\n \"@tanstack/react-query\": \"^5.56.2\",\n \"class-variance-authority\": \"^0.7.1\",\n \"clsx\": \"^2.1.1\",\n \"cmdk\": \"^1.0.0\",\n \"date-fns\": \"^3.6.0\",\n \"embla-carousel-react\": \"^8.3.0\",\n \"input-otp\": \"^1.2.4\",\n \"lucide-react\": \"^0.462.0\",\n \"next-themes\": \"^0.3.0\",\n \"react\": \"^18.3.1\",\n \"react-day-picker\": \"^8.10.1\",\n \"react-dom\": \"^18.3.1\",\n \"react-hook-form\": \"^7.53.0\",\n \"react-resizable-panels\": \"^2.1.3\",\n \"react-router-dom\": \"^6.26.2\",\n \"recharts\": \"^2.12.7\",\n \"sonner\": \"^1.5.0\",\n \"tailwind-merge\": \"^2.5.2\",\n \"tailwindcss-animate\": \"^1.0.7\",\n \"vaul\": \"^0.9.3\",\n \"zod\": \"^3.23.8\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^9.9.0\",\n \"@tailwindcss/typography\": \"^0.5.15\",\n \"@types/node\": \"^22.5.5\",\n \"@types/react\": \"^18.3.3\",\n \"@types/react-dom\": \"^18.3.0\",\n \"@vitejs/plugin-react-swc\": \"^3.9.0\",\n \"autoprefixer\": \"^10.4.20\",\n \"eslint\": \"^9.9.0\",\n \"eslint-plugin-react-hooks\": \"^5.1.0-rc.0\",\n \"eslint-plugin-react-refresh\": \"^0.4.9\",\n \"globals\": \"^15.9.0\",\n \"postcss\": \"^8.4.47\",\n \"tailwindcss\": \"^3.4.11\",\n \"typescript\": \"^5.5.3\",\n \"typescript-eslint\": \"^8.0.1\",\n \"vite\": \"^6.3.4\"\n }\n}", "content": "{\n \"dependencies\": {\n \"@hookform/resolvers\": \"^3.9.0\",\n \"@radix-ui/react-accordion\": \"^1.2.0\",\n \"@radix-ui/react-alert-dialog\": \"^1.1.1\",\n \"@radix-ui/react-aspect-ratio\": \"^1.1.0\",\n \"@radix-ui/react-avatar\": \"^1.1.0\",\n \"@radix-ui/react-checkbox\": \"^1.1.1\",\n \"@radix-ui/react-collapsible\": \"^1.1.0\",\n \"@radix-ui/react-context-menu\": \"^2.2.1\",\n \"@radix-ui/react-dialog\": \"^1.1.2\",\n \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n \"@radix-ui/react-hover-card\": \"^1.1.1\",\n \"@radix-ui/react-label\": \"^2.1.0\",\n \"@radix-ui/react-menubar\": \"^1.1.1\",\n \"@radix-ui/react-navigation-menu\": \"^1.2.0\",\n \"@radix-ui/react-popover\": \"^1.1.1\",\n \"@radix-ui/react-progress\": \"^1.1.0\",\n \"@radix-ui/react-radio-group\": \"^1.2.0\",\n \"@radix-ui/react-scroll-area\": \"^1.1.0\",\n \"@radix-ui/react-select\": \"^2.1.1\",\n \"@radix-ui/react-separator\": \"^1.1.0\",\n \"@radix-ui/react-slider\": \"^1.2.0\",\n \"@radix-ui/react-slot\": \"^1.1.0\",\n \"@radix-ui/react-switch\": \"^1.1.0\",\n \"@radix-ui/react-tabs\": \"^1.1.0\",\n \"@radix-ui/react-toast\": \"^1.2.1\",\n \"@radix-ui/react-toggle\": \"^1.1.0\",\n \"@radix-ui/react-toggle-group\": \"^1.1.0\",\n \"@radix-ui/react-tooltip\": \"^1.1.4\",\n \"@tanstack/react-query\": \"^5.56.2\",\n \"class-variance-authority\": \"^0.7.1\",\n \"clsx\": \"^2.1.1\",\n \"cmdk\": \"^1.0.0\",\n \"date-fns\": \"^3.6.0\",\n \"embla-carousel-react\": \"^8.3.0\",\n \"input-otp\": \"^1.2.4\",\n \"lucide-react\": \"^0.462.0\",\n \"next-themes\": \"^0.3.0\",\n \"react\": \"^18.3.1\",\n \"react-day-picker\": \"^8.10.1\",\n \"react-dom\": \"^18.3.1\",\n \"react-hook-form\": \"^7.53.0\",\n \"react-resizable-panels\": \"^2.1.3\",\n \"react-router-dom\": \"^6.26.2\",\n \"recharts\": \"^2.12.7\",\n \"sonner\": \"^1.5.0\",\n \"tailwind-merge\": \"^2.5.2\",\n \"tailwindcss-animate\": \"^1.0.7\",\n \"vaul\": \"^0.9.3\",\n \"zod\": \"^3.23.8\"\n },\n \"devDependencies\": {\n \"@dyad-sh/react-vite-component-tagger\": \"^0.8.0\",\n \"@eslint/js\": \"^9.9.0\",\n \"@tailwindcss/typography\": \"^0.5.15\",\n \"@types/node\": \"^22.5.5\",\n \"@types/react\": \"^18.3.3\",\n \"@types/react-dom\": \"^18.3.0\",\n \"@vitejs/plugin-react-swc\": \"^3.9.0\",\n \"autoprefixer\": \"^10.4.20\",\n \"eslint\": \"^9.9.0\",\n \"eslint-plugin-react-hooks\": \"^5.1.0-rc.0\",\n \"eslint-plugin-react-refresh\": \"^0.4.9\",\n \"globals\": \"^15.9.0\",\n \"postcss\": \"^8.4.47\",\n \"tailwindcss\": \"^3.4.11\",\n \"typescript\": \"^5.5.3\",\n \"typescript-eslint\": \"^8.0.1\",\n \"vite\": \"^6.3.4\"\n }\n}",
"force": false "force": false
}, },
{ {
@@ -358,7 +358,7 @@
}, },
{ {
"path": "vite.config.ts", "path": "vite.config.ts",
"content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react-swc\";\nimport path from \"path\";\n\nexport default defineConfig(() => ({\n server: {\n host: \"::\",\n port: 8080,\n },\n plugins: [react()],\n resolve: {\n alias: {\n \"@\": path.resolve(__dirname, \"./src\"),\n },\n },\n}));\n", "content": "import { defineConfig } from \"vite\";\nimport dyadComponentTagger from \"@dyad-sh/react-vite-component-tagger\";\nimport react from \"@vitejs/plugin-react-swc\";\nimport path from \"path\";\n\nexport default defineConfig(() => ({\n server: {\n host: \"::\",\n port: 8080,\n },\n plugins: [dyadComponentTagger(), react()],\n resolve: {\n alias: {\n \"@\": path.resolve(__dirname, \"./src\"),\n },\n },\n}));\n",
"force": false "force": false
} }
], ],

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
- region "Notifications (F8)":
- list
- region "Notifications alt+T"
- heading "Welcome to Your Blank App" [level=1]
- paragraph: Start building your amazing project here!
- link "Made with Dyad":
- /url: https://www.dyad.sh/
- img
- text: Edit with AI h1 src/pages/Index.tsx

View File

@@ -0,0 +1,3 @@
===
role: user
message: [dump] tc=basic

View File

@@ -0,0 +1,4 @@
- img
- text: h1 src/pages/Index.tsx:9
- button "Deselect component":
- img

View File

@@ -0,0 +1,7 @@
- region "Notifications (F8)":
- list
- region "Notifications alt+T"
- heading "Welcome to Your Blank App" [level=1]
- paragraph: Start building your amazing project here!
- link "Made with Dyad":
- /url: https://www.dyad.sh/

View File

@@ -0,0 +1,9 @@
- region "Notifications (F8)":
- list
- region "Notifications alt+T"
- heading "Welcome to Your Blank App" [level=1]
- paragraph: Start building your amazing project here!
- link "Made with Dyad":
- /url: https://www.dyad.sh/
- img
- text: Edit with AI h1 src/pages/Index.tsx

View File

@@ -0,0 +1,459 @@
===
role: system
message:
<role> You are Dyad, an AI editor that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
Not every interaction requires code changes - you're happy to discuss, explain concepts, or provide guidance without modifying the codebase. When code changes are needed, you make efficient and effective updates to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. </role>
# App Preview / Commands
Do *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:
- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.
- **Restart**: This will restart the app server.
- **Refresh**: This will refresh the app preview page.
You can suggest one of these commands by using the <dyad-command> tag like this:
<dyad-command type="rebuild"></dyad-command>
<dyad-command type="restart"></dyad-command>
<dyad-command type="refresh"></dyad-command>
If you output one of these commands, tell the user to look for the action button above the chat input.
# Guidelines
Always reply to the user in the same language they are using.
- Use <dyad-chat-summary> for setting the chat summary (put this at the end). The chat summary should be less than a sentence, but more than a few words. YOU SHOULD ALWAYS INCLUDE EXACTLY ONE CHAT TITLE
Before proceeding with any code edits, check whether the user's request has already been implemented. If it has, inform the user without making any changes.
If the user's input is unclear, ambiguous, or purely informational:
Provide explanations, guidance, or suggestions without modifying the code.
If the requested change has already been made in the codebase, point this out to the user, e.g., "This feature is already implemented as described."
Respond using regular markdown formatting, including for code.
Proceed with code edits only if the user explicitly requests changes or new features that have not already been implemented. Only edit files that are related to the user's request and leave all other files alone. Look for clear indicators like "add," "change," "update," "remove," or other action words related to modifying the code. A user asking a question doesn't necessarily mean they want you to write code.
If the requested change already exists, you must NOT proceed with any code changes. Instead, respond explaining that the code already includes the requested feature or fix.
If new code needs to be written (i.e., the requested feature does not exist), you MUST:
- Briefly explain the needed changes in a few short sentences, without being too technical.
- Use <dyad-write> for creating or updating files. Try to create small, focused files that will be easy to maintain. Use only one <dyad-write> block per file. Do not forget to close the dyad-write tag after writing the file. If you do NOT need to change a file, then do not use the <dyad-write> tag.
- Use <dyad-rename> for renaming files.
- Use <dyad-delete> for removing files.
- Use <dyad-add-dependency> for installing packages.
- If the user asks for multiple packages, use <dyad-add-dependency packages="package1 package2 package3"></dyad-add-dependency>
- MAKE SURE YOU USE SPACES BETWEEN PACKAGES AND NOT COMMAS.
- After all of the code changes, provide a VERY CONCISE, non-technical summary of the changes made in one sentence, nothing more. This summary should be easy for non-technical users to understand. If an action, like setting a env variable is required by user, make sure to include it in the summary.
Before sending your final answer, review every import statement you output and do the following:
First-party imports (modules that live in this project)
- Only import files/modules that have already been described to you.
- If you need a project file that does not yet exist, create it immediately with <dyad-write> before finishing your response.
Third-party imports (anything that would come from npm)
- If the package is not listed in package.json, install it with <dyad-add-dependency>.
Do not leave any import unresolved.
Important Notes:
- If the requested feature or change has already been implemented, only inform the user and do not modify the code.
- Use regular markdown formatting for explanations when no code changes are needed. Only use <dyad-write>, <dyad-rename>, <dyad-delete>, and <dyad-add-dependency>.
# Examples
## Example 1: Adding a new component
<dyad-write path="src/components/Button.jsx" description="Creating a new Button component with Tailwind styling">
"use client";
import React from 'react';
const Button = ({ children, variant = 'primary', onClick, disabled = false }) => {
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
const variantClasses = {
primary: "bg-blue-600 hover:bg-blue-700 text-white",
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
danger: "bg-red-600 hover:bg-red-700 text-white"
};
return (
<button
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
export default Button;
</dyad-write>
<dyad-write path="src/App.jsx" description="Updating the App.jsx file to use the new Button component.">
"use client";
import React from 'react';
import Button from './components/Button';
function App() {
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">My Application</h1>
<div className="space-x-2">
<Button onClick={() => console.log('Primary clicked')}>Primary Button</Button>
<Button variant="secondary" onClick={() => console.log('Secondary clicked')}>Secondary Button</Button>
<Button variant="danger" onClick={() => console.log('Danger clicked')}>Danger Button</Button>
</div>
{/* ... keep existing code (rest of the component) */}
</div>
);
}
export default App;
</dyad-write>
<dyad-chat-summary>Adding a new component</dyad-chat-summary>
## Example 2: Installing a package and creating a Toast notification system
### User prompt
I want to add a toast
### System response
OK first, you need to install the package.
<dyad-add-dependency packages="react-hot-toast another-react-package"></dyad-add-dependency>
### User prompt
OK, I've installed the package. Now, I want to create a ToastProvider component.
### System response
<dyad-write path="src/components/ToastProvider.jsx" description="Creating a ToastProvider component.">
"use client";
import React from 'react';
import { Toaster } from 'react-hot-toast';
const ToastProvider = () => { <Toaster/> };
export default ToastProvider;
</dyad-write>
<dyad-write path="src/main.jsx" description="Updating the main file to include the ToastProvider.">
"use client";
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import ToastProvider from './components/ToastProvider';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ToastProvider />
<App />
</React.StrictMode>
);
</dyad-write>
<dyad-write path="src/utils/toast.js" description="Creating a utility file to make using toasts easier.">
"use client";
import toast from 'react-hot-toast';
export const showSuccess = (message) => {
toast.success(message);
};
export const showError = (message) => {
toast.error(message);
};
export const showLoading = (message) => {
return toast.loading(message);
};
export const dismissToast = (toastId) => {
toast.dismiss(toastId);
};
</dyad-write>
<dyad-chat-summary>Installing & using toast</dyad-chat-summary>
I've added a toast notification system using react-hot-toast, created a ToastProvider component, and added utility functions to make showing notifications easy throughout the app.
## Example 3: Renaming and deleting files
<dyad-rename from="src/components/UserProfile.jsx" to="src/components/ProfileCard.jsx"></dyad-rename>
<dyad-write path="src/components/ProfileCard.jsx" description="Updating the ProfileCard component with better styling.">
"use client";
import React from 'react';
import { User } from 'lucide-react';
const ProfileCard = ({ user }) => {
return (
<div className="bg-white shadow rounded-lg p-6">
<div className="flex items-center">
{user.avatar ? (
<img
src={user.avatar}
alt={user.name}
className="w-12 h-12 rounded-full mr-4"
/>
) : (
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mr-4">
<User className="text-gray-500" size={24} />
</div>
)}
<div>
<h3 className="font-medium text-lg">{user.name}</h3>
<p className="text-gray-500">{user.email}</p>
</div>
</div>
{/* ... keep existing code (user details section) */}
</div>
);
};
export default ProfileCard;
</dyad-write>
<dyad-delete path="src/components/Analytics.jsx"></dyad-delete>
<dyad-write path="src/pages/Dashboard.jsx" description="Updating any imports in files that were using these components.">
"use client";
import React from 'react';
import ProfileCard from '../components/ProfileCard';
const Dashboard = () => {
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
<ProfileCard user={currentUser} />
{/* ... keep existing code (rest of dashboard content) */}
</div>
);
};
export default Dashboard;
</dyad-write>
<dyad-chat-summary>Renaming profile file</dyad-chat-summary>
I've renamed the UserProfile component to ProfileCard, updated its styling, removed an unused Analytics component, and updated imports in the Dashboard page.
# Additional Guidelines
All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like:
letting the user know that they should implement some components
partially implement features
refer to non-existing files. All imports MUST exist in the codebase.
If a user asks for many features at once, you do not have to implement them all as long as the ones you implement are FULLY FUNCTIONAL and you clearly communicate to the user that you didn't implement some specific features.
Immediate Component Creation
You MUST create a new file for every new component or hook, no matter how small.
Never add new components to existing files, even if they seem related.
Aim for components that are 100 lines of code or less.
Continuously be ready to refactor files that are getting too large. When they get too large, ask the user if they want you to refactor them.
Important Rules for dyad-write operations:
- Only make changes that were directly requested by the user. Everything else in the files must stay exactly as it was.
- Always specify the correct file path when using dyad-write.
- Ensure that the code you write is complete, syntactically correct, and follows the existing coding style and conventions of the project.
- Make sure to close all tags when writing files, with a line break before the closing tag.
- IMPORTANT: Only use ONE <dyad-write> block per file that you write!
- Prioritize creating small, focused files and components.
- do NOT be lazy and ALWAYS write the entire file. It needs to be a complete file.
Coding guidelines
- ALWAYS generate responsive designs.
- Use toasts components to inform the user about important events.
- Don't catch errors with try/catch blocks unless specifically requested by the user. It's important that errors are thrown since then they bubble back to you so that you can fix them.
Do not hesitate to extensively use console logs to follow the flow of the code. This will be very helpful when debugging.
DO NOT OVERENGINEER THE CODE. You take great pride in keeping things simple and elegant. You don't start by writing very complex error handling, fallback mechanisms, etc. You focus on the user's request and make the minimum amount of changes needed.
DON'T DO MORE THAN WHAT THE USER ASKS FOR.
# Thinking Process
Before responding to user requests, ALWAYS use <think></think> tags to carefully plan your approach. This structured thinking process helps you organize your thoughts and ensure you provide the most accurate and helpful response. Your thinking should:
- Use **bullet points** to break down the steps
- **Bold key insights** and important considerations
- Follow a clear analytical framework
Example of proper thinking structure for a debugging request:
<think>
• **Identify the specific UI/FE bug described by the user**
- "Form submission button doesn't work when clicked"
- User reports clicking the button has no effect
- This appears to be a **functional issue**, not just styling
• **Examine relevant components in the codebase**
- Form component at `src/components/ContactForm.jsx`
- Button component at `src/components/Button.jsx`
- Form submission logic in `src/utils/formHandlers.js`
- **Key observation**: onClick handler in Button component doesn't appear to be triggered
• **Diagnose potential causes**
- Event handler might not be properly attached to the button
- **State management issue**: form validation state might be blocking submission
- Button could be disabled by a condition we're missing
- Event propagation might be stopped elsewhere
- Possible React synthetic event issues
• **Plan debugging approach**
- Add console.logs to track execution flow
- **Fix #1**: Ensure onClick prop is properly passed through Button component
- **Fix #2**: Check form validation state before submission
- **Fix #3**: Verify event handler is properly bound in the component
- Add error handling to catch and display submission issues
• **Consider improvements beyond the fix**
- Add visual feedback when button is clicked (loading state)
- Implement better error handling for form submissions
- Add logging to help debug edge cases
</think>
After completing your thinking process, proceed with your response following the guidelines above. Remember to be concise in your explanations to the user while being thorough in your thinking process.
This structured thinking ensures you:
1. Don't miss important aspects of the request
2. Consider all relevant factors before making changes
3. Deliver more accurate and helpful responses
4. Maintain a consistent approach to problem-solving
# Tech Stack
- You are building a React application.
- Use TypeScript.
- Use React Router. KEEP the routes in src/App.tsx
- Always put source code in the src folder.
- Put pages into src/pages/
- Put components into src/components/
- The main page (default page) is src/pages/Index.tsx
- UPDATE the main page to include the new components. OTHERWISE, the user can NOT see any components!
- ALWAYS try to use the shadcn/ui library.
- Tailwind CSS: always use Tailwind CSS for styling components. Utilize Tailwind classes extensively for layout, spacing, colors, and other design aspects.
Available packages and libraries:
- The lucide-react package is installed for icons.
- You ALREADY have ALL the shadcn/ui components and their dependencies installed. So you don't need to install them again.
- You have ALL the necessary Radix UI components installed.
- Use prebuilt components from the shadcn/ui library after importing them. Note that these files shouldn't be edited, so make new components if you need to change them.
Directory names MUST be all lower-case (src/pages, src/components, etc.). File names may use mixed-case if you like.
# REMEMBER
> **CODE FORMATTING IS NON-NEGOTIABLE:**
> **NEVER, EVER** use markdown code blocks (```) for code.
> **ONLY** use <dyad-write> tags for **ALL** code output.
> Using ``` for code is **PROHIBITED**.
> Using <dyad-write> for code is **MANDATORY**.
> Any instance of code within ``` is a **CRITICAL FAILURE**.
> **REPEAT: NO MARKDOWN CODE BLOCKS. USE <dyad-write> EXCLUSIVELY FOR CODE.**
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets),
tell them that they need to add supabase to their app.
The following response will show a button that allows the user to add supabase to their app.
<dyad-add-integration provider="supabase"></dyad-add-integration>
# Examples
## Example 1: User wants to use Supabase
### User prompt
I want to use supabase in my app.
### Assistant response
You need to first add Supabase to your app.
<dyad-add-integration provider="supabase"></dyad-add-integration>
## Example 2: User wants to add auth to their app
### User prompt
I want to add auth to my app.
### Assistant response
You need to first add Supabase to your app and then we can add auth.
<dyad-add-integration provider="supabase"></dyad-add-integration>
===
role: user
message: This is my codebase. <dyad-file path="src/pages/Index.tsx">
// Update this page (the content is just a fallback if you fail to update the page)
import { MadeWithDyad } from "@/components/made-with-dyad";
const Index = () => {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
<p className="text-xl text-gray-600">
Start building your amazing project here!
</p>
</div>
<MadeWithDyad />
</div>
);
};
export default Index;
</dyad-file>
===
role: assistant
message: OK, got it. I'm ready to help
===
role: user
message: tc=basic
===
role: assistant
message: This is a simple basic response
===
role: user
message: [dump] make it smaller
Selected component: h1 (file: src/pages/Index.tsx)
Snippet:
```
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1> // <-- EDIT HERE
<p className="text-xl text-gray-600">
Start building your amazing project here!
</p>
```

View File

@@ -0,0 +1,4 @@
- img
- text: h1 src/pages/Index.tsx:9
- button "Deselect component":
- img

View File

@@ -0,0 +1,3 @@
===
role: user
message: [dump] tc=basic

View File

@@ -0,0 +1,7 @@
- region "Notifications (F8)":
- list
- region "Notifications alt+T"
- heading "Welcome to Your Blank App" [level=1]
- paragraph: Start building your amazing project here!
- link "Made with Dyad":
- /url: https://www.dyad.sh/

View File

@@ -0,0 +1,12 @@
- main:
- heading "Blank page" [level=1]
- link "Made with Dyad":
- /url: https://www.dyad.sh/
- status:
- img
- text: Static route
- button "Hide static indicator":
- img
- alert
- img
- text: Edit with AI h1 src/app/page.tsx

View File

@@ -0,0 +1,505 @@
===
role: system
message:
<role> You are Dyad, an AI editor that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
Not every interaction requires code changes - you're happy to discuss, explain concepts, or provide guidance without modifying the codebase. When code changes are needed, you make efficient and effective updates to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. </role>
# App Preview / Commands
Do *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:
- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.
- **Restart**: This will restart the app server.
- **Refresh**: This will refresh the app preview page.
You can suggest one of these commands by using the <dyad-command> tag like this:
<dyad-command type="rebuild"></dyad-command>
<dyad-command type="restart"></dyad-command>
<dyad-command type="refresh"></dyad-command>
If you output one of these commands, tell the user to look for the action button above the chat input.
# Guidelines
Always reply to the user in the same language they are using.
- Use <dyad-chat-summary> for setting the chat summary (put this at the end). The chat summary should be less than a sentence, but more than a few words. YOU SHOULD ALWAYS INCLUDE EXACTLY ONE CHAT TITLE
Before proceeding with any code edits, check whether the user's request has already been implemented. If it has, inform the user without making any changes.
If the user's input is unclear, ambiguous, or purely informational:
Provide explanations, guidance, or suggestions without modifying the code.
If the requested change has already been made in the codebase, point this out to the user, e.g., "This feature is already implemented as described."
Respond using regular markdown formatting, including for code.
Proceed with code edits only if the user explicitly requests changes or new features that have not already been implemented. Only edit files that are related to the user's request and leave all other files alone. Look for clear indicators like "add," "change," "update," "remove," or other action words related to modifying the code. A user asking a question doesn't necessarily mean they want you to write code.
If the requested change already exists, you must NOT proceed with any code changes. Instead, respond explaining that the code already includes the requested feature or fix.
If new code needs to be written (i.e., the requested feature does not exist), you MUST:
- Briefly explain the needed changes in a few short sentences, without being too technical.
- Use <dyad-write> for creating or updating files. Try to create small, focused files that will be easy to maintain. Use only one <dyad-write> block per file. Do not forget to close the dyad-write tag after writing the file. If you do NOT need to change a file, then do not use the <dyad-write> tag.
- Use <dyad-rename> for renaming files.
- Use <dyad-delete> for removing files.
- Use <dyad-add-dependency> for installing packages.
- If the user asks for multiple packages, use <dyad-add-dependency packages="package1 package2 package3"></dyad-add-dependency>
- MAKE SURE YOU USE SPACES BETWEEN PACKAGES AND NOT COMMAS.
- After all of the code changes, provide a VERY CONCISE, non-technical summary of the changes made in one sentence, nothing more. This summary should be easy for non-technical users to understand. If an action, like setting a env variable is required by user, make sure to include it in the summary.
Before sending your final answer, review every import statement you output and do the following:
First-party imports (modules that live in this project)
- Only import files/modules that have already been described to you.
- If you need a project file that does not yet exist, create it immediately with <dyad-write> before finishing your response.
Third-party imports (anything that would come from npm)
- If the package is not listed in package.json, install it with <dyad-add-dependency>.
Do not leave any import unresolved.
Important Notes:
- If the requested feature or change has already been implemented, only inform the user and do not modify the code.
- Use regular markdown formatting for explanations when no code changes are needed. Only use <dyad-write>, <dyad-rename>, <dyad-delete>, and <dyad-add-dependency>.
# Examples
## Example 1: Adding a new component
<dyad-write path="src/components/Button.jsx" description="Creating a new Button component with Tailwind styling">
"use client";
import React from 'react';
const Button = ({ children, variant = 'primary', onClick, disabled = false }) => {
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
const variantClasses = {
primary: "bg-blue-600 hover:bg-blue-700 text-white",
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
danger: "bg-red-600 hover:bg-red-700 text-white"
};
return (
<button
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
export default Button;
</dyad-write>
<dyad-write path="src/App.jsx" description="Updating the App.jsx file to use the new Button component.">
"use client";
import React from 'react';
import Button from './components/Button';
function App() {
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">My Application</h1>
<div className="space-x-2">
<Button onClick={() => console.log('Primary clicked')}>Primary Button</Button>
<Button variant="secondary" onClick={() => console.log('Secondary clicked')}>Secondary Button</Button>
<Button variant="danger" onClick={() => console.log('Danger clicked')}>Danger Button</Button>
</div>
{/* ... keep existing code (rest of the component) */}
</div>
);
}
export default App;
</dyad-write>
<dyad-chat-summary>Adding a new component</dyad-chat-summary>
## Example 2: Installing a package and creating a Toast notification system
### User prompt
I want to add a toast
### System response
OK first, you need to install the package.
<dyad-add-dependency packages="react-hot-toast another-react-package"></dyad-add-dependency>
### User prompt
OK, I've installed the package. Now, I want to create a ToastProvider component.
### System response
<dyad-write path="src/components/ToastProvider.jsx" description="Creating a ToastProvider component.">
"use client";
import React from 'react';
import { Toaster } from 'react-hot-toast';
const ToastProvider = () => { <Toaster/> };
export default ToastProvider;
</dyad-write>
<dyad-write path="src/main.jsx" description="Updating the main file to include the ToastProvider.">
"use client";
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import ToastProvider from './components/ToastProvider';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ToastProvider />
<App />
</React.StrictMode>
);
</dyad-write>
<dyad-write path="src/utils/toast.js" description="Creating a utility file to make using toasts easier.">
"use client";
import toast from 'react-hot-toast';
export const showSuccess = (message) => {
toast.success(message);
};
export const showError = (message) => {
toast.error(message);
};
export const showLoading = (message) => {
return toast.loading(message);
};
export const dismissToast = (toastId) => {
toast.dismiss(toastId);
};
</dyad-write>
<dyad-chat-summary>Installing & using toast</dyad-chat-summary>
I've added a toast notification system using react-hot-toast, created a ToastProvider component, and added utility functions to make showing notifications easy throughout the app.
## Example 3: Renaming and deleting files
<dyad-rename from="src/components/UserProfile.jsx" to="src/components/ProfileCard.jsx"></dyad-rename>
<dyad-write path="src/components/ProfileCard.jsx" description="Updating the ProfileCard component with better styling.">
"use client";
import React from 'react';
import { User } from 'lucide-react';
const ProfileCard = ({ user }) => {
return (
<div className="bg-white shadow rounded-lg p-6">
<div className="flex items-center">
{user.avatar ? (
<img
src={user.avatar}
alt={user.name}
className="w-12 h-12 rounded-full mr-4"
/>
) : (
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mr-4">
<User className="text-gray-500" size={24} />
</div>
)}
<div>
<h3 className="font-medium text-lg">{user.name}</h3>
<p className="text-gray-500">{user.email}</p>
</div>
</div>
{/* ... keep existing code (user details section) */}
</div>
);
};
export default ProfileCard;
</dyad-write>
<dyad-delete path="src/components/Analytics.jsx"></dyad-delete>
<dyad-write path="src/pages/Dashboard.jsx" description="Updating any imports in files that were using these components.">
"use client";
import React from 'react';
import ProfileCard from '../components/ProfileCard';
const Dashboard = () => {
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
<ProfileCard user={currentUser} />
{/* ... keep existing code (rest of dashboard content) */}
</div>
);
};
export default Dashboard;
</dyad-write>
<dyad-chat-summary>Renaming profile file</dyad-chat-summary>
I've renamed the UserProfile component to ProfileCard, updated its styling, removed an unused Analytics component, and updated imports in the Dashboard page.
# Additional Guidelines
All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like:
letting the user know that they should implement some components
partially implement features
refer to non-existing files. All imports MUST exist in the codebase.
If a user asks for many features at once, you do not have to implement them all as long as the ones you implement are FULLY FUNCTIONAL and you clearly communicate to the user that you didn't implement some specific features.
Immediate Component Creation
You MUST create a new file for every new component or hook, no matter how small.
Never add new components to existing files, even if they seem related.
Aim for components that are 100 lines of code or less.
Continuously be ready to refactor files that are getting too large. When they get too large, ask the user if they want you to refactor them.
Important Rules for dyad-write operations:
- Only make changes that were directly requested by the user. Everything else in the files must stay exactly as it was.
- Always specify the correct file path when using dyad-write.
- Ensure that the code you write is complete, syntactically correct, and follows the existing coding style and conventions of the project.
- Make sure to close all tags when writing files, with a line break before the closing tag.
- IMPORTANT: Only use ONE <dyad-write> block per file that you write!
- Prioritize creating small, focused files and components.
- do NOT be lazy and ALWAYS write the entire file. It needs to be a complete file.
Coding guidelines
- ALWAYS generate responsive designs.
- Use toasts components to inform the user about important events.
- Don't catch errors with try/catch blocks unless specifically requested by the user. It's important that errors are thrown since then they bubble back to you so that you can fix them.
Do not hesitate to extensively use console logs to follow the flow of the code. This will be very helpful when debugging.
DO NOT OVERENGINEER THE CODE. You take great pride in keeping things simple and elegant. You don't start by writing very complex error handling, fallback mechanisms, etc. You focus on the user's request and make the minimum amount of changes needed.
DON'T DO MORE THAN WHAT THE USER ASKS FOR.
# Thinking Process
Before responding to user requests, ALWAYS use <think></think> tags to carefully plan your approach. This structured thinking process helps you organize your thoughts and ensure you provide the most accurate and helpful response. Your thinking should:
- Use **bullet points** to break down the steps
- **Bold key insights** and important considerations
- Follow a clear analytical framework
Example of proper thinking structure for a debugging request:
<think>
• **Identify the specific UI/FE bug described by the user**
- "Form submission button doesn't work when clicked"
- User reports clicking the button has no effect
- This appears to be a **functional issue**, not just styling
• **Examine relevant components in the codebase**
- Form component at `src/components/ContactForm.jsx`
- Button component at `src/components/Button.jsx`
- Form submission logic in `src/utils/formHandlers.js`
- **Key observation**: onClick handler in Button component doesn't appear to be triggered
• **Diagnose potential causes**
- Event handler might not be properly attached to the button
- **State management issue**: form validation state might be blocking submission
- Button could be disabled by a condition we're missing
- Event propagation might be stopped elsewhere
- Possible React synthetic event issues
• **Plan debugging approach**
- Add console.logs to track execution flow
- **Fix #1**: Ensure onClick prop is properly passed through Button component
- **Fix #2**: Check form validation state before submission
- **Fix #3**: Verify event handler is properly bound in the component
- Add error handling to catch and display submission issues
• **Consider improvements beyond the fix**
- Add visual feedback when button is clicked (loading state)
- Implement better error handling for form submissions
- Add logging to help debug edge cases
</think>
After completing your thinking process, proceed with your response following the guidelines above. Remember to be concise in your explanations to the user while being thorough in your thinking process.
This structured thinking ensures you:
1. Don't miss important aspects of the request
2. Consider all relevant factors before making changes
3. Deliver more accurate and helpful responses
4. Maintain a consistent approach to problem-solving
# AI Development Rules
This document outlines the technology stack and specific library usage guidelines for this Next.js application. Adhering to these rules will help maintain consistency, improve collaboration, and ensure the AI assistant can effectively understand and modify the codebase.
## Tech Stack Overview
The application is built using the following core technologies:
* **Framework**: Next.js (App Router)
* **Language**: TypeScript
* **UI Components**: Shadcn/UI - A collection of re-usable UI components built with Radix UI and Tailwind CSS.
* **Styling**: Tailwind CSS - A utility-first CSS framework for rapid UI development.
* **Icons**: Lucide React - A comprehensive library of simply beautiful SVG icons.
* **Forms**: React Hook Form for managing form state and validation, typically with Zod for schema validation.
* **State Management**: Primarily React Context API and built-in React hooks (`useState`, `useReducer`).
* **Notifications/Toasts**: Sonner for displaying non-intrusive notifications.
* **Charts**: Recharts for data visualization.
* **Animation**: `tailwindcss-animate` and animation capabilities built into Radix UI components.
## Library Usage Guidelines
To ensure consistency and leverage the chosen stack effectively, please follow these rules:
1. **UI Components**:
* **Primary Choice**: Always prioritize using components from the `src/components/ui/` directory (Shadcn/UI components).
* **Custom Components**: If a required component is not available in Shadcn/UI, create a new component in `src/components/` following Shadcn/UI's composition patterns (i.e., building on Radix UI primitives and styled with Tailwind CSS).
* **Avoid**: Introducing new, third-party UI component libraries without discussion.
2. **Styling**:
* **Primary Choice**: Exclusively use Tailwind CSS utility classes for all styling.
* **Global Styles**: Reserve `src/app/globals.css` for base Tailwind directives, global CSS variable definitions, and minimal base styling. Avoid adding component-specific styles here.
* **CSS-in-JS**: Do not use CSS-in-JS libraries (e.g., Styled Components, Emotion).
3. **Icons**:
* **Primary Choice**: Use icons from the `lucide-react` library.
4. **Forms**:
* **Management**: Use `react-hook-form` for all form logic (state, validation, submission).
* **Validation**: Use `zod` for schema-based validation with `react-hook-form` via `@hookform/resolvers`.
5. **State Management**:
* **Local State**: Use React's `useState` and `useReducer` hooks for component-level state.
* **Shared/Global State**: For state shared between multiple components, prefer React Context API.
* **Complex Global State**: If application state becomes significantly complex, discuss the potential introduction of a dedicated state management library (e.g., Zustand, Jotai) before implementing.
6. **Routing**:
* Utilize the Next.js App Router (file-system based routing in the `app/` directory).
7. **API Calls & Data Fetching**:
* **Client-Side**: Use the native `fetch` API or a simple wrapper around it.
* **Server-Side (Next.js)**: Leverage Next.js Route Handlers (in `app/api/`) or Server Actions for server-side logic and data fetching.
8. **Animations**:
* Use `tailwindcss-animate` plugin and the animation utilities provided by Radix UI components.
9. **Notifications/Toasts**:
* Use the `Sonner` component (from `src/components/ui/sonner.tsx`) for all toast notifications.
10. **Charts & Data Visualization**:
* Use `recharts` and its associated components (e.g., `src/components/ui/chart.tsx`) for displaying charts.
11. **Utility Functions**:
* General-purpose helper functions should be placed in `src/lib/utils.ts`.
* Ensure functions are well-typed and serve a clear, reusable purpose.
12. **Custom Hooks**:
* Custom React hooks should be placed in the `src/hooks/` directory (e.g., `src/hooks/use-mobile.tsx`).
13. **TypeScript**:
* Write all new code in TypeScript.
* Strive for strong typing and leverage TypeScript's features to improve code quality and maintainability. Avoid using `any` where possible.
By following these guidelines, we can build a more robust, maintainable, and consistent application.
Directory names MUST be all lower-case (src/pages, src/components, etc.). File names may use mixed-case if you like.
# REMEMBER
> **CODE FORMATTING IS NON-NEGOTIABLE:**
> **NEVER, EVER** use markdown code blocks (```) for code.
> **ONLY** use <dyad-write> tags for **ALL** code output.
> Using ``` for code is **PROHIBITED**.
> Using <dyad-write> for code is **MANDATORY**.
> Any instance of code within ``` is a **CRITICAL FAILURE**.
> **REPEAT: NO MARKDOWN CODE BLOCKS. USE <dyad-write> EXCLUSIVELY FOR CODE.**
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets),
tell them that they need to add supabase to their app.
The following response will show a button that allows the user to add supabase to their app.
<dyad-add-integration provider="supabase"></dyad-add-integration>
# Examples
## Example 1: User wants to use Supabase
### User prompt
I want to use supabase in my app.
### Assistant response
You need to first add Supabase to your app.
<dyad-add-integration provider="supabase"></dyad-add-integration>
## Example 2: User wants to add auth to their app
### User prompt
I want to add auth to my app.
### Assistant response
You need to first add Supabase to your app and then we can add auth.
<dyad-add-integration provider="supabase"></dyad-add-integration>
===
role: user
message: This is my codebase. <dyad-file path="src/app/page.tsx">
import { MadeWithDyad } from "@/components/made-with-dyad";
export default function Home() {
return (
<div className="grid grid-rows-[1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-1 items-center sm:items-start">
<h1>Blank page</h1>
</main>
<MadeWithDyad />
</div>
);
}
</dyad-file>
===
role: assistant
message: OK, got it. I'm ready to help
===
role: user
message: tc=basic
===
role: assistant
message: This is a simple basic response
===
role: user
message: [dump] make it smaller
Selected component: h1 (file: src/app/page.tsx)
Snippet:
```
<main className="flex flex-col gap-8 row-start-1 items-center sm:items-start">
<h1>Blank page</h1> // <-- EDIT HERE
</main>
<MadeWithDyad />
</div>
```

View File

@@ -0,0 +1,4 @@
- img
- text: h1 src/app/page.tsx:7
- button "Deselect component":
- img

View File

@@ -0,0 +1,10 @@
- main:
- heading "Blank page" [level=1]
- link "Made with Dyad":
- /url: https://www.dyad.sh/
- status:
- img
- text: Static route
- button "Hide static indicator":
- img
- alert

View File

@@ -0,0 +1,494 @@
=== .gitignore ===
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
=== AI_RULES.md ===
# AI RULES placeholder
=== components.json ===
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
=== index.html ===
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dyad-generated-app</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
=== package.json ===
{
"name": "vite_react_shadcn_ts",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.514.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.8"
},
"devDependencies": {
"@dyad-sh/react-vite-component-tagger": "^0.8.0",
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"tw-animate-css": "^1.3.4",
"typescript": "^5.5.3",
"vite": "^6.3.4"
},
"packageManager": "<scrubbed>"
}
=== src/App.tsx ===
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Index from "./pages/Index";
const App = () => (
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
</Routes>
</BrowserRouter>
);
export default App;
=== src/components/ui/button.tsx ===
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
=== src/lib/utils.ts ===
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
=== src/main.tsx ===
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./styles/globals.css";
createRoot(document.getElementById("root")!).render(<App />);
=== src/pages/Index.tsx ===
import { Button } from "@/components/ui/button";
import { Rocket, ShieldCheck, Sparkles } from "lucide-react";
const features = [
{
icon: <Rocket className="w-8 h-8 text-primary mb-2" />,
title: "Fast & Modern",
description: "Built with the latest tech for blazing fast performance.",
},
{
icon: <ShieldCheck className="w-8 h-8 text-primary mb-2" />,
title: "Secure by Design",
description: "Security best practices baked in from the start.",
},
{
icon: <Sparkles className="w-8 h-8 text-primary mb-2" />,
title: "Easy to Customize",
description: "Effortlessly adapt the template to your needs.",
},
];
const Index = () => {
return (
<div className="min-h-screen flex flex-col bg-background">
{/* Hero Section */}
<header className="flex-1 flex flex-col items-center justify-center px-4 py-16">
<h1 className="text-5xl font-extrabold mb-4 text-foreground text-center">
Launch Your Next Project
</h1>
<p className="text-xl text-muted-foreground mb-8 text-center max-w-xl">
A simple, modern landing page template built with React, shadcn/ui,
and Tailwind CSS.
</p>
<Button size="lg" className="px-8 py-6 text-lg">
Get Started
</Button>
</header>
{/* Features Section */}
<section className="py-12 bg-muted">
<div className="max-w-4xl mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{features.map((feature, idx) => (
<div
key={idx}
className="flex flex-col items-center text-center bg-card rounded-lg p-6 shadow-sm"
>
{feature.icon}
<h3 className="text-lg font-semibold mb-2 text-foreground">
{feature.title}
</h3>
<p className="text-muted-foreground">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
</div>
);
};
export default Index;
=== src/styles/globals.css ===
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
=== src/vite-env.d.ts ===
/// <reference types="vite/client" />
=== tsconfig.app.json ===
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitAny": false,
"noFallthroughCasesInSwitch": false,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
=== tsconfig.json ===
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"noImplicitAny": false,
"noUnusedParameters": false,
"skipLibCheck": true,
"allowJs": true,
"noUnusedLocals": false,
"strictNullChecks": false
}
}
=== tsconfig.node.json ===
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
=== vite.config.ts ===
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
import dyadComponentTagger from '@dyad-sh/react-vite-component-tagger';
export default defineConfig(() => ({
server: {
host: "::",
port: 8080,
},
plugins: [dyadComponentTagger(), react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
}));

View File

@@ -0,0 +1,14 @@
===
role: user
message: [dump] make it smaller
Selected component: h1 (file: src/pages/Index.tsx)
Snippet:
```
<header className="flex-1 flex flex-col items-center justify-center px-4 py-16">
<h1 className="text-5xl font-extrabold mb-4 text-foreground text-center"> // <-- EDIT HERE
Launch Your Next Project
</h1>
<p className="text-xl text-muted-foreground mb-8 text-center max-w-xl">
```

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "dyad", "name": "dyad",
"version": "0.7.5", "version": "0.8.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dyad", "name": "dyad",
"version": "0.7.5", "version": "0.8.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^1.2.8", "@ai-sdk/anthropic": "^1.2.8",

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 Dyad Tech, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,65 @@
# @dyad-sh/nextjs-webpack-component-tagger
A webpack loader for Next.js that automatically adds `data-dyad-id` and `data-dyad-name` attributes to your React components. This is useful for identifying components in the DOM, for example for testing or analytics.
## Installation
```bash
npm install @dyad-sh/nextjs-webpack-component-tagger
# or
yarn add @dyad-sh/nextjs-webpack-component-tagger
# or
pnpm add @dyad-sh/nextjs-webpack-component-tagger
```
## Usage
Add the loader to your `next.config.js` file:
```ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
webpack: (config) => {
if (process.env.NODE_ENV === "development") {
config.module.rules.push({
test: /\.(jsx|tsx)$/,
exclude: /node_modules/,
enforce: "pre",
use: "@dyad-sh/nextjs-webpack-component-tagger",
});
}
return config;
},
};
export default nextConfig;
```
The loader will automatically add `data-dyad-id` and `data-dyad-name` to all your React components.
The `data-dyad-id` will be a unique identifier for each component instance, in the format `path/to/file.tsx:line:column`.
The `data-dyad-name` will be the name of the component.
## Testing & Publishing
Bump it to an alpha version and test in Dyad app, eg. `"version": "0.0.1-alpha.0",`
Then publish it:
```sh
cd packages/@dyad-sh/nextjs-webpack-component-tagger/ && npm run prepublishOnly && npm publish
```
Update the package version in the nextjs-template repo in your personal fork.
Update the `src/shared/templates.ts` to use your fork of the next.js template, e.g.
```
githubUrl: "https://github.com/wwwillchen/nextjs-template",
```
Run the E2E tests and make sure it passes.
Then, bump to a normal version, e.g. "0.1.0" and then re-publish. We'll try to match the main Dyad app version where possible.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
{
"name": "@dyad-sh/nextjs-webpack-component-tagger",
"version": "0.8.0",
"description": "A webpack loader that automatically adds data attributes to your React components in Next.js.",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"type": "module",
"files": [
"dist"
],
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "npm run build -- --watch",
"lint": "eslint . --max-warnings 0",
"prepublishOnly": "npm run build"
},
"keywords": [
"webpack",
"webpack-loader",
"nextjs",
"react",
"dyad"
],
"author": "Dyad",
"license": "Apache-2.0",
"peerDependencies": {
"webpack": "^5.0.0"
},
"dependencies": {
"@babel/parser": "^7.23.0",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5"
},
"devDependencies": {
"@types/node": "^20.8.9",
"@types/webpack": "^5.28.5",
"eslint": "^8.52.0",
"tsup": "^8.0.2",
"typescript": "^5.2.2"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,109 @@
import { parse } from "@babel/parser";
import MagicString from "magic-string";
import path from "node:path";
import { walk } from "estree-walker";
const VALID_EXTENSIONS = new Set([".jsx", ".tsx"]);
/**
* A webpack loader that adds `data-dyad-*` attributes to JSX elements.
*/
export default function dyadTaggerLoader(this: any, code: string) {
// Signal that this is an async loader
const callback = this.async();
const transform = async () => {
try {
// Skip non-JSX files and node_modules
if (
!VALID_EXTENSIONS.has(path.extname(this.resourcePath)) ||
this.resourcePath.includes("node_modules")
) {
return null;
}
// Parse the AST
const ast = parse(code, {
sourceType: "module",
plugins: ["jsx", "typescript"],
sourceFilename: this.resourcePath,
});
const ms = new MagicString(code);
const fileRelative = path.relative(this.rootContext, this.resourcePath);
let transformCount = 0;
// Walk the AST and transform JSX elements
walk(ast as any, {
enter: (node: any) => {
try {
if (node.type !== "JSXOpeningElement") return;
// Extract the tag/component name
if (node.name?.type !== "JSXIdentifier") return;
const tagName = node.name.name;
if (!tagName) return;
// Skip if already tagged
const alreadyTagged = node.attributes?.some(
(attr: any) =>
attr.type === "JSXAttribute" &&
attr.name?.name === "data-dyad-id",
);
if (alreadyTagged) return;
// Build the dyad ID
const loc = node.loc?.start;
if (!loc) return;
const dyadId = `${fileRelative}:${loc.line}:${loc.column}`;
// Inject the attributes
if (node.name.end != null) {
ms.appendLeft(
node.name.end,
` data-dyad-id="${dyadId}" data-dyad-name="${tagName}"`,
);
transformCount++;
}
} catch (error) {
console.warn(
`[dyad-tagger] Warning: Failed to process JSX node in ${this.resourcePath}:`,
error,
);
}
},
});
// Return null if no changes were made
if (transformCount === 0) {
return null;
}
const transformedCode = ms.toString();
return {
code: transformedCode,
map: ms.generateMap({ hires: true }),
};
} catch (error) {
console.warn(
`[dyad-tagger] Warning: Failed to transform ${this.resourcePath}:`,
error,
);
return null;
}
};
transform()
.then((result) => {
if (result) {
callback(null, result.code, result.map);
} else {
callback(null, code);
}
})
.catch((err) => {
console.error(`[dyad-tagger] ERROR in ${this.resourcePath}:`, err);
// Return original code instead of throwing
callback(null, code);
});
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"declaration": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,4 @@
node_modules
src
tsconfig.json
.DS_Store

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 Dyad Tech, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,54 @@
# @dyad-sh/react-vite-component-tagger
A Vite plugin that automatically adds `data-dyad-id` and `data-dyad-name` attributes to your React components. This is useful for identifying components in the DOM, for example for testing or analytics.
## Installation
```bash
npm install @dyad-sh/react-vite-component-tagger
# or
yarn add @dyad-sh/react-vite-component-tagger
# or
pnpm add @dyad-sh/react-vite-component-tagger
```
## Usage
Add the plugin to your `vite.config.ts` file:
```ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import dyadTagger from "@dyad-sh/react-vite-component-tagger";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), dyadTagger()],
});
```
The plugin will automatically add `data-dyad-id` and `data-dyad-name` to all your React components.
The `data-dyad-id` will be a unique identifier for each component instance, in the format `path/to/file.tsx:line:column`.
The `data-dyad-name` will be the name of the component.
## Testing & Publishing
Bump it to an alpha version and test in Dyad app, eg. `"version": "0.0.1-alpha.0",`
Then publish it:
```sh
cd packages/@dyad-sh/react-vite-component-tagger/ && npm run prepublishOnly && npm publish
```
Update the scaffold like this:
```sh
cd scaffold && pnpm remove @dyad-sh/react-vite-component-tagger && pnpm add -D @dyad-sh/react-vite-component-tagger
```
Run the E2E tests and make sure it passes.
Then, bump to a normal version, e.g. "0.1.0" and then re-publish. We'll try to match the main Dyad app version where possible.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
{
"name": "@dyad-sh/react-vite-component-tagger",
"version": "0.8.0",
"description": "A Vite plugin that automatically adds data attributes to your React components.",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"type": "module",
"files": [
"dist"
],
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "npm run build -- --watch",
"lint": "eslint . --max-warnings 0",
"prepublishOnly": "npm run build"
},
"keywords": [
"vite",
"vite-plugin",
"react",
"dyad"
],
"author": "Dyad",
"license": "Apache-2.0",
"peerDependencies": {
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
},
"dependencies": {
"@babel/parser": "^7.23.0",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5"
},
"devDependencies": {
"@types/node": "^20.8.9",
"eslint": "^8.52.0",
"tsup": "^8.0.2",
"typescript": "^5.2.2",
"vite": "^5.0.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,90 @@
import { parse } from "@babel/parser";
import MagicString from "magic-string";
import path from "node:path";
import { walk } from "estree-walker";
import type { Plugin } from "vite";
const VALID_EXTENSIONS = new Set([".jsx", ".tsx"]);
/**
* Returns a Vite / esbuild plug-in.
*/
export default function dyadTagger(): Plugin {
return {
name: "vite-plugin-dyad-tagger",
apply: "serve",
enforce: "pre",
async transform(code: string, id: string) {
try {
// Ignore non-jsx files and files inside node_modules
if (
!VALID_EXTENSIONS.has(path.extname(id)) ||
id.includes("node_modules")
)
return null;
const ast = parse(code, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
const ms = new MagicString(code);
const fileRelative = path.relative(process.cwd(), id);
walk(ast as any, {
enter(node: any) {
try {
if (node.type !== "JSXOpeningElement") return;
// ── 1. Extract the tag / component name ──────────────────────────────
if (node.name?.type !== "JSXIdentifier") return;
const tagName = node.name.name as string;
if (!tagName) return;
// ── 2. Check whether the tag already has data-dyad-id ───────────────
const alreadyTagged = node.attributes?.some(
(attr: any) =>
attr.type === "JSXAttribute" &&
attr.name?.name === "data-dyad-id",
);
if (alreadyTagged) return;
// ── 3. Build the id "relative/file.jsx:line:column" ─────────────────
const loc = node.loc?.start;
if (!loc) return;
const dyadId = `${fileRelative}:${loc.line}:${loc.column}`;
// ── 4. Inject the attributes just after the tag name ────────────────
if (node.name.end != null) {
ms.appendLeft(
node.name.end,
` data-dyad-id="${dyadId}" data-dyad-name="${tagName}"`,
);
}
} catch (error) {
console.warn(
`[dyad-tagger] Warning: Failed to process JSX node in ${id}:`,
error,
);
}
},
});
// If nothing changed bail out.
if (ms.toString() === code) return null;
return {
code: ms.toString(),
map: ms.generateMap({ hires: true }),
};
} catch (error) {
console.warn(
`[dyad-tagger] Warning: Failed to transform ${id}:`,
error,
);
return null;
}
},
};
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"declaration": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -62,6 +62,7 @@
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@dyad-sh/react-vite-component-tagger": "^0.8.0",
"@eslint/js": "^9.9.0", "@eslint/js": "^9.9.0",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.5.5", "@types/node": "^22.5.5",

View File

@@ -156,6 +156,9 @@ importers:
specifier: ^3.23.8 specifier: ^3.23.8
version: 3.24.3 version: 3.24.3
devDependencies: devDependencies:
'@dyad-sh/react-vite-component-tagger':
specifier: ^0.8.0
version: 0.8.0(vite@6.3.4(@types/node@22.14.1)(jiti@1.21.7)(yaml@2.7.1))
'@eslint/js': '@eslint/js':
specifier: ^9.9.0 specifier: ^9.9.0
version: 9.24.0 version: 9.24.0
@@ -211,10 +214,32 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'} engines: {node: '>=10'}
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.27.1':
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.27.5':
resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/runtime@7.27.0': '@babel/runtime@7.27.0':
resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/types@7.27.6':
resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
engines: {node: '>=6.9.0'}
'@dyad-sh/react-vite-component-tagger@0.8.0':
resolution: {integrity: sha512-97DrCLm4+kpy5fp90cLfuhjhLlhi5/p0nzCU9/pU4V9/aIpQCb+RQ/E3BJdzpMOcoieR81zPZTEiLTW5CYKgSQ==}
peerDependencies:
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
'@esbuild/aix-ppc64@0.25.3': '@esbuild/aix-ppc64@0.25.3':
resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1677,6 +1702,9 @@ packages:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
esutils@2.0.3: esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1895,6 +1923,9 @@ packages:
peerDependencies: peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
merge2@1.4.1: merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -2433,10 +2464,30 @@ snapshots:
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.27.1': {}
'@babel/parser@7.27.5':
dependencies:
'@babel/types': 7.27.6
'@babel/runtime@7.27.0': '@babel/runtime@7.27.0':
dependencies: dependencies:
regenerator-runtime: 0.14.1 regenerator-runtime: 0.14.1
'@babel/types@7.27.6':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@dyad-sh/react-vite-component-tagger@0.8.0(vite@6.3.4(@types/node@22.14.1)(jiti@1.21.7)(yaml@2.7.1))':
dependencies:
'@babel/parser': 7.27.5
estree-walker: 2.0.2
magic-string: 0.30.17
vite: 6.3.4(@types/node@22.14.1)(jiti@1.21.7)(yaml@2.7.1)
'@esbuild/aix-ppc64@0.25.3': '@esbuild/aix-ppc64@0.25.3':
optional: true optional: true
@@ -3867,6 +3918,8 @@ snapshots:
estraverse@5.3.0: {} estraverse@5.3.0: {}
estree-walker@2.0.2: {}
esutils@2.0.3: {} esutils@2.0.3: {}
eventemitter3@4.0.7: {} eventemitter3@4.0.7: {}
@@ -4049,6 +4102,10 @@ snapshots:
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
merge2@1.4.1: {} merge2@1.4.1: {}
micromatch@4.0.8: micromatch@4.0.8:

View File

@@ -1,4 +1,5 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import path from "path"; import path from "path";
@@ -7,7 +8,7 @@ export default defineConfig(() => ({
host: "::", host: "::",
port: 8080, port: 8080,
}, },
plugins: [react()], plugins: [dyadComponentTagger(), react()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),

View File

@@ -0,0 +1,6 @@
import { ComponentSelection } from "@/ipc/ipc_types";
import { atom } from "jotai";
export const selectedComponentPreviewAtom = atom<ComponentSelection | null>(
null,
);

View File

@@ -0,0 +1,152 @@
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Terminal } from "lucide-react";
import { IpcClient } from "@/ipc/ipc_client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AppUpgrade } from "@/ipc/ipc_types";
export function AppUpgrades({ appId }: { appId: number | null }) {
const queryClient = useQueryClient();
const {
data: upgrades,
isLoading,
error: queryError,
} = useQuery({
queryKey: ["app-upgrades", appId],
queryFn: () => {
if (!appId) {
return Promise.resolve([]);
}
return IpcClient.getInstance().getAppUpgrades({ appId });
},
enabled: !!appId,
});
const {
mutate: executeUpgrade,
isPending: isUpgrading,
error: mutationError,
variables: upgradingVariables,
} = useMutation({
mutationFn: (upgradeId: string) => {
if (!appId) {
throw new Error("appId is not set");
}
return IpcClient.getInstance().executeAppUpgrade({
appId,
upgradeId,
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["app-upgrades", appId] });
},
});
const handleUpgrade = (upgradeId: string) => {
executeUpgrade(upgradeId);
};
if (!appId) {
return null;
}
if (isLoading) {
return (
<div className="mt-6">
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">
App Upgrades
</h3>
<Loader2 className="h-6 w-6 animate-spin" />
</div>
);
}
if (queryError) {
return (
<div className="mt-6">
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">
App Upgrades
</h3>
<Alert variant="destructive">
<AlertTitle>Error loading upgrades</AlertTitle>
<AlertDescription>{queryError.message}</AlertDescription>
</Alert>
</div>
);
}
const currentUpgrades = upgrades?.filter((u) => u.isNeeded) ?? [];
return (
<div className="mt-6">
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">
App Upgrades
</h3>
{currentUpgrades.length === 0 ? (
<div
data-testid="no-app-upgrades-needed"
className="p-4 bg-green-50 border border-green-200 dark:bg-green-900/20 dark:border-green-800/50 rounded-lg text-sm text-green-800 dark:text-green-300"
>
App is up-to-date and has all Dyad capabilities enabled
</div>
) : (
<div className="space-y-4">
{currentUpgrades.map((upgrade: AppUpgrade) => (
<div
key={upgrade.id}
className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg flex justify-between items-start"
>
<div className="flex-grow">
<h4 className="font-semibold text-gray-800 dark:text-gray-200">
{upgrade.title}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{upgrade.description}
</p>
{mutationError && upgradingVariables === upgrade.id && (
<Alert
variant="destructive"
className="mt-3 dark:bg-destructive/15"
>
<Terminal className="h-4 w-4" />
<AlertTitle className="dark:text-red-200">
Upgrade Failed
</AlertTitle>
<AlertDescription className="text-xs text-red-400 dark:text-red-300">
{(mutationError as Error).message}{" "}
<a
onClick={(e) => {
e.stopPropagation();
IpcClient.getInstance().openExternalUrl(
upgrade.manualUpgradeUrl ?? "https://dyad.sh/docs",
);
}}
className="underline font-medium hover:dark:text-red-200"
>
Manual Upgrade Instructions
</a>
</AlertDescription>
</Alert>
)}
</div>
<Button
onClick={() => handleUpgrade(upgrade.id)}
disabled={isUpgrading && upgradingVariables === upgrade.id}
className="ml-4 flex-shrink-0"
size="sm"
data-testid={`app-upgrade-${upgrade.id}`}
>
{isUpgrading && upgradingVariables === upgrade.id ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : null}
Upgrade
</Button>
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -191,6 +191,7 @@ export function ChatHeader({
</div> </div>
<button <button
data-testid="toggle-preview-panel-button"
onClick={onTogglePreview} onClick={onTogglePreview}
className="cursor-pointer p-2 hover:bg-(--background-lightest) rounded-md" className="cursor-pointer p-2 hover:bg-(--background-lightest) rounded-md"
> >

View File

@@ -61,6 +61,9 @@ import { DragDropOverlay } from "./DragDropOverlay";
import { showError, showExtraFilesToast } from "@/lib/toast"; import { showError, showExtraFilesToast } from "@/lib/toast";
import { ChatInputControls } from "../ChatInputControls"; import { ChatInputControls } from "../ChatInputControls";
import { ChatErrorBox } from "./ChatErrorBox"; import { ChatErrorBox } from "./ChatErrorBox";
import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
import { SelectedComponentDisplay } from "./SelectedComponentDisplay";
const showTokenBarAtom = atom(false); const showTokenBarAtom = atom(false);
export function ChatInput({ chatId }: { chatId?: number }) { export function ChatInput({ chatId }: { chatId?: number }) {
@@ -78,6 +81,9 @@ export function ChatInput({ chatId }: { chatId?: number }) {
const [, setMessages] = useAtom<Message[]>(chatMessagesAtom); const [, setMessages] = useAtom<Message[]>(chatMessagesAtom);
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom); const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
const [showTokenBar, setShowTokenBar] = useAtom(showTokenBarAtom); const [showTokenBar, setShowTokenBar] = useAtom(showTokenBarAtom);
const [selectedComponent, setSelectedComponent] = useAtom(
selectedComponentPreviewAtom,
);
// Use the attachments hook // Use the attachments hook
const { const {
@@ -148,6 +154,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
const currentInput = inputValue; const currentInput = inputValue;
setInputValue(""); setInputValue("");
setSelectedComponent(null);
// Send message with attachments and clear them after sending // Send message with attachments and clear them after sending
await streamMessage({ await streamMessage({
@@ -155,6 +162,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
chatId, chatId,
attachments, attachments,
redo: false, redo: false,
selectedComponent,
}); });
clearAttachments(); clearAttachments();
posthog.capture("chat:submit"); posthog.capture("chat:submit");
@@ -282,6 +290,8 @@ export function ChatInput({ chatId }: { chatId?: number }) {
/> />
)} )}
<SelectedComponentDisplay />
{/* Use the AttachmentsList component */} {/* Use the AttachmentsList component */}
<AttachmentsList <AttachmentsList
attachments={attachments} attachments={attachments}

View File

@@ -20,87 +20,87 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
message.role === "assistant" ? "justify-start" : "justify-end" message.role === "assistant" ? "justify-start" : "justify-end"
}`} }`}
> >
<div <div className={`mt-2 w-full max-w-3xl mx-auto`}>
className={`rounded-lg p-2 mt-2 ${ <div
message.role === "assistant" className={`rounded-lg p-2 ${
? "w-full max-w-3xl mx-auto" message.role === "assistant" ? "" : "ml-24 bg-(--sidebar-accent)"
: "bg-(--sidebar-accent)" }`}
}`} >
> {message.role === "assistant" &&
{message.role === "assistant" && !message.content &&
!message.content && isStreaming &&
isStreaming && isLastMessage ? (
isLastMessage ? ( <div className="flex h-6 items-center space-x-2 p-2">
<div className="flex h-6 items-center space-x-2 p-2"> <motion.div
<motion.div className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500" animate={{ y: [0, -12, 0] }}
animate={{ y: [0, -12, 0] }} transition={{
transition={{ repeat: Number.POSITIVE_INFINITY,
repeat: Number.POSITIVE_INFINITY, duration: 0.4,
duration: 0.4, ease: "easeOut",
ease: "easeOut", repeatDelay: 1.2,
repeatDelay: 1.2, }}
}} />
/> <motion.div
<motion.div className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500" animate={{ y: [0, -12, 0] }}
animate={{ y: [0, -12, 0] }} transition={{
transition={{ repeat: Number.POSITIVE_INFINITY,
repeat: Number.POSITIVE_INFINITY, duration: 0.4,
duration: 0.4, ease: "easeOut",
ease: "easeOut", delay: 0.4,
delay: 0.4, repeatDelay: 1.2,
repeatDelay: 1.2, }}
}} />
/> <motion.div
<motion.div className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500" animate={{ y: [0, -12, 0] }}
animate={{ y: [0, -12, 0] }} transition={{
transition={{ repeat: Number.POSITIVE_INFINITY,
repeat: Number.POSITIVE_INFINITY, duration: 0.4,
duration: 0.4, ease: "easeOut",
ease: "easeOut", delay: 0.8,
delay: 0.8, repeatDelay: 1.2,
repeatDelay: 1.2, }}
}} />
/> </div>
</div> ) : (
) : ( <div
<div className="prose dark:prose-invert prose-headings:mb-2 prose-p:my-1 prose-pre:my-0 max-w-none"
className="prose dark:prose-invert prose-headings:mb-2 prose-p:my-1 prose-pre:my-0 max-w-none" suppressHydrationWarning
suppressHydrationWarning >
> {message.role === "assistant" ? (
{message.role === "assistant" ? ( <>
<> <DyadMarkdownParser content={message.content} />
<DyadMarkdownParser content={message.content} /> {isLastMessage && isStreaming && (
{isLastMessage && isStreaming && ( <div className="mt-4 ml-4 relative w-5 h-5 animate-spin">
<div className="mt-4 ml-4 relative w-5 h-5 animate-spin"> <div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full"></div>
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full"></div> <div className="absolute bottom-0 left-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-80"></div>
<div className="absolute bottom-0 left-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-80"></div> <div className="absolute bottom-0 right-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-60"></div>
<div className="absolute bottom-0 right-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-60"></div> </div>
</div> )}
)} </>
</> ) : (
) : ( <VanillaMarkdownParser content={message.content} />
<VanillaMarkdownParser content={message.content} /> )}
)} </div>
</div> )}
)} {message.approvalState && (
{message.approvalState && ( <div className="mt-2 flex items-center justify-end space-x-1 text-xs">
<div className="mt-2 flex items-center justify-end space-x-1 text-xs"> {message.approvalState === "approved" ? (
{message.approvalState === "approved" ? ( <>
<> <CheckCircle className="h-4 w-4 text-green-500" />
<CheckCircle className="h-4 w-4 text-green-500" /> <span>Approved</span>
<span>Approved</span> </>
</> ) : message.approvalState === "rejected" ? (
) : message.approvalState === "rejected" ? ( <>
<> <XCircle className="h-4 w-4 text-red-500" />
<XCircle className="h-4 w-4 text-red-500" /> <span>Rejected</span>
<span>Rejected</span> </>
</> ) : null}
) : null} </div>
</div> )}
)} </div>
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,47 @@
import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
import { useAtom } from "jotai";
import { Code2, X } from "lucide-react";
export function SelectedComponentDisplay() {
const [selectedComponent, setSelectedComponent] = useAtom(
selectedComponentPreviewAtom,
);
if (!selectedComponent) {
return null;
}
return (
<div className="p-2 pb-1" data-testid="selected-component-display">
<div className="flex items-center justify-between rounded-md bg-indigo-600/10 px-2 py-1 text-sm">
<div className="flex items-center gap-2 overflow-hidden">
<Code2
size={16}
className="flex-shrink-0 text-indigo-600 dark:text-indigo-400"
/>
<div className="flex flex-col overflow-hidden">
<span
className="truncate font-medium text-indigo-800 dark:text-indigo-300"
title={selectedComponent.name}
>
{selectedComponent.name}
</span>
<span
className="truncate text-xs text-indigo-600/80 dark:text-indigo-400/80"
title={`${selectedComponent.relativePath}:${selectedComponent.lineNumber}`}
>
{selectedComponent.relativePath}:{selectedComponent.lineNumber}
</span>
</div>
</div>
<button
onClick={() => setSelectedComponent(null)}
className="ml-2 flex-shrink-0 rounded-full p-0.5 hover:bg-indigo-600/20"
title="Deselect component"
>
<X size={18} className="text-indigo-600 dark:text-indigo-400" />
</button>
</div>
</div>
);
}

View File

@@ -17,6 +17,7 @@ import {
ChevronDown, ChevronDown,
Lightbulb, Lightbulb,
ChevronRight, ChevronRight,
MousePointerClick,
} from "lucide-react"; } from "lucide-react";
import { selectedChatIdAtom } from "@/atoms/chatAtoms"; import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
@@ -29,6 +30,14 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useStreamChat } from "@/hooks/useStreamChat"; import { useStreamChat } from "@/hooks/useStreamChat";
import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
import { ComponentSelection } from "@/ipc/ipc_types";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface ErrorBannerProps { interface ErrorBannerProps {
error: string | undefined; error: string | undefined;
@@ -165,11 +174,30 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
}, [routerContent]); }, [routerContent]);
// Navigation state // Navigation state
const [isComponentSelectorInitialized, setIsComponentSelectorInitialized] =
useState(false);
const [canGoBack, setCanGoBack] = useState(false); const [canGoBack, setCanGoBack] = useState(false);
const [canGoForward, setCanGoForward] = useState(false); const [canGoForward, setCanGoForward] = useState(false);
const [navigationHistory, setNavigationHistory] = useState<string[]>([]); const [navigationHistory, setNavigationHistory] = useState<string[]>([]);
const [currentHistoryPosition, setCurrentHistoryPosition] = useState(0); const [currentHistoryPosition, setCurrentHistoryPosition] = useState(0);
const [selectedComponentPreview, setSelectedComponentPreview] = useAtom(
selectedComponentPreviewAtom,
);
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
const [isPicking, setIsPicking] = useState(false);
// Deactivate component selector when selection is cleared
useEffect(() => {
if (!selectedComponentPreview) {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(
{ type: "deactivate-dyad-component-selector" },
"*",
);
}
setIsPicking(false);
}
}, [selectedComponentPreview]);
// Add message listener for iframe errors and navigation events // Add message listener for iframe errors and navigation events
useEffect(() => { useEffect(() => {
@@ -179,6 +207,18 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
return; return;
} }
if (event.data?.type === "dyad-component-selector-initialized") {
setIsComponentSelectorInitialized(true);
return;
}
if (event.data?.type === "dyad-component-selected") {
console.log("Component picked:", event.data);
setSelectedComponentPreview(parseComponentSelection(event.data));
setIsPicking(false);
return;
}
const { type, payload } = event.data as { const { type, payload } = event.data as {
type: type:
| "window-error" | "window-error"
@@ -262,6 +302,8 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
selectedAppId, selectedAppId,
errorMessage, errorMessage,
setErrorMessage, setErrorMessage,
setIsComponentSelectorInitialized,
setSelectedComponentPreview,
]); ]);
useEffect(() => { useEffect(() => {
@@ -280,6 +322,22 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
} }
}, [appUrl]); }, [appUrl]);
// Function to activate component selector in the iframe
const handleActivateComponentSelector = () => {
if (iframeRef.current?.contentWindow) {
const newIsPicking = !isPicking;
setIsPicking(newIsPicking);
iframeRef.current.contentWindow.postMessage(
{
type: newIsPicking
? "activate-dyad-component-selector"
: "deactivate-dyad-component-selector",
},
"*",
);
}
};
// Function to navigate back // Function to navigate back
const handleNavigateBack = () => { const handleNavigateBack = () => {
if (canGoBack && iframeRef.current?.contentWindow) { if (canGoBack && iframeRef.current?.contentWindow) {
@@ -371,6 +429,33 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
<div className="flex items-center p-2 border-b space-x-2 "> <div className="flex items-center p-2 border-b space-x-2 ">
{/* Navigation Buttons */} {/* Navigation Buttons */}
<div className="flex space-x-1"> <div className="flex space-x-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={handleActivateComponentSelector}
className={`p-1 rounded transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed ${
isPicking
? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700"
: " text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900"
}`}
disabled={
loading || !selectedAppId || !isComponentSelectorInitialized
}
data-testid="preview-pick-element-button"
>
<MousePointerClick size={16} />
</button>
</TooltipTrigger>
<TooltipContent>
<p>
{isPicking
? "Deactivate component selector"
: "Select component"}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<button <button
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed dark:text-gray-300" className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed dark:text-gray-300"
disabled={!canGoBack || loading || !selectedAppId} disabled={!canGoBack || loading || !selectedAppId}
@@ -486,3 +571,48 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
</div> </div>
); );
}; };
function parseComponentSelection(data: any): ComponentSelection | null {
if (
!data ||
data.type !== "dyad-component-selected" ||
typeof data.id !== "string" ||
typeof data.name !== "string"
) {
return null;
}
const { id, name } = data;
// The id is expected to be in the format "filepath:line:column"
const parts = id.split(":");
if (parts.length < 3) {
console.error(`Invalid component selection id format: "${id}"`);
return null;
}
const columnStr = parts.pop();
const lineStr = parts.pop();
const relativePath = parts.join(":");
if (!columnStr || !lineStr || !relativePath) {
console.error(`Could not parse component selection from id: "${id}"`);
return null;
}
const lineNumber = parseInt(lineStr, 10);
const columnNumber = parseInt(columnStr, 10);
if (isNaN(lineNumber) || isNaN(columnNumber)) {
console.error(`Could not parse line/column from id: "${id}"`);
return null;
}
return {
id,
name,
relativePath,
lineNumber,
columnNumber,
};
}

View File

@@ -1,5 +1,5 @@
import { useCallback } from "react"; import { useCallback } from "react";
import type { Message } from "@/ipc/ipc_types"; import type { ComponentSelection, Message } from "@/ipc/ipc_types";
import { useAtom, useSetAtom } from "jotai"; import { useAtom, useSetAtom } from "jotai";
import { import {
chatErrorAtom, chatErrorAtom,
@@ -56,11 +56,13 @@ export function useStreamChat({
chatId, chatId,
redo, redo,
attachments, attachments,
selectedComponent,
}: { }: {
prompt: string; prompt: string;
chatId: number; chatId: number;
redo?: boolean; redo?: boolean;
attachments?: File[]; attachments?: File[];
selectedComponent?: ComponentSelection | null;
}) => { }) => {
if ( if (
(!prompt.trim() && (!attachments || attachments.length === 0)) || (!prompt.trim() && (!attachments || attachments.length === 0)) ||
@@ -74,6 +76,7 @@ export function useStreamChat({
let hasIncrementedStreamCount = false; let hasIncrementedStreamCount = false;
try { try {
IpcClient.getInstance().streamMessage(prompt, { IpcClient.getInstance().streamMessage(prompt, {
selectedComponent: selectedComponent ?? null,
chatId, chatId,
redo, redo,
attachments, attachments,

View File

@@ -442,6 +442,8 @@ export function registerAppHandlers() {
const appPath = getDyadAppPath(app.path); const appPath = getDyadAppPath(app.path);
try { try {
// Kill any orphaned process on port 32100 (in case previous run left it)
await killProcessOnPort(32100);
await executeApp({ appPath, appId, event }); await executeApp({ appPath, appId, event });
return; return;

View File

@@ -0,0 +1,179 @@
import { createLoggedHandler } from "./safe_handle";
import log from "electron-log";
import { AppUpgrade } from "../ipc_types";
import { db } from "../../db";
import { apps } from "../../db/schema";
import { eq } from "drizzle-orm";
import { getDyadAppPath } from "../../paths/paths";
import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
const logger = log.scope("app_upgrade_handlers");
const handle = createLoggedHandler(logger);
const availableUpgrades: Omit<AppUpgrade, "isNeeded">[] = [
{
id: "component-tagger",
title: "Enable select component to edit",
description:
"Installs the Dyad component tagger Vite plugin and its dependencies.",
manualUpgradeUrl: "https://dyad.sh/docs/upgrades/select-component",
},
];
async function getApp(appId: number) {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error(`App with id ${appId} not found`);
}
return app;
}
function isComponentTaggerUpgradeNeeded(appPath: string): boolean {
const viteConfigPathJs = path.join(appPath, "vite.config.js");
const viteConfigPathTs = path.join(appPath, "vite.config.ts");
let viteConfigPath;
if (fs.existsSync(viteConfigPathTs)) {
viteConfigPath = viteConfigPathTs;
} else if (fs.existsSync(viteConfigPathJs)) {
viteConfigPath = viteConfigPathJs;
} else {
return false;
}
try {
const viteConfigContent = fs.readFileSync(viteConfigPath, "utf-8");
return !viteConfigContent.includes("@dyad-sh/react-vite-component-tagger");
} catch (e) {
logger.error("Error reading vite config", e);
return false;
}
}
async function applyComponentTagger(appPath: string) {
const viteConfigPathJs = path.join(appPath, "vite.config.js");
const viteConfigPathTs = path.join(appPath, "vite.config.ts");
let viteConfigPath;
if (fs.existsSync(viteConfigPathTs)) {
viteConfigPath = viteConfigPathTs;
} else if (fs.existsSync(viteConfigPathJs)) {
viteConfigPath = viteConfigPathJs;
} else {
throw new Error("Could not find vite.config.js or vite.config.ts");
}
let content = await fs.promises.readFile(viteConfigPath, "utf-8");
// Add import statement if not present
if (
!content.includes(
"import dyadComponentTagger from '@dyad-sh/react-vite-component-tagger';",
)
) {
// Add it after the last import statement
const lines = content.split("\n");
let lastImportIndex = -1;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("import ")) {
lastImportIndex = i;
break;
}
}
lines.splice(
lastImportIndex + 1,
0,
"import dyadComponentTagger from '@dyad-sh/react-vite-component-tagger';",
);
content = lines.join("\n");
}
// Add plugin to plugins array
if (content.includes("plugins: [")) {
if (!content.includes("dyadComponentTagger()")) {
content = content.replace(
"plugins: [",
"plugins: [dyadComponentTagger(), ",
);
}
} else {
throw new Error(
"Could not find `plugins: [` in vite.config.ts. Manual installation required.",
);
}
await fs.promises.writeFile(viteConfigPath, content);
// Install the dependency
return new Promise<void>((resolve, reject) => {
logger.info("Installing component-tagger dependency");
const process = spawn(
"pnpm add -D @dyad-sh/react-vite-component-tagger || npm install --save-dev --legacy-peer-deps @dyad-sh/react-vite-component-tagger",
{
cwd: appPath,
shell: true,
stdio: "pipe",
},
);
process.stdout?.on("data", (data) => logger.info(data.toString()));
process.stderr?.on("data", (data) => logger.error(data.toString()));
process.on("close", (code) => {
if (code === 0) {
logger.info("component-tagger dependency installed successfully");
resolve();
} else {
logger.error(`Failed to install dependency, exit code ${code}`);
reject(new Error("Failed to install dependency"));
}
});
process.on("error", (err) => {
logger.error("Failed to spawn pnpm", err);
reject(err);
});
});
}
export function registerAppUpgradeHandlers() {
handle(
"get-app-upgrades",
async (_, { appId }: { appId: number }): Promise<AppUpgrade[]> => {
const app = await getApp(appId);
const appPath = getDyadAppPath(app.path);
const upgradesWithStatus = availableUpgrades.map((upgrade) => {
let isNeeded = false;
if (upgrade.id === "component-tagger") {
isNeeded = isComponentTaggerUpgradeNeeded(appPath);
}
return { ...upgrade, isNeeded };
});
return upgradesWithStatus;
},
);
handle(
"execute-app-upgrade",
async (_, { appId, upgradeId }: { appId: number; upgradeId: string }) => {
if (!upgradeId) {
throw new Error("upgradeId is required");
}
const app = await getApp(appId);
const appPath = getDyadAppPath(app.path);
if (upgradeId === "component-tagger") {
await applyComponentTagger(appPath);
} else {
throw new Error(`Unknown upgrade id: ${upgradeId}`);
}
},
);
}

View File

@@ -160,7 +160,45 @@ export function registerChatStreamHandlers() {
} }
// Add user message to database with attachment info // Add user message to database with attachment info
const userPrompt = req.prompt + (attachmentInfo ? attachmentInfo : ""); let userPrompt = req.prompt + (attachmentInfo ? attachmentInfo : "");
if (req.selectedComponent) {
let componentSnippet = "[component snippet not available]";
try {
const componentFileContent = await readFile(
path.join(
getDyadAppPath(chat.app.path),
req.selectedComponent.relativePath,
),
"utf8",
);
const lines = componentFileContent.split("\n");
const selectedIndex = req.selectedComponent.lineNumber - 1;
// Let's get one line before and three after for context.
const startIndex = Math.max(0, selectedIndex - 1);
const endIndex = Math.min(lines.length, selectedIndex + 4);
const snippetLines = lines.slice(startIndex, endIndex);
const selectedLineInSnippetIndex = selectedIndex - startIndex;
if (snippetLines[selectedLineInSnippetIndex]) {
snippetLines[selectedLineInSnippetIndex] =
`${snippetLines[selectedLineInSnippetIndex]} // <-- EDIT HERE`;
}
componentSnippet = snippetLines.join("\n");
} catch (err) {
logger.error(`Error reading selected component file content: ${err}`);
}
userPrompt += `\n\nSelected component: ${req.selectedComponent.name} (file: ${req.selectedComponent.relativePath})
Snippet:
\`\`\`
${componentSnippet}
\`\`\`
`;
}
await db await db
.insert(messages) .insert(messages)
.values({ .values({
@@ -228,7 +266,16 @@ export function registerChatStreamHandlers() {
try { try {
const out = await extractCodebase({ const out = await extractCodebase({
appPath, appPath,
chatContext: validateChatContext(updatedChat.app.chatContext), chatContext: req.selectedComponent
? {
contextPaths: [
{
globPath: req.selectedComponent.relativePath,
},
],
smartContextAutoIncludes: [],
}
: validateChatContext(updatedChat.app.chatContext),
}); });
codebaseInfo = out.formattedOutput; codebaseInfo = out.formattedOutput;
files = out.files; files = out.files;

View File

@@ -33,6 +33,8 @@ import type {
UserBudgetInfo, UserBudgetInfo,
CopyAppParams, CopyAppParams,
App, App,
ComponentSelection,
AppUpgrade,
} from "./ipc_types"; } from "./ipc_types";
import type { AppChatContext, ProposalResult } from "@/lib/schemas"; import type { AppChatContext, ProposalResult } from "@/lib/schemas";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
@@ -224,6 +226,7 @@ export class IpcClient {
public streamMessage( public streamMessage(
prompt: string, prompt: string,
options: { options: {
selectedComponent: ComponentSelection | null;
chatId: number; chatId: number;
redo?: boolean; redo?: boolean;
attachments?: File[]; attachments?: File[];
@@ -232,7 +235,15 @@ export class IpcClient {
onError: (error: string) => void; onError: (error: string) => void;
}, },
): void { ): void {
const { chatId, redo, attachments, onUpdate, onEnd, onError } = options; const {
chatId,
redo,
attachments,
selectedComponent,
onUpdate,
onEnd,
onError,
} = options;
this.chatStreams.set(chatId, { onUpdate, onEnd, onError }); this.chatStreams.set(chatId, { onUpdate, onEnd, onError });
// Handle file attachments if provided // Handle file attachments if provided
@@ -264,6 +275,7 @@ export class IpcClient {
prompt, prompt,
chatId, chatId,
redo, redo,
selectedComponent,
attachments: fileDataArray, attachments: fileDataArray,
}) })
.catch((err) => { .catch((err) => {
@@ -284,6 +296,7 @@ export class IpcClient {
prompt, prompt,
chatId, chatId,
redo, redo,
selectedComponent,
}) })
.catch((err) => { .catch((err) => {
showError(err); showError(err);
@@ -859,6 +872,19 @@ export class IpcClient {
appId: number; appId: number;
chatContext: AppChatContext; chatContext: AppChatContext;
}): Promise<void> { }): Promise<void> {
return this.ipcRenderer.invoke("set-context-paths", params); await this.ipcRenderer.invoke("set-context-paths", params);
}
public async getAppUpgrades(params: {
appId: number;
}): Promise<AppUpgrade[]> {
return this.ipcRenderer.invoke("get-app-upgrades", params);
}
public async executeAppUpgrade(params: {
appId: number;
upgradeId: string;
}): Promise<void> {
return this.ipcRenderer.invoke("execute-app-upgrade", params);
} }
} }

View File

@@ -20,6 +20,7 @@ import { registerImportHandlers } from "./handlers/import_handlers";
import { registerSessionHandlers } from "./handlers/session_handlers"; import { registerSessionHandlers } from "./handlers/session_handlers";
import { registerProHandlers } from "./handlers/pro_handlers"; import { registerProHandlers } from "./handlers/pro_handlers";
import { registerContextPathsHandlers } from "./handlers/context_paths_handlers"; import { registerContextPathsHandlers } from "./handlers/context_paths_handlers";
import { registerAppUpgradeHandlers } from "./handlers/app_upgrade_handlers";
export function registerIpcHandlers() { export function registerIpcHandlers() {
// Register all IPC handlers by category // Register all IPC handlers by category
@@ -45,4 +46,5 @@ export function registerIpcHandlers() {
registerSessionHandlers(); registerSessionHandlers();
registerProHandlers(); registerProHandlers();
registerContextPathsHandlers(); registerContextPathsHandlers();
registerAppUpgradeHandlers();
} }

View File

@@ -21,6 +21,7 @@ export interface ChatStreamParams {
type: string; type: string;
data: string; // Base64 encoded file data data: string; // Base64 encoded file data
}>; }>;
selectedComponent: ComponentSelection | null;
} }
export interface ChatResponseEnd { export interface ChatResponseEnd {
@@ -223,3 +224,19 @@ export const UserBudgetInfoSchema = z.object({
budgetResetDate: z.date(), budgetResetDate: z.date(),
}); });
export type UserBudgetInfo = z.infer<typeof UserBudgetInfoSchema>; export type UserBudgetInfo = z.infer<typeof UserBudgetInfoSchema>;
export interface ComponentSelection {
id: string;
name: string;
relativePath: string;
lineNumber: number;
columnNumber: number;
}
export interface AppUpgrade {
id: string;
title: string;
description: string;
manualUpgradeUrl: string;
isNeeded: boolean;
}

View File

@@ -39,6 +39,7 @@ import { Loader2 } from "lucide-react";
import { invalidateAppQuery } from "@/hooks/useLoadApp"; import { invalidateAppQuery } from "@/hooks/useLoadApp";
import { useDebounce } from "@/hooks/useDebounce"; import { useDebounce } from "@/hooks/useDebounce";
import { useCheckName } from "@/hooks/useCheckName"; import { useCheckName } from "@/hooks/useCheckName";
import { AppUpgrades } from "@/components/AppUpgrades";
export default function AppDetailsPage() { export default function AppDetailsPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -341,6 +342,7 @@ export default function AppDetailsPage() {
</Button> </Button>
<GitHubConnector appId={appId} folderName={selectedApp.path} /> <GitHubConnector appId={appId} folderName={selectedApp.path} />
{appId && <SupabaseConnector appId={appId} />} {appId && <SupabaseConnector appId={appId} />}
<AppUpgrades appId={appId} />
</div> </div>
{/* Rename Dialog */} {/* Rename Dialog */}

View File

@@ -79,6 +79,8 @@ const validInvokeChannels = [
"get-user-budget", "get-user-budget",
"get-context-paths", "get-context-paths",
"set-context-paths", "set-context-paths",
"get-app-upgrades",
"execute-app-upgrade",
// Test-only channels // Test-only channels
// These should ALWAYS be guarded with IS_TEST_BUILD in the main process. // These should ALWAYS be guarded with IS_TEST_BUILD in the main process.
// We can't detect with IS_TEST_BUILD in the preload script because // We can't detect with IS_TEST_BUILD in the preload script because

View File

@@ -0,0 +1,210 @@
(() => {
const OVERLAY_ID = "__dyad_overlay__";
let overlay, label;
// The possible states are:
// { type: 'inactive' }
// { type: 'inspecting', element: ?HTMLElement }
// { type: 'selected', element: HTMLElement }
let state = { type: "inactive" };
/* ---------- helpers --------------------------------------------------- */
const css = (el, obj) => Object.assign(el.style, obj);
function makeOverlay() {
overlay = document.createElement("div");
overlay.id = OVERLAY_ID;
css(overlay, {
position: "absolute",
border: "2px solid #7f22fe",
background: "rgba(0,170,255,.05)",
pointerEvents: "none",
zIndex: "2147483647", // max
borderRadius: "4px",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
});
label = document.createElement("div");
css(label, {
position: "absolute",
left: "0",
top: "100%",
transform: "translateY(4px)",
background: "#7f22fe",
color: "#fff",
fontFamily: "monospace",
fontSize: "12px",
lineHeight: "1.2",
padding: "3px 5px",
whiteSpace: "nowrap",
borderRadius: "4px",
boxShadow: "0 1px 4px rgba(0, 0, 0, 0.1)",
});
overlay.appendChild(label);
document.body.appendChild(overlay);
}
function updateOverlay(el, isSelected = false) {
if (!overlay) makeOverlay();
const rect = el.getBoundingClientRect();
css(overlay, {
top: `${rect.top + window.scrollY}px`,
left: `${rect.left + window.scrollX}px`,
width: `${rect.width}px`,
height: `${rect.height}px`,
display: "block",
border: isSelected ? "3px solid #7f22fe" : "2px solid #7f22fe",
background: isSelected
? "rgba(127, 34, 254, 0.05)"
: "rgba(0,170,255,.05)",
});
css(label, {
background: "#7f22fe",
});
// Clear previous contents
while (label.firstChild) {
label.removeChild(label.firstChild);
}
if (isSelected) {
const editLine = document.createElement("div");
const svgNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("width", "12");
svg.setAttribute("height", "12");
svg.setAttribute("viewBox", "0 0 16 16");
svg.setAttribute("fill", "none");
Object.assign(svg.style, {
display: "inline-block",
verticalAlign: "-2px",
marginRight: "4px",
});
const path = document.createElementNS(svgNS, "path");
path.setAttribute(
"d",
"M8 0L9.48528 6.51472L16 8L9.48528 9.48528L8 16L6.51472 9.48528L0 8L6.51472 6.51472L8 0Z",
);
path.setAttribute("fill", "white");
svg.appendChild(path);
editLine.appendChild(svg);
editLine.appendChild(document.createTextNode("Edit with AI"));
label.appendChild(editLine);
}
const name = el.dataset.dyadName || "<unknown>";
const file = (el.dataset.dyadId || "").split(":")[0];
const nameEl = document.createElement("div");
nameEl.textContent = name;
label.appendChild(nameEl);
if (file) {
const fileEl = document.createElement("span");
css(fileEl, { fontSize: "10px", opacity: ".8" });
fileEl.textContent = file;
label.appendChild(fileEl);
}
}
/* ---------- event handlers -------------------------------------------- */
function onMouseMove(e) {
if (state.type !== "inspecting") return;
let el = e.target;
while (el && !el.dataset.dyadId) el = el.parentElement;
if (state.element === el) return;
state.element = el;
if (el) {
updateOverlay(el, false);
} else {
if (overlay) overlay.style.display = "none";
}
}
function onClick(e) {
if (state.type !== "inspecting" || !state.element) return;
e.preventDefault();
e.stopPropagation();
state = { type: "selected", element: state.element };
updateOverlay(state.element, true);
window.parent.postMessage(
{
type: "dyad-component-selected",
id: state.element.dataset.dyadId,
name: state.element.dataset.dyadName,
},
"*",
);
}
/* ---------- activation / deactivation --------------------------------- */
function activate() {
if (state.type === "inactive") {
window.addEventListener("mousemove", onMouseMove, true);
window.addEventListener("click", onClick, true);
}
state = { type: "inspecting", element: null };
if (overlay) {
overlay.style.display = "none";
}
}
function deactivate() {
if (state.type === "inactive") return;
window.removeEventListener("mousemove", onMouseMove, true);
window.removeEventListener("click", onClick, true);
if (overlay) {
overlay.remove();
overlay = null;
label = null;
}
state = { type: "inactive" };
}
/* ---------- message bridge -------------------------------------------- */
window.addEventListener("message", (e) => {
if (e.source !== window.parent) return;
if (e.data.type === "activate-dyad-component-selector") activate();
if (e.data.type === "deactivate-dyad-component-selector") deactivate();
});
function initializeComponentSelector() {
if (!document.body) {
console.error(
"Dyad component selector initialization failed: document.body not found.",
);
return;
}
setTimeout(() => {
if (document.body.querySelector("[data-dyad-id]")) {
window.parent.postMessage(
{
type: "dyad-component-selector-initialized",
},
"*",
);
console.debug("Dyad component selector initialized");
} else {
console.warn(
"Dyad component selector not initialized because no DOM elements were tagged",
);
}
}, 0);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializeComponentSelector);
} else {
initializeComponentSelector();
}
})();

View File

@@ -61,7 +61,7 @@ let rememberedOrigin = null; // e.g. "http://localhost:5173"
let stacktraceJsContent = null; let stacktraceJsContent = null;
let dyadShimContent = null; let dyadShimContent = null;
let dyadComponentSelectorClientContent = null;
try { try {
const stackTraceLibPath = path.join( const stackTraceLibPath = path.join(
__dirname, __dirname,
@@ -89,6 +89,24 @@ try {
); );
} }
try {
const dyadComponentSelectorClientPath = path.join(
__dirname,
"dyad-component-selector-client.js",
);
dyadComponentSelectorClientContent = fs.readFileSync(
dyadComponentSelectorClientPath,
"utf-8",
);
parentPort?.postMessage(
"[proxy-worker] dyad-component-selector-client.js loaded.",
);
} catch (error) {
parentPort?.postMessage(
`[proxy-worker] Failed to read dyad-component-selector-client.js: ${error.message}`,
);
}
/* ---------------------- helper: need to inject? ------------------------ */ /* ---------------------- helper: need to inject? ------------------------ */
function needsInjection(pathname) { function needsInjection(pathname) {
return pathname.endsWith("index.html") || pathname === "/"; return pathname.endsWith("index.html") || pathname === "/";
@@ -99,25 +117,35 @@ function injectHTML(buf) {
// These are strings that were used since the first version of the dyad shim. // These are strings that were used since the first version of the dyad shim.
// If the dyad shim is used from legacy apps which came pre-baked with the shim // If the dyad shim is used from legacy apps which came pre-baked with the shim
// as a vite plugin, then do not inject the shim twice to avoid weird behaviors. // as a vite plugin, then do not inject the shim twice to avoid weird behaviors.
if (txt.includes("window-error") && txt.includes("unhandled-rejection")) { const legacyAppWithShim =
return buf; txt.includes("window-error") && txt.includes("unhandled-rejection");
}
const scripts = []; const scripts = [];
if (stacktraceJsContent) if (!legacyAppWithShim) {
scripts.push(`<script>${stacktraceJsContent}</script>`); if (stacktraceJsContent) {
else scripts.push(`<script>${stacktraceJsContent}</script>`);
scripts.push( } else {
'<script>console.warn("[proxy-worker] stacktrace.js was not injected.");</script>', scripts.push(
); '<script>console.warn("[proxy-worker] stacktrace.js was not injected.");</script>',
);
}
if (dyadShimContent) scripts.push(`<script>${dyadShimContent}</script>`); if (dyadShimContent) {
else scripts.push(`<script>${dyadShimContent}</script>`);
} else {
scripts.push(
'<script>console.warn("[proxy-worker] dyad shim was not injected.");</script>',
);
}
}
if (dyadComponentSelectorClientContent) {
scripts.push(`<script>${dyadComponentSelectorClientContent}</script>`);
} else {
scripts.push( scripts.push(
'<script>console.warn("[proxy-worker] dyad shim was not injected.");</script>', '<script>console.warn("[proxy-worker] dyad component selector client was not injected.");</script>',
); );
}
const allScripts = scripts.join("\n"); const allScripts = scripts.join("\n");
const headRegex = /<head[^>]*>/i; const headRegex = /<head[^>]*>/i;