1- import { useCallback , useEffect , useRef , useState } from "react" ;
1+ import { useCallback , useEffect , useState } from "react" ;
22import { useAction , useQuery } from "convex/react" ;
33import { CheckIcon , CopySimpleIcon , MinusIcon , PlusIcon , SlidersHorizontalIcon , XIcon } from "@phosphor-icons/react" ;
44import { toast } from "sonner" ;
55import { api } from "../../convex/_generated/api" ;
66import { Markdown , type MermaidRepairRequest } from "@/components/markdown" ;
77import { Button } from "@/components/ui/button" ;
8+ import { CopyActionButton } from "@/components/ui/copy-action-button" ;
89import { FloatingExpandableToolbar } from "@/components/ui/floating-expandable-toolbar" ;
910import { ScrollArea } from "@/components/ui/scroll-area" ;
1011import { Select , SelectContent , SelectGroup , SelectItem , SelectTrigger , SelectValue } from "@/components/ui/select" ;
1112import { Skeleton } from "@/components/ui/skeleton" ;
12- import { useAsyncCallback } from "@/hooks/use-async-callback" ;
1313import { useLocalStorageEnum } from "@/hooks/use-persisted-state" ;
1414import { toUserErrorMessage } from "@/lib/errors" ;
1515import type { ArtifactId } from "@/lib/types" ;
@@ -78,34 +78,6 @@ export function LibraryEditor({ artifactId, className }: { artifactId: ArtifactI
7878 setToolbarExpanded ( false ) ;
7979 } , [ artifactId ] ) ;
8080
81- const [ copied , setCopied ] = useState ( false ) ;
82- const copiedResetTimer = useRef < number | null > ( null ) ;
83-
84- const clearCopiedResetTimer = useCallback ( ( ) => {
85- if ( copiedResetTimer . current === null ) return ;
86- window . clearTimeout ( copiedResetTimer . current ) ;
87- copiedResetTimer . current = null ;
88- } , [ ] ) ;
89-
90- useEffect ( ( ) => clearCopiedResetTimer , [ clearCopiedResetTimer ] ) ;
91-
92- const [ , runCopy ] = useAsyncCallback ( async ( ) => {
93- if ( ! artifact ) return ;
94- const copySource = selectedVersion !== null && selectedVersion !== artifact . version ? historicalVersion : artifact ;
95- if ( ! copySource ) return ;
96- try {
97- await navigator . clipboard . writeText ( copySource . contentMarkdown ) ;
98- clearCopiedResetTimer ( ) ;
99- setCopied ( true ) ;
100- copiedResetTimer . current = window . setTimeout ( ( ) => {
101- copiedResetTimer . current = null ;
102- setCopied ( false ) ;
103- } , 1600 ) ;
104- } catch {
105- // Browsers without clipboard API support — leave the affordance idle.
106- }
107- } ) ;
108-
10981 const [ fontSize , setFontSize ] = useLocalStorageEnum ( "systify.library.fontSize" , FONT_SIZE_STEPS , DEFAULT_FONT_SIZE ) ;
11082 const handleRepairMermaid = useCallback (
11183 async ( { chart, error } : MermaidRepairRequest ) => {
@@ -155,6 +127,7 @@ export function LibraryEditor({ artifactId, className }: { artifactId: ArtifactI
155127 const versionIsLoading = displayedArtifact === undefined ;
156128 const versionIsMissing = displayedArtifact === null ;
157129 const copyIsDisabled = versionIsLoading || versionIsMissing ;
130+ const copyMarkdown = ( ) => displayedArtifact ?. contentMarkdown ;
158131
159132 return (
160133 < div className = { cn ( "relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden" , className ) } >
@@ -169,8 +142,7 @@ export function LibraryEditor({ artifactId, className }: { artifactId: ArtifactI
169142 fontSize = { fontSize }
170143 onFontSizeChange = { setFontSize }
171144 copyIsDisabled = { copyIsDisabled }
172- copied = { copied }
173- onCopy = { ( ) => void runCopy ( ) }
145+ copyText = { copyMarkdown }
174146 />
175147
176148 < ScrollArea className = "min-h-0 flex-1" >
@@ -231,8 +203,7 @@ function ReaderToolbar({
231203 fontSize,
232204 onFontSizeChange,
233205 copyIsDisabled,
234- copied,
235- onCopy,
206+ copyText,
236207} : {
237208 expanded : boolean ;
238209 onExpandedChange : ( next : boolean | ( ( previous : boolean ) => boolean ) ) => void ;
@@ -250,8 +221,7 @@ function ReaderToolbar({
250221 fontSize : FontSize ;
251222 onFontSizeChange : ( next : FontSize ) => void ;
252223 copyIsDisabled : boolean ;
253- copied : boolean ;
254- onCopy : ( ) => void ;
224+ copyText : ( ) => string | null | undefined ;
255225} ) {
256226 return (
257227 < FloatingExpandableToolbar
@@ -262,6 +232,7 @@ function ReaderToolbar({
262232 collapseLabel = "Collapse reader tools"
263233 expandIcon = { < SlidersHorizontalIcon size = { 13 } weight = "bold" /> }
264234 collapseIcon = { < XIcon size = { 12 } weight = "bold" /> }
235+ className = "absolute top-3 right-4 z-10 max-w-[calc(100%-2rem)]"
265236 data-testid = "reader-toolbar"
266237 >
267238 < ArtifactVersionSelect
@@ -272,19 +243,21 @@ function ReaderToolbar({
272243 />
273244 { showFontSizeControl ? < FontSizeControl value = { fontSize } onChange = { onFontSizeChange } /> : null }
274245 < ReaderToolbarSeparator />
275- < Button
276- type = "button"
277- variant = "ghost"
246+ < CopyActionButton
247+ text = { copyText }
248+ idleLabel = "Copy"
249+ copiedLabel = "Copied"
250+ idleAriaLabel = "Copy markdown"
251+ showLabel
252+ tooltip = { false }
253+ copyIcon = { < CopySimpleIcon size = { 13 } weight = "bold" /> }
254+ copiedIcon = { < CheckIcon size = { 13 } weight = "bold" /> }
255+ resetAfterMs = { 1600 }
278256 size = "sm"
279- className = "h-6 w-14 shrink-0 gap-1 px-1.5 text-[11px] active:scale-100"
280- onClick = { onCopy }
257+ className = "h-6 w-14 shrink-0 gap-1 px-1.5 text-[11px]"
281258 disabled = { copyIsDisabled }
282- aria-label = "Copy markdown"
283259 tabIndex = { expanded ? undefined : - 1 }
284- >
285- { copied ? < CheckIcon size = { 13 } weight = "bold" /> : < CopySimpleIcon size = { 13 } weight = "bold" /> }
286- { copied ? "Copied" : "Copy" }
287- </ Button >
260+ />
288261 < ReaderToolbarSeparator className = "mr-1" />
289262 </ FloatingExpandableToolbar >
290263 ) ;
@@ -380,7 +353,8 @@ function FontSizeControl({ value, onChange }: { value: FontSize; onChange: (next
380353 type = "button"
381354 variant = "ghost"
382355 size = "sm"
383- className = "h-6 w-6 px-0 active:scale-100"
356+ pressEffect = "none"
357+ className = "h-6 w-6 px-0"
384358 disabled = { atMin }
385359 onClick = { ( ) => stepTo ( - 1 ) }
386360 aria-label = "Decrease text size"
@@ -391,7 +365,8 @@ function FontSizeControl({ value, onChange }: { value: FontSize; onChange: (next
391365 type = "button"
392366 variant = "ghost"
393367 size = "sm"
394- className = "h-6 w-6 px-0 active:scale-100"
368+ pressEffect = "none"
369+ className = "h-6 w-6 px-0"
395370 disabled = { atMax }
396371 onClick = { ( ) => stepTo ( 1 ) }
397372 aria-label = "Increase text size"
0 commit comments