app
Component Widgets
Public Made by Adomby adom
Reusable component widgets for pinouts, thermal vias, molecule relationships, reviewed chip outlines and lightweight four-level LOD cache listings.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
/*!
* <adom-prompt> - a paste-ready AI prompt with a copy button, for any wiki page.
*
* Drop-in (wiki README):
* ::adom-prompt{src="/OWNER/SLUG/render/docs/prompt.txt" heading="Paste into Hydrogen"}
*
* - src: a plain-text file (or JSON {"text": "..."}) on the wiki, usually the page's own render route.
* Keeping the prompt in a file means the README shows it once and an AI can read the same file.
* - text: the prompt inline instead of src (short prompts only: tag attributes are length-capped).
* - heading: optional label over the prompt (default "AI prompt").
*
* Copy uses navigator.clipboard, falling back to a selected textarea + execCommand('copy') because the wiki
* frame is a sandboxed opaque origin where the async clipboard API can be refused. If both fail the text is
* left selected so Ctrl+C works, and the button says so.
*/
(() => {
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const ICON_COPY = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
const ICON_OK = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
class AdomPrompt extends HTMLElement {
connectedCallback() {
if (this._init) return; this._init = true;
this.attachShadow({ mode: 'open' });
// only in the wiki's sandboxed widget frame (/OWNER/SLUG/widget/NAME); inline, the document is the wiki page
if (/^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname)) { document.documentElement.style.overflow = 'hidden'; document.body.style.margin = '0'; }
this._text = this.getAttribute('text') || '';
this._render(this._text ? null : 'Loading prompt...');
if (!this._text && this.getAttribute('src')) this._fetch();
else if (!this._text) this._render('No prompt: give me src or text');
}
async _fetch() {
try {
const res = await fetch(new URL(this.getAttribute('src'), location.href).href);
if (!res.ok) throw new Error('could not load the prompt (' + res.status + ')');
const body = (await res.text()).trim();
let t = body;
if (body.startsWith('{')) { try { t = JSON.parse(body).text || body; } catch (e) {} }
this._text = t.replace(/\r\n/g, '\n');
this._render();
} catch (e) { this._render(String(e && e.message || e).slice(0, 160)); }
}
_render(note) {
const h = this.getAttribute('heading') || 'AI prompt';
this.shadowRoot.innerHTML = `<style>
:host { display: block; font: 13px/1.5 Satoshi, -apple-system, 'Segoe UI', sans-serif; color: #e6edf3; }
.card { background: #0d1117; border: 1px solid #30363d; border-radius: 10px; overflow: hidden; }
.bar { display: flex; align-items: center; gap: 10px; padding: 8px 10px 8px 14px; border-bottom: 1px solid #30363d; }
.h { flex: 1; font: 600 13px/1.3 'Familjen Grotesk', -apple-system, sans-serif; color: #8b949e; letter-spacing: .02em; }
button { display: inline-flex; align-items: center; gap: 6px; font: 600 12px/1 inherit; padding: 7px 11px; border-radius: 8px;
border: 1px solid #00b8b1; background: transparent; color: #00b8b1; cursor: pointer; transition: background .12s ease, color .12s ease, transform .06s ease; }
button:hover { background: #00b8b1; color: #0d1117; }
button:active { transform: translateY(1px) scale(.98); }
button:focus-visible { outline: 2px solid #00b8b1; outline-offset: 3px; }
button.ok { background: #00b8b1; color: #0d1117; }
pre { margin: 0; padding: 14px; white-space: pre-wrap; word-break: break-word; font: 13px/1.55 'JetBrains Mono', ui-monospace, Consolas, monospace; color: #e6edf3; }
.note { padding: 14px; color: #8b949e; }
textarea { position: absolute; left: -9999px; top: 0; opacity: 0; }
</style>
<div class="card"><div class="bar"><span class="h">${esc(h)}</span>${note ? '' : `<button id="cp" type="button" title="Copy the prompt">${ICON_COPY}<span>Copy</span></button>`}</div>
${note ? `<div class="note">${esc(note)}</div>` : `<pre id="t">${esc(this._text)}</pre>`}</div>`;
const b = this.shadowRoot.getElementById('cp');
if (b) b.addEventListener('click', () => this.copy());
}
async copy() {
const b = this.shadowRoot.getElementById('cp');
let ok = false;
try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(this._text); ok = true; } } catch (e) {}
if (!ok) {
const ta = document.createElement('textarea'); ta.value = this._text; this.shadowRoot.appendChild(ta); ta.select();
try { ok = document.execCommand('copy'); } catch (e) {}
ta.remove();
}
if (!ok) { // leave the prompt selected so Ctrl+C still works
const r = document.createRange(); r.selectNodeContents(this.shadowRoot.getElementById('t'));
const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(r);
}
if (b) {
b.classList.toggle('ok', ok);
b.innerHTML = ok ? `${ICON_OK}<span>Copied</span>` : `${ICON_COPY}<span>Press Ctrl+C</span>`;
clearTimeout(this._t); this._t = setTimeout(() => { b.classList.remove('ok'); b.innerHTML = `${ICON_COPY}<span>Copy</span>`; }, 1800);
}
this.dispatchEvent(new CustomEvent('promptcopy', { detail: { ok, length: this._text.length }, bubbles: true, composed: true }));
return ok;
}
// for AIs and tests: what the widget is showing
selfCheck() { return { src: this.getAttribute('src'), length: (this._text || '').length, text: this._text }; }
}
if (!customElements.get('adom-prompt')) customElements.define('adom-prompt', AdomPrompt);
})();