diff --git a/src/client/src/components/FormattedText.ts b/src/client/src/components/FormattedText.ts index 5d3e35c..b4cf428 100644 --- a/src/client/src/components/FormattedText.ts +++ b/src/client/src/components/FormattedText.ts @@ -26,8 +26,12 @@ export class FormattedText extends LitElement { const button = document.createElement("button"); button.type = "button"; button.className = "code-copy-button"; - button.textContent = "Copy"; + button.title = "Copy code block"; button.setAttribute("aria-label", "Copy code block"); + const icon = document.createElement("span"); + icon.setAttribute("aria-hidden", "true"); + icon.textContent = "⧉"; + button.append(icon); element.before(wrapper); wrapper.append(element, button); }); @@ -44,12 +48,20 @@ export class FormattedText extends LitElement { private async copyCode(text: string, button: HTMLButtonElement): Promise { const ok = await writeClipboard(text); - button.textContent = ok ? "Copied" : "Failed"; + this.setCopyButtonState(button, ok ? "copied" : "failed"); window.setTimeout(() => { - button.textContent = "Copy"; + this.setCopyButtonState(button, "idle"); }, 1200); } + private setCopyButtonState(button: HTMLButtonElement, state: "idle" | "copied" | "failed"): void { + const icon = button.querySelector("span"); + if (icon !== null) icon.textContent = state === "copied" ? "✓" : "⧉"; + const label = state === "copied" ? "Copied code block" : state === "failed" ? "Failed to copy code block" : "Copy code block"; + button.title = label; + button.setAttribute("aria-label", label); + } + static override styles = formattedTextStyles; } diff --git a/src/client/src/components/shared.ts b/src/client/src/components/shared.ts index b72cff8..b62af2d 100644 --- a/src/client/src/components/shared.ts +++ b/src/client/src/components/shared.ts @@ -195,11 +195,11 @@ export const formattedTextStyles = css` li + li { margin-top: 3px; } code { border: 1px solid #30363d; border-radius: 4px; background: #0d1117; padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } .code-block-wrapper { position: relative; } - .code-block-wrapper pre { margin: 0; padding-right: 58px; } + .code-block-wrapper pre { margin: 0; padding-right: 40px; } pre { border: 1px solid #30363d; border-radius: 8px; background: #0d1117; padding: 10px; overflow: auto; } pre code { border: 0; padding: 0; background: transparent; } - .code-copy-button { position: absolute; top: 6px; right: 6px; z-index: 1; border: 1px solid #30363d; border-radius: 6px; background: #161b22; color: #8b949e; padding: 3px 7px; font: 12px system-ui, sans-serif; cursor: pointer; } - .code-copy-button:hover { color: #e6edf3; border-color: #58a6ff; } + .code-copy-button { position: absolute; top: 6px; right: 6px; z-index: 1; display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid #30363d; border-radius: 6px; background: #161b22; color: #8b949e; padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; } + .code-copy-button:hover, .code-copy-button:focus { color: #e6edf3; border-color: #58a6ff; } blockquote { border-left: 3px solid #30363d; padding-left: 10px; color: #8b949e; } a { color: #58a6ff; } h1, h2, h3, h4 { margin: 14px 0 8px; line-height: 1.2; }