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
- Linting or test setup changes
- Documentation-only changes
- 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 @@
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/mcp-calculator
- paragraph: I'll calculate the sum of 5 and 3 using the calculator.
......@@ -36,7 +36,7 @@
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
......
......@@ -6,7 +6,7 @@
- img
- text: file1.txt
- paragraph: More EOM
- button:
- button "Copy":
- img
- img
- text: Approved
......@@ -14,20 +14,20 @@
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/ask-read-file
- paragraph: Let me read the file to explain its contents.
- img
- 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.
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
......
......@@ -6,11 +6,15 @@
- 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":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/parallel-tools
- paragraph: I'll create two files for you in parallel.
......@@ -27,11 +31,15 @@
- img
- text: "src/utils/string.ts Summary: Create string utilities"
- paragraph: Task completed.
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
......@@ -6,7 +6,7 @@
- img
- text: file1.txt
- paragraph: More EOM
- button:
- button "Copy":
- img
- img
- text: Approved
......@@ -14,7 +14,7 @@
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/read-then-edit
- paragraph: Let me first read the current file contents to understand what we're working with.
......@@ -26,13 +26,13 @@
- img
- 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.
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
......
......@@ -6,13 +6,15 @@
- 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":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/code-search
- paragraph: I'll search for files related to React components in the codebase.
......@@ -20,13 +22,13 @@
- img
- img
- 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
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
......
......@@ -6,22 +6,30 @@
- 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":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/add-dependency
- paragraph: I'll add a dependency to your project.
- img
- 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.
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
......@@ -6,22 +6,30 @@
- 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":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/add-dependency
- paragraph: I'll add a dependency to your project.
- img
- 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.
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
......@@ -6,11 +6,15 @@
- 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":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/add-dependency
- paragraph: I'll add a dependency to your project.
......@@ -24,11 +28,15 @@
- button "Fix with AI":
- img
- paragraph: Dependency added done.
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
......@@ -6,13 +6,15 @@
- 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":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/upload-to-codebase
- paragraph: "Attachments:"
......@@ -25,13 +27,13 @@
- img
- 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.
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
......
......@@ -6,31 +6,43 @@
- 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":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/grep-search
- paragraph: I'll search for 'createRoot' in the codebase to find where the React app is initialized.
- img
- text: GREP"createRoot"(2 matches)
- 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.
- img
- text: GREP"App" in *.tsx(4 matches)
- 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.
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
......
......@@ -6,18 +6,20 @@
- 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":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/read-logs
- paragraph: Let me check the recent console logs to see what's happening in the application.
- img
- text: /LOGSReading \d+ logs/
- text: LOGSReading 8 logs
- img
- paragraph: Now let me filter for only error logs to identify any issues.
- img
......@@ -28,13 +30,13 @@
- text: "LOGSReading 0 logs (type: client)"
- img
- paragraph: I've reviewed the console logs. The application appears to be running normally with no critical errors detected.
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
......
......@@ -6,7 +6,7 @@
- img
- text: file1.txt
- paragraph: More EOM
- button:
- button "Copy":
- img
- img
- text: Approved
......@@ -14,7 +14,7 @@
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- paragraph: tc=local-agent/search-replace
- paragraph: Let me first read the file to see its contents.
......@@ -26,13 +26,13 @@
- img
- text: src/App.tsx
- paragraph: Done! I've updated the message using search_replace. The edit was applied successfully.
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
......
......@@ -6,13 +6,13 @@
- img
- text: file1.txt
- paragraph: More EOM
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
......
- paragraph: "[dump]"
- paragraph: "[[dyad-dump-path=*]]"
- button:
- button "Copy":
- img
- img
- text: Approved
- img
- text: auto
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
......@@ -3,13 +3,15 @@
- img
- text: Index.tsx Read src/pages/Index.tsx
- paragraph: Done.
- button:
- button "Copy":
- img
- img
- text: Approved
- img
- text: auto
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- paragraph: tc=update-index-1
- paragraph: First read
......@@ -19,38 +21,46 @@
- img
- img
- text: "src/pages/Index.tsx Summary: replace file"
- button:
- button "Copy":
- img
- img
- text: Approved
- img
- text: auto
- img
- text: less than a minute ago
- img
- text: wrote 1 file(s)
- button "Request ID":
- button "Copy Request ID":
- img
- paragraph: tc=read-index
- paragraph: "Read the index page:"
- img
- text: Index.tsx Read src/pages/Index.tsx
- paragraph: Done.
- button:
- button "Copy":
- img
- img
- text: Approved
- img
- text: auto
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- paragraph: "[dump]"
- paragraph: "[[dyad-dump-path=*]]"
- button:
- button "Copy":
- img
- img
- text: Approved
- img
- text: auto
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
......@@ -5,11 +5,15 @@
- img
- text: src/pages/Index.tsx
- paragraph: End of turbo edit
- button:
- button "Copy":
- img
- img
- text: auto
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
......@@ -9,17 +9,31 @@
- text: Warning Could not apply Turbo Edits properly for some of the files; re-generating code......
- 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
- button "Edit":
- img
- img
- text: "src/pages/Index.tsx Summary: Rewrite file."
- paragraph: "[[dyad-dump-path=*]]"
- button:
- button "Copy":
- img
- img
- text: auto
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
......@@ -5,6 +5,11 @@ import {
SelectItem,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useSettings } from "@/hooks/useSettings";
import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota";
import type { ChatMode } from "@/lib/schemas";
......@@ -91,19 +96,27 @@ export function ChatModeSelector() {
value={selectedMode}
onValueChange={(v) => v && handleModeChange(v)}
>
<MiniSelectTrigger
data-testid="chat-mode-selector"
title={`Open mode menu (${isMac ? "⌘ + ." : "Ctrl + ."} to toggle)`}
className={cn(
"h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5",
selectedMode === "build" || selectedMode === "local-agent"
? "bg-background hover:bg-muted/50 focus:bg-muted/50"
: "bg-primary/10 hover:bg-primary/20 focus:bg-primary/20 text-primary border-primary/20 dark:bg-primary/20 dark:hover:bg-primary/30 dark:focus:bg-primary/30",
)}
size="sm"
>
<SelectValue>{getModeDisplayName(selectedMode)}</SelectValue>
</MiniSelectTrigger>
<Tooltip>
<TooltipTrigger
render={
<MiniSelectTrigger
data-testid="chat-mode-selector"
className={cn(
"h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5",
selectedMode === "build" || selectedMode === "local-agent"
? "bg-background hover:bg-muted/50 focus:bg-muted/50"
: "bg-primary/10 hover:bg-primary/20 focus:bg-primary/20 text-primary border-primary/20 dark:bg-primary/20 dark:hover:bg-primary/30 dark:focus:bg-primary/30",
)}
size="sm"
/>
}
>
<SelectValue>{getModeDisplayName(selectedMode)}</SelectValue>
</TooltipTrigger>
<TooltipContent>
{`Open mode menu (${isMac ? "\u2318 + ." : "Ctrl + ."} to toggle)`}
</TooltipContent>
</Tooltip>
<SelectContent align="start">
{isProEnabled && (
<SelectItem value="local-agent">
......
......@@ -14,6 +14,11 @@ import { VersionPane } from "./chat/VersionPane";
import { ChatError } from "./chat/ChatError";
import { FreeAgentQuotaBanner } from "./chat/FreeAgentQuotaBanner";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { ArrowDown } from "lucide-react";
import { useSettings } from "@/hooks/useSettings";
import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota";
......@@ -166,15 +171,21 @@ export function ChatPanel({
{/* Scroll to bottom button */}
{showScrollButton && (
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-10">
<Button
onClick={handleScrollButtonClick}
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"
variant="outline"
title={"Scroll to bottom"}
>
<ArrowDown className="h-4 w-4" />
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button
onClick={handleScrollButtonClick}
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"
variant="outline"
/>
}
>
<ArrowDown className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent>Scroll to bottom</TooltipContent>
</Tooltip>
</div>
)}
</div>
......
import { Copy, Check } from "lucide-react";
import { useState } from "react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface CopyErrorMessageProps {
errorMessage: string;
......@@ -24,26 +29,34 @@ export const CopyErrorMessage = ({
};
return (
<button
onClick={handleCopy}
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
isCopied
? "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"
} ${className}`}
title={isCopied ? "Copied!" : "Copy error message"}
>
{isCopied ? (
<>
<Check size={14} />
<span>Copied</span>
</>
) : (
<>
<Copy size={14} />
<span>Copy</span>
</>
)}
</button>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={handleCopy}
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
isCopied
? "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"
} ${className}`}
/>
}
>
{isCopied ? (
<>
<Check size={14} />
<span>Copied</span>
</>
) : (
<>
<Copy size={14} />
<span>Copy</span>
</>
)}
</TooltipTrigger>
<TooltipContent>
{isCopied ? "Copied!" : "Copy error message"}
</TooltipContent>
</Tooltip>
);
};
import React from "react";
import { toast } from "sonner";
import { X, Copy, Check } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface CustomErrorToastProps {
message: string;
......@@ -41,30 +46,38 @@ export function CustomErrorToast({
{/* Action buttons */}
<div className="flex items-center space-x-1.5 ml-auto">
<button
onClick={(e) => {
e.stopPropagation();
handleCopy();
}}
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-100/70 rounded-lg transition-all duration-150"
title="Copy to clipboard"
>
{copied ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4" />
)}
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleClose();
}}
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-100/70 rounded-lg transition-all duration-150"
title="Close"
>
<X className="w-4 h-4" />
</button>
<Tooltip>
<TooltipTrigger>
<button
onClick={(e) => {
e.stopPropagation();
handleCopy();
}}
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" />
) : (
<Copy className="w-4 h-4" />
)}
</button>
</TooltipTrigger>
<TooltipContent>Copy to clipboard</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<button
onClick={(e) => {
e.stopPropagation();
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>
......
......@@ -34,6 +34,11 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import {
Dialog,
DialogContent,
......@@ -414,14 +419,22 @@ export function GithubBranchManager({
</Select>
<DropdownMenu>
<DropdownMenuTrigger
className={cn(buttonVariants({ variant: "outline", size: "icon" }))}
title="Branch actions"
aria-label="Branch actions"
data-testid="branch-actions-menu-trigger"
>
<EllipsisVertical className="h-4 w-4" />
</DropdownMenuTrigger>
<Tooltip>
<TooltipTrigger
render={
<DropdownMenuTrigger
className={cn(
buttonVariants({ variant: "outline", size: "icon" }),
)}
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">
<DropdownMenuItem
onClick={() => setShowCreateDialog(true)}
......
......@@ -7,6 +7,11 @@ import {
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Sparkles, Info } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useSettings } from "@/hooks/useSettings";
import { ipc } from "@/ipc/types";
import { hasDyadProKey, type UserSettings } from "@/lib/schemas";
......@@ -57,13 +62,17 @@ export function ProModeSelector() {
return (
<Popover>
<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>
</PopoverTrigger>
<Tooltip>
<TooltipTrigger
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>
</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">
......@@ -205,36 +214,58 @@ function TurboEditsSelector({
className="inline-flex rounded-md border border-input"
data-testid="turbo-edits-selector"
>
<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>
<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>
<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>
<Tooltip>
<TooltipTrigger
render={
<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"
/>
}
>
Off
</TooltipTrigger>
<TooltipContent>Disable Turbo Edits</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<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"
/>
}
>
Classic
</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>
);
......@@ -282,36 +313,59 @@ function SmartContextSelector({
className="inline-flex rounded-md border border-input"
data-testid="smart-context-selector"
>
<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>
<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>
<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>
<Tooltip>
<TooltipTrigger
render={
<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"
/>
}
>
Off
</TooltipTrigger>
<TooltipContent>Disable Smart Context</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<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"
/>
}
>
Balanced
</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>
);
......
......@@ -17,6 +17,11 @@ import { AlertTriangle } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import {
AlertDialog,
AlertDialogAction,
......@@ -123,26 +128,38 @@ export function ProviderSettingsGrid() {
className="flex items-center justify-end"
onClick={(e) => e.stopPropagation()}
>
<Button
data-testid="edit-custom-provider"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 hover:bg-muted rounded-md"
title="Edit Provider"
onClick={() => handleEditProvider(provider)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
data-testid="delete-custom-provider"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10 rounded-md"
title="Delete Provider"
onClick={() => setProviderToDelete(provider.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button
data-testid="edit-custom-provider"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 hover:bg-muted rounded-md"
onClick={() => handleEditProvider(provider)}
/>
}
>
<Edit className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent>Edit Provider</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<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>
)}
<CardTitle className="text-lg font-medium mb-2">
......
......@@ -38,6 +38,11 @@ import connectSupabaseDark from "../../assets/supabase/connect-supabase-dark.svg
import connectSupabaseLight from "../../assets/supabase/connect-supabase-light.svg";
import { ExternalLink, Plus, RefreshCw, Trash2 } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useTheme } from "@/contexts/ThemeContext";
import { isSupabaseConnected } from "@/lib/schemas";
......@@ -268,17 +273,23 @@ export function SupabaseConnector({ appId }: { appId: number }) {
<CardTitle className="flex items-center justify-between">
Supabase Projects
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={() => refetchProjects()}
disabled={isFetchingProjects}
title="Refresh projects"
>
<RefreshCw
className={`h-4 w-4 ${isFetchingProjects ? "animate-spin" : ""}`}
/>
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="outline"
size="icon"
onClick={() => refetchProjects()}
disabled={isFetchingProjects}
/>
}
>
<RefreshCw
className={`h-4 w-4 ${isFetchingProjects ? "animate-spin" : ""}`}
/>
</TooltipTrigger>
<TooltipContent>Refresh projects</TooltipContent>
</Tooltip>
<Button
variant="outline"
size="sm"
......@@ -333,18 +344,24 @@ export function SupabaseConnector({ appId }: { appId: number }) {
</span>
)}
</div>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-destructive shrink-0"
onClick={() =>
handleDeleteOrganization(org.organizationSlug)
}
title="Disconnect organization"
>
<Trash2 className="h-3.5 w-3.5 mr-1" />
<span className="text-xs">Disconnect</span>
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-destructive shrink-0"
onClick={() =>
handleDeleteOrganization(org.organizationSlug)
}
/>
}
>
<Trash2 className="h-3.5 w-3.5 mr-1" />
<span className="text-xs">Disconnect</span>
</TooltipTrigger>
<TooltipContent>Disconnect organization</TooltipContent>
</Tooltip>
</div>
))}
</div>
......
......@@ -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.
// import { Supabase } from "lucide-react"; // 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 { useSupabase } from "@/hooks/useSupabase";
import { showSuccess, showError } from "@/lib/toast";
......@@ -120,16 +125,24 @@ export function SupabaseIntegration() {
</span>
)}
</div>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-destructive shrink-0"
onClick={() => handleDeleteOrganization(org.organizationSlug)}
title="Disconnect organization"
>
<Trash2 className="h-3.5 w-3.5 mr-1" />
<span className="text-xs">Disconnect</span>
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-destructive shrink-0"
onClick={() =>
handleDeleteOrganization(org.organizationSlug)
}
/>
}
>
<Trash2 className="h-3.5 w-3.5 mr-1" />
<span className="text-xs">Disconnect</span>
</TooltipTrigger>
<TooltipContent>Disconnect organization</TooltipContent>
</Tooltip>
</div>
))}
</div>
......
......@@ -75,6 +75,11 @@ import { VisualEditingChangesDialog } from "@/components/preview_panel/VisualEdi
import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
ContextLimitBanner,
shouldShowContextLimitBanner,
......@@ -467,16 +472,25 @@ export function ChatInput({ chatId }: { chatId?: number }) {
) : (
selectedComponents.length > 0 && (
<div className="border-b border-border p-3 bg-muted/30">
<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>
<Tooltip>
<TooltipTrigger
render={
<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"
/>
}
>
<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>
)
)}
......@@ -505,25 +519,39 @@ export function ChatInput({ chatId }: { chatId?: number }) {
/>
{isStreaming ? (
<button
onClick={handleCancel}
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg"
title="Cancel generation"
>
<StopCircleIcon size={20} />
</button>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={handleCancel}
aria-label="Cancel generation"
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
onClick={handleSubmit}
disabled={
(!inputValue.trim() && attachments.length === 0) ||
disableSendButton
}
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
title="Send message"
>
<SendHorizontalIcon size={20} />
</button>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={handleSubmit}
disabled={
(!inputValue.trim() && attachments.length === 0) ||
disableSendButton
}
aria-label="Send message"
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 className="pl-2 pr-1 flex items-center justify-between pb-2">
......@@ -557,15 +585,21 @@ function SuggestionButton({
}) {
const { isStreaming } = useStreamChat();
return (
<Button
disabled={isStreaming}
variant="outline"
size="sm"
onClick={onClick}
title={tooltipText}
>
{children}
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button
disabled={isStreaming}
variant="outline"
size="sm"
onClick={onClick}
/>
}
>
{children}
</TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent>
</Tooltip>
);
}
......
......@@ -21,6 +21,11 @@ import { useAtomValue } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useEffect, useMemo, useRef, useState } from "react";
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface ChatMessageProps {
message: Message;
......@@ -122,19 +127,28 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
{message.role === "assistant" &&
message.content &&
!isStreaming && (
<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 ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
<span className="hidden sm:inline"></span>
</button>
<Tooltip>
<TooltipTrigger
render={
<button
data-testid="copy-message-button"
onClick={handleCopyFormatted}
aria-label="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 ? (
<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">
{message.approvalState && (
......@@ -187,41 +201,48 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
</div>
)}
{message.requestId && (
<button
onClick={() => {
if (!message.requestId) return;
navigator.clipboard
.writeText(message.requestId)
.then(() => {
setCopiedRequestId(true);
if (copiedRequestIdTimeoutRef.current) {
clearTimeout(copiedRequestIdTimeoutRef.current);
}
copiedRequestIdTimeoutRef.current = setTimeout(
() => setCopiedRequestId(false),
2000,
);
})
.catch(() => {
// noop
});
}}
title={
copiedRequestId
<Tooltip>
<TooltipTrigger
render={
<button
onClick={() => {
if (!message.requestId) return;
navigator.clipboard
.writeText(message.requestId)
.then(() => {
setCopiedRequestId(true);
if (copiedRequestIdTimeoutRef.current) {
clearTimeout(copiedRequestIdTimeoutRef.current);
}
copiedRequestIdTimeoutRef.current = setTimeout(
() => setCopiedRequestId(false),
2000,
);
})
.catch(() => {
// noop
});
}}
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!"
: `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 ? (
<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>
: `Copy Request ID: ${message.requestId.slice(0, 8)}...`}
</TooltipContent>
</Tooltip>
)}
{isLastMessage && message.totalTokens && (
<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;
......@@ -52,16 +57,22 @@ export function ContextLimitBanner({
<AlertTriangle className="h-3.5 w-3.5 shrink-0" />
<span>{message}</span>
</span>
<Button
onClick={handleSummarize}
variant="outline"
size="sm"
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"
title="Summarize to new chat"
>
Summarize
<ArrowRight className="h-3 w-3 ml-1" />
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button
onClick={handleSummarize}
variant="outline"
size="sm"
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"
/>
}
>
Summarize
<ArrowRight className="h-3 w-3 ml-1" />
</TooltipTrigger>
<TooltipContent>Summarize to new chat</TooltipContent>
</Tooltip>
</div>
);
}
import { SendIcon, StopCircleIcon } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useSettings } from "@/hooks/useSettings";
import { homeChatInputValueAtom } from "@/atoms/chatAtoms"; // Use a different atom for home input
......@@ -102,21 +107,37 @@ export function HomeChatInput({
/>
{isStreaming ? (
<button
className="px-2 py-2 mt-1 mr-1 text-(--sidebar-accent-fg) rounded-lg opacity-50 cursor-not-allowed" // Indicate disabled state
title="Cancel generation (unavailable here)"
>
<StopCircleIcon size={20} />
</button>
<Tooltip>
<TooltipTrigger
render={
<button
aria-label="Cancel generation (unavailable here)"
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
onClick={handleCustomSubmit}
disabled={!inputValue.trim() && attachments.length === 0}
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
title="Send message"
>
<SendIcon size={20} />
</button>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={handleCustomSubmit}
disabled={!inputValue.trim() && attachments.length === 0}
aria-label="Send message"
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 className="pl-2 pr-1 flex items-center justify-between pb-2">
......
......@@ -5,6 +5,11 @@ import {
} from "@/atoms/previewAtoms";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { Code2, X } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
export function SelectedComponentsDisplay() {
const [selectedComponents, setSelectedComponents] = useAtom(
......@@ -57,13 +62,19 @@ export function SelectedComponentsDisplay() {
<span className="text-xs font-medium text-muted-foreground">
Selected Components ({selectedComponents.length})
</span>
<button
onClick={handleClearAll}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
title="Clear all selected components"
>
Clear all
</button>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={handleClearAll}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
/>
}
>
Clear all
</TooltipTrigger>
<TooltipContent>Clear all selected components</TooltipContent>
</Tooltip>
</div>
{selectedComponents.map((selectedComponent, index) => (
<div key={selectedComponent.id} className="mb-1 last:mb-0">
......@@ -89,13 +100,20 @@ export function SelectedComponentsDisplay() {
</span>
</div>
</div>
<button
onClick={() => handleRemoveComponent(index)}
className="ml-2 flex-shrink-0 rounded-full p-0.5 hover:bg-indigo-600/20"
title="Deselect component"
>
<X size={18} className="text-indigo-600 dark:text-indigo-400" />
</button>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={() => handleRemoveComponent(index)}
aria-label="Deselect component"
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>
))}
......
......@@ -24,6 +24,11 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { showError, showSuccess } from "@/lib/toast";
import { useMutation } from "@tanstack/react-query";
import { useCheckProblems } from "@/hooks/useCheckProblems";
......@@ -170,19 +175,25 @@ export const ActionHeader = () => {
badge?: React.ReactNode,
) => {
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>
{!isCompact && <span>{text}</span>}
{badge}
</span>
</button>
<Tooltip>
<TooltipTrigger
render={
<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)}
/>
}
>
{icon}
<span>
{!isCompact && <span>{text}</span>}
{badge}
</span>
</TooltipTrigger>
{isCompact && <TooltipContent>{text}</TooltipContent>}
</Tooltip>
);
};
const iconSize = 15;
......@@ -255,13 +266,19 @@ export const ActionHeader = () => {
<div className="flex items-center gap-1">
<ChatActivityButton />
<DropdownMenu>
<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} />
</DropdownMenuTrigger>
<Tooltip>
<TooltipTrigger
render={
<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"
/>
}
>
<MoreVertical size={16} />
</TooltipTrigger>
<TooltipContent>More options</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuItem onClick={onCleanRestart}>
<Cog size={16} />
......
......@@ -10,6 +10,11 @@ import {
} from "lucide-react";
import { cn } from "@/lib/utils";
import { ToolbarColorPicker } from "./ToolbarColorPicker";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface AnnotatorToolbarProps {
tool: "select" | "draw" | "text";
......@@ -46,108 +51,158 @@ export const AnnotatorToolbar = ({
<div className="flex items-center justify-center p-2 border-b space-x-2">
{/* Tool Selection Buttons */}
<div className="flex space-x-1">
<button
onClick={() => onToolChange("select")}
aria-label="Select"
title="Select"
className={cn(
"p-1 rounded transition-colors duration-200",
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>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={() => onToolChange("select")}
aria-label="Select"
className={cn(
"p-1 rounded transition-colors duration-200",
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} />
</TooltipTrigger>
<TooltipContent>Select</TooltipContent>
</Tooltip>
<button
onClick={() => onToolChange("draw")}
aria-label="Draw"
title="Draw"
className={cn(
"p-1 rounded transition-colors duration-200",
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>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={() => onToolChange("draw")}
aria-label="Draw"
className={cn(
"p-1 rounded transition-colors duration-200",
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} />
</TooltipTrigger>
<TooltipContent>Draw</TooltipContent>
</Tooltip>
<button
onClick={() => onToolChange("text")}
aria-label="Text"
title="Text"
className={cn(
"p-1 rounded transition-colors duration-200",
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>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={() => onToolChange("text")}
aria-label="Text"
className={cn(
"p-1 rounded transition-colors duration-200",
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} />
</TooltipTrigger>
<TooltipContent>Text</TooltipContent>
</Tooltip>
<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>
<Tooltip>
<TooltipTrigger>
<div className="p-1 rounded transition-colors duration-200 hover:bg-purple-200 dark:hover:bg-purple-900">
<ToolbarColorPicker color={color} onChange={onColorChange} />
</div>
</TooltipTrigger>
<TooltipContent>Color</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<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>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={onDelete}
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"
disabled={!selectedId}
/>
}
>
<Trash2 size={16} />
</TooltipTrigger>
<TooltipContent>Delete Selected</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<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>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={onUndo}
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"
disabled={historyStep === 0}
/>
}
>
<Undo size={16} />
</TooltipTrigger>
<TooltipContent>Undo</TooltipContent>
</Tooltip>
<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>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={onRedo}
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"
disabled={historyStep === historyLength - 1}
/>
}
>
<Redo size={16} />
</TooltipTrigger>
<TooltipContent>Redo</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<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>
<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>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={onSubmit}
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"
disabled={!hasSubmitHandler}
/>
}
>
<Check size={16} />
</TooltipTrigger>
<TooltipContent>Add to Chat</TooltipContent>
</Tooltip>
<Tooltip>
<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>
);
......
......@@ -3,6 +3,11 @@ import { FileTree } from "./FileTree";
import { useEffect, useState } from "react";
import { useLoadApp } from "@/hooks/useLoadApp";
import { RefreshCw, Maximize2, Minimize2 } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useAtomValue } from "jotai";
import { selectedFileAtom } from "@/atoms/viewAtoms";
......@@ -59,23 +64,37 @@ export const CodeView = ({ loading, app }: CodeViewProps) => {
>
{/* Toolbar */}
<div className="flex items-center p-2 border-b space-x-2">
<button
onClick={() => refreshApp()}
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={loading || !app.id}
title="Refresh Files"
>
<RefreshCw size={16} />
</button>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={() => refreshApp()}
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={loading || !app.id}
/>
}
>
<RefreshCw size={16} />
</TooltipTrigger>
<TooltipContent>Refresh Files</TooltipContent>
</Tooltip>
<div className="text-sm text-gray-500">{app.files.length} files</div>
<div className="flex-1" />
<button
onClick={() => setIsFullscreen((value) => !value)}
className="p-1 rounded hover:bg-gray-200"
title={isFullscreen ? "Exit full screen" : "Enter full screen"}
>
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
</button>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={() => setIsFullscreen((value) => !value)}
className="p-1 rounded hover:bg-gray-200"
/>
}
>
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
</TooltipTrigger>
<TooltipContent>
{isFullscreen ? "Exit full screen" : "Enter full screen"}
</TooltipContent>
</Tooltip>
</div>
{/* Content */}
......
......@@ -7,6 +7,11 @@ import {
} from "lucide-react";
import { useSetAtom } from "jotai";
import { chatInputValueAtom } from "@/atoms/chatAtoms";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface ConsoleEntryProps {
type: "server" | "client" | "edge-function" | "network-requests";
......@@ -120,14 +125,20 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => {
)}
</span>
</div>
<button
onClick={handleSendToChat}
title="Send to chat"
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"
data-testid="send-to-chat"
>
<MessageSquare size={12} className="text-gray-500" />
</button>
<Tooltip>
<TooltipTrigger
render={
<button
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"
data-testid="send-to-chat"
/>
}
>
<MessageSquare size={12} className="text-gray-500" />
</TooltipTrigger>
<TooltipContent>Send to chat</TooltipContent>
</Tooltip>
</div>
);
};
import { Filter, X, Trash2 } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface ConsoleFiltersProps {
levelFilter: "all" | "info" | "warn" | "error";
......@@ -109,14 +114,20 @@ export const ConsoleFilters = ({
)}
{/* Clear logs button */}
<button
onClick={onClearLogs}
className="p-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
data-testid="clear-logs-button"
title="Clear logs"
>
<Trash2 size={14} />
</button>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={onClearLogs}
className="p-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
data-testid="clear-logs-button"
/>
}
>
<Trash2 size={14} />
</TooltipTrigger>
<TooltipContent>Clear logs</TooltipContent>
</Tooltip>
<div className="ml-auto text-xs text-gray-500">{totalLogs} logs</div>
</div>
......
import React, { useState, useRef, useEffect } from "react";
import { X } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface DraggableTextInputProps {
input: {
......@@ -158,18 +163,24 @@ export const DraggableTextInput = ({
/>
{/* Close Button - Rightmost */}
<button
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"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onRemove(input.id);
}}
title="Remove text input"
type="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" />
</button>
<Tooltip>
<TooltipTrigger
render={
<button
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"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onRemove(input.id);
}}
type="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>
);
......
......@@ -12,6 +12,11 @@ import { useSettings } from "@/hooks/useSettings";
import { useCheckProblems } from "@/hooks/useCheckProblems";
import { getLanguage } from "@/utils/get_language";
import { queryKeys } from "@/lib/queryKeys";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
interface FileEditorProps {
appId: number | null;
......@@ -53,17 +58,25 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({
))}
</div>
<div className="flex items-center gap-2 flex-shrink-0 ml-2">
<Button
variant="ghost"
size="sm"
onClick={onSave}
disabled={!hasUnsavedChanges || isSaving}
className="h-6 w-6 p-0"
data-testid="save-file-button"
title={hasUnsavedChanges ? "Save changes" : "No unsaved changes"}
>
<Save size={12} />
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="sm"
onClick={onSave}
disabled={!hasUnsavedChanges || isSaving}
className="h-6 w-6 p-0"
data-testid="save-file-button"
/>
}
>
<Save size={12} />
</TooltipTrigger>
<TooltipContent>
{hasUnsavedChanges ? "Save changes" : "No unsaved changes"}
</TooltipContent>
</Tooltip>
{hasUnsavedChanges && (
<Circle
size={8}
......
......@@ -57,6 +57,11 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useRunApp } from "@/hooks/useRunApp";
import { useSettings } from "@/hooks/useSettings";
import { useShortcut } from "@/hooks/useShortcut";
......@@ -989,57 +994,79 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
<div className="flex items-center p-2 border-b space-x-2">
{/* Navigation Buttons */}
<div className="flex space-x-1">
<button
onClick={() => setIsChatPanelHidden(!isChatPanelHidden)}
className="p-1 rounded transition-colors duration-200 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
data-testid="preview-toggle-chat-panel-button"
title={isChatPanelHidden ? "Show chat" : "Hide chat"}
>
{isChatPanelHidden ? (
<Maximize2 size={16} />
) : (
<Minimize2 size={16} />
)}
</button>
<button
onClick={handleActivateComponentSelector}
className={`p-1 rounded transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed ${
isPicking
? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700"
: " text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900"
}`}
disabled={
loading || !selectedAppId || !isComponentSelectorInitialized
}
data-testid="preview-pick-element-button"
title={
isPicking
<Tooltip>
<TooltipTrigger
render={
<button
onClick={() => setIsChatPanelHidden(!isChatPanelHidden)}
className="p-1 rounded transition-colors duration-200 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
data-testid="preview-toggle-chat-panel-button"
/>
}
>
{isChatPanelHidden ? (
<Maximize2 size={16} />
) : (
<Minimize2 size={16} />
)}
</TooltipTrigger>
<TooltipContent>
{isChatPanelHidden ? "Show chat" : "Hide chat"}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={handleActivateComponentSelector}
className={`p-1 rounded transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed ${
isPicking
? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700"
: " text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900"
}`}
disabled={
loading ||
!selectedAppId ||
!isComponentSelectorInitialized
}
data-testid="preview-pick-element-button"
/>
}
>
<MousePointerClick size={16} />
</TooltipTrigger>
<TooltipContent>
{isPicking
? "Deactivate component selector"
: `Select component (${isMac ? "⌘ + ⇧ + C" : "Ctrl + ⇧ + C"})`
}
>
<MousePointerClick size={16} />
</button>
<button
onClick={handleAnnotatorClick}
className={`p-1 rounded transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed ${
annotatorMode
? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700"
: " text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900"
}`}
disabled={
loading ||
!selectedAppId ||
isPicking ||
!isComponentSelectorInitialized
}
data-testid="preview-annotator-button"
title={
annotatorMode ? "Annotator mode active" : "Activate annotator"
}
>
<Pen size={16} />
</button>
: `Select component (${isMac ? "⌘ + ⇧ + C" : "Ctrl + ⇧ + C"})`}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={handleAnnotatorClick}
className={`p-1 rounded transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed ${
annotatorMode
? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700"
: " text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900"
}`}
disabled={
loading ||
!selectedAppId ||
isPicking ||
!isComponentSelectorInitialized
}
data-testid="preview-annotator-button"
/>
}
>
<Pen size={16} />
</TooltipTrigger>
<TooltipContent>
{annotatorMode ? "Annotator mode active" : "Activate annotator"}
</TooltipContent>
</Tooltip>
<button
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed dark:text-gray-300"
disabled={!canGoBack || loading || !selectedAppId}
......@@ -1106,14 +1133,20 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
{/* Action Buttons */}
<div className="flex space-x-1">
<button
onClick={onRestart}
className="flex items-center space-x-1 px-3 py-1 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
title="Restart App"
>
<Power size={16} />
<span>Restart</span>
</button>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={onRestart}
className="flex items-center space-x-1 px-3 py-1 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
/>
}
>
<Power size={16} />
<span>Restart</span>
</TooltipTrigger>
<TooltipContent>Restart App</TooltipContent>
</Tooltip>
<button
data-testid="preview-open-browser-button"
onClick={() => {
......@@ -1128,22 +1161,29 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
{/* Device Mode Button */}
<Popover open={isDevicePopoverOpen} modal={false}>
<PopoverTrigger
data-testid="device-mode-button"
onClick={() => {
// Toggle popover open/close
if (isDevicePopoverOpen)
updateSettings({ previewDeviceMode: "desktop" });
setIsDevicePopoverOpen(!isDevicePopoverOpen);
}}
className={cn(
"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-300",
deviceMode !== "desktop" && "bg-gray-200 dark:bg-gray-700",
)}
title="Device Mode"
>
<MonitorSmartphone size={16} />
</PopoverTrigger>
<Tooltip>
<TooltipTrigger
render={
<PopoverTrigger
data-testid="device-mode-button"
onClick={() => {
// Toggle popover open/close
if (isDevicePopoverOpen)
updateSettings({ previewDeviceMode: "desktop" });
setIsDevicePopoverOpen(!isDevicePopoverOpen);
}}
className={cn(
"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-300",
deviceMode !== "desktop" &&
"bg-gray-200 dark:bg-gray-700",
)}
/>
}
>
<MonitorSmartphone size={16} />
</TooltipTrigger>
<TooltipContent>Device Mode</TooltipContent>
</Tooltip>
<PopoverContent className="w-auto p-2">
<ToggleGroup
value={[deviceMode]}
......@@ -1159,27 +1199,45 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
}}
variant="outline"
>
<ToggleGroupItem
value="desktop"
aria-label="Desktop view"
title="Desktop"
>
<Monitor size={16} />
</ToggleGroupItem>
<ToggleGroupItem
value="tablet"
aria-label="Tablet view"
title="Tablet"
>
<Tablet size={16} className="scale-x-130" />
</ToggleGroupItem>
<ToggleGroupItem
value="mobile"
aria-label="Mobile view"
title="Mobile"
>
<Smartphone size={16} />
</ToggleGroupItem>
<Tooltip>
<TooltipTrigger
render={
<ToggleGroupItem
value="desktop"
aria-label="Desktop view"
/>
}
>
<Monitor size={16} />
</TooltipTrigger>
<TooltipContent>Desktop</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<ToggleGroupItem
value="tablet"
aria-label="Tablet view"
/>
}
>
<Tablet size={16} className="scale-x-130" />
</TooltipTrigger>
<TooltipContent>Tablet</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<ToggleGroupItem
value="mobile"
aria-label="Mobile view"
/>
}
>
<Smartphone size={16} />
</TooltipTrigger>
<TooltipContent>Mobile</TooltipContent>
</Tooltip>
</ToggleGroup>
</PopoverContent>
</Popover>
......
import { useState, useEffect } from "react";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { X, Move, Square, Palette, Type } from "lucide-react";
import { Label } from "@/components/ui/label";
import { ComponentSelection } from "@/ipc/types";
......@@ -314,14 +319,20 @@ export function VisualEditingToolbar({
left: `${toolbarLeft}px`,
}}
>
<button
onClick={handleDeselectComponent}
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-[#7f22fe] dark:text-gray-200"
aria-label="Deselect Component"
title="Deselect Component"
>
<X size={16} />
</button>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={handleDeselectComponent}
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-[#7f22fe] dark:text-gray-200"
aria-label="Deselect Component"
/>
}
>
<X size={16} />
</TooltipTrigger>
<TooltipContent>Deselect Component</TooltipContent>
</Tooltip>
{isDynamic ? (
<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">({
return (
<Tooltip>
<TooltipTrigger>{button}</TooltipTrigger>
<TooltipTrigger render={button} />
<TooltipContent
side="right"
align="center"
......
......@@ -18,11 +18,7 @@ function TooltipProvider({
}
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
}
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
......@@ -32,7 +28,7 @@ function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
function TooltipContent({
className,
side = "top",
sideOffset = 0,
sideOffset = 4,
align = "center",
alignOffset = 0,
children,
......@@ -54,13 +50,13 @@ function TooltipContent({
<TooltipPrimitive.Popup
data-slot="tooltip-content"
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,
)}
{...props}
>
{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.Positioner>
</TooltipPrimitive.Portal>
......
......@@ -18,6 +18,11 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { Input } from "@/components/ui/input";
import {
Dialog,
......@@ -357,17 +362,23 @@ export default function AppDetailsPage() {
Path
</span>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="ml-[-8px] p-0.5 h-auto cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
onClick={() => {
ipc.system.showItemInFolder(currentAppPath);
}}
title="Show in folder"
>
<Folder className="h-3.5 w-3.5" />
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon"
className="ml-[-8px] p-0.5 h-auto cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
onClick={() => {
ipc.system.showItemInFolder(currentAppPath);
}}
/>
}
>
<Folder className="h-3.5 w-3.5" />
</TooltipTrigger>
<TooltipContent>Show in folder</TooltipContent>
</Tooltip>
<span className="text-sm break-all">{currentAppPath}</span>
</div>
</div>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论