gabriel / musehub public
mist-detail.ts typescript
96 lines 3.8 KB
Raw
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 1 day ago
1 /**
2 * mist-detail.ts — Mist detail page module.
3 *
4 * Responsibilities:
5 * 1. Copy artifact content to clipboard (toolbar button).
6 * 2. Copy embed snippets (iframe / script) from sidebar.
7 * 3. Copy-button visual feedback (check icon for 2s).
8 */
9
10 // ── SVG icons ─────────────────────────────────────────────────────────────────
11
12 const COPY_ICON = `<svg width="11" height="11" aria-hidden="true"><use href="#icon-copy"></use></svg>`;
13 const CHECK_ICON = `<svg width="11" height="11" aria-hidden="true"><use href="#icon-check"></use></svg>`;
14
15 // ── Generic clipboard helper ──────────────────────────────────────────────────
16
17 async function copyText(text: string): Promise<boolean> {
18 try {
19 await navigator.clipboard.writeText(text);
20 return true;
21 } catch {
22 return false;
23 }
24 }
25
26 function flashButton(btn: HTMLElement, originalHtml: string): void {
27 btn.innerHTML = `${CHECK_ICON} Copied`;
28 btn.classList.add('is-copied');
29 setTimeout(() => {
30 btn.innerHTML = originalHtml;
31 btn.classList.remove('is-copied');
32 }, 2000);
33 }
34
35 // ── Copy artifact content ─────────────────────────────────────────────────────
36
37 function bindCopyContent(): void {
38 const btn = document.querySelector<HTMLButtonElement>('[data-action="copy-content"]');
39 if (!btn) return;
40
41 const originalHtml = btn.innerHTML;
42
43 btn.addEventListener('click', async () => {
44 const code = document.getElementById('artifact-content');
45 const text = (code?.textContent ?? '').trimEnd();
46 const ok = await copyText(text);
47 if (ok) flashButton(btn, originalHtml);
48 });
49 }
50
51 // ── Copy embed snippets ───────────────────────────────────────────────────────
52
53 function bindCopyEmbeds(): void {
54 document.querySelectorAll<HTMLElement>('[data-action="copy-embed"]').forEach(snippet => {
55 const copyBtn = snippet.querySelector<HTMLElement>('.ms-embed-copy');
56 const codeEl = snippet.querySelector<HTMLElement>('.ms-embed-code');
57 if (!copyBtn || !codeEl) return;
58
59 const originalHtml = copyBtn.innerHTML;
60
61 const doCopy = async () => {
62 // Decode HTML entities in the code element's text content
63 const text = codeEl.textContent ?? '';
64 const ok = await copyText(text);
65 if (ok) flashButton(copyBtn, originalHtml);
66 };
67
68 copyBtn.addEventListener('click', (e) => { e.stopPropagation(); void doCopy(); });
69 snippet.addEventListener('click', () => { void doCopy(); });
70 });
71 }
72
73 // ── Markdown heading anchors ──────────────────────────────────────────────────
74
75 function initMarkdownAnchors(): void {
76 const container = document.querySelector<HTMLElement>('.blob2-markdown');
77 if (!container) return;
78 container.querySelectorAll<HTMLElement>('h1,h2,h3,h4,h5,h6').forEach(heading => {
79 const slug = heading.id;
80 if (!slug) return;
81 const anchor = document.createElement('a');
82 anchor.href = '#' + slug;
83 anchor.className = 'blob2-md-anchor';
84 anchor.textContent = '#';
85 anchor.setAttribute('aria-hidden', 'true');
86 heading.appendChild(anchor);
87 });
88 }
89
90 // ── Entry point ───────────────────────────────────────────────────────────────
91
92 export function initMistDetail(_data?: Record<string, unknown>): void {
93 bindCopyContent();
94 bindCopyEmbeds();
95 initMarkdownAnchors();
96 }
File History 1 commit
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 1 day ago