mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
refactor: consolidate copy-to-clipboard logic and icons
This commit is contained in:
@@ -1,86 +1,26 @@
|
||||
import { useState } from 'react'
|
||||
import { usePlatform } from '@/hooks/usePlatform'
|
||||
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard'
|
||||
import { useShikiHighlighter } from '@/lib/shiki'
|
||||
|
||||
function safeCopyToClipboard(text: string): Promise<void> {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
return navigator.clipboard.writeText(text)
|
||||
}
|
||||
return Promise.reject(new Error('Clipboard API not available'))
|
||||
}
|
||||
|
||||
function CopyIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={props.className}
|
||||
>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function CheckIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={props.className}
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
import { CopyIcon, CheckIcon } from '@/components/icons'
|
||||
|
||||
export function CodeBlock(props: {
|
||||
code: string
|
||||
language?: string
|
||||
showCopyButton?: boolean
|
||||
}) {
|
||||
const { haptic } = usePlatform()
|
||||
const showCopyButton = props.showCopyButton ?? true
|
||||
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const { copied, copy } = useCopyToClipboard()
|
||||
const highlighted = useShikiHighlighter(props.code, props.language)
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await safeCopyToClipboard(props.code)
|
||||
haptic.notification('success')
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
} catch {
|
||||
haptic.notification('error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-w-0 max-w-full">
|
||||
{showCopyButton ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
onClick={() => copy(props.code)}
|
||||
className="absolute right-1.5 top-1.5 rounded p-1 text-[var(--app-hint)] hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)] transition-colors"
|
||||
title="Copy"
|
||||
>
|
||||
{copied ? <CheckIcon /> : <CopyIcon />}
|
||||
{copied ? <CheckIcon className="h-3.5 w-3.5" /> : <CopyIcon className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ComponentPropsWithoutRef } from 'react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
MarkdownTextPrimitive,
|
||||
unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
|
||||
@@ -7,75 +6,17 @@ import {
|
||||
type CodeHeaderProps,
|
||||
} from '@assistant-ui/react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { getPlatform } from '@/hooks/usePlatform'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SyntaxHighlighter } from '@/components/assistant-ui/shiki-highlighter'
|
||||
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard'
|
||||
import { CopyIcon, CheckIcon } from '@/components/icons'
|
||||
|
||||
export const MARKDOWN_PLUGINS = [remarkGfm]
|
||||
|
||||
function safeCopyToClipboard(text: string): Promise<void> {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
return navigator.clipboard.writeText(text)
|
||||
}
|
||||
return Promise.reject(new Error('Clipboard API not available'))
|
||||
}
|
||||
|
||||
function CopyIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={props.className}
|
||||
>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function CheckIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={props.className}
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function CodeHeader(props: CodeHeaderProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const { copied, copy } = useCopyToClipboard()
|
||||
const language = props.language && props.language !== 'unknown' ? props.language : ''
|
||||
|
||||
const handleCopy = async () => {
|
||||
const { haptic } = getPlatform()
|
||||
try {
|
||||
await safeCopyToClipboard(props.code)
|
||||
haptic.notification('success')
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
} catch {
|
||||
haptic.notification('error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="aui-md-codeheader flex items-center justify-between rounded-t-md bg-[var(--app-code-bg)] px-2 py-1">
|
||||
<div className="min-w-0 flex-1 pr-2 text-xs font-mono text-[var(--app-hint)]">
|
||||
@@ -83,11 +24,11 @@ function CodeHeader(props: CodeHeaderProps) {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
onClick={() => copy(props.code)}
|
||||
className="shrink-0 rounded p-1 text-[var(--app-hint)] hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)] transition-colors"
|
||||
title="Copy"
|
||||
>
|
||||
{copied ? <CheckIcon /> : <CopyIcon />}
|
||||
{copied ? <CheckIcon className="h-3.5 w-3.5" /> : <CopyIcon className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -41,3 +41,22 @@ export function PlusCircleIcon(props: IconProps) {
|
||||
props
|
||||
)
|
||||
}
|
||||
|
||||
export function CopyIcon(props: IconProps) {
|
||||
return createIcon(
|
||||
<>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</>,
|
||||
props,
|
||||
2
|
||||
)
|
||||
}
|
||||
|
||||
export function CheckIcon(props: IconProps) {
|
||||
return createIcon(
|
||||
<polyline points="20 6 9 17 4 12" />,
|
||||
props,
|
||||
2
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user