Unverified 提交 684bed8e authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Radix to Base UI migration (#2432)

<!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Migrates shadcn UI from Radix primitives to Base UI across the app and adds migration docs. Fixes interaction regressions (tooltips, submenu triggers, switch roles) to stabilize the UI and e2e tests. - **Migration** - Rewrote core UI in src/components/ui using Base UI (accordion, alert-dialog, button, checkbox, command, dialog, dropdown-menu, label, popover, scroll-area, select, separator, sheet, sidebar, switch, tabs, toggle, toggle-group, tooltip). - Updated 40+ components to new APIs (use polymorphic/as or direct elements instead of asChild, adjust Select onValueChange to handle undefined, align Accordion/Tabs props, replace data-[state] with data-open/checked, remove Tooltip wrappers around interactive elements, set submenu triggers to click/openOnHover=false, add aria-labels to switches/checkboxes, hide streaming animation text from a11y with aria-hidden). - Stabilized e2e tests (use click for submenu items, role-based locators for switches, waits for dialog close) and added .claude/run-e2e-update.sh for snapshot updates. - Added shadcn-migration.md documenting Radix→Base mappings and patterns. - **Dependencies** - Added @base-ui/react and removed 16 @radix-ui/react-* packages. <sup>Written for commit c89958b108c41de335aadb9ab9516a5140b8d63b. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it replaces core UI primitives and interaction patterns (dialogs/menus/selects/switches), which can cause subtle UX/a11y regressions despite primarily being framework-level refactors. > > **Overview** > **Migrates UI primitives from Radix to Base UI.** Adds `@base-ui/react` and removes many `@radix-ui/react-*` deps, then updates shared shadcn wrappers (e.g. `dialog`, `alert-dialog`, `select`, `switch`, `dropdown-menu`, `tooltip`, `accordion`) and consumers to the new Base UI prop/events and `data-*` state attributes. > > **Updates app components for new trigger/tooltip patterns and accessibility.** Replaces many `asChild` trigger patterns with direct triggers or `buttonVariants` styling, swaps some tooltips for `title` attributes, guards `Select` `onValueChange` against `undefined`, and adds `aria-label`s to switches/checkboxes. > > **Stabilizes Playwright E2E tests and snapshots.** Menu submenus now open via click (not hover) and tests wait for submenu items/dialog close; branch manager assertions switch to new trigger text; switch locators move to role/name; multiple aria snapshot fixtures are updated, and a helper script `.claude/run-e2e-update.sh` is added to regenerate snapshots. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c89958b108c41de335aadb9ab9516a5140b8d63b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com> Co-authored-by: 's avatarclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
上级 cb0fbe4e
#!/bin/bash
# Run e2e tests with snapshot update
export PLAYWRIGHT_HTML_OPEN=never
cd "$(dirname "$0")/.."
npx playwright test --update-snapshots --reporter=line
......@@ -21,14 +21,18 @@ test("attach image - home chat", async ({ po }) => {
.getByTestId("auxiliary-actions-menu")
.click();
// Hover over "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).hover();
// Click "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).click();
// Wait for submenu content to be visible
const chatContextItem = po.page.getByText("Attach file as chat context");
await expect(chatContextItem).toBeVisible();
// Set up file chooser listener BEFORE clicking the menu item
const fileChooserPromise = po.page.waitForEvent("filechooser");
// Click the menu item to trigger the file picker
await po.page.getByText("Attach file as chat context").click();
await chatContextItem.click();
// Handle the file chooser dialog
const fileChooser = await fileChooserPromise;
......@@ -49,14 +53,18 @@ test("attach image - chat", async ({ po }) => {
.getByTestId("auxiliary-actions-menu")
.click();
// Hover over "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).hover();
// Click "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).click();
// Wait for submenu content to be visible
const chatContextItem = po.page.getByText("Attach file as chat context");
await expect(chatContextItem).toBeVisible();
// Set up file chooser listener BEFORE clicking the menu item
const fileChooserPromise = po.page.waitForEvent("filechooser");
// Click the menu item to trigger the file picker
await po.page.getByText("Attach file as chat context").click();
await chatContextItem.click();
// Handle the file chooser dialog
const fileChooser = await fileChooserPromise;
......@@ -77,14 +85,18 @@ test("attach image - chat - upload to codebase", async ({ po }) => {
.getByTestId("auxiliary-actions-menu")
.click();
// Hover over "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).hover();
// Click "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).click();
// Wait for submenu content to be visible
const uploadItem = po.page.getByText("Upload file to codebase");
await expect(uploadItem).toBeVisible();
// Set up file chooser listener BEFORE clicking the menu item
const fileChooserPromise = po.page.waitForEvent("filechooser");
// Click the menu item to trigger the file picker
await po.page.getByText("Upload file to codebase").click();
await uploadItem.click();
// Handle the file chooser dialog
const fileChooser = await fileChooserPromise;
......
......@@ -33,6 +33,11 @@ for (const { testName, newAppName, buttonName, expectedVersion } of tests) {
// Click the "Copy app" button
await po.page.getByRole("button", { name: buttonName }).click();
// Wait for the copy dialog to close
await expect(po.page.getByRole("dialog")).not.toBeVisible({
timeout: Timeout.MEDIUM,
});
// Expect to be on the new app's detail page
await expect(
po.page.getByRole("heading", { name: newAppName }),
......
......@@ -56,7 +56,7 @@ test.describe("Git Collaboration", () => {
// First switch back to main to ensure we are not on feature-1
await po.page.getByTestId("branch-select-trigger").click();
await po.page.getByRole("option", { name: "main" }).click();
await expect(po.page.getByTestId("current-branch-display")).toHaveText(
await expect(po.page.getByTestId("branch-select-trigger")).toContainText(
"main",
);
......@@ -71,7 +71,7 @@ test.describe("Git Collaboration", () => {
await po.page.getByTestId("create-branch-submit-button").click();
// Verify creation (it auto-switches to the new branch, so we verify we're on it)
await expect(po.page.getByTestId("current-branch-display")).toHaveText(
await expect(po.page.getByTestId("branch-select-trigger")).toContainText(
featureBranch2,
);
......@@ -89,7 +89,7 @@ test.describe("Git Collaboration", () => {
// Switch back to main first since we can't rename the branch we're currently on
await po.page.getByTestId("branch-select-trigger").click();
await po.page.getByRole("option", { name: "main" }).click();
await expect(po.page.getByTestId("current-branch-display")).toHaveText(
await expect(po.page.getByTestId("branch-select-trigger")).toContainText(
"main",
);
......@@ -119,7 +119,7 @@ test.describe("Git Collaboration", () => {
// Switch to feature-1 and create a test file
await po.page.getByTestId("branch-select-trigger").click();
await po.page.getByRole("option", { name: featureBranch }).click();
await expect(po.page.getByTestId("current-branch-display")).toHaveText(
await expect(po.page.getByTestId("branch-select-trigger")).toContainText(
featureBranch,
);
......@@ -139,7 +139,7 @@ test.describe("Git Collaboration", () => {
// Switch back to main
await po.page.getByTestId("branch-select-trigger").click();
await po.page.getByRole("option", { name: "main" }).click();
await expect(po.page.getByTestId("current-branch-display")).toHaveText(
await expect(po.page.getByTestId("branch-select-trigger")).toContainText(
"main",
);
......
......@@ -772,7 +772,10 @@ export class PageObject {
}
async clickCopyErrorMessage() {
await this.page.getByRole("button", { name: /Copy/ }).click();
await this.page
.getByTestId("preview-error-banner")
.getByRole("button", { name: /Copy/ })
.click();
}
async getClipboardText(): Promise<string> {
......
......@@ -21,14 +21,18 @@ testSkipIfWindows("local-agent - upload file to codebase", async ({ po }) => {
.getByTestId("auxiliary-actions-menu")
.click();
// Hover over "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).hover();
// Click "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).click();
// Wait for submenu content to be visible
const uploadItem = po.page.getByText("Upload file to codebase");
await expect(uploadItem).toBeVisible();
// Set up file chooser listener BEFORE clicking the menu item
const fileChooserPromise = po.page.waitForEvent("filechooser");
// Click the menu item to trigger the file picker
await po.page.getByText("Upload file to codebase").click();
await uploadItem.click();
// Handle the file chooser dialog
const fileChooser = await fileChooserPromise;
......
......@@ -2,14 +2,16 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- img
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
- paragraph: Dyad will use the entire codebase as context.
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- img
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
- button "Close":
......
......@@ -2,19 +2,23 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- img
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/
- button "src/**/*.ts"
- text: /4 files, ~\d+ tokens/
- button:
- img
- text: /src\/sub\/\*\* 2 files, ~\d+ tokens/
- button "src/sub/**"
- text: /2 files, ~\d+ tokens/
- button:
- img
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- img
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
- button "Close":
......
......@@ -2,14 +2,16 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- img
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
- paragraph: Dyad will use the entire codebase as context.
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- img
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
- button "Close":
......
......@@ -2,25 +2,31 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- img
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/
- button "src/**/*.ts"
- text: /4 files, ~\d+ tokens/
- button:
- img
- text: /manual\/\*\* 3 files, ~\d+ tokens/
- button "manual/**"
- text: /3 files, ~\d+ tokens/
- button:
- img
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- img
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
- text: /src\/components\/\*\* 2 files, ~\d+ tokens/
- button "src/components/**"
- text: /2 files, ~\d+ tokens/
- button:
- img
- text: manual/exclude/** 0 files, ~0 tokens
- button "manual/exclude/**"
- text: 0 files, ~0 tokens
- button:
- img
- button "Close":
......
......@@ -2,25 +2,31 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- img
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/
- button "src/**/*.ts"
- text: /4 files, ~\d+ tokens/
- button:
- img
- text: /manual\/\*\* 3 files, ~\d+ tokens/
- button "manual/**"
- text: /3 files, ~\d+ tokens/
- button:
- img
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- img
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
- text: manual/exclude/** 0 files, ~0 tokens
- button "manual/exclude/**"
- text: 0 files, ~0 tokens
- button:
- img
- text: /src\/\*\* 7 files, ~\d+ tokens/
- button "src/**"
- text: /7 files, ~\d+ tokens/
- button:
- img
- button "Close":
......
......@@ -2,20 +2,23 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- img
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
- paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context.
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- img
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
- heading "Smart Context Auto-includes" [level=3]
- paragraph:
- text: These files will always be included in the context.
- img
- button:
- img
- textbox "src/**/*.config.ts"
- button "Add"
- button "Close":
......
......@@ -2,37 +2,46 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- img
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/
- button "src/**/*.ts"
- text: /4 files, ~\d+ tokens/
- button:
- img
- text: /manual\/\*\* 3 files, ~\d+ tokens/
- button "manual/**"
- text: /3 files, ~\d+ tokens/
- button:
- img
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- img
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
- text: /src\/components\/\*\* 2 files, ~\d+ tokens/
- button "src/components/**"
- text: /2 files, ~\d+ tokens/
- button:
- img
- text: /exclude\/exclude\.ts 1 files, ~\d+ tokens/
- button "exclude/exclude.ts"
- text: /1 files, ~\d+ tokens/
- button:
- img
- heading "Smart Context Auto-includes" [level=3]
- paragraph:
- text: These files will always be included in the context.
- img
- button:
- img
- textbox "src/**/*.config.ts"
- button "Add"
- text: /a\.ts 1 files, ~\d+ tokens/
- button "a.ts"
- text: /1 files, ~\d+ tokens/
- button:
- img
- text: /exclude\/\*\* 2 files, ~\d+ tokens/
- button "exclude/**"
- text: /2 files, ~\d+ tokens/
- button:
- img
- button "Close":
......
......@@ -2,20 +2,23 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- img
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
- paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context.
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- img
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
- heading "Smart Context Auto-includes" [level=3]
- paragraph:
- text: These files will always be included in the context.
- img
- button:
- img
- textbox "src/**/*.config.ts"
- button "Add"
- button "Close":
......
......@@ -2,26 +2,31 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- img
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
- paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context.
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- img
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
- heading "Smart Context Auto-includes" [level=3]
- paragraph:
- text: These files will always be included in the context.
- img
- button:
- img
- textbox "src/**/*.config.ts"
- button "Add"
- text: /a\.ts 1 files, ~\d+ tokens/
- button "a.ts"
- text: /1 files, ~\d+ tokens/
- button:
- img
- text: /manual\/\*\* 3 files, ~\d+ tokens/
- button "manual/**"
- text: /3 files, ~\d+ tokens/
- button:
- img
- button "Close":
......
......@@ -2,20 +2,23 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- img
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
- paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context.
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- img
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
- heading "Smart Context Auto-includes" [level=3]
- paragraph:
- text: These files will always be included in the context.
- img
- button:
- img
- textbox "src/**/*.config.ts"
- button "Add"
- button "Close":
......
......@@ -2,31 +2,38 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- img
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/
- button "src/**/*.ts"
- text: /4 files, ~\d+ tokens/
- button:
- img
- text: /src\/sub\/\*\* 2 files, ~\d+ tokens/
- button "src/sub/**"
- text: /2 files, ~\d+ tokens/
- button:
- img
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- img
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
- heading "Smart Context Auto-includes" [level=3]
- paragraph:
- text: These files will always be included in the context.
- img
- button:
- img
- textbox "src/**/*.config.ts"
- button "Add"
- text: /a\.ts 1 files, ~\d+ tokens/
- button "a.ts"
- text: /1 files, ~\d+ tokens/
- button:
- img
- text: /manual\/\*\* 3 files, ~\d+ tokens/
- button "manual/**"
- text: /3 files, ~\d+ tokens/
- button:
- img
- button "Close":
......
- paragraph: "Connected to GitHub Repo:"
- text: /testuser\/test-git-collab-\d+/
- combobox:
- img
- text: "Branch: main"
- combobox: main
- button "Branch actions":
- img
- img
......
- paragraph: "Connected to GitHub Repo:"
- text: testuser/existing-app
- combobox:
- img
- text: "Branch: new-branch"
- combobox: new-branch
- button "Branch actions":
- img
- img
......
- paragraph: "Connected to GitHub Repo:"
- text: testuser/existing-app
- combobox:
- img
- text: "Branch: main"
- combobox: main
- button "Branch actions":
- img
- img
......
- paragraph: "Connected to GitHub Repo:"
- text: testuser/test-new-repo-custom
- combobox:
- img
- text: "Branch: new-branch"
- combobox: new-branch
- button "Branch actions":
- img
- img
......
- paragraph: "Connected to GitHub Repo:"
- text: testuser/test-new-repo
- combobox:
- img
- text: "Branch: main"
- combobox: main
- button "Branch actions":
- img
- img
......
- paragraph: "Connected to GitHub Repo:"
- text: testuser/test-new-repo
- combobox:
- img
- text: "Branch: main"
- combobox: main
- button "Branch actions":
- img
- img
......
......@@ -6,9 +6,13 @@
- img
- text: file1.txt
- paragraph: More EOM
- button:
- button "Copy":
- img
- img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- img
......@@ -19,15 +23,22 @@
- img
- text: testing-mcp-server calculator_add
- img
- text: Tool Result
- text: "Error MCP tool 'testing-mcp-server__calculator_add' failed: keyValidator._parse is not a function..."
- img
- text: testing-mcp-server calculator_add
- button "Copy":
- img
- button "Fix with AI":
- img
- paragraph: The sum of 5 and 3 is 8. The calculation was performed successfully using the MCP calculator tool.
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
......@@ -61,9 +61,6 @@ role: assistant
message: <dyad-mcp-tool-call server="testing-mcp-server" tool="calculator_add">
{"a":1,"b":2}
</dyad-mcp-tool-call>
<dyad-mcp-tool-result server="testing-mcp-server" tool="calculator_add">
{"content":[{"type":"text","text":"3"}],"isError":false}
</dyad-mcp-tool-result>
<dyad-write path="file1.txt">
A file (2)
......
......@@ -61,9 +61,6 @@ role: assistant
message: <dyad-mcp-tool-call server="testing-mcp-server" tool="calculator_add">
{"a":1,"b":2}
</dyad-mcp-tool-call>
<dyad-mcp-tool-result server="testing-mcp-server" tool="calculator_add">
{"content":[{"type":"text","text":"3"}],"isError":false}
</dyad-mcp-tool-result>
<dyad-write path="file1.txt">
A file (2)
......
......@@ -25,7 +25,9 @@ testSkipIfWindows("supabase migrations", async ({ po }) => {
// --- SCENARIO 2: TOGGLE ON ---
// Go to settings to find the Supabase integration
await po.goToSettingsTab();
const migrationsSwitch = po.page.locator("#supabase-migrations");
const migrationsSwitch = po.page.getByRole("switch", {
name: "Write SQL migration files",
});
await migrationsSwitch.click();
await po.goToChatTab();
......@@ -86,7 +88,9 @@ testSkipIfWindows("supabase migrations with native git", async ({ po }) => {
// --- SCENARIO 2: TOGGLE ON ---
// Go to settings to find the Supabase integration
await po.goToSettingsTab();
const migrationsSwitch = po.page.locator("#supabase-migrations");
const migrationsSwitch = po.page.getByRole("switch", {
name: "Write SQL migration files",
});
await migrationsSwitch.click();
await po.goToChatTab();
......
......@@ -15,7 +15,7 @@ test("theme selection - dyad-wide default theme is persisted", async ({
.getHomeChatInputContainer()
.getByTestId("auxiliary-actions-menu")
.click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover();
await po.page.getByRole("menuitem", { name: "Themes" }).click();
await expect(po.page.getByTestId("theme-option-default")).toBeVisible();
await po.page.getByTestId("theme-option-none").click();
await expect(po.page.getByTestId("theme-option-none")).not.toBeVisible();
......@@ -28,7 +28,7 @@ test("theme selection - dyad-wide default theme is persisted", async ({
.getHomeChatInputContainer()
.getByTestId("auxiliary-actions-menu")
.click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover();
await po.page.getByRole("menuitem", { name: "Themes" }).click();
await expect(po.page.getByTestId("theme-option-none")).toHaveClass(
/bg-primary/,
);
......@@ -48,7 +48,7 @@ test("theme selection - app-specific theme is persisted", async ({ po }) => {
.getChatInputContainer()
.getByTestId("auxiliary-actions-menu")
.click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover();
await po.page.getByRole("menuitem", { name: "Themes" }).click();
await expect(po.page.getByTestId("theme-option-none")).toBeVisible();
await po.page.getByTestId("theme-option-default").click();
await expect(po.page.getByTestId("theme-option-default")).not.toBeVisible();
......@@ -58,7 +58,7 @@ test("theme selection - app-specific theme is persisted", async ({ po }) => {
.getChatInputContainer()
.getByTestId("auxiliary-actions-menu")
.click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover();
await po.page.getByRole("menuitem", { name: "Themes" }).click();
await expect(po.page.getByTestId("theme-option-default")).toHaveClass(
/bg-primary/,
);
......@@ -70,7 +70,7 @@ test("theme selection - app-specific theme is persisted", async ({ po }) => {
.getChatInputContainer()
.getByTestId("auxiliary-actions-menu")
.click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover();
await po.page.getByRole("menuitem", { name: "Themes" }).click();
await expect(po.page.getByTestId("theme-option-none")).toHaveClass(
/bg-primary/,
);
......
......@@ -108,7 +108,7 @@ test("themes management - create theme from chat input", async ({ po }) => {
.click();
// Hover over Themes submenu
await po.page.getByRole("menuitem", { name: "Themes" }).hover();
await po.page.getByRole("menuitem", { name: "Themes" }).click();
// Click "New Theme" option
await po.page.getByRole("menuitem", { name: "New Theme" }).click();
......@@ -142,7 +142,7 @@ test("themes management - create theme from chat input", async ({ po }) => {
.getHomeChatInputContainer()
.getByTestId("auxiliary-actions-menu")
.click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover();
await po.page.getByRole("menuitem", { name: "Themes" }).click();
// The custom theme should be visible and selected (has bg-primary class)
await expect(po.page.getByTestId("theme-option-custom:1")).toHaveClass(
......@@ -168,7 +168,7 @@ test("themes management - AI generator image upload limit", async ({ po }) => {
// Verify AI-Powered Generator tab is active by default
const aiTab = po.page.getByRole("tab", { name: "AI-Powered Generator" });
await expect(aiTab).toHaveAttribute("data-state", "active");
await expect(aiTab).toHaveAttribute("data-active", "");
// Verify upload area is visible
const uploadArea = po.page.getByText("Click to upload images");
......@@ -223,7 +223,7 @@ test("themes management - AI generator flow", async ({ po }) => {
// Verify AI-Powered Generator tab is active by default
const aiTab = po.page.getByRole("tab", { name: "AI-Powered Generator" });
await expect(aiTab).toHaveAttribute("data-state", "active");
await expect(aiTab).toHaveAttribute("data-active", "");
// Verify upload area is visible
const uploadArea = po.page.getByText("Click to upload images");
......@@ -290,7 +290,7 @@ test("themes management - AI generator from website URL", async ({ po }) => {
// Verify AI-Powered Generator tab is active by default
const aiTab = po.page.getByRole("tab", { name: "AI-Powered Generator" });
await expect(aiTab).toHaveAttribute("data-state", "active");
await expect(aiTab).toHaveAttribute("data-active", "");
// Switch to Website URL input source
await po.page.getByRole("button", { name: "Website URL" }).click();
......
差异被折叠。
......@@ -55,6 +55,7 @@
"@ai-sdk/provider-utils": "^4.0.13",
"@ai-sdk/xai": "^3.0.46",
"@babel/parser": "^7.28.5",
"@base-ui/react": "^1.1.0",
"@biomejs/biome": "^1.9.4",
"@dyad-sh/supabase-management-js": "v1.0.1",
"@lexical/react": "^0.33.1",
......@@ -62,22 +63,6 @@
"@monaco-editor/react": "^4.7.0-rc.0",
"@neondatabase/api-client": "^2.1.0",
"@neondatabase/serverless": "^1.0.1",
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-alert-dialog": "^1.1.13",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.0",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.1.3",
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
"@rollup/plugin-commonjs": "^28.0.3",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.3",
......
......@@ -13,6 +13,7 @@ export function AutoApproveSwitch({
<div className="flex items-center space-x-2">
<Switch
id="auto-approve"
aria-label="Auto-approve"
checked={settings?.autoApproveChanges}
onCheckedChange={() => {
updateSettings({ autoApproveChanges: !settings?.autoApproveChanges });
......
......@@ -10,6 +10,7 @@ export function AutoExpandPreviewSwitch() {
<div className="flex items-center space-x-2">
<Switch
id="auto-expand-preview"
aria-label="Auto-expand preview panel"
checked={isEnabled}
onCheckedChange={(checked) => {
updateSettings({
......
......@@ -14,6 +14,7 @@ export function AutoFixProblemsSwitch({
<div className="flex items-center space-x-2">
<Switch
id="auto-fix-problems"
aria-label="Auto-fix problems"
checked={settings?.enableAutoFixProblems}
onCheckedChange={() => {
updateSettings({
......
......@@ -15,6 +15,7 @@ export function AutoUpdateSwitch() {
<div className="flex items-center space-x-2">
<Switch
id="enable-auto-update"
aria-label="Auto-update"
checked={settings.enableAutoUpdate}
onCheckedChange={(checked) => {
updateSettings({ enableAutoUpdate: checked });
......
import { ipc } from "@/ipc/types";
import { Dialog, DialogTitle } from "@radix-ui/react-dialog";
import { DialogContent, DialogHeader } from "./ui/dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";
import { Button } from "./ui/button";
import { BugIcon, Camera } from "lucide-react";
import { useState } from "react";
......
......@@ -19,7 +19,7 @@ import {
SidebarMenu,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
......@@ -241,15 +241,15 @@ export function ChatList({ show }: { show?: boolean }) {
modal={false}
onOpenChange={(open) => setIsDropdownOpen(open)}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-1 w-4"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-4 w-4" />
</Button>
<DropdownMenuTrigger
className={buttonVariants({
variant: "ghost",
size: "icon",
className: "ml-1",
})}
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
......
......@@ -5,11 +5,6 @@ import {
SelectItem,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useSettings } from "@/hooks/useSettings";
import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota";
import type { ChatMode } from "@/lib/schemas";
......@@ -92,32 +87,24 @@ export function ChatModeSelector() {
const isMac = detectIsMac();
return (
<Select value={selectedMode} onValueChange={handleModeChange}>
<Tooltip>
<TooltipTrigger asChild>
<MiniSelectTrigger
data-testid="chat-mode-selector"
className={cn(
"h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5",
selectedMode === "build" || selectedMode === "local-agent"
? "bg-background hover:bg-muted/50 focus:bg-muted/50"
: "bg-primary/10 hover:bg-primary/20 focus:bg-primary/20 text-primary border-primary/20 dark:bg-primary/20 dark:hover:bg-primary/30 dark:focus:bg-primary/30",
)}
size="sm"
>
<SelectValue>{getModeDisplayName(selectedMode)}</SelectValue>
</MiniSelectTrigger>
</TooltipTrigger>
<TooltipContent>
<div className="flex flex-col">
<span>Open mode menu</span>
<span className="text-xs text-gray-200 dark:text-gray-500">
{isMac ? "⌘ + ." : "Ctrl + ."} to toggle
</span>
</div>
</TooltipContent>
</Tooltip>
<SelectContent align="start" onCloseAutoFocus={(e) => e.preventDefault()}>
<Select
value={selectedMode}
onValueChange={(v) => v && handleModeChange(v)}
>
<MiniSelectTrigger
data-testid="chat-mode-selector"
title={`Open mode menu (${isMac ? "⌘ + ." : "Ctrl + ."} to toggle)`}
className={cn(
"h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5",
selectedMode === "build" || selectedMode === "local-agent"
? "bg-background hover:bg-muted/50 focus:bg-muted/50"
: "bg-primary/10 hover:bg-primary/20 focus:bg-primary/20 text-primary border-primary/20 dark:bg-primary/20 dark:hover:bg-primary/30 dark:focus:bg-primary/30",
)}
size="sm"
>
<SelectValue>{getModeDisplayName(selectedMode)}</SelectValue>
</MiniSelectTrigger>
<SelectContent align="start">
{isProEnabled && (
<SelectItem value="local-agent">
<div className="flex flex-col items-start">
......
......@@ -120,14 +120,12 @@ export function ContextFilesPicker() {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<div
className="flex items-center py-2 px-3 hover:bg-accent hover:text-accent-foreground rounded-sm cursor-pointer text-sm"
data-testid="codebase-context-trigger"
>
<Settings2 className="size-4 mr-2" />
Codebase context
</div>
<DialogTrigger
className="flex items-center py-2 px-3 hover:bg-accent hover:text-accent-foreground rounded-sm cursor-pointer text-sm"
data-testid="codebase-context-trigger"
>
<Settings2 className="size-4 mr-2" />
Codebase context
</DialogTrigger>
<DialogContent className="max-w-md max-h-[80vh] overflow-y-auto">
......@@ -138,8 +136,8 @@ export function ContextFilesPicker() {
Select the files to use as context.{" "}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="size-4 cursor-help" />
<TooltipTrigger className="cursor-help">
<InfoIcon className="size-4" />
</TooltipTrigger>
<TooltipContent className="max-w-[300px]">
{isSmartContextEnabled ? (
......@@ -189,10 +187,8 @@ export function ContextFilesPicker() {
>
<div className="flex flex-1 flex-col overflow-hidden">
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate font-mono text-sm">
{p.globPath}
</span>
<TooltipTrigger className="truncate font-mono text-sm text-left">
{p.globPath}
</TooltipTrigger>
<TooltipContent>
<p>{p.globPath}</p>
......@@ -234,8 +230,8 @@ export function ContextFilesPicker() {
These files will be excluded from the context.{" "}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="size-4 cursor-help" />
<TooltipTrigger className="cursor-help">
<InfoIcon className="size-4" />
</TooltipTrigger>
<TooltipContent className="max-w-[300px]">
<p>
......@@ -281,10 +277,8 @@ export function ContextFilesPicker() {
>
<div className="flex flex-1 flex-col overflow-hidden">
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate font-mono text-sm text-red-600">
{p.globPath}
</span>
<TooltipTrigger className="truncate font-mono text-sm text-red-600 text-left">
{p.globPath}
</TooltipTrigger>
<TooltipContent>
<p>{p.globPath}</p>
......@@ -320,8 +314,8 @@ export function ContextFilesPicker() {
These files will always be included in the context.{" "}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="size-4 cursor-help" />
<TooltipTrigger className="cursor-help">
<InfoIcon className="size-4" />
</TooltipTrigger>
<TooltipContent className="max-w-[300px]">
<p>
......@@ -368,10 +362,8 @@ export function ContextFilesPicker() {
>
<div className="flex flex-1 flex-col overflow-hidden">
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate font-mono text-sm">
{p.globPath}
</span>
<TooltipTrigger className="truncate font-mono text-sm text-left">
{p.globPath}
</TooltipTrigger>
<TooltipContent>
<p>{p.globPath}</p>
......
import React, { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
......@@ -11,11 +11,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Plus, Save, Edit2 } from "lucide-react";
interface CreateOrEditPromptDialogProps {
......@@ -166,30 +161,19 @@ export function CreateOrEditPromptDialog({
return (
<Dialog open={open} onOpenChange={setOpen}>
{trigger ? (
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogTrigger>{trigger}</DialogTrigger>
) : mode === "create" ? (
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> New Prompt
</Button>
<DialogTrigger className={buttonVariants()}>
<Plus className="mr-2 h-4 w-4" /> New Prompt
</DialogTrigger>
) : (
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button
size="icon"
variant="ghost"
data-testid="edit-prompt-button"
>
<Edit2 className="h-4 w-4" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Edit prompt</p>
</TooltipContent>
</Tooltip>
<DialogTrigger
className={buttonVariants({ variant: "ghost", size: "icon" })}
data-testid="edit-prompt-button"
title="Edit prompt"
>
<Edit2 className="h-4 w-4" />
</DialogTrigger>
)}
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
......
......@@ -58,7 +58,7 @@ export function DefaultChatModeSelector() {
</label>
<Select
value={effectiveDefault}
onValueChange={handleDefaultChatModeChange}
onValueChange={(v) => v && handleDefaultChatModeChange(v)}
>
<SelectTrigger className="w-40" id="default-chat-mode">
<SelectValue>{getModeDisplayName(effectiveDefault)}</SelectValue>
......
import React from "react";
import { Button } from "@/components/ui/button";
import { Trash2, Loader2 } from "lucide-react";
import {
AlertDialog,
......@@ -12,11 +11,7 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { buttonVariants } from "@/components/ui/button";
interface DeleteConfirmationDialogProps {
itemName: string;
......@@ -36,25 +31,16 @@ export function DeleteConfirmationDialog({
return (
<AlertDialog>
{trigger ? (
<AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
<AlertDialogTrigger>{trigger}</AlertDialogTrigger>
) : (
<Tooltip>
<TooltipTrigger asChild>
<AlertDialogTrigger asChild>
<Button
size="icon"
variant="ghost"
data-testid="delete-prompt-button"
disabled={isDeleting}
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Delete {itemType.toLowerCase()}</p>
</TooltipContent>
</Tooltip>
<AlertDialogTrigger
className={buttonVariants({ variant: "ghost", size: "icon" })}
data-testid="delete-prompt-button"
disabled={isDeleting}
title={`Delete ${itemType.toLowerCase()}`}
>
<Trash2 className="h-4 w-4" />
</AlertDialogTrigger>
)}
<AlertDialogContent>
<AlertDialogHeader>
......
import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
......@@ -11,11 +12,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Save, Edit2, Loader2 } from "lucide-react";
import { showError } from "@/lib/toast";
import { toast } from "sonner";
......@@ -120,24 +116,15 @@ export function EditThemeDialog({
return (
<Dialog open={open} onOpenChange={setOpen}>
{trigger ? (
<DialogTrigger asChild>{trigger}</DialogTrigger>
<span onClick={() => setOpen(true)}>{trigger}</span>
) : (
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button
size="icon"
variant="ghost"
data-testid="edit-theme-button"
>
<Edit2 className="h-4 w-4" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Edit theme</p>
</TooltipContent>
</Tooltip>
<DialogTrigger
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
data-testid="edit-theme-button"
title="Edit theme"
>
<Edit2 className="h-4 w-4" />
</DialogTrigger>
)}
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
......
......@@ -39,8 +39,8 @@ export function ForceCloseDialog({
<AlertTriangle className="h-5 w-5 text-yellow-500" />
<AlertDialogTitle>Force Close Detected</AlertDialogTitle>
</div>
<AlertDialogDescription asChild>
<div className="space-y-4 pt-2">
<AlertDialogDescription render={<div />}>
<div className="space-y-4 pt-2 text-muted-foreground">
<div className="text-base">
The app was not closed properly the last time it was running.
This could indicate a crash or unexpected termination.
......
......@@ -997,7 +997,7 @@ export function UnconnectedGitHubConnector({
</Label>
<Select
value={selectedRepo}
onValueChange={setSelectedRepo}
onValueChange={(v) => setSelectedRepo(v ?? "")}
disabled={isLoadingRepos}
>
<SelectTrigger
......@@ -1037,7 +1037,7 @@ export function UnconnectedGitHubConnector({
if (value === "custom") {
setBranchInputMode("custom");
setCustomBranchName("");
} else {
} else if (value) {
setBranchInputMode("select");
setSelectedBranch(value);
}
......
import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import {
Select,
......@@ -54,12 +55,6 @@ import {
import { Label } from "@/components/ui/label";
import { showSuccess, showError, showInfo } from "@/lib/toast";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Card,
......@@ -388,7 +383,7 @@ export function GithubBranchManager({
<div className="flex gap-2">
<Select
value={currentBranch || ""}
onValueChange={handleSwitchBranch}
onValueChange={(v) => v && handleSwitchBranch(v)}
disabled={
isSwitching ||
isDeleting ||
......@@ -419,23 +414,14 @@ export function GithubBranchManager({
</Select>
<DropdownMenu>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
title="Branch actions"
data-testid="branch-actions-menu-trigger"
>
<EllipsisVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Branch actions</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuTrigger
className={cn(buttonVariants({ variant: "outline", size: "icon" }))}
title="Branch actions"
aria-label="Branch actions"
data-testid="branch-actions-menu-trigger"
>
<EllipsisVertical className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => setShowCreateDialog(true)}
......@@ -487,7 +473,10 @@ export function GithubBranchManager({
</div>
<div>
<Label htmlFor="source-branch">Source Branch</Label>
<Select value={sourceBranch} onValueChange={setSourceBranch}>
<Select
value={sourceBranch}
onValueChange={(v) => setSourceBranch(v ?? "")}
>
<SelectTrigger
className="mt-2"
data-testid="source-branch-select-trigger"
......@@ -790,15 +779,11 @@ export function GithubBranchManager({
if (open) setIsExpanded(true);
}}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
data-testid={`branch-actions-${branch}`}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
<DropdownMenuTrigger
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-6 w-6"
data-testid={`branch-actions-${branch}`}
>
<MoreHorizontal className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
......
......@@ -16,16 +16,11 @@ import { Input } from "@/components/ui/input";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@radix-ui/react-label";
import { Label } from "@/components/ui/label";
import { useNavigate } from "@tanstack/react-router";
import { useStreamChat } from "@/hooks/useStreamChat";
import type { GithubRepository } from "@/ipc/types";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useSetAtom } from "jotai";
import { useLoadApps } from "@/hooks/useLoadApps";
......@@ -402,6 +397,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<div className="flex items-center space-x-2">
<Checkbox
id="copy-to-dyad-apps"
aria-label="Copy to the dyad-apps folder"
checked={copyToDyadApps}
onCheckedChange={(checked) =>
setCopyToDyadApps(checked === true)
......@@ -446,7 +442,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</div>
</div>
<Accordion type="single" collapsible>
<Accordion>
<AccordionItem value="advanced-options">
<AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options
......@@ -489,19 +485,12 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
{hasAiRules === false && (
<Alert className="border-yellow-500/20 text-yellow-500 flex items-start gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-4 w-4 flex-shrink-0 mt-1" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
AI_RULES.md lets Dyad know which tech stack to
use for editing the app
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span
title="AI_RULES.md lets Dyad know which tech stack to use for editing the app"
className="flex-shrink-0 mt-1"
>
<Info className="h-4 w-4" />
</span>
<AlertDescription className="text-xs sm:text-sm">
No AI_RULES.md found. Dyad will automatically generate
one after importing.
......@@ -622,7 +611,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
{repos.length > 0 && (
<>
<Accordion type="single" collapsible>
<Accordion>
<AccordionItem value="advanced-options">
<AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options
......@@ -705,7 +694,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
)}
</div>
<Accordion type="single" collapsible>
<Accordion>
<AccordionItem value="advanced-options">
<AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options
......
......@@ -76,7 +76,10 @@ export const MaxChatTurnsSelector: React.FC = () => {
>
Maximum number of chat turns used in context
</label>
<Select value={currentValue} onValueChange={handleValueChange}>
<Select
value={currentValue}
onValueChange={(v) => v && handleValueChange(v)}
>
<SelectTrigger className="w-[180px]" id="max-chat-turns">
<SelectValue placeholder="Select turns" />
</SelectTrigger>
......
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge";
import { Wrench } from "lucide-react";
import { useMcp } from "@/hooks/useMcp";
......@@ -31,23 +24,13 @@ export function McpToolsPicker() {
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="outline"
className="has-[>svg]:px-2"
size="sm"
data-testid="mcp-tools-button"
>
<Wrench className="size-4" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Tools</TooltipContent>
</Tooltip>
</TooltipProvider>
<PopoverTrigger
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-8 px-2"
data-testid="mcp-tools-button"
title="Tools"
>
<Wrench className="size-4" />
</PopoverTrigger>
<PopoverContent
className="w-120 max-h-[80vh] overflow-y-auto"
align="start"
......
......@@ -36,14 +36,10 @@ export function NeonConnector() {
onClick={() => {
ipc.system.openExternalUrl("https://console.neon.tech/");
}}
className="ml-2 px-2 py-1 h-8 mb-2"
style={{ display: "inline-flex", alignItems: "center" }}
asChild
className="ml-2 px-2 py-1 h-8 mb-2 inline-flex items-center gap-1"
>
<div className="flex items-center gap-1">
Neon
<ExternalLink className="h-3 w-3" />
</div>
Neon
<ExternalLink className="h-3 w-3" />
</Button>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 pb-3">
......
......@@ -27,11 +27,6 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { CreateCustomProviderDialog } from "./CreateCustomProviderDialog";
......@@ -128,34 +123,26 @@ export function ProviderSettingsGrid() {
className="flex items-center justify-end"
onClick={(e) => e.stopPropagation()}
>
<Tooltip>
<TooltipTrigger asChild>
<Button
data-testid="edit-custom-provider"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 hover:bg-muted rounded-md"
onClick={() => handleEditProvider(provider)}
>
<Edit className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Edit Provider</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
data-testid="delete-custom-provider"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10 rounded-md"
onClick={() => setProviderToDelete(provider.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete Provider</TooltipContent>
</Tooltip>
<Button
data-testid="edit-custom-provider"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 hover:bg-muted rounded-md"
title="Edit Provider"
onClick={() => handleEditProvider(provider)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
data-testid="delete-custom-provider"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10 rounded-md"
title="Delete Provider"
onClick={() => setProviderToDelete(provider.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
<CardTitle className="text-lg font-medium mb-2">
......
......@@ -56,7 +56,7 @@ export function ReleaseChannelSelector() {
</label>
<Select
value={settings.releaseChannel}
onValueChange={handleReleaseChannelChange}
onValueChange={(v) => v && handleReleaseChannelChange(v)}
>
<SelectTrigger className="w-32" id="release-channel">
<SelectValue />
......
......@@ -36,7 +36,7 @@ export function RuntimeModeSelector() {
</Label>
<Select
value={settings.runtimeMode2 ?? "host"}
onValueChange={handleRuntimeModeChange}
onValueChange={(v) => v && handleRuntimeModeChange(v)}
>
<SelectTrigger className="w-48" id="runtime-mode">
<SelectValue />
......
import { DialogTitle } from "@radix-ui/react-dialog";
import { Dialog, DialogContent, DialogHeader } from "./ui/dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";
import { Button } from "./ui/button";
import { BugIcon } from "lucide-react";
......
......@@ -187,11 +187,7 @@ export function SetupBanner() {
setIsVisible={setIsOnboardingVisible}
/>
<div className={bannerClasses}>
<Accordion
type="multiple"
className="w-full"
defaultValue={itemsNeedAction}
>
<Accordion multiple className="w-full" defaultValue={itemsNeedAction}>
<AccordionItem
value="node-setup"
className={cn(
......
......@@ -176,18 +176,14 @@ export function SupabaseConnector({ appId }: { appId: number }) {
`https://supabase.com/dashboard/project/${app.supabaseProjectId}`,
);
}}
className="ml-2 px-2 py-1"
style={{ display: "inline-flex", alignItems: "center" }}
asChild
className="ml-2 px-2 py-1 inline-flex items-center gap-2"
>
<div className="flex items-center gap-2">
<img
src={isDarkMode ? supabaseLogoDark : supabaseLogoLight}
alt="Supabase Logo"
style={{ height: 20, width: "auto", marginRight: 4 }}
/>
<ExternalLink className="h-4 w-4" />
</div>
<img
src={isDarkMode ? supabaseLogoDark : supabaseLogoLight}
alt="Supabase Logo"
style={{ height: 20, width: "auto", marginRight: 4 }}
/>
<ExternalLink className="h-4 w-4" />
</Button>
</CardTitle>
<CardDescription className="flex flex-col gap-1.5 text-sm">
......@@ -363,7 +359,7 @@ export function SupabaseConnector({ appId }: { appId: number }) {
<Label htmlFor="project-select">Project</Label>
<Select
value={currentProjectValue}
onValueChange={handleProjectSelect}
onValueChange={(v) => v && handleProjectSelect(v)}
>
<SelectTrigger id="project-select">
<SelectValue placeholder="Select a project" />
......
......@@ -138,6 +138,7 @@ export function SupabaseIntegration() {
<div className="flex items-center space-x-3">
<Switch
id="supabase-migrations"
aria-label="Write SQL migration files"
checked={!!settings?.enableSupabaseWriteSqlMigration}
onCheckedChange={handleMigrationSettingChange}
/>
......@@ -162,6 +163,7 @@ export function SupabaseIntegration() {
<div className="flex items-center space-x-3">
<Switch
id="skip-prune-edge-functions"
aria-label="Keep extra Supabase edge functions"
checked={!!settings?.skipPruneEdgeFunctions}
onCheckedChange={handleSkipPruneSettingChange}
/>
......
......@@ -8,6 +8,7 @@ export function TelemetrySwitch() {
<div className="flex items-center space-x-2">
<Switch
id="telemetry-switch"
aria-label="Telemetry"
checked={settings?.telemetryConsent === "opted_in"}
onCheckedChange={() => {
updateSettings({
......
......@@ -59,7 +59,10 @@ export const ThinkingBudgetSelector: React.FC = () => {
>
Thinking Budget
</label>
<Select value={currentValue} onValueChange={handleValueChange}>
<Select
value={currentValue}
onValueChange={(v) => v && handleValueChange(v)}
>
<SelectTrigger className="w-[180px]" id="thinking-budget">
<SelectValue placeholder="Select budget" />
</SelectTrigger>
......
......@@ -578,7 +578,7 @@ function UnconnectedVercelConnector({
</Label>
<Select
value={selectedProject}
onValueChange={setSelectedProject}
onValueChange={(v) => setSelectedProject(v ?? "")}
disabled={isLoadingProjects}
>
<SelectTrigger
......
......@@ -201,32 +201,28 @@ function AppIcons({
return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
as={Link}
to={item.to}
size="sm"
className="font-medium w-14"
className={`font-medium w-14 flex flex-col items-center gap-1 h-14 mb-2 rounded-2xl ${
isActive ? "bg-sidebar-accent" : ""
}`}
onMouseEnter={() => {
if (item.title === "Apps") {
onHoverChange("start-hover:app");
} else if (item.title === "Chat") {
onHoverChange("start-hover:chat");
} else if (item.title === "Settings") {
onHoverChange("start-hover:settings");
} else if (item.title === "Library") {
onHoverChange("start-hover:library");
}
}}
>
<Link
to={item.to}
className={`flex flex-col items-center gap-1 h-14 mb-2 rounded-2xl ${
isActive ? "bg-sidebar-accent" : ""
}`}
onMouseEnter={() => {
if (item.title === "Apps") {
onHoverChange("start-hover:app");
} else if (item.title === "Chat") {
onHoverChange("start-hover:chat");
} else if (item.title === "Settings") {
onHoverChange("start-hover:settings");
} else if (item.title === "Library") {
onHoverChange("start-hover:library");
}
}}
>
<div className="flex flex-col items-center gap-1">
<item.icon className="h-5 w-5" />
<span className={"text-xs"}>{item.title}</span>
</div>
</Link>
<div className="flex flex-col items-center gap-1">
<item.icon className="h-5 w-5" />
<span className={"text-xs"}>{item.title}</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
);
......
......@@ -3,12 +3,6 @@ import { Star } from "lucide-react";
import { SidebarMenuItem } from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
import type { ListedApp } from "@/ipc/types/app";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type AppItemProps = {
app: ListedApp;
......@@ -27,56 +21,47 @@ export function AppItem({
}: AppItemProps) {
return (
<SidebarMenuItem className="mb-1 relative ">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex w-[190px] items-center">
<Button
variant="ghost"
onClick={() => handleAppClick(app.id)}
className={`justify-start w-full text-left py-3 hover:bg-sidebar-accent/80 ${
selectedAppId === app.id
? "bg-sidebar-accent text-sidebar-accent-foreground"
: ""
}`}
data-testid={`app-list-item-${app.name}`}
>
<div className="flex flex-col w-4/5">
<span className="truncate">{app.name}</span>
<span className="text-xs text-gray-500">
{formatDistanceToNow(new Date(app.createdAt), {
addSuffix: true,
})}
</span>
</div>
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) => handleToggleFavorite(app.id, e)}
disabled={isFavoriteLoading}
className="absolute top-1 right-1 p-1 mx-1 h-6 w-6 z-10"
key={app.id}
data-testid="favorite-button"
>
<Star
size={12}
className={
app.isFavorite
? "fill-[#6c55dc] text-[#6c55dc]"
: selectedAppId === app.id
? "hover:fill-black hover:text-black"
: "hover:fill-[#6c55dc] hover:stroke-[#6c55dc] hover:text-[#6c55dc]"
}
/>
</Button>
</div>
</TooltipTrigger>
<TooltipContent side="right">
<p>{app.name}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="flex w-[190px] items-center" title={app.name}>
<Button
variant="ghost"
onClick={() => handleAppClick(app.id)}
className={`justify-start w-full text-left py-3 hover:bg-sidebar-accent/80 ${
selectedAppId === app.id
? "bg-sidebar-accent text-sidebar-accent-foreground"
: ""
}`}
data-testid={`app-list-item-${app.name}`}
>
<div className="flex flex-col w-4/5">
<span className="truncate">{app.name}</span>
<span className="text-xs text-gray-500">
{formatDistanceToNow(new Date(app.createdAt), {
addSuffix: true,
})}
</span>
</div>
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) => handleToggleFavorite(app.id, e)}
disabled={isFavoriteLoading}
className="absolute top-1 right-1 p-1 mx-1 h-6 w-6 z-10"
key={app.id}
data-testid="favorite-button"
>
<Star
size={12}
className={
app.isFavorite
? "fill-[#6c55dc] text-[#6c55dc]"
: selectedAppId === app.id
? "hover:fill-black hover:text-black"
: "hover:fill-[#6c55dc] hover:stroke-[#6c55dc] hover:text-[#6c55dc]"
}
/>
</Button>
</div>
</SidebarMenuItem>
);
}
......@@ -72,8 +72,8 @@ export function AgentConsentBanner({
{toolDescription && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-3.5 h-3.5 text-muted-foreground cursor-help" />
<TooltipTrigger className="cursor-help">
<Info className="w-3.5 h-3.5 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<p className="text-xs">{toolDescription}</p>
......
......@@ -26,12 +26,6 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { ContextFilesPicker } from "@/components/ContextFilesPicker";
import { FileAttachmentDropdown } from "./FileAttachmentDropdown";
import { CustomThemeDialog } from "@/components/CustomThemeDialog";
......@@ -137,18 +131,14 @@ export function AuxiliaryActionsMenu({
return (
<>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="has-[>svg]:px-2 hover:bg-muted bg-primary/10 text-primary cursor-pointer rounded-xl"
data-testid="auxiliary-actions-menu"
>
<Plus
size={20}
className={`transition-transform duration-200 ${isOpen ? "rotate-45" : "rotate-0"}`}
/>
</Button>
<DropdownMenuTrigger
className="inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-muted bg-primary/10 text-primary cursor-pointer h-8 px-2"
data-testid="auxiliary-actions-menu"
>
<Plus
size={20}
className={`transition-transform duration-200 ${isOpen ? "rotate-45" : "rotate-0"}`}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/* Codebase Context */}
......@@ -193,31 +183,26 @@ export function AuxiliaryActionsMenu({
{themes?.map((theme) => {
const isSelected = currentThemeId === theme.id;
return (
<Tooltip key={theme.id}>
<TooltipTrigger asChild>
<DropdownMenuItem
onClick={() => handleThemeSelect(theme.id)}
className={`py-2 px-3 ${isSelected ? "bg-primary/10" : ""}`}
data-testid={`theme-option-${theme.id}`}
>
<div className="flex items-center w-full">
{theme.icon === "palette" && (
<Palette
size={16}
className="mr-2 text-muted-foreground"
/>
)}
<span className="flex-1">{theme.name}</span>
{isSelected && (
<Check size={16} className="text-primary ml-2" />
)}
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{theme.description}
</TooltipContent>
</Tooltip>
<DropdownMenuItem
key={theme.id}
onClick={() => handleThemeSelect(theme.id)}
className={`py-2 px-3 ${isSelected ? "bg-primary/10" : ""}`}
data-testid={`theme-option-${theme.id}`}
title={theme.description}
>
<div className="flex items-center w-full">
{theme.icon === "palette" && (
<Palette
size={16}
className="mr-2 text-muted-foreground"
/>
)}
<span className="flex-1">{theme.name}</span>
{isSelected && (
<Check size={16} className="text-primary ml-2" />
)}
</div>
</DropdownMenuItem>
);
})}
......@@ -229,32 +214,24 @@ export function AuxiliaryActionsMenu({
const themeId = `custom:${theme.id}`;
const isSelected = currentThemeId === themeId;
return (
<Tooltip key={themeId}>
<TooltipTrigger asChild>
<DropdownMenuItem
onClick={() => handleThemeSelect(themeId)}
className={`py-2 px-3 ${isSelected ? "bg-primary/10" : ""}`}
data-testid={`theme-option-${themeId}`}
>
<div className="flex items-center w-full">
<Brush
size={16}
className="mr-2 text-muted-foreground"
/>
<span className="flex-1">{theme.name}</span>
{isSelected && (
<Check
size={16}
className="text-primary ml-2"
/>
)}
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{theme.description || "Custom theme"}
</TooltipContent>
</Tooltip>
<DropdownMenuItem
key={themeId}
onClick={() => handleThemeSelect(themeId)}
className={`py-2 px-3 ${isSelected ? "bg-primary/10" : ""}`}
data-testid={`theme-option-${themeId}`}
title={theme.description || "Custom theme"}
>
<div className="flex items-center w-full">
<Brush
size={16}
className="mr-2 text-muted-foreground"
/>
<span className="flex-1">{theme.name}</span>
{isSelected && (
<Check size={16} className="text-primary ml-2" />
)}
</div>
</DropdownMenuItem>
);
})}
</>
......
......@@ -6,11 +6,6 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { ChatSummary } from "@/lib/schemas";
import { useAtomValue } from "jotai";
import {
......@@ -32,24 +27,18 @@ export function ChatActivityButton() {
}, [isStreamingById]);
return (
<Popover open={open} onOpenChange={setOpen}>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<button
className="no-app-region-drag relative flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
data-testid="chat-activity-button"
>
{isAnyStreaming && (
<span className="pointer-events-none absolute inset-0 flex items-center justify-center">
<span className="block size-7 rounded-full border-3 border-blue-500/60 border-t-transparent animate-spin" />
</span>
)}
<Bell size={16} />
</button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Recent chat activity</TooltipContent>
</Tooltip>
<PopoverTrigger
className="no-app-region-drag relative flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
data-testid="chat-activity-button"
title="Recent chat activity"
>
{isAnyStreaming && (
<span className="pointer-events-none absolute inset-0 flex items-center justify-center">
<span className="block size-7 rounded-full border-3 border-blue-500/60 border-t-transparent animate-spin" />
</span>
)}
<Bell size={16} />
</PopoverTrigger>
<PopoverContent
align="end"
className="w-80 p-0 max-h-[50vh] overflow-y-auto"
......
......@@ -119,7 +119,7 @@ export function ChatHeader({
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<TooltipTrigger>
<span className="flex items-center gap-1">
{isAnyCheckoutVersionInProgress ? (
<>
......
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论