helwasm: footer version badge + changelog overlay

This commit is contained in:
2026-06-10 14:46:14 +02:00
parent db05834fd1
commit a31ae9669f
2 changed files with 145 additions and 0 deletions
+99
View File
@@ -153,6 +153,7 @@
A free tool by <a href="https://kaizenkodo.no" target="_blank" rel="noopener">Kaizenkodo</a> (kaizenkodo.no).
Runs the real hel core compiled to WebAssembly, the same generator as the CLI.
Everything runs in your browser; nothing you type is ever sent to a server.
<button class="ver" id="verBtn" type="button" title="What's new — changelog"></button>
</footer>
<div class="toast" id="toast"></div>
@@ -199,6 +200,18 @@
</div>
</div>
<!-- Changelog overlay (opened from the footer version badge) -->
<div class="modal-bg" id="changelogModal">
<div class="modal">
<h3>What's new</h3>
<p class="hint">Recent changes to LesS/KEY, newest first.</p>
<div class="changelog" id="changelogBody"></div>
<div class="row">
<button class="btn ghost" id="changelogClose">Close</button>
</div>
</div>
</div>
<script type="module">
import init, { hel_init, hel_command, hel_load_script, hel_parse, hel_parse_name, hel_entry } from "./pkg/helwasm.js";
@@ -206,6 +219,28 @@
const masterEl = () => document.getElementById("master");
const CATALOG_KEY = "hel_catalog";
// Single source of truth for the displayed version AND the changelog dialog.
// Newest first: CHANGELOG[0].version is the footer badge and gets the "Latest"
// flag. To cut a release, bump the top version and prepend an entry. Each change
// is { tag: "New" | "Improved", text }; `text` may hold inline <code> for command
// names (authored/trusted content, rendered as HTML). `title` = the release theme.
const CHANGELOG = [
{ version: "1.0.0", date: "2026-06-10", title: "Keyboard-first, and installable", changes: [
{ tag: "New", text: "Press Enter in the name field to jump to the master; Enter in the master generates and copies." },
{ tag: "New", text: "Install LesS/KEY to your home screen and run it as a standalone app." },
{ tag: "Improved", text: "Fresh ensō + key icon; the result shrinks to stay on one line on narrow phones." },
] },
{ version: "0.9.0", date: "2026-06-09", title: "A smarter catalog", changes: [
{ tag: "New", text: "Quick password follows <code>^parent</code> chains, all computed from your one master." },
{ tag: "New", text: "<code>pb</code> copies a command's output to the clipboard; help now lists every command." },
{ tag: "Improved", text: "One progressive Store → Correct button; verified results show in colour." },
] },
{ version: "0.8.0", date: "2026-06-08", title: "Public launch", changes: [
{ tag: "New", text: "Runs entirely in your browser via WebAssembly — nothing you type is ever sent to a server." },
{ tag: "New", text: "Self-hosted fonts, installable and mobile-friendly; a bare name makes six memorable S/KEY words by default." },
] },
];
// When launched as an installed app (home-screen / standalone), hide the
// "back to Kaizenkodo" link — leaving the app context would feel wrong there.
// navigator.standalone covers older iOS; the media query covers the rest.
@@ -477,6 +512,58 @@
appendLine('<span class="muted"># password cached for ' + esc(name) + "</span>");
}
// ---- changelog overlay ----
// Render once from CHANGELOG (newest first). Each entry: a head with the
// version + date, then a bullet list of changes. The <code> in a change is
// authored, so innerHTML is safe here (no user input ever reaches this).
function renderChangelog() {
const body = $("#changelogBody");
if (!body || body.childElementCount) return; // build once
CHANGELOG.forEach((rel, i) => {
const entry = document.createElement("div");
entry.className = "cl-entry";
const head = document.createElement("div");
head.className = "cl-head";
const ver = document.createElement("span");
ver.className = "cl-ver";
ver.textContent = "v" + rel.version;
const date = document.createElement("span");
date.className = "cl-date";
date.textContent = rel.date;
head.append(ver, date);
if (i === 0) { // flag the newest release
const latest = document.createElement("span");
latest.className = "cl-latest";
latest.textContent = "Latest";
head.appendChild(latest);
}
entry.appendChild(head);
if (rel.title) {
const title = document.createElement("div");
title.className = "cl-title";
title.textContent = rel.title;
entry.appendChild(title);
}
const ul = document.createElement("ul");
for (const c of rel.changes) {
const li = document.createElement("li");
const tag = document.createElement("span");
tag.className = "cl-tag cl-tag-" + c.tag.toLowerCase();
tag.textContent = c.tag;
const text = document.createElement("span");
text.className = "cl-text";
text.innerHTML = c.text;
li.append(tag, text);
ul.appendChild(li);
}
entry.appendChild(ul);
body.appendChild(entry);
});
}
// ---- boot ----
async function boot() {
await init();
@@ -598,6 +685,18 @@
};
$("#exportClose").onclick = () => $("#exportModal").classList.remove("show");
$("#exportCopy").onclick = () => copyText($("#exportText").value);
// changelog overlay: footer badge shows the top version and opens the dialog
const changelog = $("#changelogModal");
$("#verBtn").textContent = "v" + CHANGELOG[0].version;
renderChangelog();
$("#verBtn").onclick = () => changelog.classList.add("show");
$("#changelogClose").onclick = () => changelog.classList.remove("show");
// Esc closes the changelog (scoped — leaves the other modals' behaviour alone).
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && changelog.classList.contains("show")) changelog.classList.remove("show");
});
// click backdrop closes modals
document.querySelectorAll(".modal-bg").forEach((bg) =>
bg.addEventListener("click", (e) => e.target === bg && bg.classList.remove("show"))
+46
View File
@@ -281,6 +281,52 @@ input.mask.revealed { -webkit-text-security: none; }
.foot { color: var(--muted); font-family: var(--mono); font-size: 12px; text-align: center; padding-block: 8px 40px; }
.foot a { color: var(--ocean); text-decoration: none; border-bottom: 1px solid rgba(27, 65, 97, 0.3); }
.foot a:hover { border-bottom-color: var(--ocean); }
/* Version badge: a quiet mono pill under the footer prose (mirrors .backlink).
Decent and out of the way; click opens the changelog overlay. */
.ver {
display: inline-block; margin-top: 10px;
font-family: var(--mono); font-size: 11px; letter-spacing: 0.04em;
color: var(--muted); background: transparent;
border: 1px solid var(--line); border-radius: 999px; padding: 3px 11px;
cursor: pointer; transition: color .15s, border-color .15s, background .15s;
}
.ver:hover { color: var(--ocean); border-color: var(--ocean); background: rgba(27, 65, 97, 0.05); }
.ver:focus-visible { outline: none; border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(27, 65, 97, 0.12); }
/* Changelog overlay body (inside the shared .modal). Scrolls if it ever grows. */
.changelog { max-height: 60vh; overflow: auto; margin-top: 4px; }
.cl-entry { padding: 16px 0; border-top: 1px solid var(--line); }
.cl-entry:first-child { border-top: none; padding-top: 2px; }
.cl-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 3px; }
.cl-ver { font-family: var(--disp); font-weight: 700; font-size: 16px; color: var(--ocean); letter-spacing: -0.01em; }
.cl-date { font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em; color: var(--muted); }
/* "Latest" flag, pushed to the right edge of the newest release's head. */
.cl-latest {
margin-left: auto; flex: none; align-self: center;
font-family: var(--mono); font-size: 9.5px; font-weight: 500;
text-transform: uppercase; letter-spacing: 0.1em;
color: #fff; background: var(--seal); border-radius: 999px; padding: 2px 9px;
}
/* Release theme line under the version. */
.cl-title { font-family: var(--disp); font-weight: 600; font-size: 14px; color: var(--ink); margin: 0 0 12px; }
.changelog ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 9px; }
.changelog li { display: flex; align-items: baseline; gap: 12px; color: var(--ink); font-size: 14px; line-height: 1.5; }
/* Category tag in a fixed-width column, so the change text shares one clean left edge. */
.cl-tag {
flex: none; width: 68px; text-align: center; box-sizing: border-box;
font-family: var(--mono); font-size: 9.5px; font-weight: 500;
text-transform: uppercase; letter-spacing: 0.08em;
padding: 3px 0; border-radius: 6px; transform: translateY(-1px);
}
.cl-tag-new { color: var(--ocean); background: rgba(27, 65, 97, 0.10); }
.cl-tag-improved { color: var(--muted); background: rgba(106, 103, 94, 0.13); }
.cl-text { min-width: 0; }
.changelog code { font-family: var(--mono); font-size: 12px; color: var(--ocean-2); background: rgba(27,65,97,.06); padding: 1px 5px; border-radius: 5px; }
/* On phones the fixed tag column would crowd the text — let each item stack. */
@media (max-width: 540px) {
.changelog li { flex-wrap: wrap; gap: 5px 10px; }
.cl-tag { width: auto; padding: 3px 9px; }
}
@media (max-width: 680px) {
.fields { grid-template-columns: 1fr; align-items: stretch; }