diff --git a/packages/review/app/src/ReviewCommitsView.tsx b/packages/review/app/src/ReviewCommitsView.tsx
index afd4c8fe4..48c6c7d9d 100644
--- a/packages/review/app/src/ReviewCommitsView.tsx
+++ b/packages/review/app/src/ReviewCommitsView.tsx
@@ -1,5 +1,6 @@
import { fontSize, fontWeight, radius } from "@canvas/scale.stylex";
import { IconButton } from "@canvas/ui/button";
+import { CopyButton } from "@canvas/ui/copy";
import { textStyles } from "@canvas/ui/text";
import {
type ReviewCommitSummary,
@@ -11,7 +12,6 @@ import { useMemo, useState } from "react";
import { canvasQueryKeys } from "./canvas-query";
import { controlStyles } from "./controls-styles";
-import { CopyButton } from "./copy-text";
import { DiffCount } from "./diff-count";
import { FileMark } from "./file-mark";
import { countLabel } from "./format";
diff --git a/packages/review/app/src/agent-selection.test.tsx b/packages/review/app/src/agent-selection.test.tsx
index ca2570a51..949459a60 100644
--- a/packages/review/app/src/agent-selection.test.tsx
+++ b/packages/review/app/src/agent-selection.test.tsx
@@ -1,10 +1,10 @@
+import * as clipboard from "@canvas/ui/copy";
// @vitest-environment jsdom
import { act } from "react";
import { createRoot } from "react-dom/client";
import { expect, it, vi } from "vitest";
import { AgentSelectionProvider, useAgentSelection } from "./agent-selection";
-import * as clipboard from "./copy-text";
import { ReviewSessionProvider } from "./host/review-session";
import { testReviewSession } from "./review-session-test-utils";
diff --git a/packages/review/app/src/agent-selection.tsx b/packages/review/app/src/agent-selection.tsx
index f2482f13e..c7ee801a2 100644
--- a/packages/review/app/src/agent-selection.tsx
+++ b/packages/review/app/src/agent-selection.tsx
@@ -1,3 +1,4 @@
+import { CopyIcon } from "@canvas/ui/copy";
import type { AgentSelection } from "@review/agent-selection";
import type { AskAgentId } from "@review/ask/thread-state";
import * as stylex from "@stylexjs/stylex";
@@ -22,7 +23,7 @@ import { askAnchor } from "./ask-anchor";
import { controlStyles } from "./controls-styles";
import { copyAgentContext } from "./copy-agent-context";
import { useReviewSession } from "./host/review-session";
-import { ChatIcon, ChevronDownIcon, CopyIcon } from "./icons";
+import { ChatIcon, ChevronDownIcon } from "./icons";
import { useNotify } from "./notify";
import { useReviewDiffFiles } from "./review-diff-files-context";
import { useOptionalReviewPanelStore } from "./review-panel";
diff --git a/packages/review/app/src/api-canvas.test.tsx b/packages/review/app/src/api-canvas.test.tsx
index 5eb5f7926..8ab7dbfd2 100644
--- a/packages/review/app/src/api-canvas.test.tsx
+++ b/packages/review/app/src/api-canvas.test.tsx
@@ -4,6 +4,7 @@ import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
+import * as clipboard from "@canvas/ui/copy";
import type {
JsonObject,
ReviewApiSummary,
@@ -23,7 +24,6 @@ import { Hono } from "hono";
import { act } from "react";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
-import * as clipboard from "./copy-text";
import { mountReviewCanvas as mount } from "./desktop-entry";
import { createSequenceTourEntry, sequenceView } from "./diagrams";
import { testReviewBridge } from "./review-session-test-utils";
diff --git a/packages/review/app/src/ask-setup.tsx b/packages/review/app/src/ask-setup.tsx
index a3e2a170f..a1b1a1299 100644
--- a/packages/review/app/src/ask-setup.tsx
+++ b/packages/review/app/src/ask-setup.tsx
@@ -1,3 +1,4 @@
+import { CopyButton, CopyIcon } from "@canvas/ui/copy";
import type { AgentSelection } from "@review/agent-selection";
import * as stylex from "@stylexjs/stylex";
import type { ReactElement } from "react";
@@ -7,9 +8,7 @@ import { AskSelectionQuote } from "./ask-panel-shared";
import { askPanelStyles } from "./ask-styles";
import { controlStyles } from "./controls-styles";
import { copyAgentContext } from "./copy-agent-context";
-import { CopyButton } from "./copy-text";
import { useReviewSession } from "./host/review-session";
-import { CopyIcon } from "./icons";
import { useNotify } from "./notify";
import { fontSize, radius } from "./scale.stylex";
import { tokens } from "./tokens.stylex";
diff --git a/packages/review/app/src/code-block.tsx b/packages/review/app/src/code-block.tsx
index b01063499..f8f7bbb5b 100644
--- a/packages/review/app/src/code-block.tsx
+++ b/packages/review/app/src/code-block.tsx
@@ -1,4 +1,5 @@
import { fontSize, fontWeight, radius } from "@canvas/scale.stylex";
+import { CopyButton } from "@canvas/ui/copy";
import {
type ShjLanguage,
type ShjToken,
@@ -12,7 +13,6 @@ import {
useState,
} from "react";
-import { CopyButton } from "./copy-text";
import { DiagramHeader } from "./diagram-header";
import { drawStyles } from "./draw-styles";
import { documentMarker } from "./markers.stylex";
diff --git a/packages/review/app/src/connect-card.tsx b/packages/review/app/src/connect-card.tsx
index 39367923a..603f79efe 100644
--- a/packages/review/app/src/connect-card.tsx
+++ b/packages/review/app/src/connect-card.tsx
@@ -1,5 +1,6 @@
import { fontSize, radius } from "@canvas/scale.stylex";
import { Button } from "@canvas/ui/button";
+import { CopyIcon, copyText } from "@canvas/ui/copy";
import {
REVIEW_CLI_INSTALL_TARGET_LABELS,
type ReviewCanvasInstallContent,
@@ -12,7 +13,6 @@ import { useEffect, useRef, useState } from "react";
import { AGENT_LOGOS } from "./agent-logos";
import { cliInstallReady } from "./cli-install-status";
-import { CopyIcon, copyText } from "./copy-text";
import { DrawnCheckIcon } from "./icons";
import { newTabLinkProps } from "./link-props";
import { OptionMenu } from "./option-menu";
@@ -213,7 +213,11 @@ export function ConnectCard({
aria-label={`${copied ? "Copied" : "Copy"} ${noun} for ${agent}`}
onClick={() => copy(text)}
>
- {copied ? : }
+ {copied ? (
+
+ ) : (
+
+ )}
{copied
? "Copied"
: `Copy ${mode === "prompt" ? "prompt" : "command"}`}
diff --git a/packages/review/app/src/copy-agent-context.ts b/packages/review/app/src/copy-agent-context.ts
index 9253fd483..30620beb4 100644
--- a/packages/review/app/src/copy-agent-context.ts
+++ b/packages/review/app/src/copy-agent-context.ts
@@ -1,7 +1,7 @@
+import { copyText } from "@canvas/ui/copy";
import type { AgentSelection } from "@review/agent-selection";
import { z } from "zod";
-import { copyText } from "./copy-text";
import type { ReviewSession } from "./host/review-session";
/** Copies a selection as the Markdown an agent reads; throws when it can't. */
diff --git a/packages/review/app/src/desktop-entry.tsx b/packages/review/app/src/desktop-entry.tsx
index 02a3d7b83..c7e4de48c 100644
--- a/packages/review/app/src/desktop-entry.tsx
+++ b/packages/review/app/src/desktop-entry.tsx
@@ -1,5 +1,6 @@
import { fontSize } from "@canvas/scale.stylex";
import { EmptyState } from "@canvas/ui/empty-state";
+import { UiHostProvider } from "@canvas/ui/host";
import { textStyles } from "@canvas/ui/text";
import type {
ReviewCanvasContent,
@@ -21,6 +22,7 @@ import { SettingsPage } from "./settings-page";
import { shellStyles } from "./shell-styles";
import { themeStyles } from "./theme-styles";
import { tokens } from "./tokens.stylex";
+import { useTooltip } from "./use-tooltip";
import "./styles.css";
@@ -272,7 +274,9 @@ export function mountReviewCanvas(
root.render(
-
+
+
+
,
);
diff --git a/packages/review/app/src/host/review-session.tsx b/packages/review/app/src/host/review-session.tsx
index 7cb9cc4a8..ea0725a23 100644
--- a/packages/review/app/src/host/review-session.tsx
+++ b/packages/review/app/src/host/review-session.tsx
@@ -1,6 +1,8 @@
import type { NormalizedSoftwareModel } from "@canvas/software-map/model";
import type { PinnedSoftwareMapData } from "@canvas/software-map/SoftwareMap";
import { createReviewAppSessionId } from "@canvas/tab-dwell-telemetry";
+import { UiHostProvider } from "@canvas/ui/host";
+import { useTooltip } from "@canvas/use-tooltip";
import type {
ReviewCanvasBridge,
ReviewCanvasDiagnostic,
@@ -98,7 +100,7 @@ export function ReviewSessionProvider({
}) {
return (
- {children}
+ {children}
);
}
diff --git a/packages/review/app/src/icons.tsx b/packages/review/app/src/icons.tsx
index 708b05961..3b011a2c6 100644
--- a/packages/review/app/src/icons.tsx
+++ b/packages/review/app/src/icons.tsx
@@ -219,20 +219,6 @@ export function ShareIcon({ xstyle }: IconProps = {}): ReactElement {
);
}
-export function CopyIcon({ xstyle }: IconProps = {}): ReactElement {
- return (
-
- );
-}
-
export function CodeIcon({ xstyle }: IconProps = {}): ReactElement {
return (
- );
-}
+export { CheckIcon } from "@canvas/ui/icons";
/** A check that draws itself in when it mounts, for a just-finished action. */
export function DrawnCheckIcon(): ReactElement {
diff --git a/packages/review/app/src/onboarding/draw-flow.tsx b/packages/review/app/src/onboarding/draw-flow.tsx
index 6b0f6a59d..a132bc323 100644
--- a/packages/review/app/src/onboarding/draw-flow.tsx
+++ b/packages/review/app/src/onboarding/draw-flow.tsx
@@ -1,5 +1,5 @@
-import { copyText } from "@canvas/copy-text";
import { useNotify } from "@canvas/notify";
+import { copyText } from "@canvas/ui/copy";
import type { ReviewCanvasSetupActions } from "@dev.fast/review-protocol";
import { useCallback, useEffect, useState } from "react";
diff --git a/packages/review/app/src/onboarding/install-step.tsx b/packages/review/app/src/onboarding/install-step.tsx
index ef7279396..5e0194f71 100644
--- a/packages/review/app/src/onboarding/install-step.tsx
+++ b/packages/review/app/src/onboarding/install-step.tsx
@@ -1,11 +1,11 @@
import { cliInstallReady } from "@canvas/cli-install-status";
-import { copyText } from "@canvas/copy-text";
import { DrawnCheckIcon } from "@canvas/icons";
import { installRowMarker } from "@canvas/markers.stylex";
import { fontSize, fontWeight, motion, radius } from "@canvas/scale.stylex";
import { tokens } from "@canvas/tokens.stylex";
import { Button } from "@canvas/ui/button";
import { Checkbox } from "@canvas/ui/checkbox";
+import { copyText } from "@canvas/ui/copy";
import type {
ReviewCanvasSetupActions,
ReviewCliInstallStatus,
diff --git a/packages/review/app/src/prompt-card.tsx b/packages/review/app/src/prompt-card.tsx
index da17a4ef0..078475910 100644
--- a/packages/review/app/src/prompt-card.tsx
+++ b/packages/review/app/src/prompt-card.tsx
@@ -1,7 +1,7 @@
+import { CopyIcon, copyText } from "@canvas/ui/copy";
import * as stylex from "@stylexjs/stylex";
import { useEffect, useRef, useState } from "react";
-import { CopyIcon, copyText } from "./copy-text";
import { promptStyles } from "./prompt-styles";
import { SegmentedControl } from "./ui/segmented";
@@ -94,7 +94,7 @@ export function PromptCard() {
aria-label={copied ? "Prompt copied" : "Copy prompt"}
onClick={copyPrompt}
>
-
+
{copied ? "Copied" : "Copy prompt"}
diff --git a/packages/review/app/src/prompt-styles.ts b/packages/review/app/src/prompt-styles.ts
index 794c25890..605146d25 100644
--- a/packages/review/app/src/prompt-styles.ts
+++ b/packages/review/app/src/prompt-styles.ts
@@ -48,6 +48,11 @@ export const promptStyles = stylex.create({
outline: { default: null, ":focus-visible": `1px solid ${tokens.accent}` },
outlineOffset: { default: null, ":focus-visible": "2px" },
},
+ copyIcon: {
+ width: "12px",
+ height: "12px",
+ strokeWidth: "2.4px",
+ },
error: {
margin: "10px 0 0",
color: tokens.changeRemoved,
diff --git a/packages/review/app/src/remote-hosts-section.tsx b/packages/review/app/src/remote-hosts-section.tsx
index de5bc1659..1e9dd8221 100644
--- a/packages/review/app/src/remote-hosts-section.tsx
+++ b/packages/review/app/src/remote-hosts-section.tsx
@@ -1,5 +1,6 @@
import { Button } from "@canvas/ui/button";
import { Checkbox } from "@canvas/ui/checkbox";
+import { CopyableText } from "@canvas/ui/copy";
import { TextField } from "@canvas/ui/text-field";
import {
REVIEW_CLI_INSTALL_TARGET_LABELS,
@@ -10,7 +11,6 @@ import {
import * as stylex from "@stylexjs/stylex";
import { useEffect, useRef, useState } from "react";
-import { CopyableText } from "./copy-text";
import {
ACTION_WORDS,
useHostAction,
diff --git a/packages/review/app/src/review-branch-range.tsx b/packages/review/app/src/review-branch-range.tsx
index 2b1e8251f..5bce10f08 100644
--- a/packages/review/app/src/review-branch-range.tsx
+++ b/packages/review/app/src/review-branch-range.tsx
@@ -1,8 +1,8 @@
import { fontSize, radius } from "@canvas/scale.stylex";
+import { copyText } from "@canvas/ui/copy";
import * as stylex from "@stylexjs/stylex";
import { type ReactElement, useEffect, useRef, useState } from "react";
-import { copyText } from "./copy-text";
import { tokens } from "./tokens.stylex";
import { useTooltip } from "./use-tooltip";
diff --git a/packages/review/app/src/review-home-view.tsx b/packages/review/app/src/review-home-view.tsx
index 08f3d6146..d28629ff2 100644
--- a/packages/review/app/src/review-home-view.tsx
+++ b/packages/review/app/src/review-home-view.tsx
@@ -1,3 +1,4 @@
+import { CopyableText, copyText } from "@canvas/ui/copy";
import {
REVIEW_DISCORD_URL,
type ReviewApiSummary,
@@ -21,7 +22,6 @@ import {
} from "react";
import { useHostActionButton } from "./connection-chip";
-import { CopyableText, copyText } from "./copy-text";
import { countLabel, formatRelativeTime } from "./format";
import { homeStyles } from "./home-styles";
import { CanvasUiContext, useCanvasMenu } from "./host/canvas-ui";
diff --git a/packages/review/app/src/share-control.tsx b/packages/review/app/src/share-control.tsx
index fcbc97cb9..b0738f581 100644
--- a/packages/review/app/src/share-control.tsx
+++ b/packages/review/app/src/share-control.tsx
@@ -1,5 +1,6 @@
import { layer } from "@canvas/scale.stylex";
import { Button, IconButton } from "@canvas/ui/button";
+import { copyText } from "@canvas/ui/copy";
import { surfaceStyles } from "@canvas/ui/surface";
import { TextField } from "@canvas/ui/text-field";
import {
@@ -17,7 +18,6 @@ import { createContext, useContext, useEffect, useRef, useState } from "react";
import { canvasQueryKeys } from "./canvas-query";
import { controlStyles } from "./controls-styles";
-import { copyText } from "./copy-text";
import { useOptionalReviewSession } from "./host/review-session";
import { ShareIcon } from "./icons";
import { topbarActionsMarker } from "./markers.stylex";
diff --git a/packages/review/app/src/copy-text.tsx b/packages/review/app/src/ui/copy.tsx
similarity index 82%
rename from packages/review/app/src/copy-text.tsx
rename to packages/review/app/src/ui/copy.tsx
index 34e49061e..f55d1200e 100644
--- a/packages/review/app/src/copy-text.tsx
+++ b/packages/review/app/src/ui/copy.tsx
@@ -1,10 +1,9 @@
import { IconButton } from "@canvas/ui/button";
+import { useUiTooltip } from "@canvas/ui/host";
+import { CheckIcon } from "@canvas/ui/icons";
import * as stylex from "@stylexjs/stylex";
import { type ReactElement, type ReactNode, useEffect, useState } from "react";
-import { CheckIcon, CopyIcon as CopyGlyph } from "./icons";
-import { useTooltip } from "./use-tooltip";
-
export async function copyText(text: string): Promise {
try {
await navigator.clipboard.writeText(text);
@@ -50,26 +49,6 @@ export async function copyText(text: string): Promise {
return copied;
}
-/** The prompt cards' copy glyph. */
-export function CopyIcon() {
- return (
-
- );
-}
-
const COPIED_FOR_MS = 1200;
function useCopy(text: string) {
@@ -91,6 +70,22 @@ function useCopy(text: string) {
return [copied, copy] as const;
}
+export function CopyIcon({
+ xstyle,
+}: { xstyle?: stylex.StyleXStyles } = {}): ReactElement {
+ return (
+
+ );
+}
+
export function CopyableText({
text,
children = text,
@@ -100,7 +95,7 @@ export function CopyableText({
}): ReactElement {
const [copied, copy] = useCopy(text);
- const tooltip = useTooltip(
+ const tooltip = useUiTooltip(
copied ? "Copied" : "Click to copy",
{ instant: true },
);
@@ -141,7 +136,7 @@ export function CopyButton({
iconStyle: stylex.StyleXStyles;
}): ReactElement {
const [copied, copy] = useCopy(text);
- const tooltip = useTooltip(label);
+ const tooltip = useUiTooltip(label);
return (
) : (
-
+
)}
);
@@ -168,14 +163,12 @@ const styles = stylex.create({
textUnderlineOffset: "2px",
},
icon: {
- width: "12px",
- height: "12px",
- },
- stroke: {
+ width: "15px",
+ height: "15px",
fill: "none",
stroke: "currentColor",
strokeLinecap: "round",
strokeLinejoin: "round",
- strokeWidth: "1.2",
+ strokeWidth: "1.7px",
},
});
diff --git a/packages/review/app/src/ui/host.tsx b/packages/review/app/src/ui/host.tsx
new file mode 100644
index 000000000..4509dd663
--- /dev/null
+++ b/packages/review/app/src/ui/host.tsx
@@ -0,0 +1,50 @@
+import {
+ type ReactNode,
+ type RefCallback,
+ createContext,
+ useContext,
+} from "react";
+
+/** Attaches a tooltip to an element; the host supplies the real one. */
+export type UseTooltip = (
+ text: string,
+ options?: { instant?: boolean },
+) => RefCallback;
+
+const noTooltip: RefCallback = () => {};
+
+let warned = false;
+
+// Every root installs UiHostProvider; without one, tooltips silently vanish.
+const missingHost: UseTooltip = () => {
+ if (!warned)
+ console.warn(
+ "A UI kit component has no UiHostProvider above it, so it shows no tooltip.",
+ );
+ warned = true;
+
+ return noTooltip;
+};
+
+const TooltipContext = createContext(missingHost);
+
+export function UiHostProvider({
+ useTooltip,
+ children,
+}: {
+ useTooltip: UseTooltip;
+ children: ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function useUiTooltip(
+ text: string,
+ options?: { instant?: boolean },
+): RefCallback {
+ return useContext(TooltipContext)(text, options);
+}
diff --git a/packages/review/app/src/ui/icons.tsx b/packages/review/app/src/ui/icons.tsx
new file mode 100644
index 000000000..878be3bdc
--- /dev/null
+++ b/packages/review/app/src/ui/icons.tsx
@@ -0,0 +1,29 @@
+import * as stylex from "@stylexjs/stylex";
+import type { ReactElement } from "react";
+
+export function CheckIcon({
+ xstyle,
+}: { xstyle?: stylex.StyleXStyles } = {}): ReactElement {
+ return (
+
+ );
+}
+
+const styles = stylex.create({
+ icon: {
+ width: "15px",
+ height: "15px",
+ fill: "none",
+ stroke: "currentColor",
+ strokeLinecap: "round",
+ strokeLinejoin: "round",
+ strokeWidth: "1.7px",
+ },
+});