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,6 +2,7 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
......@@ -9,6 +10,7 @@
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
......
......@@ -2,18 +2,22 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- 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.
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
......
......@@ -2,6 +2,7 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
......@@ -9,6 +10,7 @@
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- button:
- img
- textbox "node_modules/**/*"
- button "Add"
......
......@@ -2,25 +2,31 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- 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.
- 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.
- 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.
- 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,6 +2,7 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
......@@ -9,12 +10,14 @@
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- 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.
- button:
- img
- textbox "src/**/*.config.ts"
- button "Add"
......
......@@ -2,37 +2,46 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- 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.
- 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.
- 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,6 +2,7 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
......@@ -9,12 +10,14 @@
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- 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.
- button:
- img
- textbox "src/**/*.config.ts"
- button "Add"
......
......@@ -2,6 +2,7 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
......@@ -9,19 +10,23 @@
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- 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.
- 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,6 +2,7 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- button:
- img
- textbox "src/**/*.tsx"
- button "Add"
......@@ -9,12 +10,14 @@
- heading "Exclude Paths" [level=3]
- paragraph:
- text: These files will be excluded from the context.
- 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.
- button:
- img
- textbox "src/**/*.config.ts"
- button "Add"
......
......@@ -2,31 +2,38 @@
- heading "Codebase Context" [level=2]
- paragraph:
- text: Select the files to use as context.
- 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.
- 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.
- 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"
<DropdownMenuTrigger
className={buttonVariants({
variant: "ghost",
size: "icon",
className: "ml-1",
})}
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-4 w-4" />
</Button>
</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,11 +87,13 @@ export function ChatModeSelector() {
const isMac = detectIsMac();
return (
<Select value={selectedMode} onValueChange={handleModeChange}>
<Tooltip>
<TooltipTrigger asChild>
<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"
......@@ -107,17 +104,7 @@ export function ChatModeSelector() {
>
<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()}>
<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
<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
</div>
</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">
<TooltipTrigger className="truncate font-mono text-sm text-left">
{p.globPath}
</span>
</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">
<TooltipTrigger className="truncate font-mono text-sm text-red-600 text-left">
{p.globPath}
</span>
</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">
<TooltipTrigger className="truncate font-mono text-sm text-left">
{p.globPath}
</span>
</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>
<DialogTrigger className={buttonVariants()}>
<Plus className="mr-2 h-4 w-4" /> New Prompt
</Button>
</DialogTrigger>
) : (
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button
size="icon"
variant="ghost"
<DialogTrigger
className={buttonVariants({ variant: "ghost", size: "icon" })}
data-testid="edit-prompt-button"
title="Edit prompt"
>
<Edit2 className="h-4 w-4" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Edit prompt</p>
</TooltipContent>
</Tooltip>
)}
<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"
<AlertDialogTrigger
className={buttonVariants({ variant: "ghost", size: "icon" })}
data-testid="delete-prompt-button"
disabled={isDeleting}
title={`Delete ${itemType.toLowerCase()}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Delete {itemType.toLowerCase()}</p>
</TooltipContent>
</Tooltip>
)}
<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"
<DialogTrigger
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
data-testid="edit-theme-button"
title="Edit theme"
>
<Edit2 className="h-4 w-4" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Edit theme</p>
</TooltipContent>
</Tooltip>
)}
<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"
<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" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Branch actions</TooltipContent>
</Tooltip>
</TooltipProvider>
<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"
<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" />
</Button>
</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"
<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" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Tools</TooltipContent>
</Tooltip>
</TooltipProvider>
<PopoverContent
className="w-120 max-h-[80vh] overflow-y-auto"
align="start"
......
import { isDyadProEnabled, type LargeLanguageModel } from "@/lib/schemas";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
DropdownMenu,
DropdownMenuContent,
......@@ -170,34 +165,20 @@ export function ModelPicker() {
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="flex items-center gap-2 h-8 max-w-[130px] px-1.5 text-xs-sm"
<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 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-8 max-w-[130px] px-1.5 text-xs-sm gap-2"
title={modelDisplayName}
>
<span className="truncate">
{modelDisplayName === "Auto" && (
<>
<span className="text-xs text-muted-foreground">
Model:
</span>{" "}
<span className="text-xs text-muted-foreground">Model:</span>{" "}
</>
)}
{modelDisplayName}
</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>{modelDisplayName}</TooltipContent>
</Tooltip>
<DropdownMenuContent
className="w-64"
align="start"
onCloseAutoFocus={(e) => e.preventDefault()}
>
<DropdownMenuContent className="w-64" align="start">
<DropdownMenuLabel>Cloud Models</DropdownMenuLabel>
<DropdownMenuSeparator />
......@@ -259,9 +240,9 @@ export function ModelPicker() {
{autoModels.length > 0 && (
<>
{autoModels.map((model) => (
<Tooltip key={`auto-${model.apiName}`}>
<TooltipTrigger asChild>
<DropdownMenuItem
key={`auto-${model.apiName}`}
title={model.description}
className={
selectedModel.provider === "auto" &&
selectedModel.name === model.apiName
......@@ -294,11 +275,6 @@ export function ModelPicker() {
</div>
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))}
{Object.keys(modelsByProviders).length > 1 && (
<DropdownMenuSeparator />
......@@ -355,9 +331,9 @@ export function ModelPicker() {
</DropdownMenuLabel>
<DropdownMenuSeparator />
{models.map((model) => (
<Tooltip key={`${providerId}-${model.apiName}`}>
<TooltipTrigger asChild>
<DropdownMenuItem
key={`${providerId}-${model.apiName}`}
title={model.description}
className={
selectedModel.provider === providerId &&
selectedModel.name === model.apiName
......@@ -366,9 +342,7 @@ export function ModelPicker() {
}
onClick={() => {
const customModelId =
model.type === "custom"
? model.id
: undefined;
model.type === "custom" ? model.id : undefined;
onModelSelect({
name: model.apiName,
provider: providerId,
......@@ -387,11 +361,6 @@ export function ModelPicker() {
)}
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
......@@ -439,9 +408,9 @@ export function ModelPicker() {
</DropdownMenuLabel>
<DropdownMenuSeparator />
{models.map((model) => (
<Tooltip key={`${providerId}-${model.apiName}`}>
<TooltipTrigger asChild>
<DropdownMenuItem
key={`${providerId}-${model.apiName}`}
title={model.description}
className={
selectedModel.provider === providerId &&
selectedModel.name === model.apiName
......@@ -470,11 +439,6 @@ export function ModelPicker() {
)}
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
......
......@@ -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>
</Button>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 pb-3">
......
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Popover,
PopoverContent,
......@@ -62,21 +57,13 @@ export function ProModeSelector() {
return (
<Popover>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="has-[>svg]:px-1.5 flex items-center gap-1.5 h-8 border-primary/50 hover:bg-primary/10 font-medium shadow-sm shadow-primary/10 transition-all hover:shadow-md hover:shadow-primary/15"
<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-primary/50 bg-background shadow-sm hover:bg-primary/10 h-8 px-1.5 gap-1.5 shadow-primary/10 hover:shadow-md hover:shadow-primary/15"
title="Configure Dyad Pro settings"
>
<Sparkles className="h-4 w-4 text-primary" />
<span className="text-primary font-medium text-xs-sm">Pro</span>
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Configure Dyad Pro settings</TooltipContent>
</Tooltip>
<PopoverContent className="w-80 border-primary/20">
<div className="space-y-4">
<div className="space-y-1">
......@@ -88,21 +75,15 @@ export function ProModeSelector() {
</div>
{!hasProKey && (
<div className="text-sm text-center text-muted-foreground">
<Tooltip>
<TooltipTrigger asChild>
<a
className="inline-flex items-center justify-center gap-2 rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-sm font-medium text-primary shadow-sm transition-colors hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
onClick={() => {
ipc.system.openExternalUrl("https://dyad.sh/pro#ai");
}}
title="Visit dyad.sh/pro to unlock Pro features"
>
Unlock Pro modes
</a>
</TooltipTrigger>
<TooltipContent>
Visit dyad.sh/pro to unlock Pro features
</TooltipContent>
</Tooltip>
</div>
)}
<div className="flex flex-col gap-5">
......@@ -164,19 +145,15 @@ function SelectorRow({
>
{label}
</Label>
<Tooltip>
<TooltipTrigger asChild>
<span title={tooltip}>
<Info
className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`}
/>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-72">
{tooltip}
</TooltipContent>
</Tooltip>
</span>
</div>
<Switch
id={id}
aria-label={label}
checked={isTogglable ? settingEnabled : false}
onCheckedChange={toggle}
disabled={!isTogglable}
......@@ -218,76 +195,46 @@ function TurboEditsSelector({
<Label className={!isTogglable ? "text-muted-foreground/50" : ""}>
Turbo Edits
</Label>
<Tooltip>
<TooltipTrigger asChild>
<span title="Edits files efficiently without full rewrites. Classic: Uses a smaller model to complete edits. Search & replace: Find and replaces specific text blocks.">
<Info
className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`}
/>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-72">
Edits files efficiently without full rewrites.
<br />
<ul className="list-disc ml-4">
<li>
<b>Classic:</b> Uses a smaller model to complete edits.
</li>
<li>
<b>Search & replace:</b> Find and replaces specific text blocks.
</li>
</ul>
</TooltipContent>
</Tooltip>
</span>
</div>
<div
className="inline-flex rounded-md border border-input"
data-testid="turbo-edits-selector"
>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "off" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("off")}
disabled={!isTogglable}
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
title="Disable Turbo Edits"
>
Off
</Button>
</TooltipTrigger>
<TooltipContent>Disable Turbo Edits</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "v1" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("v1")}
disabled={!isTogglable}
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
title="Uses a smaller model to complete edits"
>
Classic
</Button>
</TooltipTrigger>
<TooltipContent>
Uses a smaller model to complete edits
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "v2" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("v2")}
disabled={!isTogglable}
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0"
title="Find and replaces specific text blocks"
>
Search & replace
</Button>
</TooltipTrigger>
<TooltipContent>
Find and replaces specific text blocks
</TooltipContent>
</Tooltip>
</div>
</div>
);
......@@ -325,69 +272,46 @@ function SmartContextSelector({
<Label className={!isTogglable ? "text-muted-foreground/50" : ""}>
Smart Context
</Label>
<Tooltip>
<TooltipTrigger asChild>
<span title="Selects the most relevant files as context to save credits working on large codebases.">
<Info
className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`}
/>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-72">
Selects the most relevant files as context to save credits working
on large codebases.
</TooltipContent>
</Tooltip>
</span>
</div>
<div
className="inline-flex rounded-md border border-input"
data-testid="smart-context-selector"
>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "off" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("off")}
disabled={!isTogglable}
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
title="Disable Smart Context"
>
Off
</Button>
</TooltipTrigger>
<TooltipContent>Disable Smart Context</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "balanced" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("balanced")}
disabled={!isTogglable}
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
title="Selects most relevant files with balanced context size"
>
Balanced
</Button>
</TooltipTrigger>
<TooltipContent>
Selects most relevant files with balanced context size
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={currentValue === "deep" ? "default" : "ghost"}
size="sm"
onClick={() => onValueChange("deep")}
disabled={!isTogglable}
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0"
title="Experimental: Keeps full conversation history for maximum context and cache-optimized to control costs"
>
Deep
</Button>
</TooltipTrigger>
<TooltipContent>
<b>Experimental:</b> Keeps full conversation history for maximum
context and cache-optimized to control costs
</TooltipContent>
</Tooltip>
</div>
</div>
);
......
......@@ -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"
title="Edit Provider"
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"
title="Delete Provider"
onClick={() => setProviderToDelete(provider.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete Provider</TooltipContent>
</Tooltip>
</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>
</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,13 +201,10 @@ function AppIcons({
return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
size="sm"
className="font-medium w-14"
>
<Link
as={Link}
to={item.to}
className={`flex flex-col items-center gap-1 h-14 mb-2 rounded-2xl ${
size="sm"
className={`font-medium w-14 flex flex-col items-center gap-1 h-14 mb-2 rounded-2xl ${
isActive ? "bg-sidebar-accent" : ""
}`}
onMouseEnter={() => {
......@@ -226,7 +223,6 @@ function AppIcons({
<item.icon className="h-5 w-5" />
<span className={"text-xs"}>{item.title}</span>
</div>
</Link>
</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,10 +21,7 @@ export function AppItem({
}: AppItemProps) {
return (
<SidebarMenuItem className="mb-1 relative ">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex w-[190px] items-center">
<div className="flex w-[190px] items-center" title={app.name}>
<Button
variant="ghost"
onClick={() => handleAppClick(app.id)}
......@@ -71,12 +62,6 @@ export function AppItem({
/>
</Button>
</div>
</TooltipTrigger>
<TooltipContent side="right">
<p>{app.name}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</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"
<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"}`}
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/* Codebase Context */}
......@@ -193,12 +183,12 @@ export function AuxiliaryActionsMenu({
{themes?.map((theme) => {
const isSelected = currentThemeId === theme.id;
return (
<Tooltip key={theme.id}>
<TooltipTrigger asChild>
<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" && (
......@@ -213,11 +203,6 @@ export function AuxiliaryActionsMenu({
)}
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{theme.description}
</TooltipContent>
</Tooltip>
);
})}
......@@ -229,12 +214,12 @@ export function AuxiliaryActionsMenu({
const themeId = `custom:${theme.id}`;
const isSelected = currentThemeId === themeId;
return (
<Tooltip key={themeId}>
<TooltipTrigger asChild>
<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
......@@ -243,18 +228,10 @@ export function AuxiliaryActionsMenu({
/>
<span className="flex-1">{theme.name}</span>
{isSelected && (
<Check
size={16}
className="text-primary ml-2"
/>
<Check size={16} className="text-primary ml-2" />
)}
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{theme.description || "Custom theme"}
</TooltipContent>
</Tooltip>
);
})}
</>
......
......@@ -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,12 +27,10 @@ export function ChatActivityButton() {
}, [isStreamingById]);
return (
<Popover open={open} onOpenChange={setOpen}>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<button
<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">
......@@ -45,11 +38,7 @@ export function ChatActivityButton() {
</span>
)}
<Bell size={16} />
</button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Recent chat activity</TooltipContent>
</Tooltip>
<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 ? (
<>
......
......@@ -48,12 +48,6 @@ import { AutoApproveSwitch } from "../AutoApproveSwitch";
import { usePostHog } from "posthog-js/react";
import { CodeHighlight } from "./CodeHighlight";
import { TokenBar } from "./TokenBar";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import { useVersions } from "@/hooks/useVersions";
import { useAttachments } from "@/hooks/useAttachments";
......@@ -410,25 +404,16 @@ export function ChatInput({ chatId }: { chatId?: number }) {
) : (
selectedComponents.length > 0 && (
<div className="border-b border-border p-3 bg-muted/30">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => {
ipc.system.openExternalUrl("https://dyad.sh/pro");
}}
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-primary transition-colors cursor-pointer"
title="Visual editing lets you make UI changes without AI and is a Pro-only feature"
>
<Lock size={16} />
<span className="font-medium">Visual editor (Pro)</span>
</button>
</TooltipTrigger>
<TooltipContent>
Visual editing lets you make UI changes without AI and is
a Pro-only feature
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)
)}
......@@ -508,21 +493,15 @@ function SuggestionButton({
}) {
const { isStreaming } = useStreamChat();
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
disabled={isStreaming}
variant="outline"
size="sm"
onClick={onClick}
title={tooltipText}
>
{children}
</Button>
</TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
......
......@@ -21,12 +21,6 @@ import { useAtomValue } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useEffect, useMemo, useRef, useState } from "react";
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
interface ChatMessageProps {
message: Message;
......@@ -128,12 +122,10 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
{message.role === "assistant" &&
message.content &&
!isStreaming && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
data-testid="copy-message-button"
onClick={handleCopyFormatted}
title={copied ? "Copied!" : "Copy"}
className="flex items-center space-x-1 px-2 py-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors duration-200 cursor-pointer"
>
{copied ? (
......@@ -143,12 +135,6 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
)}
<span className="hidden sm:inline"></span>
</button>
</TooltipTrigger>
<TooltipContent>
{copied ? "Copied!" : "Copy"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<div className="flex flex-wrap gap-2">
{message.approvalState && (
......@@ -187,25 +173,20 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
<div className="flex items-center space-x-1">
<GitCommit className="h-3 w-3" />
{messageVersion && messageVersion.message && (
<Tooltip>
<TooltipTrigger asChild>
<span className="max-w-50 truncate font-medium">
<span
className="max-w-50 truncate font-medium"
title={messageVersion.message}
>
{
messageVersion.message
.replace(/^\[dyad\]\s*/i, "")
.split("\n")[0]
}
</span>
</TooltipTrigger>
<TooltipContent>{messageVersion.message}</TooltipContent>
</Tooltip>
)}
</div>
)}
{message.requestId && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => {
if (!message.requestId) return;
......@@ -225,6 +206,11 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
// noop
});
}}
title={
copiedRequestId
? "Copied!"
: `Copy Request ID: ${message.requestId.slice(0, 8)}...`
}
className="flex items-center space-x-1 px-1 py-0.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors duration-200 cursor-pointer"
>
{copiedRequestId ? (
......@@ -236,28 +222,14 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
{copiedRequestId ? "Copied" : "Request ID"}
</span>
</button>
</TooltipTrigger>
<TooltipContent>
{copiedRequestId
? "Copied!"
: `Copy Request ID: ${message.requestId.slice(0, 8)}...`}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{isLastMessage && message.totalTokens && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center space-x-1 px-1 py-0.5">
<div
className="flex items-center space-x-1 px-1 py-0.5"
title={`Max tokens used: ${message.totalTokens.toLocaleString()}`}
>
<Info className="h-3 w-3" />
</div>
</TooltipTrigger>
<TooltipContent>
Max tokens used: {message.totalTokens.toLocaleString()}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)}
......
import { AlertTriangle, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useSummarizeInNewChat } from "./SummarizeInNewChatButton";
const CONTEXT_LIMIT_THRESHOLD = 40_000;
......@@ -44,33 +39,14 @@ export function ContextLimitBanner({
data-testid="context-limit-banner"
>
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-5 w-5 p-0 hover:bg-transparent text-amber-600 dark:text-amber-400 cursor-help"
title={`Used: ${formatTokenCount(totalTokens)} / Limit: ${formatTokenCount(contextWindow)}`}
>
<AlertTriangle className="h-4 w-4 shrink-0" />
</Button>
</TooltipTrigger>
<TooltipContent className="w-auto p-2 text-xs" side="top">
<div className="grid gap-1">
<div className="flex justify-between gap-4">
<span>Used:</span>
<span className="font-medium">
{formatTokenCount(totalTokens)}
</span>
</div>
<div className="flex justify-between gap-4">
<span>Limit:</span>
<span className="font-medium">
{formatTokenCount(contextWindow)}
</span>
</div>
</div>
</TooltipContent>
</Tooltip>
<p className="text-sm font-medium">
You're close to the context limit for this chat.
</p>
......
......@@ -16,7 +16,7 @@ export const DyadTokenSavings: React.FC<DyadTokenSavingsProps> = ({
return (
<Tooltip>
<TooltipTrigger asChild>
<TooltipTrigger>
<div className="bg-green-50 dark:bg-green-950 hover:bg-green-100 dark:hover:bg-green-900 rounded-lg px-4 py-2 border border-green-200 dark:border-green-800 my-2 cursor-pointer">
<div className="flex items-center gap-2 text-green-700 dark:text-green-300">
<Zap size={16} className="text-green-600 dark:text-green-400" />
......
import { MessageSquare, Upload } from "lucide-react";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useRef } from "react";
interface FileAttachmentDropdownProps {
......@@ -46,49 +40,33 @@ export function FileAttachmentDropdown({
const menuItems = (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuItem
onSelect={(e) => {
onClick={(e: React.MouseEvent) => {
// Prevent default so menu doesn't close in order to keep the hidden inputs in the DOM
// Manually close menu after file selection
e.preventDefault();
handleChatContextClick();
}}
className="py-3 px-4"
title="Example use case: screenshot of the app to point out a UI issue"
>
<MessageSquare size={16} className="mr-2" />
Attach file as chat context
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
Example use case: screenshot of the app to point out a UI issue
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuItem
onSelect={(e) => {
onClick={(e: React.MouseEvent) => {
// Prevent default so menu doesn't close in order to keep the hidden inputs in the DOM
// Manually close menu after file selection
e.preventDefault();
handleUploadToCodebaseClick();
}}
className="py-3 px-4"
title="Example use case: add an image to use for your app"
>
<Upload size={16} className="mr-2" />
Upload file to codebase
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
Example use case: add an image to use for your app
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
);
......
......@@ -133,7 +133,10 @@ function useScrambleText(text: string) {
function ScrambleVerb({ verb }: { verb: string }) {
const display = useScrambleText(verb);
return (
<span className="inline-block text-sm text-muted-foreground">
<span
className="inline-block text-sm text-muted-foreground"
aria-hidden="true"
>
{display}
</span>
);
......
......@@ -54,7 +54,7 @@ export function TokenBar({ chatId }: TokenBarProps) {
<div className="px-4 pb-2 text-xs" data-testid="token-bar">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<TooltipTrigger>
<div className="w-full">
<div className="flex justify-between mb-1 text-xs text-muted-foreground">
<span>Tokens: {totalTokens.toLocaleString()}</span>
......
......@@ -154,7 +154,7 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
Date.now() - timestampMs > 24 * 60 * 60 * 1000;
return (
<Tooltip>
<TooltipTrigger asChild>
<TooltipTrigger>
<div
className={cn(
"inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded-md",
......@@ -221,8 +221,6 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
<div className="flex items-center gap-1">
{/* Restore button */}
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={async (e) => {
e.stopPropagation();
......@@ -241,10 +239,14 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
className={cn(
"invisible mt-1 flex items-center gap-1 px-2 py-0.5 text-sm font-medium bg-(--primary) text-(--primary-foreground) hover:bg-background-lightest rounded-md transition-colors",
selectedVersionId === version.oid && "visible",
isRevertingVersion &&
"opacity-50 cursor-not-allowed",
isRevertingVersion && "opacity-50 cursor-not-allowed",
)}
aria-label="Restore to this version"
title={
isRevertingVersion
? "Restoring to this version..."
: "Restore to this version"
}
>
{isRevertingVersion ? (
<Loader2 size={12} className="animate-spin" />
......@@ -255,13 +257,6 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
{isRevertingVersion ? "Restoring..." : "Restore"}
</span>
</button>
</TooltipTrigger>
<TooltipContent>
{isRevertingVersion
? "Restoring to this version..."
: "Restore to this version"}
</TooltipContent>
</Tooltip>
</div>
</div>
</div>
......
......@@ -24,12 +24,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { showError, showSuccess } from "@/lib/toast";
import { useMutation } from "@tanstack/react-query";
import { useCheckProblems } from "@/hooks/useCheckProblems";
......@@ -175,12 +169,13 @@ export const ActionHeader = () => {
testId: string,
badge?: React.ReactNode,
) => {
const buttonContent = (
return (
<button
data-testid={testId}
ref={ref}
className="no-app-region-drag cursor-pointer relative flex items-center gap-0.5 px-2 py-0.5 rounded-md text-xs font-medium z-10 hover:bg-[var(--background)] flex-col"
onClick={() => selectPanel(mode)}
title={isCompact ? text : undefined}
>
{icon}
<span>
......@@ -189,24 +184,10 @@ export const ActionHeader = () => {
</span>
</button>
);
if (isCompact) {
return (
<Tooltip>
<TooltipTrigger asChild>{buttonContent}</TooltipTrigger>
<TooltipContent>
<p>{text}</p>
</TooltipContent>
</Tooltip>
);
}
return buttonContent;
};
const iconSize = 15;
return (
<TooltipProvider>
<div className="flex items-center justify-between px-1 py-2 mt-1 border-b border-border">
<div className="relative flex rounded-md p-0.5 gap-0.5">
<motion.div
......@@ -274,14 +255,12 @@ export const ActionHeader = () => {
<div className="flex items-center gap-1">
<ChatActivityButton />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
<DropdownMenuTrigger
data-testid="preview-more-options-button"
className="no-app-region-drag flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
title="More options"
>
<MoreVertical size={16} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuItem onClick={onCleanRestart}>
......@@ -306,6 +285,5 @@ export const ActionHeader = () => {
</DropdownMenu>
</div>
</div>
</TooltipProvider>
);
};
......@@ -9,12 +9,6 @@ import {
X,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ToolbarColorPicker } from "./ToolbarColorPicker";
interface AnnotatorToolbarProps {
......@@ -50,14 +44,12 @@ export const AnnotatorToolbar = ({
}: AnnotatorToolbarProps) => {
return (
<div className="flex items-center justify-center p-2 border-b space-x-2">
<TooltipProvider>
{/* Tool Selection Buttons */}
<div className="flex space-x-1">
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onToolChange("select")}
aria-label="Select"
title="Select"
className={cn(
"p-1 rounded transition-colors duration-200",
tool === "select"
......@@ -67,17 +59,11 @@ export const AnnotatorToolbar = ({
>
<MousePointer2 size={16} />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Select</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onToolChange("draw")}
aria-label="Draw"
title="Draw"
className={cn(
"p-1 rounded transition-colors duration-200",
tool === "draw"
......@@ -87,17 +73,11 @@ export const AnnotatorToolbar = ({
>
<Pencil size={16} />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Draw</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onToolChange("text")}
aria-label="Text"
title="Text"
className={cn(
"p-1 rounded transition-colors duration-200",
tool === "text"
......@@ -107,108 +87,68 @@ export const AnnotatorToolbar = ({
>
<Type size={16} />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Text</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div className="p-1 rounded transition-colors duration-200 hover:bg-purple-200 dark:hover:bg-purple-900">
<div
className="p-1 rounded transition-colors duration-200 hover:bg-purple-200 dark:hover:bg-purple-900"
title="Color"
>
<ToolbarColorPicker color={color} onChange={onColorChange} />
</div>
</TooltipTrigger>
<TooltipContent>
<p>Color</p>
</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onDelete}
aria-label="Delete"
title="Delete Selected"
className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!selectedId}
>
<Trash2 size={16} />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Delete Selected</p>
</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onUndo}
aria-label="Undo"
title="Undo"
className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={historyStep === 0}
>
<Undo size={16} />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Undo</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onRedo}
aria-label="Redo"
title="Redo"
className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={historyStep === historyLength - 1}
>
<Redo size={16} />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Redo</p>
</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onSubmit}
aria-label="Add to Chat"
title="Add to Chat"
className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!hasSubmitHandler}
>
<Check size={16} />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Add to Chat</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onDeactivate}
aria-label="Close Annotator"
title="Close Annotator"
className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900"
>
<X size={16} />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Close Annotator</p>
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</div>
);
};
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论