From 3731f1eedc51c851def38e052c0d7ccdf28389b7 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:20:30 +0200 Subject: [PATCH 01/17] Add a notify message type --- src/ts/shared/types.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ts/shared/types.ts b/src/ts/shared/types.ts index d669fa1..c7ddcf1 100644 --- a/src/ts/shared/types.ts +++ b/src/ts/shared/types.ts @@ -62,6 +62,11 @@ export interface NavMessage { // the two (or WsMessage's "position" variant below). export type SyncPosition = Omit; +// The colour a notification carries — the same vocabulary as inkflow.logging's +// report()/`_level_render` (green for a completed action, yellow for attention, +// red for an error), so client and server never invent two severity dialects. +export type NotifyStyle = "green" | "yellow" | "red"; + export type WsMessage = | { type: "update"; @@ -70,5 +75,5 @@ export type WsMessage = logs: LogEntry[]; } | { type: "error"; message: string } - | { type: "edit-error"; message: string } + | { type: "notify"; message: string; style: NotifyStyle } | ({ type: "position" } & SyncPosition); From e389b3b7c4d17474830a3b3424f177649d6723af Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:21:18 +0200 Subject: [PATCH 02/17] Add notify to the server side --- src/inkflow/server.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/inkflow/server.py b/src/inkflow/server.py index 97bcb93..2cb4785 100644 --- a/src/inkflow/server.py +++ b/src/inkflow/server.py @@ -16,7 +16,7 @@ from collections.abc import Awaitable, Callable from html import escape as escape_html from pathlib import Path -from typing import TypedDict, cast +from typing import Literal, TypedDict, cast from urllib.parse import unquote from rich.console import Console @@ -193,6 +193,26 @@ async def broadcast(msg: str, sender: ServerConnection | None = None) -> None: _state["ws_clients"] -= dead +NotifyStyle = Literal["green", "yellow", "red"] + + +async def notify( + target: ServerConnection | None, message: str, *, style: NotifyStyle = "green" +) -> None: + """Push a transient, colour-coded notification: to one client (a reply) when + `target` is given, otherwise to every connected client. + + A pure transport primitive, independent of the rebuild-cycle log banner + (`collect_logs`) above. Whether the event is also worth a `logger` call is the + caller's decision, not this function's — the two are separate concerns. + """ + payload = json.dumps({"type": "notify", "message": message, "style": style}) + if target is None: + await broadcast(payload) + else: + await target.send(payload) + + # ── WebSocket handler ───────────────────────────────────────────────────────── @@ -302,9 +322,7 @@ async def handler(websocket: ServerConnection) -> None: if request is not None: error = open_in_editor(*request) if error is not None: - await websocket.send( - json.dumps({"type": "edit-error", "message": error}) - ) + await notify(websocket, error, style="red") finally: _state["ws_clients"].discard(websocket) logger.debug(f"client disconnected ({len(_state['ws_clients'])} total)") From 698474bcc1a44e2caaf9c9ce0bb01a9a0c6aff9a Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:22:07 +0200 Subject: [PATCH 03/17] Add notify type to ui.ts, which is going to own this feature --- src/ts/presenter/ui.ts | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/ts/presenter/ui.ts b/src/ts/presenter/ui.ts index 010cf10..c1eebe6 100644 --- a/src/ts/presenter/ui.ts +++ b/src/ts/presenter/ui.ts @@ -1,4 +1,4 @@ -import type { LogEntry } from "../shared/types"; +import type { LogEntry, NotifyStyle } from "../shared/types"; const curtain = document.getElementById("curtain")!; const help = document.getElementById("help")!; @@ -9,6 +9,9 @@ const logList = document.getElementById("log-list")!; const logClose = document.getElementById("log-close")!; const logIndicator = document.getElementById("log-indicator")!; const statusBarEl = document.getElementById("statusbar")!; +const notify = document.getElementById("notify")!; +const notifyText = document.getElementById("notify-text")!; +const notifyClose = document.getElementById("notify-close")!; // biome-ignore lint/suspicious/noExplicitAny: webkit prefix not in TS DOM lib const _doc = document as any; @@ -119,6 +122,39 @@ export function toggleLogs(): void { } } +// ── Notification toast ── +// A transient, self-dismissing confirmation/warning/error, styled by the same +// green/yellow/red vocabulary as inkflow.logging's report(). Pushed by the server +// (websocket.ts's "notify" message) or called directly by a client-only action +// (edit.ts's clipboard-copy confirmation, which never touches the server). + +// Must match the CSS animation duration on #notify-progress (overlays.css). +const NOTIFY_DURATION_MS = 3000; + +let notifyTimeout: ReturnType | null = null; + +export function hideNotify(): void { + if (notifyTimeout) clearTimeout(notifyTimeout); + notify.classList.remove("visible"); + notifyTimeout = null; +} + +export function showNotify( + message: string, + style: NotifyStyle = "green", +): void { + notifyText.textContent = message; + notify.dataset.style = style; + // Drop .visible and force a reflow before re-adding it, even if a notification + // is already showing (two in quick succession) — otherwise the browser never + // sees the class go away and won't restart the progress-bar animation. + notify.classList.remove("visible"); + void notify.offsetWidth; + notify.classList.add("visible"); + if (notifyTimeout) clearTimeout(notifyTimeout); + notifyTimeout = setTimeout(hideNotify, NOTIFY_DURATION_MS); +} + // ── Theme ── export function toggleTheme(): void { const html = document.documentElement; @@ -207,6 +243,7 @@ logClose.addEventListener("click", hideLogs); logIndicator.addEventListener("click", () => { logBanner.classList.add("visible"); }); +notifyClose.addEventListener("click", hideNotify); curtain.addEventListener("click", hideCurtain); help.addEventListener("click", (e) => { if (e.target === help) toggleHelp(); From dffdbe453a6fc273b5cd034db41a96f27c333fa5 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:22:44 +0200 Subject: [PATCH 04/17] Route edit notification through new notify primitive --- src/ts/presenter/edit.ts | 51 +++-------------------------------- src/ts/presenter/websocket.ts | 7 +++-- 2 files changed, 6 insertions(+), 52 deletions(-) diff --git a/src/ts/presenter/edit.ts b/src/ts/presenter/edit.ts index 06aab48..81c7700 100644 --- a/src/ts/presenter/edit.ts +++ b/src/ts/presenter/edit.ts @@ -1,6 +1,7 @@ import type { EditableFile, EditCommandsConfig } from "../shared/types"; import { menuClosed, menuOpened } from "./menus"; import { state } from "./state"; +import { showNotify } from "./ui"; // Status-bar control for editing the current slide's source file(s). Owns the // button + dropdown DOM (structurally mirrors syncmenu.ts). @@ -22,17 +23,8 @@ import { state } from "./state"; const btnEdit = document.getElementById("btn-edit")!; const editMenu = document.getElementById("edit-menu")!; const editWrap = btnEdit.closest(".edit-wrap")!; -const editToast = document.getElementById("edit-toast")!; -const editToastText = document.getElementById("edit-toast-text")!; -const editToastClose = document.getElementById("edit-toast-close")!; - -// Must match the CSS animation duration on #edit-toast-progress (overlays.css). -const TOAST_DURATION_MS = 3000; let config: EditCommandsConfig = { default: false, svg: false }; -let toastTimeout: ReturnType | null = null; - -editToastClose.addEventListener("click", () => hideToast()); // One small icon per editableFiles label — a plain signifier, not decoration, so // entries with the same generic label (several "Parent" rows) still read apart at @@ -54,43 +46,6 @@ function isConfigured(file: EditableFile): boolean { return config.default; } -function hideToast(): void { - if (toastTimeout) clearTimeout(toastTimeout); - editToast.classList.remove("visible"); - toastTimeout = null; -} - -// Flashes a confirmation styled like the #log-banner message boxes (same -// surface/border/shadow treatment, an accent colour instead of its warning -// yellow), with its own close button and (for a success message) a shrinking -// progress bar (pure CSS, see overlays.css) showing time left before it -// dismisses itself. An error stays until manually dismissed — it's diagnostic -// text the click that triggered it didn't expect, worth more than a glance. -function flashToast( - message: string, - kind: "success" | "error" = "success", -): void { - editToastText.textContent = message; - editToast.classList.toggle("error", kind === "error"); - // Drop .visible and force a reflow before re-adding it, even if the toast is - // already showing (two edits in quick succession) — otherwise the browser - // never sees the class go away and won't restart the progress-bar animation. - editToast.classList.remove("visible"); - void editToast.offsetWidth; - editToast.classList.add("visible"); - if (toastTimeout) clearTimeout(toastTimeout); - toastTimeout = - kind === "error" ? null : setTimeout(hideToast, TOAST_DURATION_MS); -} - -// Called from websocket.ts when the server replies with an "edit-error" -// message: the configured command failed to launch (e.g. the binary isn't on -// PATH). Without this, a failed launch was only ever logged server-side — -// invisible from the browser tab where the click actually happened. -export function showEditError(message: string): void { - flashToast(message, "error"); -} - function actOn(file: EditableFile): void { if ( isConfigured(file) && @@ -98,14 +53,14 @@ function actOn(file: EditableFile): void { state.ws.readyState === WebSocket.OPEN ) { state.ws.send(JSON.stringify({ type: "edit", path: file.path })); - flashToast(`Opened ${file.name}`); + showNotify(`Opened ${file.name}`); return; } try { void navigator.clipboard.writeText(file.path); // The full path, not just file.name: a bare filename here would read as // though only the name (not the whole path) had been copied. - flashToast(`Copied ${file.path}`); + showNotify(`Copied ${file.path}`); } catch (_) {} } diff --git a/src/ts/presenter/websocket.ts b/src/ts/presenter/websocket.ts index 56a818f..1c579c7 100644 --- a/src/ts/presenter/websocket.ts +++ b/src/ts/presenter/websocket.ts @@ -5,7 +5,6 @@ import type { TransitionData, WsMessage, } from "../shared/types"; -import { showEditError } from "./edit"; import { renderPv, renderPvNext, updatePvInfo } from "./pv"; import { state } from "./state"; import { @@ -15,7 +14,7 @@ import { snapStepRun, } from "./status"; import { CUT, loadSlide, snapInflight } from "./transitions"; -import { hideError, showError, showLogs } from "./ui"; +import { hideError, showError, showLogs, showNotify } from "./ui"; const wsDot = document.getElementById("ws-dot")!; // Direct DOM refs to avoid circular import with overview.ts @@ -210,8 +209,8 @@ export function connectWS(wsPort: number | null, authoritative: boolean): void { renderPv(); } else if (msg.type === "error") { showError(msg.message); - } else if (msg.type === "edit-error") { - showEditError(msg.message); + } else if (msg.type === "notify") { + showNotify(msg.message, msg.style); } else if (msg.type === "position") { // Discard exactly the stale connect-time push so an authoritative window // keeps its own position; later updates apply normally. Mirrors the From b2a05c50fbc25ea436b0cb614e7afee5cfaf552b Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:24:07 +0200 Subject: [PATCH 05/17] Transform edit toast to general notify ui --- src/css/presenter/overlays.css | 68 ++++++++++++++++++++-------------- src/inkflow/presenter.html | 19 ++++++---- 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/css/presenter/overlays.css b/src/css/presenter/overlays.css index 0fef9c3..61f9f80 100644 --- a/src/css/presenter/overlays.css +++ b/src/css/presenter/overlays.css @@ -206,14 +206,16 @@ kbd { color: var(--red); } -/* ── Edit-action toast (non-modal) ── - Same corner and box treatment as #log-banner (surface/border/shadow), an - accent colour instead of its warning yellow. Times itself out (edit.ts) but +/* ── Notification toast (non-modal) ── + Same corner and box treatment as #log-banner (surface/border/shadow); its + accent colour and icon are driven by data-style, the same green/yellow/red + vocabulary as inkflow.logging's report(). Always times itself out (ui.ts) but also offers a close button, like #log-banner, plus a bar along the bottom edge filling left-to-right as it approaches that — purely CSS (an animation - timed to match edit.ts's setTimeout), restarted by re-toggling .visible so a - second edit in quick succession doesn't inherit the first one's progress. */ -#edit-toast { + timed to match ui.ts's setTimeout), restarted by re-toggling .visible so a + second notification in quick succession doesn't inherit the first one's + progress. */ +#notify { display: none; position: fixed; right: 1rem; @@ -231,24 +233,24 @@ kbd { overflow: hidden; pointer-events: none; } -#edit-toast.visible { +#notify.visible { display: flex; } -#edit-toast-body { +#notify-body { display: flex; align-items: flex-start; gap: 0.4rem; padding: 0.5rem 0.5rem 0.5rem 0.75rem; } -#edit-toast svg { +#notify svg { color: var(--green); flex-shrink: 0; margin-top: 0.15rem; } -#edit-toast-text { +#notify-text { word-break: break-word; } -#edit-toast-close { +#notify-close { background: none; border: none; color: var(--subtext); @@ -259,16 +261,16 @@ kbd { flex-shrink: 0; pointer-events: auto; } -#edit-toast-close:hover { +#notify-close:hover { color: var(--text); } -#edit-toast-progress { +#notify-progress { height: 2px; background: var(--green); transform-origin: left; - animation: edit-toast-grow 3s linear forwards; + animation: notify-grow 3s linear forwards; } -@keyframes edit-toast-grow { +@keyframes notify-grow { from { transform: scaleX(0); } @@ -276,24 +278,34 @@ kbd { transform: scaleX(1); } } -/* Error state: launching the configured command failed (e.g. the binary isn't - on PATH) — red instead of green, the error glyph instead of the checkmark, - and no progress bar, since an error toast has no auto-dismiss timer to show - the progress of. Declared last so it reliably overrides the base rules above - regardless of its own (lower) specificity. */ -.edit-toast-icon-error { +/* Non-default styles: yellow (warning) and red (error) each swap the border, + progress-bar colour and icon. Declared last so they reliably override the + base rules above regardless of their own (lower) specificity. */ +.notify-icon-warning, +.notify-icon-error { display: none; } -#edit-toast.error { +#notify[data-style="yellow"] .notify-icon-ok, +#notify[data-style="red"] .notify-icon-ok { + display: none; +} +#notify[data-style="yellow"] { + border-color: var(--yellow); +} +#notify[data-style="yellow"] #notify-progress { + background: var(--yellow); +} +#notify[data-style="yellow"] .notify-icon-warning { + display: block; + color: var(--yellow); +} +#notify[data-style="red"] { border-color: var(--red); } -#edit-toast.error .edit-toast-icon-ok { - display: none; +#notify[data-style="red"] #notify-progress { + background: var(--red); } -#edit-toast.error .edit-toast-icon-error { +#notify[data-style="red"] .notify-icon-error { display: block; color: var(--red); } -#edit-toast.error #edit-toast-progress { - display: none; -} diff --git a/src/inkflow/presenter.html b/src/inkflow/presenter.html index c33073a..65dfbb4 100644 --- a/src/inkflow/presenter.html +++ b/src/inkflow/presenter.html @@ -23,20 +23,25 @@

BUILD ERROR

    -
    -
    -
    +
    + - - + +
    -
    +
    - MESSAGES - + DIAGNOSTICS +
      @@ -67,13 +67,23 @@

      KEYBINDINGS

      View tDark / light mode pPresenter panel - mMessages + dDiagnostics + nNotifications sCycle sync mode ?This help

      Press ? or Esc to close

      +
      + +
      `; vi.resetModules(); ({ initEditMenu, renderEditButton } = await import("./edit")); diff --git a/src/ts/presenter/logs.test.ts b/src/ts/presenter/logs.test.ts index 1036676..b52b7ec 100644 --- a/src/ts/presenter/logs.test.ts +++ b/src/ts/presenter/logs.test.ts @@ -28,6 +28,8 @@ beforeAll(async () => {
      + +
      `; vi.resetModules(); ({ showLogs, hideLogs, toggleLogs } = await import("./ui")); diff --git a/src/ts/presenter/overview.test.ts b/src/ts/presenter/overview.test.ts index 9cb09b5..3071019 100644 --- a/src/ts/presenter/overview.test.ts +++ b/src/ts/presenter/overview.test.ts @@ -50,6 +50,8 @@ beforeAll(async () => {
      + +
      `; // computeStageFlip() divides stage/thumbnail rect sizes; happy-dom has no diff --git a/src/ts/presenter/picker.test.ts b/src/ts/presenter/picker.test.ts index f3d1e75..91e0b82 100644 --- a/src/ts/presenter/picker.test.ts +++ b/src/ts/presenter/picker.test.ts @@ -44,6 +44,8 @@ beforeAll(async () => {
      + +
      `; vi.resetModules(); ({ filterPicker } = await import("./picker")); diff --git a/src/ts/presenter/windowsync.test.ts b/src/ts/presenter/windowsync.test.ts index 2627618..51c5095 100644 --- a/src/ts/presenter/windowsync.test.ts +++ b/src/ts/presenter/windowsync.test.ts @@ -42,6 +42,8 @@ beforeAll(async () => {
      + +
      `; ({ isSyncPayload } = await import("./windowsync")); }); From 5806fe2e0f9ee5912841fbb5560e001ac1348e95 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:08:42 +0200 Subject: [PATCH 13/17] Rebuild bundles --- src/inkflow/bundles/presenter.css | 91 +++++++++++++++++++++++++++++++ src/inkflow/bundles/presenter.js | 71 +++++++++++++++++++++++- 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/src/inkflow/bundles/presenter.css b/src/inkflow/bundles/presenter.css index cc0cb4f..8a66b73 100644 --- a/src/inkflow/bundles/presenter.css +++ b/src/inkflow/bundles/presenter.css @@ -526,6 +526,97 @@ kbd { display: block; color: var(--red); } +#notify-history { + display: none; + position: fixed; + inset: 0; + z-index: 30; + background: rgba(0, 0, 0, 0.6); + align-items: center; + justify-content: center; +} +#notify-history.visible { + display: flex; +} +#notify-history-box { + background: var(--surface); + border: 1px solid var(--accent); + border-radius: 8px; + padding: 1.5rem 2rem; + min-width: 26rem; + max-width: 32rem; + max-height: 70vh; + display: flex; + flex-direction: column; +} +#notify-history-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + flex-shrink: 0; +} +#notify-history-head h2 { + color: var(--accent); + font-size: 0.85rem; + letter-spacing: 0.05em; +} +#notify-history-close { + background: none; + border: none; + color: var(--subtext); + font-size: 1.1rem; + line-height: 1; + cursor: pointer; + padding: 0 0.25rem; +} +#notify-history-close:hover { + color: var(--text); +} +#notify-history-list { + margin: 0; + padding: 0; + list-style: none; + overflow-y: auto; +} +#notify-history-list .nh-row { + display: flex; + gap: 0.5rem; + align-items: baseline; + font-size: 0.8rem; + line-height: 1.6; + color: var(--text); + padding: 0.3rem 0; +} +#notify-history-list .nh-row + .nh-row { + border-top: 1px solid var(--overlay); +} +#notify-history-list .nh-time { + flex-shrink: 0; + color: var(--subtext); + font-variant-numeric: tabular-nums; +} +#notify-history-list .nh-ico { + flex-shrink: 0; +} +#notify-history-list .nh-ico.nh-green { + color: var(--green); +} +#notify-history-list .nh-ico.nh-yellow { + color: var(--yellow); +} +#notify-history-list .nh-ico.nh-red { + color: var(--red); +} +#notify-history-list .nh-message { + word-break: break-word; +} +#notify-history-empty { + color: var(--subtext); + text-align: center; + justify-content: center; + padding: 1rem 0; +} /* src/css/presenter/picker.css */ #picker { diff --git a/src/inkflow/bundles/presenter.js b/src/inkflow/bundles/presenter.js index 170d745..21f79b5 100644 --- a/src/inkflow/bundles/presenter.js +++ b/src/inkflow/bundles/presenter.js @@ -40,6 +40,10 @@ var notify = document.getElementById("notify"); var notifyText = document.getElementById("notify-text"); var notifyClose = document.getElementById("notify-close"); + var notifyHistoryBtn = document.getElementById("notify-history-btn"); + var notifyHistoryEl = document.getElementById("notify-history"); + var notifyHistoryList = document.getElementById("notify-history-list"); + var notifyHistoryClose = document.getElementById("notify-history-close"); var _doc = document; var _fsHideTimer; function showCurtain(color) { @@ -117,6 +121,7 @@ logBanner.classList.add("visible"); } } + var notifyHistory = []; var NOTIFY_DURATION_MS = 3e3; var notifyTimeout = null; function hideNotify() { @@ -125,6 +130,7 @@ notifyTimeout = null; } function showNotify(message, style = "green") { + notifyHistory.push({ message, style, time: Date.now() }); notifyText.textContent = message; notify.dataset.style = style; notify.classList.remove("visible"); @@ -133,6 +139,56 @@ if (notifyTimeout) clearTimeout(notifyTimeout); notifyTimeout = setTimeout(hideNotify, NOTIFY_DURATION_MS); } + var NOTIFY_HISTORY_ICON = { + green: "\u2713", + yellow: "\u26A0\uFE0E", + red: "\u2716\uFE0E" + }; + function pad2(n) { + return String(n).padStart(2, "0"); + } + function formatHistoryTime(time) { + const d = new Date(time); + return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`; + } + function renderNotifyHistory() { + if (notifyHistory.length === 0) { + const empty = document.createElement("li"); + empty.id = "notify-history-empty"; + empty.className = "nh-row"; + empty.textContent = "No notifications yet."; + notifyHistoryList.replaceChildren(empty); + return; + } + notifyHistoryList.replaceChildren( + ...notifyHistory.slice().reverse().map((entry) => { + const li = document.createElement("li"); + li.className = "nh-row"; + const time = document.createElement("span"); + time.className = "nh-time"; + time.textContent = formatHistoryTime(entry.time); + const ico = document.createElement("span"); + ico.className = `nh-ico nh-${entry.style}`; + ico.textContent = NOTIFY_HISTORY_ICON[entry.style]; + const msg = document.createElement("span"); + msg.className = "nh-message"; + msg.textContent = entry.message; + li.append(time, ico, msg); + return li; + }) + ); + } + function openNotifyHistory() { + renderNotifyHistory(); + notifyHistoryEl.classList.add("visible"); + } + function closeNotifyHistory() { + notifyHistoryEl.classList.remove("visible"); + } + function toggleNotifyHistory() { + if (notifyHistoryEl.classList.contains("visible")) closeNotifyHistory(); + else openNotifyHistory(); + } function toggleTheme() { const html = document.documentElement; html.dataset.theme = html.dataset.theme === "light" ? "" : "light"; @@ -202,6 +258,11 @@ logBanner.classList.add("visible"); }); notifyClose.addEventListener("click", hideNotify); + notifyHistoryBtn.addEventListener("click", toggleNotifyHistory); + notifyHistoryClose.addEventListener("click", closeNotifyHistory); + notifyHistoryEl.addEventListener("click", (e) => { + if (e.target === notifyHistoryEl) closeNotifyHistory(); + }); curtain.addEventListener("click", hideCurtain); help.addEventListener("click", (e) => { if (e.target === help) toggleHelp(); @@ -3693,7 +3754,8 @@ "?": { action: toggleHelp }, t: { action: toggleTheme }, p: { action: togglePv }, - m: { action: toggleLogs }, + d: { action: toggleLogs }, + n: { action: toggleNotifyHistory }, s: { action: cycleSyncMode } }; var helpEl = document.getElementById("help"); @@ -3701,6 +3763,7 @@ var pickerEl = document.getElementById("picker"); var curtainEl = document.getElementById("curtain"); var logBannerEl = document.getElementById("log-banner"); + var notifyHistoryEl2 = document.getElementById("notify-history"); document.addEventListener("keydown", (e) => { if (helpEl.classList.contains("visible")) { if (e.key === "?" || e.key === "Escape" || e.key === "q") { @@ -3709,6 +3772,12 @@ } if (e.key !== "t") return; } + if (notifyHistoryEl2.classList.contains("visible")) { + if (e.key === "Escape" || e.key === "q" || e.key === "n") { + toggleNotifyHistory(); + } + return; + } if (overviewEl2.classList.contains("visible")) { if (e.key === "Escape" || e.key === "q") { closeOverview(); From 4a5826bf437486787ada7e277cb5cc7b363d65e5 Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:23:00 +0200 Subject: [PATCH 14/17] Fix factual errors in the guides Six claims in the guides did not match the code. A reader copying any of them would hit an error. - `Slide(style=...)` is not a parameter, it is `extra_style`. - A bare `str` in `style` / `extra_style` is a *path*, not literal CSS. `load_style` only treats `Inline(...)` as content, so `Deck(style='text { ... }')` tried to open a file with that name. - Transition `easing` does not default to `None`. It is an `Easing` with a concrete default per type. - The `inkflow` namespace URI is `urn:inkflow`, not `https://inkflow.dev/ns`. - The docs-embedding recipe referenced a `poe` task; the project uses mise. - `Image(fit=...)` and `SlideIn(direction=...)` took raw strings where the rest of the docs use the enums. Also narrows the "available for every palette token" claim about the `inkflow-fill-*` / `inkflow-stroke-*` classes: `link`, `heading` and `blockquote` are palette tokens with no utility class. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Q1Ni7BiNGgd2taCCHTgjXb --- docs/guides/export.md | 21 +++++++++++++++------ docs/guides/layout-system.md | 2 +- docs/guides/slides.md | 30 ++++++++++++++++++++---------- docs/guides/themes.md | 16 +++++++++++----- docs/guides/transitions.md | 6 ++++-- 5 files changed, 51 insertions(+), 24 deletions(-) diff --git a/docs/guides/export.md b/docs/guides/export.md index 898dbcb..5d1175c 100644 --- a/docs/guides/export.md +++ b/docs/guides/export.md @@ -130,12 +130,21 @@ To embed in an MkDocs page, place the built output in `docs/demo/` and add an if ``` -To generate and place the demo as part of your docs build, -add a `poe` task: +Build the deck into place before building the site, +so the embedded demo is always current: -```toml -[tool.poe.tasks.docs-build-demo] -cmd = "inkflow build --deck src/inkflow/theme/showcase/deck.py --output docs/demo" +```bash +inkflow build --deck deck.py --output docs/demo +mkdocs build ``` -Then run `poe docs-build-demo` before `mkdocs build` or `mkdocs gh-deploy`. +Wire that pair into whichever task runner your project already uses. +These docs do it with [mise](https://mise.jdx.dev/): + +```toml +[tasks.docs-build] +run = [ + "uv run inkflow build --deck demo/deck.py --output docs/demo", + "uv run mkdocs build", +] +``` diff --git a/docs/guides/layout-system.md b/docs/guides/layout-system.md index 4bbf8f1..2cc495f 100644 --- a/docs/guides/layout-system.md +++ b/docs/guides/layout-system.md @@ -108,7 +108,7 @@ Layouts chain to their parents via an `inkflow:parent` attribute on the SVG root ```xml diff --git a/docs/guides/slides.md b/docs/guides/slides.md index 838c8c7..7a9d28c 100644 --- a/docs/guides/slides.md +++ b/docs/guides/slides.md @@ -93,7 +93,9 @@ the [animations reference](../reference/animations.md) for the full table. | `Highlight` | Pulse a glow (`color`, `iterations`), without hiding | Visible | ```python -animations.SlideIn("box", direction="left", duration=0.6) +from inkflow import Direction, animations + +animations.SlideIn("box", direction=Direction.LEFT, duration=0.6) animations.ZoomIn("logo", scale=0.6) animations.Highlight("total", color="#cba6f7", iterations=2) ``` @@ -107,7 +109,7 @@ slide — it can enter, be emphasized, exit, and even re-enter, each on its own animations = [ animations.FadeIn("hero"), # step 1: enters animations.Highlight("hero"), # step 2: emphasized - animations.SlideOut("hero", direction="down"), # step 3: exits + animations.SlideOut("hero", direction=Direction.DOWN), # step 3: exits animations.Bounce("hero"), # step 4: returns ] ``` @@ -218,13 +220,13 @@ Fill them the same way in either case. Pass content into named zones directly: ```python -from inkflow import Image, Slide +from inkflow import Image, MediaFit, Slide Slide( "title", zones={ "title": "My talk title", - "media": Image("assets/headshot.jpg", fit="cover"), + "media": Image("assets/headshot.jpg", fit=MediaFit.COVER), }, ) ``` @@ -571,12 +573,12 @@ For images and video that should fill a zone (rather than sit inline in text), pass an `Image` or a `Video` through the `zones` dict: ```python -from inkflow import Image, Slide +from inkflow import Image, MediaFit, Slide Slide( "media-right", md="feature", - zones={"media": Image("assets/screenshot.png", fit="cover")}, + zones={"media": Image("assets/screenshot.png", fit=MediaFit.COVER)}, ) ``` @@ -694,19 +696,27 @@ reveal's step, pin it with `Trigger.at(n)`. ## Per-slide styling -The `style` parameter injects a CSS `