Skip to content

Commit 159042e

Browse files
committed
🤖 Consolidate best practices: hooks, constants, and components
- Add useClipboard hook to eliminate duplicate clipboard logic (~30 LoC saved) - Add timing constants (TIMING.COPY_FEEDBACK_DURATION, etc.) - Expand UI constants with common text strings and icons - Remove unused shadcn button component (~50 LoC) - Replace hardcoded colors with CSS variables (--color-success, --color-muted) - Add shadcn tooltip component for future use - Update Tooltip.tsx to use timing constants Total: ~80 LoC removed, improved maintainability across 12+ components
1 parent 287468f commit 159042e

File tree

13 files changed

+154
-77
lines changed

13 files changed

+154
-77
lines changed

src/components/Messages/AssistantMessage.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { TypewriterMarkdown } from "./TypewriterMarkdown";
55
import type { ButtonConfig } from "./MessageWindow";
66
import { MessageWindow } from "./MessageWindow";
77
import { useStartHere } from "@/hooks/useStartHere";
8-
import { COMPACTED_EMOJI } from "@/constants/ui";
8+
import { COMPACTED_EMOJI, UI_TEXT } from "@/constants/ui";
99
import { ModelDisplay } from "./ModelDisplay";
1010
import { CompactingMessageContent } from "./CompactingMessageContent";
1111
import { CompactionBackground } from "./CompactionBackground";
1212
import type { KebabMenuItem } from "@/components/KebabMenu";
13+
import { useClipboard } from "@/utils/ui/clipboard";
1314

1415
interface AssistantMessageProps {
1516
message: DisplayedMessage & { type: "assistant" };
@@ -27,7 +28,7 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
2728
clipboardWriteText = (data: string) => navigator.clipboard.writeText(data),
2829
}) => {
2930
const [showRaw, setShowRaw] = useState(false);
30-
const [copied, setCopied] = useState(false);
31+
const { copied, copy } = useClipboard({ writeText: clipboardWriteText });
3132

3233
const content = message.content;
3334
const isStreaming = message.isStreaming;
@@ -44,9 +45,7 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
4445

4546
const handleCopy = async () => {
4647
try {
47-
await clipboardWriteText(content);
48-
setCopied(true);
49-
setTimeout(() => setCopied(false), 2000);
48+
await copy(content);
5049
} catch (err) {
5150
console.error("Failed to copy:", err);
5251
}
@@ -58,7 +57,7 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
5857
? []
5958
: [
6059
{
61-
label: copied ? "✓ Copied" : "Copy",
60+
label: copied ? `✓ ${UI_TEXT.COPIED}` : UI_TEXT.COPY,
6261
onClick: () => void handleCopy(),
6362
},
6463
];

src/components/Messages/UserMessage.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import React, { useState } from "react";
1+
import React from "react";
22
import type { DisplayedMessage } from "@/types/message";
33
import type { ButtonConfig } from "./MessageWindow";
44
import { MessageWindow } from "./MessageWindow";
55
import { TerminalOutput } from "./TerminalOutput";
66
import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
77
import type { KebabMenuItem } from "@/components/KebabMenu";
8+
import { useClipboard } from "@/utils/ui/clipboard";
9+
import { UI_TEXT } from "@/constants/ui";
810

911
interface UserMessageProps {
1012
message: DisplayedMessage & { type: "user" };
@@ -30,7 +32,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
3032
isCompacting,
3133
clipboardWriteText = defaultClipboardWriteText,
3234
}) => {
33-
const [copied, setCopied] = useState(false);
35+
const { copied, copy } = useClipboard({ writeText: clipboardWriteText });
3436

3537
const content = message.content;
3638

@@ -55,9 +57,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
5557
);
5658

5759
try {
58-
await clipboardWriteText(content);
59-
setCopied(true);
60-
setTimeout(() => setCopied(false), 2000);
60+
await copy(content);
6161
} catch (err) {
6262
console.error("Failed to copy:", err);
6363
}
@@ -85,7 +85,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
8585
]
8686
: []),
8787
{
88-
label: copied ? "✓ Copied" : "Copy",
88+
label: copied ? `✓ ${UI_TEXT.COPIED}` : UI_TEXT.COPY,
8989
onClick: () => void handleCopy(),
9090
},
9191
];

