Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions skills/htmlit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,8 @@ The injected client auto-renders, on first load and after every morph:
diagram element tracks it as it moves. A minimal zoom control sits at the
bottom-left showing the current **zoom %** (relative to the fitted view); click
the percentage to reset to fit. A **Code** button at the top-right toggles the
diagram for its Mermaid source (read or copy the syntax in place). A diagram
diagram for its Mermaid source, shown as a syntax-highlighted code block (line
numbers) with a **Copy** button to grab the syntax in place. A diagram
inside a hidden tab or a closed `<details>` renders **automatically the moment it
is revealed**, so it is safe to put diagrams in tabs/accordions - they never
collapse. Manual
Expand All @@ -314,10 +315,10 @@ The injected client auto-renders, on first load and after every morph:
shows the normal arrow cursor at rest and switches to a grabbing cursor only
while you actually drag.)
- **Code** - `<pre><code class="language-python">...</code></pre>` is highlighted
by highlight.js and rendered in a framed block with a **language-name header**
and a **line-number gutter** (the gutter stays pinned while long lines scroll
by highlight.js and rendered with a **language name** and a **Copy** button on top
and a pinned **line-number gutter** (the gutter stays put while long lines scroll
horizontally). The language shown comes from the `language-xxx` class.
- **Diffs** - write a fenced `diff` block or `<pre><code class="language-diff">...</code></pre>`. htmlit renders it with full-row red and green lines, aligned old and new line numbers, and a Unified/Split toggle. No custom CSS is needed.
- **Diffs** - write a fenced `diff` block or `<pre><code class="language-diff">...</code></pre>`. htmlit renders it in a framed block with full-row red and green lines, aligned old and new line numbers, and a Unified/Split toggle (no Copy button). No custom CSS is needed.

- **Theme** - the panel's **Dark** toggle flips `html.dark`, `html[data-theme]`,
`color-scheme`, the highlight.js theme, and Mermaid's theme. Style your
Expand Down
2 changes: 1 addition & 1 deletion skills/htmlit/client/client.css
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
.rail .card { position: fixed; width: 276px; box-sizing: border-box; pointer-events: auto; background: var(--elev); color: var(--text); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: 8px; box-shadow: var(--shadow); padding: 10px 12px 10px 11px; transition: top 0.12s ease, left 0.12s ease; }
.rail .card.draft { border-left-color: var(--muted); border-left-style: dashed; }
.rail .card-draft { font-size: 11px; color: var(--muted); margin: -2px 0 7px; }
.rail .card-q { font-size: 11.5px; color: var(--muted); background: color-mix(in srgb, var(--accent) 10%, transparent); border-radius: 4px; padding: 3px 7px; margin: 0 0 8px; max-height: 34px; overflow: hidden; cursor: pointer; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.rail .card-q { font-size: 11.5px; line-height: 1.5; color: var(--muted); background: color-mix(in srgb, var(--accent) 10%, transparent); border-radius: 4px; padding: 3px 7px; margin: 0 0 8px; max-height: calc(3em + 6px); overflow: hidden; cursor: pointer; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.rail .card-q::before { content: "\201C"; } .rail .card-q::after { content: "\201D"; }
.rail .card-thread { display: flex; flex-direction: column; gap: 6px; }
.rail .cmt { display: flex; align-items: flex-start; gap: 6px; }
Expand Down
79 changes: 79 additions & 0 deletions skills/htmlit/client/codeblock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { copyText } from "./util.js";

// A lightweight highlight.js grammar for Mermaid source, registered on first use, so
// the diagram "Code" view reads like code (diagram-type keywords, directions, quoted
// labels, edge-label pipes, arrows/links and %% comments). highlight.js ships no
// Mermaid language of its own, and the raw diagram block is turned into an SVG rather
// than highlighted, so this only styles the toggled source view.
function mermaidGrammar(hljs) {
var TYPE = "graph flowchart sequenceDiagram classDiagram stateDiagram stateDiagram-v2 " +
"erDiagram journey gantt pie gitGraph mindmap timeline quadrantChart requirementDiagram " +
"sankey-beta xychart-beta block-beta";
var KW = "subgraph end participant actor note over loop alt opt par and rect activate " +
"deactivate direction click class style classDef linkStyle state as call href section " +
"title autonumber";
return {
name: "mermaid",
case_insensitive: true,
keywords: { keyword: TYPE + " " + KW, literal: "LR RL TB TD BT true false" },
contains: [
hljs.COMMENT("%%", "$"),
hljs.QUOTE_STRING_MODE,
hljs.APOS_STRING_MODE,
{ className: "string", begin: /\|/, end: /\|/ },
{ className: "operator", begin: /<?[ox]?(?:-{2,}|-\.-+|={2,}|~{2,})[->ox]*/ },
{ className: "number", begin: /\b\d+(?:\.\d+)?\b/ },
],
};
}

var mermaidRegistered = false;
// Syntax-highlight a <code> element holding Mermaid source, in place. No-op when
// highlight.js is unavailable (offline with no vendored copy) - the plain source
// still shows.
export function highlightMermaidSource(codeEl) {
var hljs = window.hljs;
if (!codeEl || !hljs || !hljs.registerLanguage) return;
if (!mermaidRegistered) {
try { hljs.registerLanguage("mermaid", mermaidGrammar); mermaidRegistered = true; } catch (e) { return; }
}
codeEl.className = "language-mermaid";
codeEl.removeAttribute("data-highlighted");
try { hljs.highlightElement(codeEl); } catch (e) {}
}

// A "Copy" button that copies getText() to the clipboard, with brief feedback.
export function makeCopyButton(getText) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "htmlit-copy";
btn.textContent = "Copy";
btn.title = "Copy to clipboard";
btn.addEventListener("click", function (e) {
e.stopPropagation();
e.preventDefault();
Promise.resolve(copyText(getText())).catch(function () {});
btn.textContent = "Copied";
btn.classList.add("htmlit-copied");
clearTimeout(btn._copyReset);
btn._copyReset = setTimeout(function () { btn.textContent = "Copy"; btn.classList.remove("htmlit-copied"); }, 1400);
});
return btn;
}

// Line count of a source string (trailing blank lines trimmed), at least 1.
export function lineCount(src) {
src = (src || "").replace(/\n+$/, "");
return src.length ? src.split("\n").length : 1;
}

// A right-aligned, non-selectable line-number gutter for a code-block body.
export function makeLinenos(count) {
var gutter = document.createElement("span");
gutter.className = "htmlit-linenos";
gutter.setAttribute("aria-hidden", "true");
var nums = [];
for (var i = 1; i <= count; i++) nums.push(i);
gutter.textContent = nums.join("\n");
return gutter;
}
72 changes: 57 additions & 15 deletions skills/htmlit/client/diagram.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { root, shadow, ui } from "./dom.js";
import { theme } from "./theme.js";
import { renderHighlights, scheduleReposition } from "./overlays.js";
import { positionRail } from "./rail.js";
import { makeCopyButton, makeLinenos, lineCount, highlightMermaidSource } from "./codeblock.js";
import { clearPending, diagramClick } from "./annotate.js";

export const diagramLayouts = {}; // diagram source text -> saved arrangement
Expand Down Expand Up @@ -180,7 +181,7 @@ export function enhanceMermaid() {
// (the bar is data-htmlit, so the morph won't delete it and relocates it
// instead). Drop any bar that is no longer parented by a .mermaid frame
// before (re)building - this also cleans up any already-stranded bar.
document.querySelectorAll("[data-htmlit-tools],[data-htmlit-codebtn],[data-htmlit-code]").forEach(function (barEl) {
document.querySelectorAll("[data-htmlit-tools],[data-htmlit-codebtn],[data-htmlit-code],[data-htmlit-codecopy]").forEach(function (barEl) {
var pp = barEl.parentElement;
if (!pp || !pp.classList || !pp.classList.contains("mermaid")) barEl.remove();
});
Expand Down Expand Up @@ -665,18 +666,59 @@ if (box && box.classList && box.classList.contains("mermaid")) {
bar.addEventListener("pointerdown", function (ev) { ev.stopPropagation(); });
}

// "Code" toggle (top-right): reveal the diagram's Mermaid source in place, so a
// reader can inspect or copy the syntax without leaving the page. Added once and
// cleaned up on morph alongside the zoom bar.
// "Code" toggle (top-right): reveal the diagram's Mermaid source in place - as a
// real code block (line numbers + a Copy button) - so a reader can inspect or copy
// the syntax without leaving the page. Added once and cleaned up on morph alongside
// the zoom bar.
if (box && box.classList && box.classList.contains("mermaid") && box.dataset.htmlitSrc && !box.querySelector("[data-htmlit-codebtn]")) {
var cdark = theme === "dark";
var codeView = document.createElement("pre");
var mSrc = box.dataset.htmlitSrc;

// The source view mirrors a normal enhanced code block: a titled header
// ("MERMAID" + Copy) over a line-numbered, syntax-highlighted body, reusing the
// same classes so it shares the chrome and background. It overlays the diagram
// and is opened by the floating "Code" button; the header's "Diagram" button
// (and Escape) closes it again.
var codeView = document.createElement("div");
codeView.className = "htmlit-code";
codeView.setAttribute("data-htmlit", "");
codeView.setAttribute("data-htmlit-code", "");
codeView.textContent = box.dataset.htmlitSrc;
codeView.style.cssText = "position:absolute;inset:0;margin:0;display:none;z-index:6;overflow:auto;padding:14px 16px 44px;box-sizing:border-box;" +
"font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre;tab-size:2;-webkit-user-select:text;user-select:text;" +
"background:" + (cdark ? "#0d1117" : "#f6f8fa") + ";color:" + (cdark ? "#e6edf3" : "#1f2328") + ";";
codeView.style.cssText = "position:absolute;inset:0;margin:0;display:none;z-index:6;overflow:auto;box-sizing:border-box;border-radius:inherit;";

var codeHd = document.createElement("div");
codeHd.className = "htmlit-code-hd";
var codeLang = document.createElement("span");
codeLang.className = "htmlit-code-lang";
codeLang.textContent = "mermaid";
var codeActions = document.createElement("span");
codeActions.className = "htmlit-code-actions";
var copyBtn = makeCopyButton(function () { return mSrc; });
copyBtn.setAttribute("data-htmlit", "");
copyBtn.setAttribute("data-htmlit-codecopy", "");
copyBtn.addEventListener("pointerdown", function (ev) { ev.stopPropagation(); });
var backBtn = document.createElement("button");
backBtn.type = "button";
backBtn.className = "htmlit-copy";
backBtn.setAttribute("data-htmlit", "");
backBtn.textContent = "Diagram";
backBtn.title = "Show the diagram";
backBtn.addEventListener("pointerdown", function (ev) { ev.stopPropagation(); });
codeActions.appendChild(copyBtn);
codeActions.appendChild(backBtn);
codeHd.appendChild(codeLang);
codeHd.appendChild(codeActions);
codeView.appendChild(codeHd);

var codeBody = document.createElement("div");
codeBody.className = "htmlit-code-body";
codeBody.appendChild(makeLinenos(lineCount(mSrc)));
var codePre = document.createElement("pre");
var codeEl = document.createElement("code");
codeEl.textContent = mSrc;
codePre.appendChild(codeEl);
highlightMermaidSource(codeEl);
codeBody.appendChild(codePre);
codeView.appendChild(codeBody);
codeView.addEventListener("pointerdown", function (ev) { ev.stopPropagation(); });
box.appendChild(codeView);

Expand All @@ -691,17 +733,17 @@ if (box && box.classList && box.classList.contains("mermaid")) {
"background:" + (cdark ? "rgba(36,38,45,.92)" : "rgba(255,255,255,.94)") + ";color:" + (cdark ? "#e8e8ea" : "#222") +
";border:1px solid " + (cdark ? "rgba(255,255,255,.14)" : "rgba(0,0,0,.12)") + ";box-shadow:0 2px 8px rgba(0,0,0,.16);";
codeBtn.addEventListener("pointerdown", function (ev) { ev.stopPropagation(); });
codeBtn.addEventListener("click", function (ev) {
ev.stopPropagation(); ev.preventDefault();
var opening = codeView.style.display === "none";

function showCode(opening) {
codeView.style.display = opening ? "block" : "none";
codeBtn.style.display = opening ? "none" : "";
var svgEl = box.querySelector("svg");
if (svgEl) svgEl.style.display = opening ? "none" : "";
var zbar = box.querySelector("[data-htmlit-tools]");
if (zbar) zbar.style.display = opening ? "none" : "inline-flex";
codeBtn.textContent = opening ? "Diagram" : "Code";
codeBtn.title = opening ? "Show the diagram" : "Show the Mermaid source";
});
}
codeBtn.addEventListener("click", function (ev) { ev.stopPropagation(); ev.preventDefault(); showCode(true); });
backBtn.addEventListener("click", function (ev) { ev.stopPropagation(); ev.preventDefault(); showCode(false); });
box.appendChild(codeBtn);
}

Expand Down
22 changes: 19 additions & 3 deletions skills/htmlit/client/exporting.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,32 @@ var ARTIFACT_CSS =
'.mermaid{position:relative!important;width:100%;height:clamp(260px,52vh,480px)!important;min-height:0!important;margin:14px 0;box-sizing:border-box;overflow:hidden;border:1px solid rgba(0,0,0,.13);border-radius:12px;background:#fcfcfd;}' +
'.mermaid svg{max-width:100%!important;width:100%!important;height:100%!important;display:block;}' +
'html.dark .mermaid{border-color:rgba(255,255,255,.13);background:#1b1c21;}' +
'.htmlit-code{margin:14px 0;border:1px solid rgba(128,128,140,.28);border-radius:8px;overflow:hidden;background:#fff;}' +
'html.dark .htmlit-code{border-color:rgba(140,140,160,.26);background:#0d1117;}' +
'.htmlit-code{margin:14px 0;border-radius:8px;overflow:hidden;background:#fff;}' +
'html.dark .htmlit-code{background:#0d1117;}' +
// A diff keeps its framed box and titled header; a plain code block is borderless.
'.htmlit-code.htmlit-diff{border:1px solid rgba(128,128,140,.28);}' +
'html.dark .htmlit-code.htmlit-diff{border-color:rgba(140,140,160,.26);}' +
'.htmlit-code-hd{font:600 11px/1.6 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:.06em;text-transform:uppercase;color:#6b7280;background:rgba(128,128,140,.09);padding:6px 12px;border-bottom:1px solid rgba(128,128,140,.22);}' +
'html.dark .htmlit-code-hd{color:#8b949e;background:rgba(255,255,255,.04);border-bottom-color:rgba(140,140,160,.2);}' +
// Plain code header: no bar, just the language on the left and Copy on the right.
'.htmlit-code:not(.htmlit-diff)>.htmlit-code-hd{display:flex;align-items:center;justify-content:space-between;gap:10px;background:transparent;border-bottom:0;padding:4px 6px 2px;}' +
'.htmlit-copy{font:500 11px/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;text-transform:none;letter-spacing:0;color:#57606a;background:rgba(128,128,140,.1);border:1px solid rgba(128,128,140,.3);border-radius:6px;padding:4px 9px;cursor:pointer;}' +
'.htmlit-copy:hover{background:rgba(128,128,140,.2);color:#1f2328;}' +
'html.dark .htmlit-copy{color:#9198a1;background:rgba(255,255,255,.06);border-color:rgba(140,140,160,.24);}' +
'html.dark .htmlit-copy:hover{background:rgba(255,255,255,.11);color:#e6edf3;}' +
'.htmlit-copy.htmlit-copied{color:#1a7f37;border-color:rgba(26,127,55,.5);}' +
'html.dark .htmlit-copy.htmlit-copied{color:#3fb950;border-color:rgba(63,185,80,.5);}' +
'.htmlit-code-actions{display:inline-flex;align-items:center;gap:6px;}' +
// The Mermaid source view overlays the (scrollable) diagram, so keep its titled
// header pinned and opaque - reusing the code bg - instead of scrolling away.
'.htmlit-code[data-htmlit-code]>.htmlit-code-hd{position:sticky;top:0;z-index:2;background:#fff;}' +
'html.dark .htmlit-code[data-htmlit-code]>.htmlit-code-hd{background:#0d1117;}' +
'.htmlit-code-body{display:flex;overflow-x:auto;background:#fff;}' +
'html.dark .htmlit-code-body{background:#0d1117;}' +
'.htmlit-linenos{position:sticky;left:0;z-index:1;flex:0 0 auto;box-sizing:border-box;text-align:right;padding:12px 12px;white-space:pre;user-select:none;color:#c2c8d0;background:inherit;border-right:1px solid rgba(128,128,140,.2);}' +
'html.dark .htmlit-linenos{color:#4b5563;}' +
'.htmlit-code-body>pre{margin:0!important;padding:12px 16px!important;flex:1 1 auto;min-width:0;overflow:visible!important;background:transparent!important;border:0!important;border-radius:0!important;}' +
'.htmlit-code-body>pre>code{background:transparent!important;padding:0!important;overflow:visible!important;white-space:pre!important;display:block;}' +
'.htmlit-code-body>pre>code{background:transparent!important;border:0!important;border-radius:0!important;box-shadow:none!important;padding:0!important;overflow:visible!important;white-space:pre!important;display:block;}' +
'.htmlit-code-body>pre>code,.htmlit-linenos{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace!important;font-size:13px!important;line-height:1.55!important;tab-size:2;}' +
'.htmlit-diff .htmlit-code-hd{display:flex;align-items:center;justify-content:space-between;gap:12px;-webkit-user-select:none;user-select:none;}' +
'.htmlit-diff-toggle{display:inline-flex;align-items:center;gap:2px;padding:2px;border:1px solid rgba(128,128,140,.22);border-radius:999px;background:rgba(255,255,255,.55);}' +
Expand Down
9 changes: 1 addition & 8 deletions skills/htmlit/client/morph.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CFG, KEY, Presence, SseEvent, state as appState } from "./state.js";
import { root, ui, applyDocTitle } from "./dom.js";
import { api, esc } from "./util.js";
import { api, copyText, esc } from "./util.js";
import { decorateAnswers } from "./exporting.js";
import { detectNewContent, highlightNew, initContent } from "./content.js";
import { refreshFavicon } from "./favicon.js";
Expand Down Expand Up @@ -61,13 +61,6 @@ export function reload() {
}

export function resumeCmd() { return "htmlit resume " + (CFG.file || ""); }
export function copyText(t) {
try { if (navigator.clipboard) return navigator.clipboard.writeText(t); } catch (e) {}
try {
var ta = document.createElement("textarea"); ta.value = t; ta.setAttribute("data-htmlit", "");
document.body.appendChild(ta); ta.select(); document.execCommand("copy"); ta.remove();
} catch (e) {}
}
// Render the "kept - here's how to resume" box. `mode`: "live" (still open) or "ended".
export function renderKeepHint(mode) {
if (!ui.keephint) return;
Expand Down
15 changes: 13 additions & 2 deletions skills/htmlit/client/rail.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,19 @@ export function anchorContainerEl(h) {
export function anchorRight(h) {
var el = anchorContainerEl(h);
if (!el) return null;
var r = el.getBoundingClientRect();
return (r.width || r.height) ? r.right : null;
// The rail hugs the right of the content COLUMN, not the immediate container: a
// comment inside a narrow cell, grid card or <code> span would otherwise drag its
// card into the middle of the page. Take the widest block-level ancestor's right
// edge (up to, but not including, <body>) - that is the text column's edge.
var best = null;
for (var n = el; n && n !== document.body && n.nodeType === 1; n = n.parentElement) {
var display;
try { display = getComputedStyle(n).display; } catch (e) { continue; }
if (display === "inline") continue; // inline spans don't define the column width
var r = n.getBoundingClientRect();
if ((r.width || r.height) && (best == null || r.right > best)) best = r.right;
}
return best;
}
export function setRailMargin(on) {
if (on === railOn) return;
Expand Down
Loading
Loading