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). 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. 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. 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> </footer>
<div class="toast" id="toast"></div> <div class="toast" id="toast"></div>
@@ -199,6 +200,18 @@
</div> </div>
</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"> <script type="module">
import init, { hel_init, hel_command, hel_load_script, hel_parse, hel_parse_name, hel_entry } from "./pkg/helwasm.js"; 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 masterEl = () => document.getElementById("master");
const CATALOG_KEY = "hel_catalog"; 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 // When launched as an installed app (home-screen / standalone), hide the
// "back to Kaizenkodo" link — leaving the app context would feel wrong there. // "back to Kaizenkodo" link — leaving the app context would feel wrong there.
// navigator.standalone covers older iOS; the media query covers the rest. // 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>"); 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 ---- // ---- boot ----
async function boot() { async function boot() {
await init(); await init();
@@ -598,6 +685,18 @@
}; };
$("#exportClose").onclick = () => $("#exportModal").classList.remove("show"); $("#exportClose").onclick = () => $("#exportModal").classList.remove("show");
$("#exportCopy").onclick = () => copyText($("#exportText").value); $("#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 // click backdrop closes modals
document.querySelectorAll(".modal-bg").forEach((bg) => document.querySelectorAll(".modal-bg").forEach((bg) =>
bg.addEventListener("click", (e) => e.target === bg && bg.classList.remove("show")) 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 { 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 { color: var(--ocean); text-decoration: none; border-bottom: 1px solid rgba(27, 65, 97, 0.3); }
.foot a:hover { border-bottom-color: var(--ocean); } .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) { @media (max-width: 680px) {
.fields { grid-template-columns: 1fr; align-items: stretch; } .fields { grid-template-columns: 1fr; align-items: stretch; }