Unverified 提交 7910e2ee authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Replace button title attributes with shadcn Tooltip components (#2470)

## Summary - Replace native HTML `title` attributes on button elements across 25 files with proper `<Tooltip>` / `<TooltipTrigger>` / `<TooltipContent>` components from the existing UI library - Native browser tooltips are delayed and unstyled; the shadcn tooltips provide a consistent, polished tooltip experience with animations and proper positioning - Only button/interactive elements were converted; `title` on non-interactive elements like `<span>` (text overflow), `<iframe>`, and component props were intentionally left as-is ## Test plan - [x] TypeScript type check passes (`npm run ts`) - [x] Lint passes (`npm run lint`) - [x] Formatting passes (`npm run fmt`) - [x] All 661 unit tests pass (`npm test`) - [ ] Manual: hover over icon buttons throughout the app (chat input, preview toolbar, annotator, settings) to verify styled tooltips appear instead of browser-native ones 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2470"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Primarily UI/UX refactoring of hover tooltips with minimal behavioral impact; main risk is small layout/accessibility regressions from the updated tooltip styling/positioning. > > **Overview** > Replaces native `title` tooltips on many interactive controls (chat, preview/annotator toolbars, settings/connectors, branch manager, etc.) with the app’s `Tooltip` components, using `TooltipTrigger`’s `render` prop to avoid invalid nested `<button>` markup. > > Updates the tooltip implementation (`src/components/ui/tooltip.tsx`) to remove the implicit provider wrapper, tweak default positioning (`sideOffset`), and restyle tooltip/arrow classes; adjusts E2E aria snapshots accordingly and documents the `TooltipTrigger render` pitfall in `AGENTS.md`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8055379edcec73b53ca54ee82b9f4fb4a2f92c63. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Replaced native title tooltips on interactive buttons with shadcn Tooltip for consistent, styled, and responsive tooltips across the app. Improves usability and visual polish without changing button behavior. - **Refactors** - Converted titles to Tooltip/TooltipTrigger/TooltipContent across ~25 files, using TooltipTrigger’s render prop to avoid nested button HTML. - Kept native title for drag handles and resize rails, and left titles on non-interactive elements (e.g., span, iframe, component props). - Updated tooltip defaults (sideOffset 4) and removed the implicit provider; added aria-labels to icon-only buttons; fixed ToggleGroupItem corner styles and preserved ref forwarding where needed. <sup>Written for commit 8055379edcec73b53ca54ee82b9f4fb4a2f92c63. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 fb615c33
...@@ -214,3 +214,20 @@ Add `#skip-bugbot` to the PR description for trivial PRs that won't affect end-u ...@@ -214,3 +214,20 @@ Add `#skip-bugbot` to the PR description for trivial PRs that won't affect end-u
- Linting or test setup changes - Linting or test setup changes
- Documentation-only changes - Documentation-only changes
- CI/build configuration updates - CI/build configuration updates
## Learnings
### TooltipTrigger render prop (Base UI)
- `TooltipTrigger` from `@base-ui/react/tooltip` (wrapped in `src/components/ui/tooltip.tsx`) renders a `<button>` by default. Wrapping another button-like element (`<button>`, `<Button>`, `<DropdownMenuTrigger>`, `<PopoverTrigger>`, `<MiniSelectTrigger>`, `<ToggleGroupItem>`) inside it creates invalid nested `<button>` HTML. Use the `render` prop instead:
```tsx
// Wrong: nested buttons
<TooltipTrigger><Button onClick={fn}>Click</Button></TooltipTrigger>
// Correct: render prop merges into a single element
<TooltipTrigger render={<Button onClick={fn} />}>Click</TooltipTrigger>
```
- Wrapping `ToggleGroupItem` in `TooltipTrigger` without `render` also breaks `:first-child`/`:last-child` CSS selectors for rounded corners on the group.
- For drag handles and resize rails, prefer the native `title` attribute over `Tooltip` — tooltips appear immediately on hover and interfere with drag interactions, while `title` has a built-in delay.
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/mcp-calculator - paragraph: tc=local-agent/mcp-calculator
- paragraph: I'll calculate the sum of 5 and 3 using the calculator. - paragraph: I'll calculate the sum of 5 and 3 using the calculator.
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- button "Undo": - button "Undo":
- img - img
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved - text: Approved
...@@ -14,20 +14,20 @@ ...@@ -14,20 +14,20 @@
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/ask-read-file - paragraph: tc=local-agent/ask-read-file
- paragraph: Let me read the file to explain its contents. - paragraph: Let me read the file to explain its contents.
- img - img
- text: App.tsx Read src/App.tsx - text: App.tsx Read src/App.tsx
- paragraph: This is a simple React component that renders a div with the text 'Minimal imported app'. The component is exported as the default export. - paragraph: This is a simple React component that renders a div with the text 'Minimal imported app'. The component is exported as the default export.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- button "Undo": - button "Undo":
- img - img
......
...@@ -6,11 +6,15 @@ ...@@ -6,11 +6,15 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/parallel-tools - paragraph: tc=local-agent/parallel-tools
- paragraph: I'll create two files for you in parallel. - paragraph: I'll create two files for you in parallel.
...@@ -27,11 +31,15 @@ ...@@ -27,11 +31,15 @@
- img - img
- text: "src/utils/string.ts Summary: Create string utilities" - text: "src/utils/string.ts Summary: Create string utilities"
- paragraph: Task completed. - paragraph: Task completed.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img
- button "Undo":
- img - img
- button "Retry": - button "Retry":
- img - img
\ No newline at end of file
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved - text: Approved
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/read-then-edit - paragraph: tc=local-agent/read-then-edit
- paragraph: Let me first read the current file contents to understand what we're working with. - paragraph: Let me first read the current file contents to understand what we're working with.
...@@ -26,13 +26,13 @@ ...@@ -26,13 +26,13 @@
- img - img
- text: src/App.tsx - text: src/App.tsx
- paragraph: Done! I've updated the title from 'Minimal imported app' to 'UPDATED imported app'. The change has been applied successfully. - paragraph: Done! I've updated the title from 'Minimal imported app' to 'UPDATED imported app'. The change has been applied successfully.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- button "Undo": - button "Undo":
- img - img
......
...@@ -6,13 +6,15 @@ ...@@ -6,13 +6,15 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved
- img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/code-search - paragraph: tc=local-agent/code-search
- paragraph: I'll search for files related to React components in the codebase. - paragraph: I'll search for files related to React components in the codebase.
...@@ -20,13 +22,13 @@ ...@@ -20,13 +22,13 @@
- img - img
- img - img
- paragraph: I found the relevant files! The main React component is in src/App.tsx which handles the app rendering. - paragraph: I found the relevant files! The main React component is in src/App.tsx which handles the app rendering.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- button "Undo": - button "Undo":
- img - img
......
...@@ -6,22 +6,30 @@ ...@@ -6,22 +6,30 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/add-dependency - paragraph: tc=local-agent/add-dependency
- paragraph: I'll add a dependency to your project. - paragraph: I'll add a dependency to your project.
- img - img
- text: Do you want to install these packages? @dyad-sh/supabase-management-js Make sure these packages are what you want. - text: Do you want to install these packages? @dyad-sh/supabase-management-js Make sure these packages are what you want.
- paragraph: Dependency added done. - paragraph: Dependency added done.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img
- button "Undo":
- img - img
- button "Retry": - button "Retry":
- img - img
\ No newline at end of file
...@@ -6,22 +6,30 @@ ...@@ -6,22 +6,30 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/add-dependency - paragraph: tc=local-agent/add-dependency
- paragraph: I'll add a dependency to your project. - paragraph: I'll add a dependency to your project.
- img - img
- text: Do you want to install these packages? @dyad-sh/supabase-management-js Make sure these packages are what you want. - text: Do you want to install these packages? @dyad-sh/supabase-management-js Make sure these packages are what you want.
- paragraph: Dependency added done. - paragraph: Dependency added done.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img
- button "Undo":
- img - img
- button "Retry": - button "Retry":
- img - img
\ No newline at end of file
...@@ -6,11 +6,15 @@ ...@@ -6,11 +6,15 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/add-dependency - paragraph: tc=local-agent/add-dependency
- paragraph: I'll add a dependency to your project. - paragraph: I'll add a dependency to your project.
...@@ -24,11 +28,15 @@ ...@@ -24,11 +28,15 @@
- button "Fix with AI": - button "Fix with AI":
- img - img
- paragraph: Dependency added done. - paragraph: Dependency added done.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img
- button "Undo":
- img - img
- button "Retry": - button "Retry":
- img - img
\ No newline at end of file
...@@ -6,13 +6,15 @@ ...@@ -6,13 +6,15 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved
- img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/upload-to-codebase - paragraph: tc=local-agent/upload-to-codebase
- paragraph: "Attachments:" - paragraph: "Attachments:"
...@@ -25,13 +27,13 @@ ...@@ -25,13 +27,13 @@
- img - img
- text: "assets/uploaded-file.png Summary: Upload file to codebase" - text: "assets/uploaded-file.png Summary: Upload file to codebase"
- paragraph: I've successfully uploaded your file to assets/uploaded-file.png in the codebase. - paragraph: I've successfully uploaded your file to assets/uploaded-file.png in the codebase.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- button "Undo": - button "Undo":
- img - img
......
...@@ -6,31 +6,43 @@ ...@@ -6,31 +6,43 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved
- img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/grep-search - paragraph: tc=local-agent/grep-search
- paragraph: I'll search for 'createRoot' in the codebase to find where the React app is initialized. - paragraph: I'll search for 'createRoot' in the codebase to find where the React app is initialized.
- img - img
- text: GREP"createRoot"(2 matches) - text: GREP"createRoot"(2 matches)
- img - img
- text: log
- button "Copy":
- img
- text: log
- code: "src/main.tsx:1: import { createRoot } from \"react-dom/client\"; src/main.tsx:4: createRoot(document.getElementById(\"root\")!).render(<App />);"
- paragraph: Now I'll search specifically in .tsx files for 'App' to find component references. - paragraph: Now I'll search specifically in .tsx files for 'App' to find component references.
- img - img
- text: GREP"App" in *.tsx(4 matches) - text: GREP"App" in *.tsx(4 matches)
- img - img
- text: log
- button "Copy":
- img
- text: log
- code: "src/main.tsx:2: import App from \"./App.tsx\"; src/main.tsx:4: createRoot(document.getElementById(\"root\")!).render(<App />); src/App.tsx:1: const App = () => <div>Minimal imported app</div>; src/App.tsx:3: export default App;"
- paragraph: I found the matches! The React app is initialized in src/main.tsx using createRoot, and the App component is defined in src/App.tsx and imported in src/main.tsx. - paragraph: I found the matches! The React app is initialized in src/main.tsx using createRoot, and the App component is defined in src/App.tsx and imported in src/main.tsx.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- button "Undo": - button "Undo":
- img - img
......
...@@ -6,18 +6,20 @@ ...@@ -6,18 +6,20 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved
- img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/read-logs - paragraph: tc=local-agent/read-logs
- paragraph: Let me check the recent console logs to see what's happening in the application. - paragraph: Let me check the recent console logs to see what's happening in the application.
- img - img
- text: /LOGSReading \d+ logs/ - text: LOGSReading 8 logs
- img - img
- paragraph: Now let me filter for only error logs to identify any issues. - paragraph: Now let me filter for only error logs to identify any issues.
- img - img
...@@ -28,13 +30,13 @@ ...@@ -28,13 +30,13 @@
- text: "LOGSReading 0 logs (type: client)" - text: "LOGSReading 0 logs (type: client)"
- img - img
- paragraph: I've reviewed the console logs. The application appears to be running normally with no critical errors detected. - paragraph: I've reviewed the console logs. The application appears to be running normally with no critical errors detected.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- button "Undo": - button "Undo":
- img - img
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved - text: Approved
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=local-agent/search-replace - paragraph: tc=local-agent/search-replace
- paragraph: Let me first read the file to see its contents. - paragraph: Let me first read the file to see its contents.
...@@ -26,13 +26,13 @@ ...@@ -26,13 +26,13 @@
- img - img
- text: src/App.tsx - text: src/App.tsx
- paragraph: Done! I've updated the message using search_replace. The edit was applied successfully. - paragraph: Done! I've updated the message using search_replace. The edit was applied successfully.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- button "Undo": - button "Undo":
- img - img
......
...@@ -6,13 +6,13 @@ ...@@ -6,13 +6,13 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5 - text: claude-opus-4-5
- img - img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- button "Undo": - button "Undo":
- img - img
......
- paragraph: "[dump]" - paragraph: "[dump]"
- paragraph: "[[dyad-dump-path=*]]" - paragraph: "[[dyad-dump-path=*]]"
- button: - button "Copy":
- img - img
- img - img
- text: Approved - text: Approved
- img - img
- text: auto
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img
- button "Undo":
- img - img
- button "Retry": - button "Retry":
- img - img
\ No newline at end of file
...@@ -3,13 +3,15 @@ ...@@ -3,13 +3,15 @@
- img - img
- text: Index.tsx Read src/pages/Index.tsx - text: Index.tsx Read src/pages/Index.tsx
- paragraph: Done. - paragraph: Done.
- button: - button "Copy":
- img - img
- img - img
- text: Approved - text: Approved
- img - img
- text: auto
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=update-index-1 - paragraph: tc=update-index-1
- paragraph: First read - paragraph: First read
...@@ -19,38 +21,46 @@ ...@@ -19,38 +21,46 @@
- img - img
- img - img
- text: "src/pages/Index.tsx Summary: replace file" - text: "src/pages/Index.tsx Summary: replace file"
- button: - button "Copy":
- img - img
- img - img
- text: Approved - text: Approved
- img - img
- text: auto
- img
- text: less than a minute ago - text: less than a minute ago
- img - img
- text: wrote 1 file(s) - text: wrote 1 file(s)
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: tc=read-index - paragraph: tc=read-index
- paragraph: "Read the index page:" - paragraph: "Read the index page:"
- img - img
- text: Index.tsx Read src/pages/Index.tsx - text: Index.tsx Read src/pages/Index.tsx
- paragraph: Done. - paragraph: Done.
- button: - button "Copy":
- img - img
- img - img
- text: Approved - text: Approved
- img - img
- text: auto
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img - img
- paragraph: "[dump]" - paragraph: "[dump]"
- paragraph: "[[dyad-dump-path=*]]" - paragraph: "[[dyad-dump-path=*]]"
- button: - button "Copy":
- img - img
- img - img
- text: Approved - text: Approved
- img - img
- text: auto
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img
- button "Undo":
- img - img
- button "Retry": - button "Retry":
- img - img
\ No newline at end of file
...@@ -5,11 +5,15 @@ ...@@ -5,11 +5,15 @@
- img - img
- text: src/pages/Index.tsx - text: src/pages/Index.tsx
- paragraph: End of turbo edit - paragraph: End of turbo edit
- button: - button "Copy":
- img - img
- img - img
- text: auto
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img
- button "Undo":
- img - img
- button "Retry": - button "Retry":
- img - img
\ No newline at end of file
...@@ -9,17 +9,31 @@ ...@@ -9,17 +9,31 @@
- text: Warning Could not apply Turbo Edits properly for some of the files; re-generating code...... - text: Warning Could not apply Turbo Edits properly for some of the files; re-generating code......
- img - img
- img - img
- text: Index.tsx Read src/pages/Index.tsx
- img
- text: Search & Replace Index.tsx
- img
- text: src/pages/Index.tsx
- paragraph: "[[dyad-dump-path=*]]"
- img
- text: Warning Could not apply Turbo Edits properly for some of the files; re-generating code......
- img
- img
- text: Index.tsx - text: Index.tsx
- button "Edit": - button "Edit":
- img - img
- img - img
- text: "src/pages/Index.tsx Summary: Rewrite file." - text: "src/pages/Index.tsx Summary: Rewrite file."
- paragraph: "[[dyad-dump-path=*]]" - paragraph: "[[dyad-dump-path=*]]"
- button: - button "Copy":
- img - img
- img - img
- text: auto
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Copy Request ID":
- img
- button "Undo":
- img - img
- button "Retry": - button "Retry":
- img - img
\ No newline at end of file
...@@ -5,6 +5,11 @@ import { ...@@ -5,6 +5,11 @@ import {
SelectItem, SelectItem,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota"; import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota";
import type { ChatMode } from "@/lib/schemas"; import type { ChatMode } from "@/lib/schemas";
...@@ -91,19 +96,27 @@ export function ChatModeSelector() { ...@@ -91,19 +96,27 @@ export function ChatModeSelector() {
value={selectedMode} value={selectedMode}
onValueChange={(v) => v && handleModeChange(v)} onValueChange={(v) => v && handleModeChange(v)}
> >
<MiniSelectTrigger <Tooltip>
data-testid="chat-mode-selector" <TooltipTrigger
title={`Open mode menu (${isMac ? "⌘ + ." : "Ctrl + ."} to toggle)`} render={
className={cn( <MiniSelectTrigger
"h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5", data-testid="chat-mode-selector"
selectedMode === "build" || selectedMode === "local-agent" className={cn(
? "bg-background hover:bg-muted/50 focus:bg-muted/50" "h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5",
: "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", selectedMode === "build" || selectedMode === "local-agent"
)} ? "bg-background hover:bg-muted/50 focus:bg-muted/50"
size="sm" : "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",
> )}
<SelectValue>{getModeDisplayName(selectedMode)}</SelectValue> size="sm"
</MiniSelectTrigger> />
}
>
<SelectValue>{getModeDisplayName(selectedMode)}</SelectValue>
</TooltipTrigger>
<TooltipContent>
{`Open mode menu (${isMac ? "\u2318 + ." : "Ctrl + ."} to toggle)`}
</TooltipContent>
</Tooltip>
<SelectContent align="start"> <SelectContent align="start">
{isProEnabled && ( {isProEnabled && (
<SelectItem value="local-agent"> <SelectItem value="local-agent">
......
...@@ -14,6 +14,11 @@ import { VersionPane } from "./chat/VersionPane"; ...@@ -14,6 +14,11 @@ import { VersionPane } from "./chat/VersionPane";
import { ChatError } from "./chat/ChatError"; import { ChatError } from "./chat/ChatError";
import { FreeAgentQuotaBanner } from "./chat/FreeAgentQuotaBanner"; import { FreeAgentQuotaBanner } from "./chat/FreeAgentQuotaBanner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { ArrowDown } from "lucide-react"; import { ArrowDown } from "lucide-react";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota"; import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota";
...@@ -166,15 +171,21 @@ export function ChatPanel({ ...@@ -166,15 +171,21 @@ export function ChatPanel({
{/* Scroll to bottom button */} {/* Scroll to bottom button */}
{showScrollButton && ( {showScrollButton && (
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-10"> <div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-10">
<Button <Tooltip>
onClick={handleScrollButtonClick} <TooltipTrigger
size="icon" render={
className="rounded-full shadow-lg hover:shadow-xl transition-all border border-border/50 backdrop-blur-sm bg-background/95 hover:bg-accent" <Button
variant="outline" onClick={handleScrollButtonClick}
title={"Scroll to bottom"} size="icon"
> className="rounded-full shadow-lg hover:shadow-xl transition-all border border-border/50 backdrop-blur-sm bg-background/95 hover:bg-accent"
<ArrowDown className="h-4 w-4" /> variant="outline"
</Button> />
}
>
<ArrowDown className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent>Scroll to bottom</TooltipContent>
</Tooltip>
</div> </div>
)} )}
</div> </div>
......
import { Copy, Check } from "lucide-react"; import { Copy, Check } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface CopyErrorMessageProps { interface CopyErrorMessageProps {
errorMessage: string; errorMessage: string;
...@@ -24,26 +29,34 @@ export const CopyErrorMessage = ({ ...@@ -24,26 +29,34 @@ export const CopyErrorMessage = ({
}; };
return ( return (
<button <Tooltip>
onClick={handleCopy} <TooltipTrigger
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${ render={
isCopied <button
? "bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300" onClick={handleCopy}
: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600" className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
} ${className}`} isCopied
title={isCopied ? "Copied!" : "Copy error message"} ? "bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300"
> : "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
{isCopied ? ( } ${className}`}
<> />
<Check size={14} /> }
<span>Copied</span> >
</> {isCopied ? (
) : ( <>
<> <Check size={14} />
<Copy size={14} /> <span>Copied</span>
<span>Copy</span> </>
</> ) : (
)} <>
</button> <Copy size={14} />
<span>Copy</span>
</>
)}
</TooltipTrigger>
<TooltipContent>
{isCopied ? "Copied!" : "Copy error message"}
</TooltipContent>
</Tooltip>
); );
}; };
import React from "react"; import React from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { X, Copy, Check } from "lucide-react"; import { X, Copy, Check } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface CustomErrorToastProps { interface CustomErrorToastProps {
message: string; message: string;
...@@ -41,30 +46,38 @@ export function CustomErrorToast({ ...@@ -41,30 +46,38 @@ export function CustomErrorToast({
{/* Action buttons */} {/* Action buttons */}
<div className="flex items-center space-x-1.5 ml-auto"> <div className="flex items-center space-x-1.5 ml-auto">
<button <Tooltip>
onClick={(e) => { <TooltipTrigger>
e.stopPropagation(); <button
handleCopy(); onClick={(e) => {
}} e.stopPropagation();
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-100/70 rounded-lg transition-all duration-150" handleCopy();
title="Copy to clipboard" }}
> className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-100/70 rounded-lg transition-all duration-150"
{copied ? ( >
<Check className="w-4 h-4 text-green-500" /> {copied ? (
) : ( <Check className="w-4 h-4 text-green-500" />
<Copy className="w-4 h-4" /> ) : (
)} <Copy className="w-4 h-4" />
</button> )}
<button </button>
onClick={(e) => { </TooltipTrigger>
e.stopPropagation(); <TooltipContent>Copy to clipboard</TooltipContent>
handleClose(); </Tooltip>
}} <Tooltip>
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-100/70 rounded-lg transition-all duration-150" <TooltipTrigger>
title="Close" <button
> onClick={(e) => {
<X className="w-4 h-4" /> e.stopPropagation();
</button> handleClose();
}}
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-100/70 rounded-lg transition-all duration-150"
>
<X className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent>Close</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
<div> <div>
......
...@@ -34,6 +34,11 @@ import { ...@@ -34,6 +34,11 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
...@@ -414,14 +419,22 @@ export function GithubBranchManager({ ...@@ -414,14 +419,22 @@ export function GithubBranchManager({
</Select> </Select>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <Tooltip>
className={cn(buttonVariants({ variant: "outline", size: "icon" }))} <TooltipTrigger
title="Branch actions" render={
aria-label="Branch actions" <DropdownMenuTrigger
data-testid="branch-actions-menu-trigger" className={cn(
> buttonVariants({ variant: "outline", size: "icon" }),
<EllipsisVertical className="h-4 w-4" /> )}
</DropdownMenuTrigger> aria-label="Branch actions"
data-testid="branch-actions-menu-trigger"
/>
}
>
<EllipsisVertical className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent>Branch actions</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem <DropdownMenuItem
onClick={() => setShowCreateDialog(true)} onClick={() => setShowCreateDialog(true)}
......
...@@ -7,6 +7,11 @@ import { ...@@ -7,6 +7,11 @@ import {
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Sparkles, Info } from "lucide-react"; import { Sparkles, Info } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { ipc } from "@/ipc/types"; import { ipc } from "@/ipc/types";
import { hasDyadProKey, type UserSettings } from "@/lib/schemas"; import { hasDyadProKey, type UserSettings } from "@/lib/schemas";
...@@ -57,13 +62,17 @@ export function ProModeSelector() { ...@@ -57,13 +62,17 @@ export function ProModeSelector() {
return ( return (
<Popover> <Popover>
<PopoverTrigger <Tooltip>
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" <TooltipTrigger
title="Configure Dyad Pro settings" render={
> <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" />
<Sparkles className="h-4 w-4 text-primary" /> }
<span className="text-primary font-medium text-xs-sm">Pro</span> >
</PopoverTrigger> <Sparkles className="h-4 w-4 text-primary" />
<span className="text-primary font-medium text-xs-sm">Pro</span>
</TooltipTrigger>
<TooltipContent>Configure Dyad Pro settings</TooltipContent>
</Tooltip>
<PopoverContent className="w-80 border-primary/20"> <PopoverContent className="w-80 border-primary/20">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
...@@ -205,36 +214,58 @@ function TurboEditsSelector({ ...@@ -205,36 +214,58 @@ function TurboEditsSelector({
className="inline-flex rounded-md border border-input" className="inline-flex rounded-md border border-input"
data-testid="turbo-edits-selector" data-testid="turbo-edits-selector"
> >
<Button <Tooltip>
variant={currentValue === "off" ? "default" : "ghost"} <TooltipTrigger
size="sm" render={
onClick={() => onValueChange("off")} <Button
disabled={!isTogglable} variant={currentValue === "off" ? "default" : "ghost"}
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0" size="sm"
title="Disable Turbo Edits" onClick={() => onValueChange("off")}
> disabled={!isTogglable}
Off className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
</Button> />
<Button }
variant={currentValue === "v1" ? "default" : "ghost"} >
size="sm" Off
onClick={() => onValueChange("v1")} </TooltipTrigger>
disabled={!isTogglable} <TooltipContent>Disable Turbo Edits</TooltipContent>
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0" </Tooltip>
title="Uses a smaller model to complete edits" <Tooltip>
> <TooltipTrigger
Classic render={
</Button> <Button
<Button variant={currentValue === "v1" ? "default" : "ghost"}
variant={currentValue === "v2" ? "default" : "ghost"} size="sm"
size="sm" onClick={() => onValueChange("v1")}
onClick={() => onValueChange("v2")} disabled={!isTogglable}
disabled={!isTogglable} className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0" />
title="Find and replaces specific text blocks" }
> >
Search & replace Classic
</Button> </TooltipTrigger>
<TooltipContent>
Uses a smaller model to complete edits
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<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"
/>
}
>
Search & replace
</TooltipTrigger>
<TooltipContent>
Find and replaces specific text blocks
</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
); );
...@@ -282,36 +313,59 @@ function SmartContextSelector({ ...@@ -282,36 +313,59 @@ function SmartContextSelector({
className="inline-flex rounded-md border border-input" className="inline-flex rounded-md border border-input"
data-testid="smart-context-selector" data-testid="smart-context-selector"
> >
<Button <Tooltip>
variant={currentValue === "off" ? "default" : "ghost"} <TooltipTrigger
size="sm" render={
onClick={() => onValueChange("off")} <Button
disabled={!isTogglable} variant={currentValue === "off" ? "default" : "ghost"}
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0" size="sm"
title="Disable Smart Context" onClick={() => onValueChange("off")}
> disabled={!isTogglable}
Off className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
</Button> />
<Button }
variant={currentValue === "balanced" ? "default" : "ghost"} >
size="sm" Off
onClick={() => onValueChange("balanced")} </TooltipTrigger>
disabled={!isTogglable} <TooltipContent>Disable Smart Context</TooltipContent>
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0" </Tooltip>
title="Selects most relevant files with balanced context size" <Tooltip>
> <TooltipTrigger
Balanced render={
</Button> <Button
<Button variant={currentValue === "balanced" ? "default" : "ghost"}
variant={currentValue === "deep" ? "default" : "ghost"} size="sm"
size="sm" onClick={() => onValueChange("balanced")}
onClick={() => onValueChange("deep")} disabled={!isTogglable}
disabled={!isTogglable} className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
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 Balanced
</Button> </TooltipTrigger>
<TooltipContent>
Selects most relevant files with balanced context size
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<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"
/>
}
>
Deep
</TooltipTrigger>
<TooltipContent>
Experimental: Keeps full conversation history for maximum context
and cache-optimized to control costs
</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
); );
......
...@@ -17,6 +17,11 @@ import { AlertTriangle } from "lucide-react"; ...@@ -17,6 +17,11 @@ import { AlertTriangle } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
...@@ -123,26 +128,38 @@ export function ProviderSettingsGrid() { ...@@ -123,26 +128,38 @@ export function ProviderSettingsGrid() {
className="flex items-center justify-end" className="flex items-center justify-end"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<Button <Tooltip>
data-testid="edit-custom-provider" <TooltipTrigger
variant="ghost" render={
size="sm" <Button
className="h-8 w-8 p-0 hover:bg-muted rounded-md" data-testid="edit-custom-provider"
title="Edit Provider" variant="ghost"
onClick={() => handleEditProvider(provider)} size="sm"
> className="h-8 w-8 p-0 hover:bg-muted rounded-md"
<Edit className="h-4 w-4" /> onClick={() => handleEditProvider(provider)}
</Button> />
<Button }
data-testid="delete-custom-provider" >
variant="ghost" <Edit className="h-4 w-4" />
size="sm" </TooltipTrigger>
className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10 rounded-md" <TooltipContent>Edit Provider</TooltipContent>
title="Delete Provider" </Tooltip>
onClick={() => setProviderToDelete(provider.id)} <Tooltip>
> <TooltipTrigger
<Trash2 className="h-4 w-4" /> render={
</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"
onClick={() => setProviderToDelete(provider.id)}
/>
}
>
<Trash2 className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent>Delete Provider</TooltipContent>
</Tooltip>
</div> </div>
)} )}
<CardTitle className="text-lg font-medium mb-2"> <CardTitle className="text-lg font-medium mb-2">
......
...@@ -38,6 +38,11 @@ import connectSupabaseDark from "../../assets/supabase/connect-supabase-dark.svg ...@@ -38,6 +38,11 @@ import connectSupabaseDark from "../../assets/supabase/connect-supabase-dark.svg
import connectSupabaseLight from "../../assets/supabase/connect-supabase-light.svg"; import connectSupabaseLight from "../../assets/supabase/connect-supabase-light.svg";
import { ExternalLink, Plus, RefreshCw, Trash2 } from "lucide-react"; import { ExternalLink, Plus, RefreshCw, Trash2 } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { isSupabaseConnected } from "@/lib/schemas"; import { isSupabaseConnected } from "@/lib/schemas";
...@@ -268,17 +273,23 @@ export function SupabaseConnector({ appId }: { appId: number }) { ...@@ -268,17 +273,23 @@ export function SupabaseConnector({ appId }: { appId: number }) {
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between">
Supabase Projects Supabase Projects
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Tooltip>
variant="outline" <TooltipTrigger
size="icon" render={
onClick={() => refetchProjects()} <Button
disabled={isFetchingProjects} variant="outline"
title="Refresh projects" size="icon"
> onClick={() => refetchProjects()}
<RefreshCw disabled={isFetchingProjects}
className={`h-4 w-4 ${isFetchingProjects ? "animate-spin" : ""}`} />
/> }
</Button> >
<RefreshCw
className={`h-4 w-4 ${isFetchingProjects ? "animate-spin" : ""}`}
/>
</TooltipTrigger>
<TooltipContent>Refresh projects</TooltipContent>
</Tooltip>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
...@@ -333,18 +344,24 @@ export function SupabaseConnector({ appId }: { appId: number }) { ...@@ -333,18 +344,24 @@ export function SupabaseConnector({ appId }: { appId: number }) {
</span> </span>
)} )}
</div> </div>
<Button <Tooltip>
variant="ghost" <TooltipTrigger
size="sm" render={
className="h-7 px-2 text-muted-foreground hover:text-destructive shrink-0" <Button
onClick={() => variant="ghost"
handleDeleteOrganization(org.organizationSlug) size="sm"
} className="h-7 px-2 text-muted-foreground hover:text-destructive shrink-0"
title="Disconnect organization" onClick={() =>
> handleDeleteOrganization(org.organizationSlug)
<Trash2 className="h-3.5 w-3.5 mr-1" /> }
<span className="text-xs">Disconnect</span> />
</Button> }
>
<Trash2 className="h-3.5 w-3.5 mr-1" />
<span className="text-xs">Disconnect</span>
</TooltipTrigger>
<TooltipContent>Disconnect organization</TooltipContent>
</Tooltip>
</div> </div>
))} ))}
</div> </div>
......
...@@ -5,6 +5,11 @@ import { Label } from "@/components/ui/label"; ...@@ -5,6 +5,11 @@ import { Label } from "@/components/ui/label";
// We might need a Supabase icon here, but for now, let's use a generic one or text. // We might need a Supabase icon here, but for now, let's use a generic one or text.
// import { Supabase } from "lucide-react"; // Placeholder // import { Supabase } from "lucide-react"; // Placeholder
import { DatabaseZap, Trash2 } from "lucide-react"; // Using DatabaseZap as a placeholder import { DatabaseZap, Trash2 } from "lucide-react"; // Using DatabaseZap as a placeholder
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { useSupabase } from "@/hooks/useSupabase"; import { useSupabase } from "@/hooks/useSupabase";
import { showSuccess, showError } from "@/lib/toast"; import { showSuccess, showError } from "@/lib/toast";
...@@ -120,16 +125,24 @@ export function SupabaseIntegration() { ...@@ -120,16 +125,24 @@ export function SupabaseIntegration() {
</span> </span>
)} )}
</div> </div>
<Button <Tooltip>
variant="ghost" <TooltipTrigger
size="sm" render={
className="h-7 px-2 text-muted-foreground hover:text-destructive shrink-0" <Button
onClick={() => handleDeleteOrganization(org.organizationSlug)} variant="ghost"
title="Disconnect organization" size="sm"
> className="h-7 px-2 text-muted-foreground hover:text-destructive shrink-0"
<Trash2 className="h-3.5 w-3.5 mr-1" /> onClick={() =>
<span className="text-xs">Disconnect</span> handleDeleteOrganization(org.organizationSlug)
</Button> }
/>
}
>
<Trash2 className="h-3.5 w-3.5 mr-1" />
<span className="text-xs">Disconnect</span>
</TooltipTrigger>
<TooltipContent>Disconnect organization</TooltipContent>
</Tooltip>
</div> </div>
))} ))}
</div> </div>
......
...@@ -75,6 +75,11 @@ import { VisualEditingChangesDialog } from "@/components/preview_panel/VisualEdi ...@@ -75,6 +75,11 @@ import { VisualEditingChangesDialog } from "@/components/preview_panel/VisualEdi
import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo"; import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys"; import { queryKeys } from "@/lib/queryKeys";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { import {
ContextLimitBanner, ContextLimitBanner,
shouldShowContextLimitBanner, shouldShowContextLimitBanner,
...@@ -467,16 +472,25 @@ export function ChatInput({ chatId }: { chatId?: number }) { ...@@ -467,16 +472,25 @@ export function ChatInput({ chatId }: { chatId?: number }) {
) : ( ) : (
selectedComponents.length > 0 && ( selectedComponents.length > 0 && (
<div className="border-b border-border p-3 bg-muted/30"> <div className="border-b border-border p-3 bg-muted/30">
<button <Tooltip>
onClick={() => { <TooltipTrigger
ipc.system.openExternalUrl("https://dyad.sh/pro"); render={
}} <button
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-primary transition-colors cursor-pointer" onClick={() => {
title="Visual editing lets you make UI changes without AI and is a Pro-only feature" ipc.system.openExternalUrl("https://dyad.sh/pro");
> }}
<Lock size={16} /> className="flex items-center gap-2 text-sm text-muted-foreground hover:text-primary transition-colors cursor-pointer"
<span className="font-medium">Visual editor (Pro)</span> />
</button> }
>
<Lock size={16} />
<span className="font-medium">Visual editor (Pro)</span>
</TooltipTrigger>
<TooltipContent>
Visual editing lets you make UI changes without AI and is a
Pro-only feature
</TooltipContent>
</Tooltip>
</div> </div>
) )
)} )}
...@@ -505,25 +519,39 @@ export function ChatInput({ chatId }: { chatId?: number }) { ...@@ -505,25 +519,39 @@ export function ChatInput({ chatId }: { chatId?: number }) {
/> />
{isStreaming ? ( {isStreaming ? (
<button <Tooltip>
onClick={handleCancel} <TooltipTrigger
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg" render={
title="Cancel generation" <button
> onClick={handleCancel}
<StopCircleIcon size={20} /> aria-label="Cancel generation"
</button> className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg"
/>
}
>
<StopCircleIcon size={20} />
</TooltipTrigger>
<TooltipContent>Cancel generation</TooltipContent>
</Tooltip>
) : ( ) : (
<button <Tooltip>
onClick={handleSubmit} <TooltipTrigger
disabled={ render={
(!inputValue.trim() && attachments.length === 0) || <button
disableSendButton onClick={handleSubmit}
} disabled={
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50" (!inputValue.trim() && attachments.length === 0) ||
title="Send message" disableSendButton
> }
<SendHorizontalIcon size={20} /> aria-label="Send message"
</button> className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
/>
}
>
<SendHorizontalIcon size={20} />
</TooltipTrigger>
<TooltipContent>Send message</TooltipContent>
</Tooltip>
)} )}
</div> </div>
<div className="pl-2 pr-1 flex items-center justify-between pb-2"> <div className="pl-2 pr-1 flex items-center justify-between pb-2">
...@@ -557,15 +585,21 @@ function SuggestionButton({ ...@@ -557,15 +585,21 @@ function SuggestionButton({
}) { }) {
const { isStreaming } = useStreamChat(); const { isStreaming } = useStreamChat();
return ( return (
<Button <Tooltip>
disabled={isStreaming} <TooltipTrigger
variant="outline" render={
size="sm" <Button
onClick={onClick} disabled={isStreaming}
title={tooltipText} variant="outline"
> size="sm"
{children} onClick={onClick}
</Button> />
}
>
{children}
</TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent>
</Tooltip>
); );
} }
......
...@@ -21,6 +21,11 @@ import { useAtomValue } from "jotai"; ...@@ -21,6 +21,11 @@ import { useAtomValue } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { useCopyToClipboard } from "@/hooks/useCopyToClipboard";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface ChatMessageProps { interface ChatMessageProps {
message: Message; message: Message;
...@@ -122,19 +127,28 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => { ...@@ -122,19 +127,28 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
{message.role === "assistant" && {message.role === "assistant" &&
message.content && message.content &&
!isStreaming && ( !isStreaming && (
<button <Tooltip>
data-testid="copy-message-button" <TooltipTrigger
onClick={handleCopyFormatted} render={
title={copied ? "Copied!" : "Copy"} <button
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" data-testid="copy-message-button"
> onClick={handleCopyFormatted}
{copied ? ( aria-label="Copy"
<Check className="h-4 w-4 text-green-500" /> 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"
) : ( />
<Copy className="h-4 w-4" /> }
)} >
<span className="hidden sm:inline"></span> {copied ? (
</button> <Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
<span className="hidden sm:inline"></span>
</TooltipTrigger>
<TooltipContent>
{copied ? "Copied!" : "Copy"}
</TooltipContent>
</Tooltip>
)} )}
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{message.approvalState && ( {message.approvalState && (
...@@ -187,41 +201,48 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => { ...@@ -187,41 +201,48 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
</div> </div>
)} )}
{message.requestId && ( {message.requestId && (
<button <Tooltip>
onClick={() => { <TooltipTrigger
if (!message.requestId) return; render={
navigator.clipboard <button
.writeText(message.requestId) onClick={() => {
.then(() => { if (!message.requestId) return;
setCopiedRequestId(true); navigator.clipboard
if (copiedRequestIdTimeoutRef.current) { .writeText(message.requestId)
clearTimeout(copiedRequestIdTimeoutRef.current); .then(() => {
} setCopiedRequestId(true);
copiedRequestIdTimeoutRef.current = setTimeout( if (copiedRequestIdTimeoutRef.current) {
() => setCopiedRequestId(false), clearTimeout(copiedRequestIdTimeoutRef.current);
2000, }
); copiedRequestIdTimeoutRef.current = setTimeout(
}) () => setCopiedRequestId(false),
.catch(() => { 2000,
// noop );
}); })
}} .catch(() => {
title={ // noop
copiedRequestId });
}}
aria-label="Copy Request ID"
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 ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
<span className="text-xs">
{copiedRequestId ? "Copied" : "Request ID"}
</span>
</TooltipTrigger>
<TooltipContent>
{copiedRequestId
? "Copied!" ? "Copied!"
: `Copy Request ID: ${message.requestId.slice(0, 8)}...` : `Copy Request ID: ${message.requestId.slice(0, 8)}...`}
} </TooltipContent>
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" </Tooltip>
>
{copiedRequestId ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
<span className="text-xs">
{copiedRequestId ? "Copied" : "Request ID"}
</span>
</button>
)} )}
{isLastMessage && message.totalTokens && ( {isLastMessage && message.totalTokens && (
<div <div
......
import { AlertTriangle, ArrowRight } from "lucide-react"; import { AlertTriangle, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useSummarizeInNewChat } from "./SummarizeInNewChatButton"; import { useSummarizeInNewChat } from "./SummarizeInNewChatButton";
const CONTEXT_LIMIT_THRESHOLD = 40_000; const CONTEXT_LIMIT_THRESHOLD = 40_000;
...@@ -52,16 +57,22 @@ export function ContextLimitBanner({ ...@@ -52,16 +57,22 @@ export function ContextLimitBanner({
<AlertTriangle className="h-3.5 w-3.5 shrink-0" /> <AlertTriangle className="h-3.5 w-3.5 shrink-0" />
<span>{message}</span> <span>{message}</span>
</span> </span>
<Button <Tooltip>
onClick={handleSummarize} <TooltipTrigger
variant="outline" render={
size="sm" <Button
className="h-6 px-2 text-xs border-amber-500/40 bg-amber-500/5 text-amber-600 dark:text-amber-500 hover:bg-amber-500/20 hover:border-amber-500/60" onClick={handleSummarize}
title="Summarize to new chat" variant="outline"
> size="sm"
Summarize className="h-6 px-2 text-xs border-amber-500/40 bg-amber-500/5 text-amber-600 dark:text-amber-500 hover:bg-amber-500/20 hover:border-amber-500/60"
<ArrowRight className="h-3 w-3 ml-1" /> />
</Button> }
>
Summarize
<ArrowRight className="h-3 w-3 ml-1" />
</TooltipTrigger>
<TooltipContent>Summarize to new chat</TooltipContent>
</Tooltip>
</div> </div>
); );
} }
import { SendIcon, StopCircleIcon } from "lucide-react"; import { SendIcon, StopCircleIcon } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { homeChatInputValueAtom } from "@/atoms/chatAtoms"; // Use a different atom for home input import { homeChatInputValueAtom } from "@/atoms/chatAtoms"; // Use a different atom for home input
...@@ -102,21 +107,37 @@ export function HomeChatInput({ ...@@ -102,21 +107,37 @@ export function HomeChatInput({
/> />
{isStreaming ? ( {isStreaming ? (
<button <Tooltip>
className="px-2 py-2 mt-1 mr-1 text-(--sidebar-accent-fg) rounded-lg opacity-50 cursor-not-allowed" // Indicate disabled state <TooltipTrigger
title="Cancel generation (unavailable here)" render={
> <button
<StopCircleIcon size={20} /> aria-label="Cancel generation (unavailable here)"
</button> className="px-2 py-2 mt-1 mr-1 text-(--sidebar-accent-fg) rounded-lg opacity-50 cursor-not-allowed"
/>
}
>
<StopCircleIcon size={20} />
</TooltipTrigger>
<TooltipContent>
Cancel generation (unavailable here)
</TooltipContent>
</Tooltip>
) : ( ) : (
<button <Tooltip>
onClick={handleCustomSubmit} <TooltipTrigger
disabled={!inputValue.trim() && attachments.length === 0} render={
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50" <button
title="Send message" onClick={handleCustomSubmit}
> disabled={!inputValue.trim() && attachments.length === 0}
<SendIcon size={20} /> aria-label="Send message"
</button> className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
/>
}
>
<SendIcon size={20} />
</TooltipTrigger>
<TooltipContent>Send message</TooltipContent>
</Tooltip>
)} )}
</div> </div>
<div className="pl-2 pr-1 flex items-center justify-between pb-2"> <div className="pl-2 pr-1 flex items-center justify-between pb-2">
......
...@@ -5,6 +5,11 @@ import { ...@@ -5,6 +5,11 @@ import {
} from "@/atoms/previewAtoms"; } from "@/atoms/previewAtoms";
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { Code2, X } from "lucide-react"; import { Code2, X } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
export function SelectedComponentsDisplay() { export function SelectedComponentsDisplay() {
const [selectedComponents, setSelectedComponents] = useAtom( const [selectedComponents, setSelectedComponents] = useAtom(
...@@ -57,13 +62,19 @@ export function SelectedComponentsDisplay() { ...@@ -57,13 +62,19 @@ export function SelectedComponentsDisplay() {
<span className="text-xs font-medium text-muted-foreground"> <span className="text-xs font-medium text-muted-foreground">
Selected Components ({selectedComponents.length}) Selected Components ({selectedComponents.length})
</span> </span>
<button <Tooltip>
onClick={handleClearAll} <TooltipTrigger
className="text-xs text-muted-foreground hover:text-foreground transition-colors" render={
title="Clear all selected components" <button
> onClick={handleClearAll}
Clear all className="text-xs text-muted-foreground hover:text-foreground transition-colors"
</button> />
}
>
Clear all
</TooltipTrigger>
<TooltipContent>Clear all selected components</TooltipContent>
</Tooltip>
</div> </div>
{selectedComponents.map((selectedComponent, index) => ( {selectedComponents.map((selectedComponent, index) => (
<div key={selectedComponent.id} className="mb-1 last:mb-0"> <div key={selectedComponent.id} className="mb-1 last:mb-0">
...@@ -89,13 +100,20 @@ export function SelectedComponentsDisplay() { ...@@ -89,13 +100,20 @@ export function SelectedComponentsDisplay() {
</span> </span>
</div> </div>
</div> </div>
<button <Tooltip>
onClick={() => handleRemoveComponent(index)} <TooltipTrigger
className="ml-2 flex-shrink-0 rounded-full p-0.5 hover:bg-indigo-600/20" render={
title="Deselect component" <button
> onClick={() => handleRemoveComponent(index)}
<X size={18} className="text-indigo-600 dark:text-indigo-400" /> aria-label="Deselect component"
</button> className="ml-2 flex-shrink-0 rounded-full p-0.5 hover:bg-indigo-600/20"
/>
}
>
<X size={18} className="text-indigo-600 dark:text-indigo-400" />
</TooltipTrigger>
<TooltipContent>Deselect component</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
))} ))}
......
...@@ -24,6 +24,11 @@ import { ...@@ -24,6 +24,11 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { showError, showSuccess } from "@/lib/toast"; import { showError, showSuccess } from "@/lib/toast";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { useCheckProblems } from "@/hooks/useCheckProblems"; import { useCheckProblems } from "@/hooks/useCheckProblems";
...@@ -170,19 +175,25 @@ export const ActionHeader = () => { ...@@ -170,19 +175,25 @@ export const ActionHeader = () => {
badge?: React.ReactNode, badge?: React.ReactNode,
) => { ) => {
return ( return (
<button <Tooltip>
data-testid={testId} <TooltipTrigger
ref={ref} render={
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" <button
onClick={() => selectPanel(mode)} data-testid={testId}
title={isCompact ? text : undefined} 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"
{icon} onClick={() => selectPanel(mode)}
<span> />
{!isCompact && <span>{text}</span>} }
{badge} >
</span> {icon}
</button> <span>
{!isCompact && <span>{text}</span>}
{badge}
</span>
</TooltipTrigger>
{isCompact && <TooltipContent>{text}</TooltipContent>}
</Tooltip>
); );
}; };
const iconSize = 15; const iconSize = 15;
...@@ -255,13 +266,19 @@ export const ActionHeader = () => { ...@@ -255,13 +266,19 @@ export const ActionHeader = () => {
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<ChatActivityButton /> <ChatActivityButton />
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <Tooltip>
data-testid="preview-more-options-button" <TooltipTrigger
className="no-app-region-drag flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors" render={
title="More options" <DropdownMenuTrigger
> data-testid="preview-more-options-button"
<MoreVertical size={16} /> className="no-app-region-drag flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
</DropdownMenuTrigger> />
}
>
<MoreVertical size={16} />
</TooltipTrigger>
<TooltipContent>More options</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="w-60"> <DropdownMenuContent align="end" className="w-60">
<DropdownMenuItem onClick={onCleanRestart}> <DropdownMenuItem onClick={onCleanRestart}>
<Cog size={16} /> <Cog size={16} />
......
...@@ -10,6 +10,11 @@ import { ...@@ -10,6 +10,11 @@ import {
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ToolbarColorPicker } from "./ToolbarColorPicker"; import { ToolbarColorPicker } from "./ToolbarColorPicker";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface AnnotatorToolbarProps { interface AnnotatorToolbarProps {
tool: "select" | "draw" | "text"; tool: "select" | "draw" | "text";
...@@ -46,108 +51,158 @@ export const AnnotatorToolbar = ({ ...@@ -46,108 +51,158 @@ export const AnnotatorToolbar = ({
<div className="flex items-center justify-center p-2 border-b space-x-2"> <div className="flex items-center justify-center p-2 border-b space-x-2">
{/* Tool Selection Buttons */} {/* Tool Selection Buttons */}
<div className="flex space-x-1"> <div className="flex space-x-1">
<button <Tooltip>
onClick={() => onToolChange("select")} <TooltipTrigger
aria-label="Select" render={
title="Select" <button
className={cn( onClick={() => onToolChange("select")}
"p-1 rounded transition-colors duration-200", aria-label="Select"
tool === "select" className={cn(
? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700" "p-1 rounded transition-colors duration-200",
: " text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900", tool === "select"
)} ? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700"
> : " text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900",
<MousePointer2 size={16} /> )}
</button> />
}
>
<MousePointer2 size={16} />
</TooltipTrigger>
<TooltipContent>Select</TooltipContent>
</Tooltip>
<button <Tooltip>
onClick={() => onToolChange("draw")} <TooltipTrigger
aria-label="Draw" render={
title="Draw" <button
className={cn( onClick={() => onToolChange("draw")}
"p-1 rounded transition-colors duration-200", aria-label="Draw"
tool === "draw" className={cn(
? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700" "p-1 rounded transition-colors duration-200",
: " text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900", tool === "draw"
)} ? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700"
> : " text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900",
<Pencil size={16} /> )}
</button> />
}
>
<Pencil size={16} />
</TooltipTrigger>
<TooltipContent>Draw</TooltipContent>
</Tooltip>
<button <Tooltip>
onClick={() => onToolChange("text")} <TooltipTrigger
aria-label="Text" render={
title="Text" <button
className={cn( onClick={() => onToolChange("text")}
"p-1 rounded transition-colors duration-200", aria-label="Text"
tool === "text" className={cn(
? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700" "p-1 rounded transition-colors duration-200",
: "text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900", tool === "text"
)} ? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700"
> : "text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900",
<Type size={16} /> )}
</button> />
}
>
<Type size={16} />
</TooltipTrigger>
<TooltipContent>Text</TooltipContent>
</Tooltip>
<div <Tooltip>
className="p-1 rounded transition-colors duration-200 hover:bg-purple-200 dark:hover:bg-purple-900" <TooltipTrigger>
title="Color" <div className="p-1 rounded transition-colors duration-200 hover:bg-purple-200 dark:hover:bg-purple-900">
> <ToolbarColorPicker color={color} onChange={onColorChange} />
<ToolbarColorPicker color={color} onChange={onColorChange} /> </div>
</div> </TooltipTrigger>
<TooltipContent>Color</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" /> <div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<button <Tooltip>
onClick={onDelete} <TooltipTrigger
aria-label="Delete" render={
title="Delete Selected" <button
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" onClick={onDelete}
disabled={!selectedId} aria-label="Delete"
> 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"
<Trash2 size={16} /> disabled={!selectedId}
</button> />
}
>
<Trash2 size={16} />
</TooltipTrigger>
<TooltipContent>Delete Selected</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" /> <div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<button <Tooltip>
onClick={onUndo} <TooltipTrigger
aria-label="Undo" render={
title="Undo" <button
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" onClick={onUndo}
disabled={historyStep === 0} aria-label="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"
<Undo size={16} /> disabled={historyStep === 0}
</button> />
}
>
<Undo size={16} />
</TooltipTrigger>
<TooltipContent>Undo</TooltipContent>
</Tooltip>
<button <Tooltip>
onClick={onRedo} <TooltipTrigger
aria-label="Redo" render={
title="Redo" <button
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" onClick={onRedo}
disabled={historyStep === historyLength - 1} aria-label="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"
<Redo size={16} /> disabled={historyStep === historyLength - 1}
</button> />
}
>
<Redo size={16} />
</TooltipTrigger>
<TooltipContent>Redo</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" /> <div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<button <Tooltip>
onClick={onSubmit} <TooltipTrigger
aria-label="Add to Chat" render={
title="Add to Chat" <button
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" onClick={onSubmit}
disabled={!hasSubmitHandler} aria-label="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"
<Check size={16} /> disabled={!hasSubmitHandler}
</button> />
<button }
onClick={onDeactivate} >
aria-label="Close Annotator" <Check size={16} />
title="Close Annotator" </TooltipTrigger>
className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900" <TooltipContent>Add to Chat</TooltipContent>
> </Tooltip>
<X size={16} /> <Tooltip>
</button> <TooltipTrigger
render={
<button
onClick={onDeactivate}
aria-label="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} />
</TooltipTrigger>
<TooltipContent>Close Annotator</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
); );
......
...@@ -3,6 +3,11 @@ import { FileTree } from "./FileTree"; ...@@ -3,6 +3,11 @@ import { FileTree } from "./FileTree";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useLoadApp } from "@/hooks/useLoadApp"; import { useLoadApp } from "@/hooks/useLoadApp";
import { RefreshCw, Maximize2, Minimize2 } from "lucide-react"; import { RefreshCw, Maximize2, Minimize2 } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { selectedFileAtom } from "@/atoms/viewAtoms"; import { selectedFileAtom } from "@/atoms/viewAtoms";
...@@ -59,23 +64,37 @@ export const CodeView = ({ loading, app }: CodeViewProps) => { ...@@ -59,23 +64,37 @@ export const CodeView = ({ loading, app }: CodeViewProps) => {
> >
{/* Toolbar */} {/* Toolbar */}
<div className="flex items-center p-2 border-b space-x-2"> <div className="flex items-center p-2 border-b space-x-2">
<button <Tooltip>
onClick={() => refreshApp()} <TooltipTrigger
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed" render={
disabled={loading || !app.id} <button
title="Refresh Files" onClick={() => refreshApp()}
> className="p-1 rounded hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
<RefreshCw size={16} /> disabled={loading || !app.id}
</button> />
}
>
<RefreshCw size={16} />
</TooltipTrigger>
<TooltipContent>Refresh Files</TooltipContent>
</Tooltip>
<div className="text-sm text-gray-500">{app.files.length} files</div> <div className="text-sm text-gray-500">{app.files.length} files</div>
<div className="flex-1" /> <div className="flex-1" />
<button <Tooltip>
onClick={() => setIsFullscreen((value) => !value)} <TooltipTrigger
className="p-1 rounded hover:bg-gray-200" render={
title={isFullscreen ? "Exit full screen" : "Enter full screen"} <button
> onClick={() => setIsFullscreen((value) => !value)}
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />} className="p-1 rounded hover:bg-gray-200"
</button> />
}
>
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
</TooltipTrigger>
<TooltipContent>
{isFullscreen ? "Exit full screen" : "Enter full screen"}
</TooltipContent>
</Tooltip>
</div> </div>
{/* Content */} {/* Content */}
......
...@@ -7,6 +7,11 @@ import { ...@@ -7,6 +7,11 @@ import {
} from "lucide-react"; } from "lucide-react";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { chatInputValueAtom } from "@/atoms/chatAtoms"; import { chatInputValueAtom } from "@/atoms/chatAtoms";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface ConsoleEntryProps { interface ConsoleEntryProps {
type: "server" | "client" | "edge-function" | "network-requests"; type: "server" | "client" | "edge-function" | "network-requests";
...@@ -120,14 +125,20 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => { ...@@ -120,14 +125,20 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => {
)} )}
</span> </span>
</div> </div>
<button <Tooltip>
onClick={handleSendToChat} <TooltipTrigger
title="Send to chat" render={
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded" <button
data-testid="send-to-chat" onClick={handleSendToChat}
> className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
<MessageSquare size={12} className="text-gray-500" /> data-testid="send-to-chat"
</button> />
}
>
<MessageSquare size={12} className="text-gray-500" />
</TooltipTrigger>
<TooltipContent>Send to chat</TooltipContent>
</Tooltip>
</div> </div>
); );
}; };
import { Filter, X, Trash2 } from "lucide-react"; import { Filter, X, Trash2 } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface ConsoleFiltersProps { interface ConsoleFiltersProps {
levelFilter: "all" | "info" | "warn" | "error"; levelFilter: "all" | "info" | "warn" | "error";
...@@ -109,14 +114,20 @@ export const ConsoleFilters = ({ ...@@ -109,14 +114,20 @@ export const ConsoleFilters = ({
)} )}
{/* Clear logs button */} {/* Clear logs button */}
<button <Tooltip>
onClick={onClearLogs} <TooltipTrigger
className="p-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" render={
data-testid="clear-logs-button" <button
title="Clear logs" onClick={onClearLogs}
> className="p-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
<Trash2 size={14} /> data-testid="clear-logs-button"
</button> />
}
>
<Trash2 size={14} />
</TooltipTrigger>
<TooltipContent>Clear logs</TooltipContent>
</Tooltip>
<div className="ml-auto text-xs text-gray-500">{totalLogs} logs</div> <div className="ml-auto text-xs text-gray-500">{totalLogs} logs</div>
</div> </div>
......
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import { X } from "lucide-react"; import { X } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface DraggableTextInputProps { interface DraggableTextInputProps {
input: { input: {
...@@ -158,18 +163,24 @@ export const DraggableTextInput = ({ ...@@ -158,18 +163,24 @@ export const DraggableTextInput = ({
/> />
{/* Close Button - Rightmost */} {/* Close Button - Rightmost */}
<button <Tooltip>
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded transition-colors z-10 group" <TooltipTrigger
onClick={(e) => { render={
e.preventDefault(); <button
e.stopPropagation(); className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded transition-colors z-10 group"
onRemove(input.id); onClick={(e) => {
}} e.preventDefault();
title="Remove text input" e.stopPropagation();
type="button" onRemove(input.id);
> }}
<X className="w-3 h-3 text-gray-400 dark:text-gray-500 group-hover:text-red-600 dark:group-hover:text-red-400" /> type="button"
</button> />
}
>
<X className="w-3 h-3 text-gray-400 dark:text-gray-500 group-hover:text-red-600 dark:group-hover:text-red-400" />
</TooltipTrigger>
<TooltipContent>Remove text input</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
); );
......
...@@ -12,6 +12,11 @@ import { useSettings } from "@/hooks/useSettings"; ...@@ -12,6 +12,11 @@ import { useSettings } from "@/hooks/useSettings";
import { useCheckProblems } from "@/hooks/useCheckProblems"; import { useCheckProblems } from "@/hooks/useCheckProblems";
import { getLanguage } from "@/utils/get_language"; import { getLanguage } from "@/utils/get_language";
import { queryKeys } from "@/lib/queryKeys"; import { queryKeys } from "@/lib/queryKeys";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface FileEditorProps { interface FileEditorProps {
appId: number | null; appId: number | null;
...@@ -53,17 +58,25 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({ ...@@ -53,17 +58,25 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({
))} ))}
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0 ml-2"> <div className="flex items-center gap-2 flex-shrink-0 ml-2">
<Button <Tooltip>
variant="ghost" <TooltipTrigger
size="sm" render={
onClick={onSave} <Button
disabled={!hasUnsavedChanges || isSaving} variant="ghost"
className="h-6 w-6 p-0" size="sm"
data-testid="save-file-button" onClick={onSave}
title={hasUnsavedChanges ? "Save changes" : "No unsaved changes"} disabled={!hasUnsavedChanges || isSaving}
> className="h-6 w-6 p-0"
<Save size={12} /> data-testid="save-file-button"
</Button> />
}
>
<Save size={12} />
</TooltipTrigger>
<TooltipContent>
{hasUnsavedChanges ? "Save changes" : "No unsaved changes"}
</TooltipContent>
</Tooltip>
{hasUnsavedChanges && ( {hasUnsavedChanges && (
<Circle <Circle
size={8} size={8}
......
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { X, Move, Square, Palette, Type } from "lucide-react"; import { X, Move, Square, Palette, Type } from "lucide-react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { ComponentSelection } from "@/ipc/types"; import { ComponentSelection } from "@/ipc/types";
...@@ -314,14 +319,20 @@ export function VisualEditingToolbar({ ...@@ -314,14 +319,20 @@ export function VisualEditingToolbar({
left: `${toolbarLeft}px`, left: `${toolbarLeft}px`,
}} }}
> >
<button <Tooltip>
onClick={handleDeselectComponent} <TooltipTrigger
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-[#7f22fe] dark:text-gray-200" render={
aria-label="Deselect Component" <button
title="Deselect Component" onClick={handleDeselectComponent}
> className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-[#7f22fe] dark:text-gray-200"
<X size={16} /> aria-label="Deselect Component"
</button> />
}
>
<X size={16} />
</TooltipTrigger>
<TooltipContent>Deselect Component</TooltipContent>
</Tooltip>
{isDynamic ? ( {isDynamic ? (
<div className="flex items-center px-2 py-1 text-yellow-800 dark:text-yellow-200 rounded text-xs font-medium"> <div className="flex items-center px-2 py-1 text-yellow-800 dark:text-yellow-200 rounded text-xs font-medium">
......
...@@ -513,7 +513,7 @@ function SidebarMenuButton<T extends React.ElementType = "button">({ ...@@ -513,7 +513,7 @@ function SidebarMenuButton<T extends React.ElementType = "button">({
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger>{button}</TooltipTrigger> <TooltipTrigger render={button} />
<TooltipContent <TooltipContent
side="right" side="right"
align="center" align="center"
......
...@@ -18,11 +18,7 @@ function TooltipProvider({ ...@@ -18,11 +18,7 @@ function TooltipProvider({
} }
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) { function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return ( return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
} }
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) { function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
...@@ -32,7 +28,7 @@ function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) { ...@@ -32,7 +28,7 @@ function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
function TooltipContent({ function TooltipContent({
className, className,
side = "top", side = "top",
sideOffset = 0, sideOffset = 4,
align = "center", align = "center",
alignOffset = 0, alignOffset = 0,
children, children,
...@@ -54,13 +50,13 @@ function TooltipContent({ ...@@ -54,13 +50,13 @@ function TooltipContent({
<TooltipPrimitive.Popup <TooltipPrimitive.Popup
data-slot="tooltip-content" data-slot="tooltip-content"
className={cn( className={cn(
"bg-primary text-primary-foreground data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance origin-(--transform-origin)", "data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 rounded-md px-3 py-1.5 text-xs data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-foreground text-background z-50 w-fit max-w-xs origin-(--transform-origin)",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> <TooltipPrimitive.Arrow className="size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 bg-foreground fill-foreground z-50 data-[side=bottom]:top-1 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
</TooltipPrimitive.Popup> </TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner> </TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
......
...@@ -18,6 +18,11 @@ import { ...@@ -18,6 +18,11 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
Dialog, Dialog,
...@@ -357,17 +362,23 @@ export default function AppDetailsPage() { ...@@ -357,17 +362,23 @@ export default function AppDetailsPage() {
Path Path
</span> </span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button <Tooltip>
variant="ghost" <TooltipTrigger
size="icon" render={
className="ml-[-8px] p-0.5 h-auto cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" <Button
onClick={() => { variant="ghost"
ipc.system.showItemInFolder(currentAppPath); size="icon"
}} className="ml-[-8px] p-0.5 h-auto cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
title="Show in folder" onClick={() => {
> ipc.system.showItemInFolder(currentAppPath);
<Folder className="h-3.5 w-3.5" /> }}
</Button> />
}
>
<Folder className="h-3.5 w-3.5" />
</TooltipTrigger>
<TooltipContent>Show in folder</TooltipContent>
</Tooltip>
<span className="text-sm break-all">{currentAppPath}</span> <span className="text-sm break-all">{currentAppPath}</span>
</div> </div>
</div> </div>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论