clone

tiny dependency-free nostr libraries for plain html, each under 5kb and named for an element. public domain

Make the site simple: one table, a page per library, links on-site

Dami committed 1066474
Like the fixi project's site: an emoji menu, a plain table grouped by
element, a one-line installer, and each library's README as its own
page with previous and next links. Library files, the bundle, the demos,
the license and llms.txt are all served from the site.
commit
10664748ed6b2a10b7d6734c162b5cfb160ee3b7
parent
f5cbb8fcf76c23080626cba088e193c13552ab94
author
Dami <dami.etoile@gmail.com>
2 files changed, +109 -129
Mscripts/libs.ts34+++---
Mscripts/site.ts204++++++++++++++----------------
M scripts/libs.ts +17 -17
@@ -1,7 +1,7 @@
11 // The one list of nobel libraries. Its order is the canonical load order for single files:22 // the base first, then peers that only listen, then libraries that start work, and xenon last,33 // so any subset concatenated in this order works however the file is loaded.4-export type Lib = { name: string; sym: string; z: number; group: string; needs: string[] };4+export type Lib = { name: string; sym: string; z: number; group: string; needs: string[]; of: string; does: string };55 66 export const groups: Record<string, string> = {77 base: "period 1, the base: relays and keys",
@@ -13,22 +13,22 @@ export const groups: Record<string, string> = {
1313 };1414 1515 export const libs: Lib[] = [16- { name: "hydrogen", sym: "H", z: 1, group: "base", needs: [] },17- { name: "helium", sym: "He", z: 2, group: "base", needs: [] },18- { name: "neon", sym: "Ne", z: 10, group: "noble gas", needs: [] },19- { name: "fluorine", sym: "F", z: 9, group: "halogen", needs: ["helium"] },20- { name: "chlorine", sym: "Cl", z: 17, group: "halogen", needs: ["helium", "fluorine"] },21- { name: "argon", sym: "Ar", z: 18, group: "noble gas", needs: [] },22- { name: "lithium", sym: "Li", z: 3, group: "alkali metal", needs: ["hydrogen", "argon"] },23- { name: "gold", sym: "Au", z: 79, group: "transition metal", needs: ["hydrogen"] },24- { name: "mercury", sym: "Hg", z: 80, group: "transition metal", needs: [] },25- { name: "radon", sym: "Rn", z: 86, group: "noble gas", needs: ["hydrogen"] },26- { name: "iodine", sym: "I", z: 53, group: "halogen", needs: ["hydrogen", "helium", "fluorine", "xenon"] },27- { name: "oganesson", sym: "Og", z: 118, group: "noble gas", needs: ["hydrogen"] },28- { name: "praseodymium", sym: "Pr", z: 59, group: "lanthanide", needs: ["hydrogen", "radon"] },29- { name: "europium", sym: "Eu", z: 63, group: "lanthanide", needs: ["hydrogen", "xenon"] },30- { name: "cerium", sym: "Ce", z: 58, group: "lanthanide", needs: [] },31- { name: "xenon", sym: "Xe", z: 54, group: "noble gas", needs: ["hydrogen"] },16+ { name: "hydrogen", sym: "H", z: 1, group: "base", needs: [], of: "a relay pool", does: "relays, signing and the family's hooks" },17+ { name: "helium", sym: "He", z: 2, group: "base", needs: [], of: "@noble/secp256k1", does: "keys, Schnorr signing and verification" },18+ { name: "neon", sym: "Ne", z: 10, group: "noble gas", needs: [], of: "nip19", does: "npub/nevent/naddr, linked content, times" },19+ { name: "fluorine", sym: "F", z: 9, group: "halogen", needs: ["helium"], of: "NIP-44", does: "encryption for helium's keys" },20+ { name: "chlorine", sym: "Cl", z: 17, group: "halogen", needs: ["helium", "fluorine"], of: "a NIP-46 client", does: "remote signing behind a NIP-07 face" },21+ { name: "argon", sym: "Ar", z: 18, group: "noble gas", needs: [], of: "a relay", does: "IndexedDB cache, offline-first feeds" },22+ { name: "lithium", sym: "Li", z: 3, group: "alkali metal", needs: ["hydrogen", "argon"], of: "Negentropy", does: "NIP-77 sync between argon and relays" },23+ { name: "gold", sym: "Au", z: 79, group: "transition metal", needs: ["hydrogen"], of: "a NIP-42 client", does: "relay AUTH for members-only relays" },24+ { name: "mercury", sym: "Hg", z: 80, group: "transition metal", needs: [], of: "rexi.js", does: "NIP-98 and Blossom signed HTTP" },25+ { name: "radon", sym: "Rn", z: 86, group: "noble gas", needs: ["hydrogen"], of: "a profile loader", does: "names, pictures, NIP-05" },26+ { name: "iodine", sym: "I", z: 53, group: "halogen", needs: ["hydrogen", "helium", "fluorine", "xenon"], of: "an encrypted messenger", does: "NIP-17 private messages" },27+ { name: "oganesson", sym: "Og", z: 118, group: "noble gas", needs: ["hydrogen"], of: "a chat app's live layer", does: "presence, typing, cursors" },28+ { name: "praseodymium", sym: "Pr", z: 59, group: "lanthanide", needs: ["hydrogen", "radon"], of: "nostr-name and nostr-picture", does: "profile cards, chips, avatars, copy" },29+ { name: "europium", sym: "Eu", z: 63, group: "lanthanide", needs: ["hydrogen", "xenon"], of: "nostr-note", does: "note embeds" },30+ { name: "cerium", sym: "Ce", z: 58, group: "lanthanide", needs: [], of: "a theme", does: "the one stylesheet for the lanthanides" },31+ { name: "xenon", sym: "Xe", z: 54, group: "noble gas", needs: ["hydrogen"], of: "a nostr client", does: "subscribe, render and publish in HTML" },3232 ];3333 3434 // n + symbol; one-letter symbols add the next letter of the name that is not another element's symbol.
M scripts/site.ts +92 -112
@@ -1,137 +1,117 @@
1-// Builds the static site in _site/: every library, the bundle, the demos, and a landing page2-// with a bundle picker that writes jsDelivr combine URLs in the canonical order from libs.ts.1+// Builds the static site in _site/: a home page, one page per library (from its README),2+// the library files, the bundle, the demos, the license and llms.txt. Everything links on-site.33 // Run `deno task bundle` first so dist/ exists.44 import { brotliCompressSync } from "node:zlib";55 import { copy } from "jsr:@std/fs@1/copy";6+import { marked } from "npm:marked@15";67 import { groups, libs, prefix } from "./libs.ts";78 8-const REPO = "FelineStateMachine/nobel", SITE = "https://felinestatemachine.github.io/nobel/";9+const SITE = "https://felinestatemachine.github.io/nobel/";910 const root = new URL("../", import.meta.url), out = new URL("_site/", root);10-const esc = (s: string) => s.replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);1111 const kb = (n: number) => (n / 1024).toFixed(1);12+const read = (p: string) => Deno.readTextFile(new URL(p, root));1213 1314 await Deno.remove(out, { recursive: true }).catch(() => {});1415 await Deno.mkdir(out, { recursive: true });1516 for (const d of ["dist", "demo"]) await copy(new URL(d, root), new URL(d, out));17+await copy(new URL("LICENSE", root), new URL("LICENSE.txt", out));1618 17-let cells = "";18-for (const [group, about] of Object.entries(groups)) {19- const members = libs.filter((l) => l.group == group).sort((a, b) => a.z - b.z);20- cells += `<section class="g-${group.split(" ")[0]}"><h2>${group} <small>${about}</small></h2><div class="row">`;21- for (const l of members) {22- const src = await Deno.readFile(new URL(`${l.name}/${l.name}.js`, root));23- const tagline = (await Deno.readTextFile(new URL(`${l.name}/README.md`, root))).split("\n")[0].split(" - ").slice(1).join(" - ");24- await Deno.mkdir(new URL(`${l.name}/`, out), { recursive: true });25- await Deno.writeFile(new URL(`${l.name}/${l.name}.js`, out), src);26- cells += `<article><label class="pick"><input type="checkbox" value="${l.name}"${group == "base" ? " checked" : ""}> <span class="why"></span></label>27-<a class="doc" href="https://github.com/${REPO}/tree/main/${l.name}#readme"><span class="z">${l.z}</span><b class="sym">${l.sym}</b><span class="name">${l.name}</span><span class="tag">${esc(tagline)}</span></a>28-<span class="meta"><code>${prefix(l)}-</code> ${kb(src.length)}/${kb(brotliCompressSync(src).length)}kb</span>29-<a class="src" href="${l.name}/${l.name}.js">${l.name}.js</a></article>`;30- }31- cells += `</div></section>`;19+// Per library: emoji and tagline from its README heading, sizes from its file.20+const info: Record<string, { emoji: string; tagline: string; md: string; size: string }> = {};21+for (const l of libs) {22+ const md = await read(`${l.name}/README.md`), src = await Deno.readFile(new URL(`${l.name}/${l.name}.js`, root));23+ const [, emoji, tagline] = md.split("\n")[0].match(/^# (\S+) \S+ - (.*)$/)!;24+ info[l.name] = { emoji, tagline, md, size: `${kb(src.length)}/${kb(brotliCompressSync(src).length)}kb` };25+ await Deno.mkdir(new URL(`${l.name}/`, out), { recursive: true });26+ await Deno.writeFile(new URL(`${l.name}/${l.name}.js`, out), src);3227 }3328 34-const min = await Deno.readFile(new URL("dist/nobel.min.js", root));35-const data = JSON.stringify(libs.map((l) => [l.name, l.needs]));36-const html = `<!doctype html>29+// Pages are listed in table order: by group, then atomic number.30+const pages = Object.keys(groups).flatMap((g) => libs.filter((l) => l.group == g).sort((a, b) => a.z - b.z));31+32+const page = (title: string, body: string, prev?: string, next?: string) => `<!doctype html>3733 <html lang="en">3834 <head>3935 <meta charset="utf-8">4036 <meta name="viewport" content="width=device-width, initial-scale=1">41-<title>nobel: tiny nostr libraries for plain HTML</title>42-<meta name="description" content="A family of tiny, dependency-free nostr libraries for plain HTML, each named for an element.">37+<title>${title}</title>4338 <style>44- :root { --ink:#1d1b16; --paper:#f4efe4; --line:#d8cfbd; --muted:#7a7263; color-scheme:light dark; }45- @media (prefers-color-scheme:dark) { :root { --ink:#ece6d8; --paper:#171512; --line:#3a352c; --muted:#9c9384; } }46- * { box-sizing:border-box; }47- body { margin:0; background:var(--paper); color:var(--ink); font:15px/1.55 ui-serif, Georgia, serif; }48- main { max-width:1080px; margin:0 auto; padding:48px 20px 80px; }49- h1 { font:600 15px/1 ui-monospace, Menlo, monospace; letter-spacing:.14em; text-transform:uppercase; margin:0 0 10px; }50- .lede { max-width:62ch; margin:0 0 28px; font-size:17px; }51- h2 { font:600 12px/1 ui-monospace, Menlo, monospace; letter-spacing:.12em; text-transform:uppercase; margin:28px 0 10px; }52- h2 small { color:var(--muted); letter-spacing:0; text-transform:none; font-weight:400; margin-left:.5em; }53- .row { display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:8px; }54- article { border:1px solid var(--line); border-top:4px solid var(--g); border-radius:6px; padding:8px 12px 10px; display:flex; flex-direction:column; gap:6px; min-height:190px; }55- article:has(:checked) { background:color-mix(in srgb, var(--g) 9%, transparent); }56- .pick { font:11px ui-monospace, Menlo, monospace; color:var(--muted); display:flex; align-items:center; gap:4px; cursor:pointer; }57- .pick input { accent-color:var(--g); margin:0; }58- .doc { color:inherit; text-decoration:none; display:flex; flex-direction:column; flex:1; }59- .z { font:11px ui-monospace, Menlo, monospace; color:var(--muted); }60- .sym { font:600 34px/1.1 ui-serif, Georgia, serif; color:var(--g); }61- .name { font:600 13px ui-monospace, Menlo, monospace; }62- .tag { font-size:13px; color:var(--muted); }63- .meta, .src { font:11px ui-monospace, Menlo, monospace; color:var(--muted); }64- .src { color:var(--g); }65- .g-base { --g:#2f7d5b; } .g-alkali { --g:#b0682a; } .g-noble { --g:#6a4fb3; } .g-halogen { --g:#b0452a; } .g-transition { --g:#a17a12; } .g-lanthanide { --g:#2d6fa3; }66- .bundle { position:sticky; bottom:0; margin-top:28px; background:var(--paper); border:1px solid var(--line); border-radius:8px; padding:12px 14px; box-shadow:0 -6px 18px color-mix(in srgb, var(--ink) 8%, transparent); }67- .bundle h2 { margin:0 0 8px; }68- .out { display:grid; grid-template-columns:auto 1fr auto; gap:6px 10px; align-items:center; font:12px ui-monospace, Menlo, monospace; }69- .out code { overflow-x:auto; white-space:nowrap; padding:6px 8px; border:1px solid var(--line); border-radius:4px; }70- .out span { color:var(--muted); }71- button, input[type=text] { font:12px ui-monospace, Menlo, monospace; color:inherit; background:transparent; border:1px solid var(--line); border-radius:4px; padding:4px 8px; }72- button { cursor:pointer; }73- pre { background:color-mix(in srgb, var(--ink) 6%, transparent); border:1px solid var(--line); border-radius:6px; padding:12px 14px; overflow:auto; font:13px/1.5 ui-monospace, Menlo, monospace; }74- a { color:inherit; }75- footer { margin-top:40px; color:var(--muted); font-size:13px; }39+ :root { color-scheme:light dark; }40+ body { max-width:46rem; margin:0 auto; padding:1rem; font:16px/1.5 system-ui, sans-serif; }41+ nav { font-size:.9rem; line-height:1.9; }42+ nav a { white-space:nowrap; margin-right:.6rem; text-decoration:none; }43+ table { border-collapse:collapse; width:100%; font-size:.95rem; }44+ th, td { text-align:left; padding:.25rem .5rem; border-bottom:1px solid #8884; vertical-align:top; }45+ pre { background:#8881; padding:.6rem .8rem; overflow-x:auto; }46+ code { font-size:.9em; }47+ td:first-child code { white-space:nowrap; }48+ .pager { display:flex; justify-content:space-between; margin:2rem 0 1rem; }7649 </style>7750 </head>7851 <body>79-<main>80- <h1>nobel</h1>81- <p class="lede">Tiny nostr libraries for plain HTML, in the spirit of <a href="https://fixiproject.org">the fixi project</a>. Each one is a single dependency-free script under 5kb, named for an element, and they compose through DOM events. Tick the ones you need: the picker adds what they depend on and gives you one file in a safe order.</p>82- <pre>&lt;script src="https://cdn.jsdelivr.net/combine/gh/${REPO}@main/hydrogen/hydrogen.min.js,gh/${REPO}@main/xenon/xenon.min.js"&gt;&lt;/script&gt;83-84-&lt;ol nhy-relay="wss://relay.example" nxe-filter='{"kinds":[1],"limit":20}'&gt;85- &lt;template&gt;&lt;li&gt;{content}&lt;/li&gt;&lt;/template&gt;86-&lt;/ol&gt;</pre>87- ${cells}88- <section class="bundle" aria-live="polite">89- <h2>your bundle <small id="summary"></small></h2>90- <div class="out">91- <span>version</span><input type="text" id="ver" value="main" size="12" aria-label="version: main, a tag or a commit"><span></span>92- <span>script</span><code id="tag"></code><button data-copy="tag">copy</button>93- <span>url</span><code id="url"></code><button data-copy="url">copy</button>94- <span>vendor</span><code id="curl"></code><button data-copy="curl">copy</button>95- </div>96- </section>97- <h2>all of them <small>one file, ${kb(min.length)}kb minified, ${kb(brotliCompressSync(min).length)}kb brotli</small></h2>98- <pre>&lt;script src="${SITE}dist/nobel.min.js"&gt;&lt;/script&gt;</pre>99- <p>Plain script tags work in any order. Use one file (a bundle above, or this one) when you load with <code>async</code> or inject scripts after the page has loaded. Pin a tag or commit for production: <code>main</code> on jsDelivr can lag by up to a day.</p>100- <h2>demos <small>add ?relay=wss://... to each</small></h2>101- <p><a href="demo/">feed</a>, <a href="demo/iodine.html">private messages</a>, <a href="demo/oganesson.html">presence, typing, cursors</a>, <a href="demo/lanthanides.html">profiles and theme</a>, <a href="demo/europium.html">note embeds</a></p>102- <footer>Always the latest <code>main</code>. <a href="https://github.com/${REPO}">Source on GitHub</a>, released into the public domain (Unlicense).</footer>103-</main>104-<script>105- let libs = ${data}, boxes = [...document.querySelectorAll(".pick input")], $ = (id)=>document.getElementById(id)106- let update = ()=>{107- let chosen = new Set(boxes.filter((b)=>b.checked && !b.dataset.auto).map((b)=>b.value)), need = {}108- let add = (n, by)=>libs.find((l)=>l[0] == n)[1].forEach((d)=>{ (need[d] ??= new Set()).add(by); add(d, by) })109- chosen.forEach((n)=>add(n, n))110- for (let b of boxes){111- let by = need[b.value] && !chosen.has(b.value)112- b.dataset.auto = by ? 1 : ""113- b.checked = chosen.has(b.value) || !!by114- b.nextElementSibling.textContent = by ? "needed by " + [...need[b.value]].join(", ") : ""115- }116- let names = libs.map((l)=>l[0]).filter((n)=>chosen.has(n) || need[n]), v = $("ver").value.trim() || "main"117- let file = (n, m)=>"gh/${REPO}@" + v + "/" + n + "/" + n + (m ? ".min.js" : ".js")118- let url = "https://cdn.jsdelivr.net/combine/" + names.map((n)=>file(n, 1)).join(",")119- $("summary").textContent = names.length ? names.join(", ") : "nothing picked yet"120- $("tag").textContent = names.length ? '<script src="' + url + '"></' + 'script>' : ""121- $("url").textContent = names.length ? url : ""122- $("curl").textContent = names.length ? 'curl -o nobel.js "https://cdn.jsdelivr.net/combine/' + names.map((n)=>file(n)).join(",") + '"' : ""123- }124- boxes.forEach((b)=>b.addEventListener("change", ()=>{ if (b.checked) b.dataset.auto = ""; update() }))125- $("ver").addEventListener("input", update)126- document.querySelectorAll("[data-copy]").forEach((b)=>b.addEventListener("click", async()=>{127- await navigator.clipboard.writeText($(b.dataset.copy).textContent)128- b.textContent = "copied"129- setTimeout(()=>b.textContent = "copy", 1200)130- }))131- update()132-</script>52+<nav><a href="./">⚗️ nobel</a> ${pages.map((l) => `<a href="${l.name}.html">${info[l.name].emoji} ${l.name}</a>`).join(" ")} <a href="llms.txt">🤖 llms.txt</a></nav>53+<hr>54+${body}55+<div class="pager"><span>${prev ?? ""}</span><span>${next ?? ""}</span></div>13356 </body>13457 </html>13558 `;136-await Deno.writeTextFile(new URL("index.html", out), html);137-console.log(`_site/ built: ${libs.length} libraries`);59+const link = (l?: typeof libs[0], dir = "") =>60+ l ? (dir == "prev" ? `&larr; ${info[l.name].emoji} <a href="${l.name}.html">${l.name}</a>` : `${info[l.name].emoji} <a href="${l.name}.html">${l.name}</a> &rarr;`) : `<a href="./">🏠 home</a>`;61+62+// One page per library.63+for (const [i, l] of pages.entries()) {64+ const html = await marked.parse(info[l.name].md);65+ const src = `<p><a href="${l.name}/${l.name}.js">${l.name}.js</a> (${info[l.name].size} raw/brotli), prefix <code>${prefix(l)}-</code>${l.needs.length ? `, needs ${l.needs.map((n) => `<a href="${n}.html">${n}</a>`).join(", ")}` : ""}</p>`;66+ await Deno.writeTextFile(new URL(`${l.name}.html`, out), page(`${l.name}.js - nobel`, html + src, link(pages[i - 1], "prev"), link(pages[i + 1])));67+}68+69+// Home.70+let rows = "";71+for (const [g, about] of Object.entries(groups)) {72+ rows += `<tr><th colspan="5">${g} <small>(${about})</small></th></tr>\n`;73+ for (const l of pages.filter((l) => l.group == g)) {74+ rows += `<tr><td><input type="checkbox" value="${l.name}"${g == "base" ? " checked" : ""}></td><td>${info[l.name].emoji} <a href="${l.name}.html">${l.name}</a></td><td><a href="${l.name}/${l.name}.js">${info[l.name].size}</a></td><td>${l.of}</td><td>${l.does}</td></tr>\n`;75+ }76+}77+const min = await Deno.readFile(new URL("dist/nobel.min.js", root));78+const order = JSON.stringify(libs.map((l) => [l.name, l.needs]));79+const home = `<h1>⚗️ nobel</h1>80+<p>nobel is a family of small nostr libraries for plain HTML, each a lightweight take on something bigger and each named for an element. Every library is one dependency-free script under 5kb, and they work together through DOM events, the way the <a href="https://fixiproject.org">fixi project</a> libraries do.</p>81+<table>82+<tr><th></th><th>library</th><th>size (raw/br)</th><th>miniature of</th><th>provides</th></tr>83+${rows}</table>84+<h2>Installing</h2>85+<p>Plain <code>&lt;script&gt;</code> tags work in any order. Tick what you need above (what it needs is added) for one file from jsDelivr, in an order that also works with <code>async</code>:</p>86+<pre id="out"></pre>87+<p>Or download the files from the table, or take all of them in one file: <a href="dist/nobel.js">nobel.js</a>, <a href="dist/nobel.min.js">nobel.min.js</a> (${kb(min.length)}kb, ${kb(brotliCompressSync(min).length)}kb brotli).</p>88+<h2>Demos</h2>89+<p>Add <code>?relay=wss://...</code> to each: 📰 <a href="demo/">feed</a>, ✉️ <a href="demo/iodine.html">private messages</a>, 👻 <a href="demo/oganesson.html">presence, typing, cursors</a>, 🪞 <a href="demo/lanthanides.html">profiles</a>, 🔴 <a href="demo/europium.html">note embeds</a>.</p>90+<h2>License</h2>91+<p>Public domain: <a href="LICENSE.txt">the Unlicense</a>.</p>92+<script>93+ let libs = ${order}, boxes = [...document.querySelectorAll("input[type=checkbox]")]94+ let show = ()=>{95+ let want = new Set(boxes.filter((b)=>b.checked).map((b)=>b.value)), add = (n)=>libs.find((l)=>l[0] == n)[1].forEach((d)=>(want.add(d), add(d)))96+ want.forEach(add)97+ boxes.forEach((b)=>b.checked = want.has(b.value))98+ let files = libs.filter((l)=>want.has(l[0])).map((l)=>"gh/FelineStateMachine/nobel@main/" + l[0] + "/" + l[0] + ".min.js")99+ document.getElementById("out").textContent = files.length ? '<script src="https://cdn.jsdelivr.net/combine/' + files.join(",") + '"></' + 'script>' : ""100+ }101+ boxes.forEach((b)=>b.addEventListener("change", show))102+ show()103+</script>`;104+await Deno.writeTextFile(new URL("index.html", out), page("nobel: small nostr libraries for plain HTML", home, "", link(pages[0])));105+106+// llms.txt, as fixi has.107+await Deno.writeTextFile(new URL("llms.txt", out), `# nobel108+109+> A family of small, dependency-free nostr libraries for plain HTML, each named for an element. Each library is one script under 5kb; they compose through DOM events.110+111+${Object.entries(groups).map(([g, about]) => `## ${g} (${about})\n\n` + pages.filter((l) => l.group == g).map((l) => `- [${l.name}](${SITE}${l.name}.html): ${l.does}. Source: ${SITE}${l.name}/${l.name}.js`).join("\n")).join("\n\n")}112+113+## All in one114+115+- ${SITE}dist/nobel.js116+`);117+console.log(`_site/ built: home, ${pages.length} library pages, llms.txt`);