clone

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

Add a bundle picker and document how to load nobel

Dami committed f5cbb8f
The site's picker adds each library's dependencies and writes a jsDelivr
combine URL in the canonical order. Plain script tags work in any order;
async or late-loaded pages should use one file.
commit
f5cbb8fcf76c23080626cba088e193c13552ab94
parent
961372d6e3af2ba7a51de91123092505e3c7a66a
author
Dami <dami.etoile@gmail.com>
2 files changed, +92 -49
MREADME.md35+++++-----
Mscripts/site.ts106+++++++++++++++++++++---------
M README.md +19 -16
@@ -4,15 +4,15 @@ nobel is a family of small nostr libraries for plain HTML, each a lightweight ta
44 55 | group | library | size (raw/br) | miniature of | provides | prefix |66 |---|---|---|---|---|---|7-| nonmetal | ๐Ÿ’ง [hydrogen](hydrogen/) | 2.8/1.2kb | a relay pool | relays, signing and the family's hooks | `nhy-` |7+| period 1 (base) | ๐Ÿ’ง [hydrogen](hydrogen/) | 2.8/1.2kb | a relay pool | relays, signing and the family's hooks | `nhy-` |8+| period 1 (base) | ๐Ÿ”‘ [helium](helium/) | 3.8/1.6kb | @noble/secp256k1 | keys, Schnorr signing and verification | `nhe-` |9+| alkali metal | ๐Ÿ”‹ [lithium](lithium/) | 5.0/2.0kb | Negentropy | NIP-77 sync between argon and relays | `nli-` |810 | noble gas | ๐Ÿ›ฐ๏ธ [xenon](xenon/) | 3.1/1.2kb | a nostr client | subscribe, render and publish in HTML | `nxe-` |9-| noble gas | ๐Ÿ”‘ [krypton](krypton/) | 3.8/1.6kb | @noble/secp256k1 | keys, Schnorr signing and verification | `nkr-` |1011 | noble gas | ๐Ÿ“ฆ [argon](argon/) | 2.5/1.0kb | a relay | IndexedDB cache, offline-first feeds | `nar-` |11-| noble gas | ๐ŸŽˆ [helium](helium/) | 5.0/2.0kb | Negentropy | NIP-77 sync between argon and relays | `nhe-` |1212 | noble gas | ๐Ÿ”— [neon](neon/) | 3.9/1.8kb | nip19 | npub/nevent/naddr, linked content, times | `nne-` |1313 | noble gas | ๐Ÿชช [radon](radon/) | 3.1/1.3kb | a profile loader | names, pictures, NIP-05 | `nrn-` |14-| noble gas | ๐Ÿ‘ป [oganesson](oganesson/) | 4.7/1.8kb | a chat app's live layer | presence, typing, cursors (ephemeral) | `nog-` |15-| halogen | ๐Ÿงช [fluorine](fluorine/) | 3.4/1.4kb | NIP-44 | encryption for krypton's keys | `nfl-` |14+| noble gas | ๐Ÿ‘ป [oganesson](oganesson/) | 4.8/1.9kb | a chat app's live layer | presence, typing, cursors (ephemeral) | `nog-` |15+| halogen | ๐Ÿงช [fluorine](fluorine/) | 3.4/1.4kb | NIP-44 | encryption for helium's keys | `nfl-` |1616 | halogen | ๐Ÿ”Œ [chlorine](chlorine/) | 4.6/1.8kb | a NIP-46 client | remote signing behind a NIP-07 face | `ncl-` |1717 | halogen | โœ‰๏ธ [iodine](iodine/) | 3.9/1.6kb | an encrypted messenger | NIP-17 private messages | `nio-` |1818 | transition metal | ๐Ÿฅ‡ [gold](gold/) | 2.2/1.0kb | a NIP-42 client | relay AUTH for members-only relays | `nau-` |
@@ -21,15 +21,15 @@ nobel is a family of small nostr libraries for plain HTML, each a lightweight ta
2121 | lanthanide | ๐Ÿ”ด [europium](europium/) | 2.7/1.1kb | fiatjaf's nostr-note | note embeds | `neu-` |2222 | lanthanide | ๐ŸŽจ [cerium](cerium/) | 2.8/0.9kb | a theme | the one stylesheet for the lanthanides | `nce-` |2323 24-hydrogen is the base: relays, signatures and three hooks (`nhy:config`, `nhy:event`, `nhy:message`) that every other library plugs into. The noble gases are the core, inert pieces for data and transport. The halogens handle secrets and trust. The transition metals do signed exchanges outside plain subscriptions. The lanthanides are visual presets: they only write markup you could have written by hand from the other libraries' attributes, in the light DOM, and leave anything you wrote alone. cerium holds all of their styling, in one zero-specificity layer you can override or drop.24+Period 1 is the base, and it has exactly two elements: hydrogen moves events (relays, signatures and the three hooks `nhy:config`, `nhy:event`, `nhy:message` that every other library plugs into) and helium makes them trustworthy (keys, signing, verification). Neither needs the other; everything else builds on them. The alkali metals charge and flow: lithium syncs argon's store with relays. The noble gases are the core pieces for data and transport. The halogens handle secrets and trust. The transition metals do signed exchanges outside plain subscriptions. The lanthanides are visual presets: they only write markup you could have written by hand from the other libraries' attributes, in the light DOM, and leave anything you wrote alone. cerium holds all of their styling, in one zero-specificity layer you can override or drop.2525 2626 ```html27+<script src="hydrogen.js"></script>28+<script src="helium.js"></script>2729 <script src="neon.js"></script>28-<script src="krypton.js"></script>2930 <script src="argon.js"></script>30-<script src="hydrogen.js"></script>31-<script src="xenon.js"></script>3231 <script src="radon.js"></script>32+<script src="xenon.js"></script>3333 3434 <form nhy-relay="wss://relay.example" nxe-publish>3535 <textarea name="content"></textarea><button>Post</button>
@@ -43,18 +43,21 @@ hydrogen is the base: relays, signatures and three hooks (`nhy:config`, `nhy:eve
4343 4444 That page posts and reads notes, signs with a NIP-07 extension or a local key, shows names and linked content, and opens instantly from cache on reload.4545 46-Load hydrogen before the libraries that use it, and the peers before xenon so their listeners are in place when it starts. `deno task bundle` writes all of them, in a working order, to `dist/nobel.js` and `dist/nobel.min.js` (42kb, 15kb brotli).47-4846 ## Use it4947 50-Every library is a plain script. Take them from jsDelivr, pinned to a commit or following `main`:48+Plain `<script>` tags work in **any order**: every library starts on `DOMContentLoaded` and looks its peers up only when it uses them. Load with `async`, or inject scripts after the page has loaded, and order starts to matter; use one file instead.49+50+**One file, just what you need.** The [site](https://felinestatemachine.github.io/nobel/) has a picker: tick libraries, it adds their dependencies and gives you a jsDelivr `combine` URL in the canonical order from [scripts/libs.ts](scripts/libs.ts) (base first, listeners next, xenon last), minified by jsDelivr:5151 5252 ```html53-<script src="https://cdn.jsdelivr.net/gh/FelineStateMachine/nobel@main/hydrogen/hydrogen.js"></script>54-<script src="https://cdn.jsdelivr.net/gh/FelineStateMachine/nobel@main/xenon/xenon.js"></script>53+<script src="https://cdn.jsdelivr.net/combine/gh/FelineStateMachine/nobel@main/hydrogen/hydrogen.min.js,gh/FelineStateMachine/nobel@main/xenon/xenon.min.js"></script>5554 ```5655 57-Or copy the files into your project: each is one file with no dependencies. The site at [felinestatemachine.github.io/nobel](https://felinestatemachine.github.io/nobel/) always serves the latest `main`, including the bundle `dist/nobel.min.js` and the demos.56+**One file, everything.** `https://felinestatemachine.github.io/nobel/dist/nobel.min.js` (42kb, 15kb brotli), rebuilt on every push to `main`.57+58+**Separate files.** `https://cdn.jsdelivr.net/gh/FelineStateMachine/nobel@main/xenon/xenon.js`, or copy them into your project: each is one file with no dependencies.59+60+Replace `@main` with a tag or commit to pin a version; jsDelivr can serve a branch up to a day stale.5861 5962 ## Naming6063
@@ -74,7 +77,7 @@ Demos: `/demo/` (feed), `/demo/iodine.html` (private messages), `/demo/oganesson
7477 7578 Some tests reach beyond the repo, and skip themselves when their dependency is missing:7679 77-- helium reconciles against bindws's Negentropy when `../bindws` is checked out. `NOBEL_RELAYS="wss://a ws://b" deno test -A test/live.test.js` syncs against real relays.80+- lithium reconciles against bindws's Negentropy when `../bindws` is checked out. `NOBEL_RELAYS="wss://a ws://b" deno test -A test/live.test.js` syncs against real relays.7881 - chlorine runs against a real `nak bunker` when `nak` is on the PATH.7982 - gold creates members-only tenants on a local tinyrelay container named `noble-tiny`.8083 - mercury checks NIP-86 and Blossom on tinyrelay when `NHG_TINY` and `NHG_KEY` are set.
M scripts/site.ts +73 -33
@@ -1,17 +1,11 @@
1-// Builds the static site in _site/: every library, the bundle, the demos and a landing page.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.23 // Run `deno task bundle` first so dist/ exists.34 import { brotliCompressSync } from "node:zlib";45 import { copy } from "jsr:@std/fs@1/copy";6+import { groups, libs, prefix } from "./libs.ts";57 6-const REPO = "FelineStateMachine/nobel";7-const groups: [string, string, [string, string, number][]][] = [8- ["nonmetal", "the base: relays, signatures, hooks", [["hydrogen", "H", 1]]],9- ["noble gases", "the core: data and transport", [["xenon", "Xe", 54], ["krypton", "Kr", 36], ["argon", "Ar", 18], ["helium", "He", 2], ["neon", "Ne", 10], ["radon", "Rn", 86], ["oganesson", "Og", 118]]],10- ["halogens", "secrets and trust", [["fluorine", "F", 9], ["chlorine", "Cl", 17], ["iodine", "I", 53]]],11- ["transition metals", "signed exchanges", [["gold", "Au", 79], ["mercury", "Hg", 80]]],12- ["lanthanides", "visual presets", [["praseodymium", "Pr", 59], ["europium", "Eu", 63], ["cerium", "Ce", 58]]],13-];14-const prefix: Record<string, string> = { hydrogen: "nhy", fluorine: "nfl", iodine: "nio" };8+const REPO = "FelineStateMachine/nobel", SITE = "https://felinestatemachine.github.io/nobel/";159 const root = new URL("../", import.meta.url), out = new URL("_site/", root);1610 const esc = (s: string) => s.replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);1711 const kb = (n: number) => (n / 1024).toFixed(1);
@@ -21,25 +15,24 @@ await Deno.mkdir(out, { recursive: true });
2115 for (const d of ["dist", "demo"]) await copy(new URL(d, root), new URL(d, out));2216 2317 let cells = "";24-for (const [group, about, libs] of groups) {25- cells += `<section class="${group.split(" ")[0]}"><h2>${group} <small>${about}</small></h2><div class="row">`;26- for (const [name, sym, z] of libs) {27- const src = await Deno.readFile(new URL(`${name}/${name}.js`, root));28- const head = (await Deno.readTextFile(new URL(`${name}/README.md`, root))).split("\n")[0];29- const tagline = head.split(" - ").slice(1).join(" - ");30- await Deno.mkdir(new URL(`${name}/`, out), { recursive: true });31- await Deno.writeFile(new URL(`${name}/${name}.js`, out), src);32- const p = prefix[name] ?? `n${sym.toLowerCase()}`;33- cells += `<article><a class="doc" href="https://github.com/${REPO}/tree/main/${name}#readme">34-<span class="z">${z}</span><b class="sym">${sym}</b><span class="name">${name}</span><span class="tag">${esc(tagline)}</span></a>35-<span class="meta"><code>${p}-</code> ${kb(src.length)}/${kb(brotliCompressSync(src).length)}kb</span>36-<a class="src" href="${name}/${name}.js">${name}.js</a></article>`;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>`;3730 }3831 cells += `</div></section>`;3932 }4033 4134 const min = await Deno.readFile(new URL("dist/nobel.min.js", root));42-const cdn = `https://cdn.jsdelivr.net/gh/${REPO}@main`, site = "https://felinestatemachine.github.io/nobel/";35+const data = JSON.stringify(libs.map((l) => [l.name, l.needs]));4336 const html = `<!doctype html>4437 <html lang="en">4538 <head>
@@ -54,11 +47,14 @@ const html = `<!doctype html>
5447 body { margin:0; background:var(--paper); color:var(--ink); font:15px/1.55 ui-serif, Georgia, serif; }5548 main { max-width:1080px; margin:0 auto; padding:48px 20px 80px; }5649 h1 { font:600 15px/1 ui-monospace, Menlo, monospace; letter-spacing:.14em; text-transform:uppercase; margin:0 0 10px; }57- .lede { max-width:62ch; margin:0 0 32px; font-size:17px; }50+ .lede { max-width:62ch; margin:0 0 28px; font-size:17px; }5851 h2 { font:600 12px/1 ui-monospace, Menlo, monospace; letter-spacing:.12em; text-transform:uppercase; margin:28px 0 10px; }5952 h2 small { color:var(--muted); letter-spacing:0; text-transform:none; font-weight:400; margin-left:.5em; }6053 .row { display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:8px; }61- article { border:1px solid var(--line); border-top:4px solid var(--g); border-radius:6px; padding:10px 12px; display:flex; flex-direction:column; gap:6px; min-height:170px; }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; }6258 .doc { color:inherit; text-decoration:none; display:flex; flex-direction:column; flex:1; }6359 .z { font:11px ui-monospace, Menlo, monospace; color:var(--muted); }6460 .sym { font:600 34px/1.1 ui-serif, Georgia, serif; color:var(--g); }
@@ -66,7 +62,14 @@ const html = `<!doctype html>
6662 .tag { font-size:13px; color:var(--muted); }6763 .meta, .src { font:11px ui-monospace, Menlo, monospace; color:var(--muted); }6864 .src { color:var(--g); }69- .nonmetal { --g:#2f7d5b; } .noble { --g:#6a4fb3; } .halogens { --g:#b0452a; } .transition { --g:#a17a12; } .lanthanides { --g:#2d6fa3; }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; }7073 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; }7174 a { color:inherit; }7275 footer { margin-top:40px; color:var(--muted); font-size:13px; }
@@ -75,23 +78,60 @@ const html = `<!doctype html>
7578 <body>7679 <main>7780 <h1>nobel</h1>78- <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. Pick the ones you need, or take them all.</p>79- <pre>&lt;script src="${cdn}/hydrogen/hydrogen.js"&gt;&lt;/script&gt;80-&lt;script src="${cdn}/xenon/xenon.js"&gt;&lt;/script&gt;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;8183 8284 &lt;ol nhy-relay="wss://relay.example" nxe-filter='{"kinds":[1],"limit":20}'&gt;8385 &lt;template&gt;&lt;li&gt;{content}&lt;/li&gt;&lt;/template&gt;8486 &lt;/ol&gt;</pre>8587 ${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>8697 <h2>all of them <small>one file, ${kb(min.length)}kb minified, ${kb(brotliCompressSync(min).length)}kb brotli</small></h2>87- <pre>&lt;script src="${site}dist/nobel.min.js"&gt;&lt;/script&gt;</pre>88- <p>Or from this site: <a href="dist/nobel.js">dist/nobel.js</a>, <a href="dist/nobel.min.js">dist/nobel.min.js</a>. Load hydrogen before the libraries that use it, and the peers before xenon.</p>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>89100 <h2>demos <small>add ?relay=wss://... to each</small></h2>90101 <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>91102 <footer>Always the latest <code>main</code>. <a href="https://github.com/${REPO}">Source on GitHub</a>, released into the public domain (Unlicense).</footer>92103 </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>93133 </body>94134 </html>95135 `;96136 await Deno.writeTextFile(new URL("index.html", out), html);97-console.log(`_site/ built: ${groups.flatMap((g) => g[2]).length} libraries`);137+console.log(`_site/ built: ${libs.length} libraries`);