docs: code-block copy buttons + 'Connect your broker' guide (0.58.0)

- fix fenced code blocks: reset the doubled inline-code background on
  'pre code', readable monospace styling (applies to all technical docs)
- DocsApp decorates rendered code blocks with a hover copy button
- seed a 'Connect your broker' guide (guides/) — the lasting reference for
  the supply-side last mile: heartbeat loop (curl + Node) to stay online,
  the relay WebSocket, and how it fits together (accurate from the API)
- verified live on docs.amerc.ai: guide renders, 3 code blocks, 3 copy
  buttons wired, clean styling
This commit is contained in:
artheru 2026-06-10 08:17:15 +08:00
parent fb3c523d4a
commit 6a8fc0a5c0
5 changed files with 28 additions and 5 deletions

View File

@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0e0a14" />
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
<meta name="version" content="0.57.0-loadstates" />
<meta name="version" content="0.58.0-docscode" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />

View File

@ -1,6 +1,6 @@
{
"name": "amerc-site",
"version": "0.57.0-loadstates",
"version": "0.58.0-docscode",
"private": true,
"type": "module",
"scripts": {

View File

@ -1,7 +1,8 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { marked } from 'marked';
import ConsoleShell from './ConsoleShell.jsx';
import { api } from './api.js';
import { copyToast } from './toast.js';
marked.setOptions({ breaks: true });
@ -13,6 +14,7 @@ function DocsWorkspace({ canEdit }) {
const [draft, setDraft] = useState({ title: '', folder: '', body: '' });
const [err, setErr] = useState('');
const [q, setQ] = useState('');
const renderRef = useRef(null);
const loadList = useCallback(async () => {
try { const d = await api('/docs'); setDocs(d.docs || []); } catch (e) { setErr(e.message); }
@ -22,6 +24,18 @@ function DocsWorkspace({ canEdit }) {
if (sel == null) { setDoc(null); return; }
api(`/docs/${sel}`).then((d) => { setDoc(d.doc); setDraft({ title: d.doc.title, folder: d.doc.folder, body: d.doc.body }); setEditing(false); }).catch((e) => setErr(e.message));
}, [sel]);
// decorate rendered code blocks with a hover copy button
useEffect(() => {
const el = renderRef.current;
if (!el || editing) return;
el.querySelectorAll('pre').forEach((pre) => {
if (pre.parentElement?.classList.contains('docs-code')) return;
const wrap = document.createElement('div'); wrap.className = 'docs-code';
const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'docs-copy'; btn.textContent = 'copy';
btn.addEventListener('click', () => copyToast(pre.innerText, 'Copied'));
pre.parentNode.insertBefore(wrap, pre); wrap.appendChild(btn); wrap.appendChild(pre);
});
}, [doc, editing]);
const create = async () => {
const title = window.prompt('Document title', 'New document'); if (!title) return;
@ -73,7 +87,7 @@ function DocsWorkspace({ canEdit }) {
<p className="docs-byline">updated by {doc.updated_by || '—'}</p>
{editing
? <textarea className="docs-editor" value={draft.body} onChange={(e) => setDraft({ ...draft, body: e.target.value })} />
: <article className="docs-render" dangerouslySetInnerHTML={{ __html: marked.parse(doc.body || '') }} />}
: <article ref={renderRef} className="docs-render" dangerouslySetInnerHTML={{ __html: marked.parse(doc.body || '') }} />}
</>
)}
</section>

View File

@ -7,7 +7,7 @@ import Account from './Account.jsx';
import TavernGame from './TavernGame.jsx';
import { api } from './api.js';
const RELEASE = '0.57.0-loadstates';
const RELEASE = '0.58.0-docscode';
const NAV = [
{ id: 'home', label: 'Tavern' },

View File

@ -1413,3 +1413,12 @@ button {
.ap-skel-row { display: flex; align-items: center; gap: 11px; }
.mn-skel { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
.mn-skel .ap-skel-row .sk-p { height: 12px; }
/* docs code blocks + copy button (0.58) */
.docs-render pre { position: relative; }
.docs-render pre code { background: none; padding: 0; color: #cfe8ff; font-size: 0.86em; line-height: 1.6; }
.docs-code { position: relative; margin: 14px 0; }
.docs-code pre { margin: 0; }
.docs-copy { position: absolute; top: 8px; right: 8px; font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 11px; padding: 3px 10px; border-radius: 6px; background: #1b2536; color: #9fc8e8; border: 1px solid #2a3a52; cursor: pointer; opacity: 0; transition: opacity .15s, background .15s, color .15s; }
.docs-code:hover .docs-copy { opacity: 1; }
.docs-copy:hover { background: var(--accent); color: #061018; border-color: transparent; }