From f79da5d62e566fec58b44f6c3e4205e13277a21f Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Wed, 14 Oct 2026 12:57:53 -0400 Subject: [PATCH 1/3] Move the copy controls into the UI kit copyText, CopyableText, CopyButton and the single copy glyph now live in @canvas/ui/copy, and prompt cards use that glyph instead of their own inline SVG. The kit reads tooltips from a UiHostProvider context that the app installs at its two roots, so it stays free of host imports. --- packages/review/app/src/ReviewCommitsView.tsx | 2 +- .../review/app/src/agent-selection.test.tsx | 2 +- packages/review/app/src/agent-selection.tsx | 3 +- packages/review/app/src/api-canvas.test.tsx | 2 +- packages/review/app/src/ask-setup.tsx | 3 +- packages/review/app/src/code-block.tsx | 2 +- packages/review/app/src/connect-card.tsx | 8 ++- packages/review/app/src/copy-agent-context.ts | 2 +- packages/review/app/src/desktop-entry.tsx | 6 +- .../review/app/src/host/review-session.tsx | 4 +- packages/review/app/src/icons.tsx | 14 ---- packages/review/app/src/prompt-card.tsx | 4 +- packages/review/app/src/prompt-styles.ts | 5 ++ .../review/app/src/remote-hosts-section.tsx | 2 +- .../review/app/src/review-branch-range.tsx | 2 +- packages/review/app/src/review-home-view.tsx | 2 +- packages/review/app/src/share-control.tsx | 2 +- .../app/src/{copy-text.tsx => ui/copy.tsx} | 69 ++++++++++--------- packages/review/app/src/ui/host.tsx | 37 ++++++++++ 19 files changed, 107 insertions(+), 64 deletions(-) rename packages/review/app/src/{copy-text.tsx => ui/copy.tsx} (79%) create mode 100644 packages/review/app/src/ui/host.tsx 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..735a6064d 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 ( - + {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 79% rename from packages/review/app/src/copy-text.tsx rename to packages/review/app/src/ui/copy.tsx index 34e49061e..e7a5fc882 100644 --- a/packages/review/app/src/copy-text.tsx +++ b/packages/review/app/src/ui/copy.tsx @@ -1,10 +1,8 @@ import { IconButton } from "@canvas/ui/button"; +import { useUiTooltip } from "@canvas/ui/host"; 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 +48,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 +69,35 @@ function useCopy(text: string) { return [copied, copy] as const; } +export function CopyIcon({ + xstyle, +}: { xstyle?: stylex.StyleXStyles } = {}): ReactElement { + return ( + + ); +} + +function CheckGlyph({ xstyle }: { xstyle: stylex.StyleXStyles }) { + return ( + + ); +} + export function CopyableText({ text, children = text, @@ -100,7 +107,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 +148,7 @@ export function CopyButton({ iconStyle: stylex.StyleXStyles; }): ReactElement { const [copied, copy] = useCopy(text); - const tooltip = useTooltip(label); + const tooltip = useUiTooltip(label); return ( {copied ? ( - + ) : ( - + )} ); @@ -168,14 +175,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..d7fca26f6 --- /dev/null +++ b/packages/review/app/src/ui/host.tsx @@ -0,0 +1,37 @@ +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 = () => {}; + +const TooltipContext = createContext(() => noTooltip); + +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); +} From e1c552ea4e8fbcf97ac04a95af3cecee7c64043d Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Wed, 14 Oct 2026 20:40:02 -0400 Subject: [PATCH 2/3] Share one check icon from the UI kit, and warn once when a kit component has no tooltip host --- packages/review/app/src/icons.tsx | 13 +------------ packages/review/app/src/ui/copy.tsx | 16 ++------------- packages/review/app/src/ui/host.tsx | 15 +++++++++++++- packages/review/app/src/ui/icons.tsx | 29 ++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 27 deletions(-) create mode 100644 packages/review/app/src/ui/icons.tsx diff --git a/packages/review/app/src/icons.tsx b/packages/review/app/src/icons.tsx index 735a6064d..3b011a2c6 100644 --- a/packages/review/app/src/icons.tsx +++ b/packages/review/app/src/icons.tsx @@ -232,18 +232,7 @@ export function CodeIcon({ xstyle }: IconProps = {}): ReactElement { ); } -export function CheckIcon({ 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/ui/copy.tsx b/packages/review/app/src/ui/copy.tsx index e7a5fc882..f55d1200e 100644 --- a/packages/review/app/src/ui/copy.tsx +++ b/packages/review/app/src/ui/copy.tsx @@ -1,5 +1,6 @@ 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"; @@ -85,19 +86,6 @@ export function CopyIcon({ ); } -function CheckGlyph({ xstyle }: { xstyle: stylex.StyleXStyles }) { - return ( - - ); -} - export function CopyableText({ text, children = text, @@ -160,7 +148,7 @@ export function CopyButton({ onClick={copy} > {copied ? ( - + ) : ( )} diff --git a/packages/review/app/src/ui/host.tsx b/packages/review/app/src/ui/host.tsx index d7fca26f6..4509dd663 100644 --- a/packages/review/app/src/ui/host.tsx +++ b/packages/review/app/src/ui/host.tsx @@ -13,7 +13,20 @@ export type UseTooltip = ( const noTooltip: RefCallback = () => {}; -const TooltipContext = createContext(() => noTooltip); +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, 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", + }, +}); From 340b90c8a117869abe204e5a3e33c156b89365ad Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Wed, 14 Oct 2026 21:12:15 -0400 Subject: [PATCH 3/3] Import copyText from the UI kit in the onboarding screens --- packages/review/app/src/onboarding/draw-flow.tsx | 2 +- packages/review/app/src/onboarding/install-step.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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,