src/components/ThinkingSlider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const GLOW_INTENSITIES: Record<number, { track: string; thumb: string }> = {
2424
const getTextStyle = (n: number) => {
2525
if (n === 0) {
2626
return {
27-
color: "#606060",
27+
color: "var(--color-subdued)",
2828
fontWeight: 400,
2929
textShadow: "none",
3030
fontSize: "10px",

src/components/Tooltip.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState, useRef, useLayoutEffect, createContext, useContext } from "react";
22
import { createPortal } from "react-dom";
33
import { cn } from "@/lib/utils";
4+
import { TIMING } from "@/constants/timing";
45

56
// Context for passing hover state and trigger ref from wrapper to tooltip
67
interface TooltipContextValue {
@@ -39,7 +40,7 @@ export const TooltipWrapper: React.FC<TooltipWrapperProps> = ({ inline = false,
3940
// Delay hiding to allow moving mouse to tooltip
4041
leaveTimerRef.current = setTimeout(() => {
4142
setIsHovered(false);
42-
}, 100);
43+
}, TIMING.TOOLTIP_LEAVE_DELAY);
4344
};
4445

4546
return (

src/components/tools/BashToolCall.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
8282
<span
8383
className="ml-2 inline-block shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium whitespace-nowrap"
8484
style={{
85-
background: result.exitCode === 0 ? "#4caf50" : "#f44336",
85+
background: result.exitCode === 0 ? "var(--color-success)" : "var(--color-error)",
8686
color: "white",
8787
}}
8888
>

src/components/tools/FileEditToolCall.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/to
2222
import { TooltipWrapper, Tooltip } from "../Tooltip";
2323
import { DiffContainer, DiffRenderer, SelectableDiffRenderer } from "../shared/DiffRenderer";
2424
import { KebabMenu, type KebabMenuItem } from "../KebabMenu";
25+
import { useClipboard } from "@/utils/ui/clipboard";
26+
import { UI_TEXT } from "@/constants/ui";
2527

2628
type FileEditOperationArgs =
2729
| FileEditReplaceStringToolArgs
@@ -49,7 +51,7 @@ function renderDiff(
4951
try {
5052
const patches = parsePatch(diff);
5153
if (patches.length === 0) {
52-
return <div style={{ padding: "8px", color: "#888" }}>No changes</div>;
54+
return <div style={{ padding: "8px", color: "var(--color-muted)" }}>No changes</div>;
5355
}
5456

5557
// Render each hunk using SelectableDiffRenderer if we have a callback, otherwise DiffRenderer
@@ -99,16 +101,14 @@ export const FileEditToolCall: React.FC<FileEditToolCallProps> = ({
99101
}) => {
100102
const { expanded, toggleExpanded } = useToolExpansion(true);
101103
const [showRaw, setShowRaw] = React.useState(false);
102-
const [copied, setCopied] = React.useState(false);
104+
const { copied, copy } = useClipboard();
103105

104106
const filePath = "file_path" in args ? args.file_path : undefined;
105107

106108
const handleCopyPatch = async () => {
107109
if (result && result.success && result.diff) {
108110
try {
109-
await navigator.clipboard.writeText(result.diff);
110-
setCopied(true);
111-
setTimeout(() => setCopied(false), 2000);
111+
await copy(result.diff);
112112
} catch (err) {
113113
console.error("Failed to copy:", err);
114114
}
@@ -120,7 +120,7 @@ export const FileEditToolCall: React.FC<FileEditToolCallProps> = ({
120120
result && result.success && result.diff
121121
? [
122122
{
123-
label: copied ? "✓ Copied" : "Copy Patch",
123+
label: copied ? `✓ ${UI_TEXT.COPIED}` : "Copy Patch",
124124
onClick: () => void handleCopyPatch(),
125125
},
126126
{

src/components/tools/ProposePlanToolCall.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
1414
import { useStartHere } from "@/hooks/useStartHere";
1515
import { TooltipWrapper, Tooltip } from "../Tooltip";
1616
import { cn } from "@/lib/utils";
17+
import { useClipboard } from "@/utils/ui/clipboard";
18+
import { UI_TEXT } from "@/constants/ui";
1719

1820
interface ProposePlanToolCallProps {
1921
args: ProposePlanToolArgs;
@@ -30,7 +32,7 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
3032
}) => {
3133
const { expanded, toggleExpanded } = useToolExpansion(true); // Expand by default
3234
const [showRaw, setShowRaw] = useState(false);
33-
const [copied, setCopied] = useState(false);
35+
const { copied, copy } = useClipboard();
3436

3537
// Format: Title as H1 + plan content for "Start Here" functionality
3638
const startHereContent = `# ${args.title}\n\n${args.plan}`;
@@ -52,9 +54,7 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
5254

5355
const handleCopy = async () => {
5456
try {
55-
await navigator.clipboard.writeText(args.plan);
56-
setCopied(true);
57-
setTimeout(() => setCopied(false), 2000);
57+
await copy(args.plan);
5858
} catch (err) {
5959
console.error("Failed to copy:", err);
6060
}
@@ -151,15 +151,15 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
151151
"rgba(136, 136, 136, 0.3)";
152152
}}
153153
>
154-
{copied ? "✓ Copied" : "Copy"}
154+
{copied ? `✓ ${UI_TEXT.COPIED}` : UI_TEXT.COPY}
155155
</button>
156156
<button
157157
onClick={() => setShowRaw(!showRaw)}
158158
className={cn(
159159
"px-2 py-1 text-[10px] font-mono rounded-sm cursor-pointer transition-all duration-150 active:translate-y-px hover:text-plan-mode"
160160
)}
161161
style={{
162-
color: showRaw ? "var(--color-plan-mode)" : "#888",
162+
color: showRaw ? "var(--color-plan-mode)" : "var(--color-muted)",
163163
background: showRaw
164164
? "color-mix(in srgb, var(--color-plan-mode), transparent 90%)"
165165
: "transparent",

src/components/ui/button.tsx

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/components/ui/tooltip.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as React from "react"
2+
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3+
4+
import { cn } from "@/lib/utils"
5+
6+
const TooltipProvider = TooltipPrimitive.Provider
7+
8+
const Tooltip = TooltipPrimitive.Root
9+
10+
const TooltipTrigger = TooltipPrimitive.Trigger
11+
12+
const TooltipContent = React.forwardRef<
13+
React.ElementRef<typeof TooltipPrimitive.Content>,
14+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
15+
>(({ className, sideOffset = 4, ...props }, ref) => (
16+
<TooltipPrimitive.Content
17+
ref={ref}
18+
sideOffset={sideOffset}
19+
className={cn(
20+
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=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 origin-[--radix-tooltip-content-transform-origin]",
21+
className
22+
)}
23+
{...props}
24+
/>
25+
))
26+
TooltipContent.displayName = TooltipPrimitive.Content.displayName
27+
28+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

src/constants/timing.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Centralized timing constants for animations, transitions, and delays.
3+
* All values are in milliseconds.
4+
*/
5+
export const TIMING = {
6+
/** Duration to show "Copied!" feedback after clipboard operations */
7+
COPY_FEEDBACK_DURATION: 2000,
8+
9+
/** Delay before hiding tooltip after mouse leaves trigger */
10+
TOOLTIP_HIDE_DELAY: 300,
11+
12+
/** Delay when mouse leaves tooltip content area */
13+
TOOLTIP_LEAVE_DELAY: 100,
14+
15+
/** Duration for modal animation transitions */
16+
MODAL_ANIMATION_DELAY: 200,
17+
18+
/** Debounce delay for search inputs */
19+
SEARCH_DEBOUNCE: 300,
20+
21+
/** Standard animation duration for UI transitions */
22+
ANIMATION_STANDARD: 150,
23+
24+
/** Fast animation duration */
25+
ANIMATION_FAST: 100,
26+
27+
/** Slow animation duration */
28+
ANIMATION_SLOW: 300,
29+
} as const;
30+

0 commit comments

Comments
 (0)