summaryrefslogtreecommitdiffhomepage
path: root/packages/web/src/components
diff options
context:
space:
mode:
authorJohn Henry Rudden <[email protected]>2025-07-15 16:56:33 -0500
committerGitHub <[email protected]>2025-07-15 17:56:33 -0400
commit7c91f668d1b8faa7ec373145ce0e2e351118dbe8 (patch)
tree813bea27c52d0273e724c830940fa6c386b0ccb0 /packages/web/src/components
parent1af103d29e4c1e37533a85cc6f3f8333ca16eb2a (diff)
downloadopencode-7c91f668d1b8faa7ec373145ce0e2e351118dbe8.tar.gz
opencode-7c91f668d1b8faa7ec373145ce0e2e351118dbe8.zip
docs: share add copy button to messages in web interface (#902)
Co-authored-by: Jay <[email protected]>
Diffstat (limited to 'packages/web/src/components')
-rw-r--r--packages/web/src/components/share/content-markdown.tsx3
-rw-r--r--packages/web/src/components/share/content-text.module.css1
-rw-r--r--packages/web/src/components/share/content-text.tsx3
-rw-r--r--packages/web/src/components/share/copy-button.module.css47
-rw-r--r--packages/web/src/components/share/copy-button.tsx35
5 files changed, 89 insertions, 0 deletions
diff --git a/packages/web/src/components/share/content-markdown.tsx b/packages/web/src/components/share/content-markdown.tsx
index a083872e7..8efa5f9a9 100644
--- a/packages/web/src/components/share/content-markdown.tsx
+++ b/packages/web/src/components/share/content-markdown.tsx
@@ -2,6 +2,7 @@ import { marked } from "marked"
import { codeToHtml } from "shiki"
import markedShiki from "marked-shiki"
import { createOverflow } from "./common"
+import { CopyButton } from "./copy-button"
import { createResource, createSignal } from "solid-js"
import { transformerNotationDiff } from "@shikijs/transformers"
import style from "./content-markdown.module.css"
@@ -41,6 +42,7 @@ export function ContentMarkdown(props: Props) {
class={style.root}
data-highlight={props.highlight === true ? true : undefined}
data-expanded={expanded() || props.expand === true ? true : undefined}
+ style={{ position: "relative" }}
>
<div data-slot="markdown" ref={overflow.ref} innerHTML={html()} />
@@ -54,6 +56,7 @@ export function ContentMarkdown(props: Props) {
{expanded() ? "Show less" : "Show more"}
</button>
)}
+ <CopyButton text={props.text} />
</div>
)
}
diff --git a/packages/web/src/components/share/content-text.module.css b/packages/web/src/components/share/content-text.module.css
index 203f0d06d..a3842275c 100644
--- a/packages/web/src/components/share/content-text.module.css
+++ b/packages/web/src/components/share/content-text.module.css
@@ -2,6 +2,7 @@
color: var(--sl-color-text);
background-color: var(--sl-color-bg-surface);
padding: 0.5rem calc(0.5rem + 3px);
+ padding-right: calc(1rem + 18px);
border-radius: 0.25rem;
display: flex;
flex-direction: column;
diff --git a/packages/web/src/components/share/content-text.tsx b/packages/web/src/components/share/content-text.tsx
index c52e0dfcc..c898bd8d7 100644
--- a/packages/web/src/components/share/content-text.tsx
+++ b/packages/web/src/components/share/content-text.tsx
@@ -1,6 +1,7 @@
import style from "./content-text.module.css"
import { createSignal } from "solid-js"
import { createOverflow } from "./common"
+import { CopyButton } from "./copy-button"
interface Props {
text: string
@@ -16,6 +17,7 @@ export function ContentText(props: Props) {
class={style.root}
data-expanded={expanded() || props.expand === true ? true : undefined}
data-compact={props.compact === true ? true : undefined}
+ style={{ position: "relative" }}
>
<pre data-slot="text" ref={overflow.ref}>
{props.text}
@@ -30,6 +32,7 @@ export function ContentText(props: Props) {
{expanded() ? "Show less" : "Show more"}
</button>
)}
+ <CopyButton text={props.text} />
</div>
)
}
diff --git a/packages/web/src/components/share/copy-button.module.css b/packages/web/src/components/share/copy-button.module.css
new file mode 100644
index 000000000..7494c2b81
--- /dev/null
+++ b/packages/web/src/components/share/copy-button.module.css
@@ -0,0 +1,47 @@
+.copyButtonWrapper {
+ position: absolute;
+ top: 0.5rem;
+ right: 0.5rem;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.15s ease;
+}
+
+.copyButton {
+ width: 18px;
+ cursor: pointer;
+ background: none;
+ border: none;
+ padding: 0;
+ color: var(--sl-color-text-secondary);
+
+ svg {
+ display: block;
+ width: 16px;
+ height: 16px;
+ }
+
+ &[data-copied="true"] {
+ color: var(--sl-color-green-high);
+ }
+}
+
+/* Show copy button when parent is hovered */
+*:hover > .copyButtonWrapper {
+ opacity: 0.65;
+ visibility: visible;
+}
+
+.copyTooltip {
+ position: absolute;
+ top: 50%;
+ left: calc(100% + 12px);
+ transform: translate(0, -50%);
+ padding: 0.375em 0.5em;
+ background: var(--sl-color-white);
+ color: var(--sl-color-text-invert);
+ font-size: 0.6875rem;
+ border-radius: 7px;
+ white-space: nowrap;
+ z-index: 11;
+}
diff --git a/packages/web/src/components/share/copy-button.tsx b/packages/web/src/components/share/copy-button.tsx
new file mode 100644
index 000000000..a4434b1c5
--- /dev/null
+++ b/packages/web/src/components/share/copy-button.tsx
@@ -0,0 +1,35 @@
+import { createSignal } from "solid-js"
+import { IconClipboard, IconCheckCircle } from "../icons"
+import styles from "./copy-button.module.css"
+
+interface CopyButtonProps {
+ text: string
+}
+
+export function CopyButton(props: CopyButtonProps) {
+ const [copied, setCopied] = createSignal(false)
+
+ function handleCopyClick() {
+ if (props.text) {
+ navigator.clipboard.writeText(props.text).catch((err) => console.error("Copy failed", err))
+
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+ }
+
+ return (
+ <div class={styles.copyButtonWrapper}>
+ <button
+ type="button"
+ class={styles.copyButton}
+ onClick={handleCopyClick}
+ data-copied={copied() ? true : undefined}
+ title="Copy content"
+ >
+ {copied() ? <IconCheckCircle width={16} height={16} /> : <IconClipboard width={16} height={16} />}
+ </button>
+ {copied() && <span class={styles.copyTooltip}>Copied!</span>}
+ </div>
+ )
+}