Phase 7: UI-Verbesserungen — Code-Copy, Edit, Regenerate
- Code-Blöcke mit Copy-Button (📋) und Sprach-Label - Nachrichten bearbeiten (✏️) mit Speichern & Senden - Antwort regenerieren (🔄) für letzte Assistant-Nachricht - Custom marked-Renderer für Code-Block-Wrapper - MutationObserver für Streaming-kompatible Copy-Buttons Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
adb11fd121
commit
9d837efae6
2 changed files with 450 additions and 11 deletions
|
|
@ -1,11 +1,22 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, type Message } from '$lib/stores/app';
|
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, type Message } from '$lib/stores/app';
|
||||||
import { marked } from 'marked';
|
import { marked, type Tokens } from 'marked';
|
||||||
import { tick, onDestroy } from 'svelte';
|
import { tick, onDestroy } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
marked.setOptions({ breaks: true, gfm: true });
|
// Custom Renderer für Code-Blöcke mit Wrapper
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
renderer.code = function ({ text, lang }: Tokens.Code): string {
|
||||||
|
const language = lang || '';
|
||||||
|
const escapedCode = text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
return `<div class="code-block-wrapper" data-lang="${language}"><pre><code class="language-${language}">${escapedCode}</code></pre></div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
marked.setOptions({ breaks: true, gfm: true, renderer });
|
||||||
|
|
||||||
function renderMarkdown(text: string): string {
|
function renderMarkdown(text: string): string {
|
||||||
try {
|
try {
|
||||||
|
|
@ -15,8 +26,67 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Svelte Action: Copy-Buttons zu Code-Blöcken hinzufügen
|
||||||
|
function addCopyButtons(node: HTMLElement) {
|
||||||
|
function processCodeBlocks() {
|
||||||
|
const wrappers = node.querySelectorAll('.code-block-wrapper:not([data-copy-added])');
|
||||||
|
wrappers.forEach((wrapper) => {
|
||||||
|
wrapper.setAttribute('data-copy-added', 'true');
|
||||||
|
|
||||||
|
const lang = wrapper.getAttribute('data-lang') || '';
|
||||||
|
const codeEl = wrapper.querySelector('code');
|
||||||
|
const codeText = codeEl?.textContent || '';
|
||||||
|
|
||||||
|
// Header mit Sprache und Copy-Button erstellen
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'code-header';
|
||||||
|
|
||||||
|
if (lang) {
|
||||||
|
const langSpan = document.createElement('span');
|
||||||
|
langSpan.className = 'code-lang';
|
||||||
|
langSpan.textContent = lang;
|
||||||
|
header.appendChild(langSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyBtn = document.createElement('button');
|
||||||
|
copyBtn.className = 'copy-btn';
|
||||||
|
copyBtn.title = 'Code kopieren';
|
||||||
|
copyBtn.innerHTML = '📋';
|
||||||
|
copyBtn.onclick = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(codeText);
|
||||||
|
copyBtn.innerHTML = '<span class="copied">✓</span>';
|
||||||
|
setTimeout(() => (copyBtn.innerHTML = '📋'), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Kopieren fehlgeschlagen:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
header.appendChild(copyBtn);
|
||||||
|
|
||||||
|
wrapper.insertBefore(header, wrapper.firstChild);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial verarbeiten
|
||||||
|
processCodeBlocks();
|
||||||
|
|
||||||
|
// MutationObserver für dynamische Inhalte (Streaming)
|
||||||
|
const observer = new MutationObserver(processCodeBlocks);
|
||||||
|
observer.observe(node, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let messagesContainer: HTMLDivElement;
|
let messagesContainer: HTMLDivElement;
|
||||||
|
|
||||||
|
// Edit-Modus State
|
||||||
|
let editingMessageId: string | null = $state(null);
|
||||||
|
let editingContent: string = $state('');
|
||||||
|
|
||||||
async function scrollToBottom() {
|
async function scrollToBottom() {
|
||||||
await tick();
|
await tick();
|
||||||
if (messagesContainer) {
|
if (messagesContainer) {
|
||||||
|
|
@ -94,6 +164,107 @@
|
||||||
sendMessage();
|
sendMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit-Funktionen
|
||||||
|
function startEdit(message: Message) {
|
||||||
|
editingMessageId = message.id;
|
||||||
|
editingContent = message.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
editingMessageId = null;
|
||||||
|
editingContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmEdit() {
|
||||||
|
if (!editingMessageId || !editingContent.trim()) return;
|
||||||
|
|
||||||
|
const msgIndex = $messages.findIndex((m) => m.id === editingMessageId);
|
||||||
|
if (msgIndex === -1) return;
|
||||||
|
|
||||||
|
const updatedMessage = {
|
||||||
|
...$messages[msgIndex],
|
||||||
|
content: editingContent.trim(),
|
||||||
|
timestamp: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Nachricht aktualisieren und alle folgenden entfernen
|
||||||
|
// (da sie auf der alten Nachricht basieren)
|
||||||
|
messages.update((msgs) => {
|
||||||
|
const newMsgs = [...msgs.slice(0, msgIndex), updatedMessage];
|
||||||
|
return newMsgs;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nachricht in DB aktualisieren
|
||||||
|
await saveMessageToDb(updatedMessage);
|
||||||
|
|
||||||
|
// Edit-Modus beenden
|
||||||
|
const newContent = editingContent.trim();
|
||||||
|
editingMessageId = null;
|
||||||
|
editingContent = '';
|
||||||
|
|
||||||
|
// Nachricht neu an Claude senden
|
||||||
|
$isProcessing = true;
|
||||||
|
try {
|
||||||
|
await invoke('send_message', { message: newContent });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Senden:', err);
|
||||||
|
addMessage('system', `Fehler: ${err}`);
|
||||||
|
$isProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
confirmEdit();
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
cancelEdit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regenerate: Antwort neu generieren
|
||||||
|
async function regenerateResponse(assistantMsgIndex: number) {
|
||||||
|
if ($isProcessing) return;
|
||||||
|
|
||||||
|
// Vorherige User-Nachricht finden
|
||||||
|
let userMsg: Message | null = null;
|
||||||
|
for (let i = assistantMsgIndex - 1; i >= 0; i--) {
|
||||||
|
if ($messages[i].role === 'user') {
|
||||||
|
userMsg = $messages[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userMsg) {
|
||||||
|
console.error('Keine User-Nachricht zum Regenerieren gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Nachrichten ab der Assistant-Nachricht entfernen
|
||||||
|
messages.update((msgs) => msgs.slice(0, assistantMsgIndex));
|
||||||
|
|
||||||
|
// User-Nachricht erneut senden
|
||||||
|
$isProcessing = true;
|
||||||
|
try {
|
||||||
|
await invoke('send_message', { message: userMsg.content });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Regenerieren:', err);
|
||||||
|
addMessage('system', `Fehler: ${err}`);
|
||||||
|
$isProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob eine Nachricht die letzte Assistant-Nachricht ist
|
||||||
|
function isLastAssistantMessage(index: number): boolean {
|
||||||
|
// Finde die letzte Assistant-Nachricht
|
||||||
|
for (let i = $messages.length - 1; i >= 0; i--) {
|
||||||
|
if ($messages[i].role === 'assistant' && $messages[i].content) {
|
||||||
|
return i === index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="chat-panel">
|
<div class="chat-panel">
|
||||||
|
|
@ -102,7 +273,7 @@
|
||||||
<span class="msg-count">{$messages.length} Nachrichten</span>
|
<span class="msg-count">{$messages.length} Nachrichten</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-messages" bind:this={messagesContainer}>
|
<div class="chat-messages" bind:this={messagesContainer} use:addCopyButtons>
|
||||||
{#if $messages.length === 0}
|
{#if $messages.length === 0}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">🤖</div>
|
<div class="empty-icon">🤖</div>
|
||||||
|
|
@ -110,8 +281,8 @@
|
||||||
<p class="hint">Enter = Senden, Shift+Enter = Neue Zeile, Escape = Stopp</p>
|
<p class="hint">Enter = Senden, Shift+Enter = Neue Zeile, Escape = Stopp</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each $messages as message}
|
{#each $messages as message, index}
|
||||||
<div class="message" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'} class:system={message.role === 'system'}>
|
<div class="message" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'} class:system={message.role === 'system'} class:editing={editingMessageId === message.id}>
|
||||||
<div class="message-header">
|
<div class="message-header">
|
||||||
<span class="message-role">
|
<span class="message-role">
|
||||||
{#if message.role === 'user'}
|
{#if message.role === 'user'}
|
||||||
|
|
@ -122,12 +293,33 @@
|
||||||
⚙️ System
|
⚙️ System
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
<div class="message-actions">
|
||||||
|
{#if message.role === 'user' && !$isProcessing && editingMessageId !== message.id}
|
||||||
|
<button class="action-btn" onclick={() => startEdit(message)} title="Bearbeiten">✏️</button>
|
||||||
|
{/if}
|
||||||
|
{#if message.role === 'assistant' && !$isProcessing && isLastAssistantMessage(index) && message.content}
|
||||||
|
<button class="action-btn" onclick={() => regenerateResponse(index)} title="Antwort neu generieren">🔄</button>
|
||||||
|
{/if}
|
||||||
<span class="message-time">
|
<span class="message-time">
|
||||||
{message.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
|
{message.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
{#if message.role === 'assistant'}
|
{#if editingMessageId === message.id}
|
||||||
|
<textarea
|
||||||
|
class="edit-textarea"
|
||||||
|
bind:value={editingContent}
|
||||||
|
onkeydown={handleEditKeydown}
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<div class="edit-actions">
|
||||||
|
<button class="edit-btn cancel" onclick={cancelEdit}>Abbrechen</button>
|
||||||
|
<button class="edit-btn confirm" onclick={confirmEdit} disabled={!editingContent.trim()}>
|
||||||
|
Speichern & Senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else if message.role === 'assistant'}
|
||||||
{@html renderMarkdown(message.content)}
|
{@html renderMarkdown(message.content)}
|
||||||
{:else}
|
{:else}
|
||||||
{message.content}
|
{message.content}
|
||||||
|
|
@ -268,11 +460,101 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 0.15rem 0.3rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:hover .action-btn {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
.message-time {
|
.message-time {
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Edit-Modus */
|
||||||
|
.message.editing {
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 60px;
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn.cancel {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn.cancel:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn.confirm {
|
||||||
|
background: var(--accent);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn.confirm:hover:not(:disabled) {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn.confirm:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
@ -299,7 +581,68 @@
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content :global(pre) {
|
/* Code-Block mit Header und Copy-Button */
|
||||||
|
.message-content :global(.code-block-wrapper) {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content :global(.code-header) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content :global(.code-lang) {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content :global(.copy-btn) {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content :global(.copy-btn:hover) {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content :global(.copy-btn .copied) {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content :global(.code-block-wrapper pre) {
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content :global(.code-block-wrapper pre code) {
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback für inline pre (ohne wrapper) */
|
||||||
|
.message-content :global(pre:not(.code-block-wrapper pre)) {
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
|
|
@ -309,7 +652,7 @@
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content :global(pre code) {
|
.message-content :global(pre:not(.code-block-wrapper pre) code) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
96
src/lib/components/CodeBlock.svelte
Normal file
96
src/lib/components/CodeBlock.svelte
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script lang="ts">
|
||||||
|
// CodeBlock — Code mit Syntax-Highlight und Copy-Button
|
||||||
|
// Standalone-Komponente für manuellen Einsatz
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
code: string;
|
||||||
|
language?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { code, language = '' }: Props = $props();
|
||||||
|
|
||||||
|
let copied = $state(false);
|
||||||
|
|
||||||
|
async function copyToClipboard() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(code);
|
||||||
|
copied = true;
|
||||||
|
setTimeout(() => (copied = false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Kopieren fehlgeschlagen:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="code-block">
|
||||||
|
<div class="code-header">
|
||||||
|
{#if language}
|
||||||
|
<span class="code-lang">{language}</span>
|
||||||
|
{/if}
|
||||||
|
<button class="copy-btn" onclick={copyToClipboard} title="Code kopieren">
|
||||||
|
{#if copied}
|
||||||
|
<span class="copied">✓ Kopiert</span>
|
||||||
|
{:else}
|
||||||
|
<span>📋</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre><code class="language-{language}">{code}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.code-block {
|
||||||
|
position: relative;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-lang {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copied {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in a new issue