helwasm: footer version badge + changelog overlay
This commit is contained in:
@@ -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"))
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user