diff --git a/editable-html-slides/LICENSE b/editable-html-slides/LICENSE
new file mode 100644
index 0000000..17dd25f
--- /dev/null
+++ b/editable-html-slides/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 rossyao2022
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/editable-html-slides/SKILL.md b/editable-html-slides/SKILL.md
new file mode 100644
index 0000000..723f698
--- /dev/null
+++ b/editable-html-slides/SKILL.md
@@ -0,0 +1,116 @@
+---
+name: editable-html-slides
+description: Add a self-contained, dependency-free in-browser WYSIWYG editor to any HTML slide deck so the deck stays editable after it is generated — click text to rewrite it, drop in images, drag/resize/delete objects, undo/redo, reorder slides via a filmstrip, autosave to localStorage, and export a clean standalone HTML. Use this skill WHENEVER the user wants an editable HTML presentation, wants to tweak or adjust slides in the browser instead of editing source, says the generated HTML cannot be edited / 可编辑的 HTML / 随手改 / 加点图片, asks to add inline or visual or WYSIWYG editing to a deck, or wants drag-and-drop slide reordering — even if they don't name this skill. Pairs especially well with html-ppt / reveal-style decks (preserves presenter view and speaker notes) but works on any deck using the .slide / .is-active convention. Needs no Node, npm, build step, or network.
+license: MIT
+author: rossyao2022
+version: "1.0"
+tags:
+ - presentation
+ - slides
+ - html
+ - editor
+ - wysiwyg
+ - frontend
+---
+
+# editable-html-slides
+
+Turn a *static* HTML deck into one a non-developer can edit in the browser —
+without rebuilding it, without a framework, and without touching the original
+design. The whole editor is two small files (`editor.js` + `editor.css`) that
+layer on top of an existing deck.
+
+## Why this exists
+
+AI coding agents are great at *generating* gorgeous HTML decks, but the output
+is "read-only" to a normal user — to change a title or swap an image they'd have
+to open the source. This skill closes that gap: it bolts a visual editor onto
+the deck so the user can keep editing after handoff, while the original theme,
+animations, and (for presenter decks) the speaker-notes / presenter view stay
+exactly as they were.
+
+It is deliberately **zero-dependency**: no Node, no npm, no bundler, no CDN. The
+editor is plain ES5-ish JavaScript and CSS that run from `file://`.
+
+## What the editor gives the end user
+
+- **Inline text editing** — click any heading / paragraph / list item and type.
+- **Add image** — pick a local file (embedded as a data URL, so the deck stays a
+ single file) or paste with ⌘/Ctrl+V.
+- **Add text box** — free-floating text, double-click to edit.
+- **Drag / resize / delete** added objects (corner handle; Delete key).
+- **Undo / redo** — ⌘/Ctrl+Z and ⌘/Ctrl+Shift+Z (snapshot history).
+- **Reorder slides** — a draggable thumbnail filmstrip (▦ 页序 / "Pages").
+- **Autosave** to `localStorage` (survives reload) and **export** a clean
+ standalone HTML with all edits baked in and the editor code stripped out.
+- Toggle the whole thing with the **✎ button** or the **E** key; **Esc** / ✓ to exit.
+
+Everything is audience-facing-safe: in normal (non-edit) mode the deck looks and
+behaves identically to before, so you can still present it.
+
+## How to apply it (the normal workflow)
+
+You usually have a deck folder like `my-talk/index.html`. Run the injector:
+
+```bash
+python3 scripts/inject.py /abs/path/to/my-talk/index.html
+```
+
+That copies `editor.css` + `editor.js` next to the deck and wires two tags in
+(idempotently — safe to run twice). Then open `index.html` and press **E**.
+
+If you'd rather wire it by hand, add these two lines to the deck:
+
+```html
+
+
+
+
+
+```
+
+Loading order matters: `editor.js` must come **after** any deck runtime
+(e.g. html-ppt's `runtime.js`) so it can see the rendered slides. Also copy
+`assets/editor.css` and `assets/editor.js` into the deck folder.
+
+## Requirements the deck must meet
+
+The editor targets the common reveal/html-ppt slide convention:
+
+- Each slide is an element with class **`.slide`**; the visible one has
+ **`.is-active`** (the editor edits whichever slide is active).
+- Ideally slides live inside a **`.deck`** container. (The editor scopes to
+ `.deck .slide` and ignores any `.overview` clone grid — see the gotcha below.)
+- Speaker notes, if any, are in **``** — the editor never
+ turns these into editable on-slide text.
+
+If a deck doesn't use these conventions, point the user to
+`references/runtime-notes.md` for how to adapt the two selectors.
+
+## When NOT to reach for this
+
+- The user wants an editable **PowerPoint (.pptx)** for Office users → that's a
+ different tool (e.g. a pptx skill / Claude for PowerPoint), not this.
+- The user wants to *generate* a brand-new deck from scratch → generate it first
+ (e.g. with an html-ppt / frontend-slides skill), then apply this skill to make
+ it editable.
+- The deck has no `.slide` structure and you can't adapt the selectors.
+
+## Files in this skill
+
+- `assets/editor.js` — the editor (no deps). Safe to read; ~500 lines.
+- `assets/editor.css` — editor chrome (toolbar, filmstrip, handles, toast).
+- `scripts/inject.py` — idempotent injector + asset copier.
+- `examples/demo.html` — a tiny self-contained deck (with a 30-line minimal
+ runtime) so you can see the editor work with no other dependencies. Open it and
+ press E.
+- `references/runtime-notes.md` — internals & adaptation: the data model
+ (slot-fixed content-swap reorder), the `.overview` double-count gotcha, key
+ handling vs the deck runtime, and how to retarget the selectors.
+
+## Verifying it works
+
+Open the deck with `?edtest=1` in a headless browser; the editor runs a self-test
+(add image + text box + move + undo/redo + reorder + export) and writes the
+result to `document.body[data-edtest]` as `PASS …` or `FAIL …`. `examples/demo.html`
+is the quickest thing to try.
diff --git a/editable-html-slides/assets/editor.css b/editable-html-slides/assets/editor.css
new file mode 100644
index 0000000..a9a67c6
--- /dev/null
+++ b/editable-html-slides/assets/editor.css
@@ -0,0 +1,110 @@
+/* editor.css — in-browser WYSIWYG layer for claude-edit-skill
+ * Adds: inline text editing, add image / text box, drag / resize / delete,
+ * localStorage persistence, export. Only visible when edit mode is on. */
+
+/* ---- floating launcher (always visible) ---- */
+#ed-launch{
+ position:fixed; right:18px; bottom:16px; z-index:9000;
+ font-family:var(--font-sans,sans-serif); font-size:13px; font-weight:600;
+ color:#fff; background:var(--accent,#d97757);
+ border:none; border-radius:999px; padding:10px 16px;
+ box-shadow:0 8px 24px rgba(0,0,0,.4); cursor:pointer;
+ display:flex; align-items:center; gap:7px;
+ transition:transform .15s ease, filter .15s ease;
+}
+#ed-launch:hover{ transform:translateY(-1px); filter:brightness(1.06); }
+
+/* ---- toolbar (only in edit mode) ---- */
+#ed-bar{
+ position:fixed; top:14px; left:50%; transform:translateX(-50%);
+ z-index:9000; display:none; gap:8px; align-items:center;
+ padding:8px 10px; border-radius:12px;
+ background:rgba(28,25,23,.92); backdrop-filter:blur(10px);
+ border:1px solid var(--border-strong,rgba(217,119,87,.32));
+ box-shadow:0 14px 40px rgba(0,0,0,.5);
+ font-family:var(--font-sans,sans-serif);
+}
+body.ed-on #ed-bar{ display:flex; }
+#ed-bar button{
+ font-size:13px; font-weight:600; color:var(--text-1,#f5efe9);
+ background:var(--surface-2,#332d29); border:1px solid var(--border,rgba(217,119,87,.16));
+ border-radius:8px; padding:7px 11px; cursor:pointer; white-space:nowrap;
+ transition:background .12s ease;
+}
+#ed-bar button:hover{ background:var(--accent,#d97757); color:#fff; }
+#ed-bar button.primary{ background:var(--accent,#d97757); color:#fff; border-color:transparent; }
+#ed-bar button:disabled{ opacity:.38; cursor:default; }
+#ed-bar button:disabled:hover{ background:var(--surface-2,#332d29); color:var(--text-1,#f5efe9); }
+#ed-bar .ed-sep{ width:1px; height:22px; background:var(--border,rgba(217,119,87,.22)); margin:0 2px; }
+#ed-bar .ed-page{ font-family:var(--font-mono,monospace); font-size:12px; color:var(--text-3,#9a8b7d); padding:0 4px; }
+
+/* ---- filmstrip (reorder by drag) ---- */
+#ed-film{
+ position:fixed; left:0; right:0; bottom:0; z-index:8999; display:none;
+ gap:10px; align-items:stretch; padding:12px 14px; overflow-x:auto;
+ background:rgba(22,19,16,.96); backdrop-filter:blur(10px);
+ border-top:1px solid var(--border-strong,rgba(217,119,87,.32));
+}
+#ed-film .ed-chip{
+ flex:0 0 auto; width:150px; min-height:62px; cursor:grab;
+ display:flex; flex-direction:column; gap:6px; padding:10px 12px;
+ background:var(--surface,#262220); border:1px solid var(--border,rgba(217,119,87,.18));
+ border-radius:10px; font-family:var(--font-sans,sans-serif); user-select:none;
+ transition:border-color .12s, transform .12s;
+}
+#ed-film .ed-chip:hover{ border-color:var(--accent,#d97757); }
+#ed-film .ed-chip.cur{ border-color:var(--accent,#d97757); box-shadow:0 0 0 1px var(--accent,#d97757) inset; }
+#ed-film .ed-chip.dragging{ opacity:.45; }
+#ed-film .ed-chip.over{ transform:translateY(-3px); border-color:var(--accent-2,#e8a87c); border-style:dashed; }
+#ed-film .ed-chip .ci{ font-family:var(--font-mono,monospace); font-size:11px; color:var(--accent,#d97757); font-weight:700; }
+#ed-film .ed-chip .ct{ font-size:13px; color:var(--text-1,#f5efe9); line-height:1.35; }
+body.ed-on #ed-launch{ bottom:74px; } /* lift launcher above an open filmstrip */
+
+/* ---- editable text affordance ---- */
+body.ed-on [data-ed-text]{
+ outline:1px dashed rgba(217,119,87,.45); outline-offset:3px;
+ border-radius:3px; cursor:text;
+}
+body.ed-on [data-ed-text]:hover{ outline-color:var(--accent,#d97757); }
+body.ed-on [data-ed-text]:focus{
+ outline:2px solid var(--accent,#d97757); background:rgba(217,119,87,.07);
+}
+
+/* ---- added objects (images / text boxes) ---- */
+.ed-obj{ position:absolute; z-index:5; cursor:grab; }
+.ed-obj img{ display:block; width:100%; height:auto; border-radius:8px; pointer-events:none; }
+.ed-obj.ed-textbox{
+ min-width:80px; padding:8px 12px; color:var(--text-1,#f5efe9);
+ font-family:var(--font-sans,sans-serif); font-size:22px; line-height:1.4;
+}
+body.ed-on .ed-obj{ outline:1px solid rgba(217,119,87,.4); }
+body.ed-on .ed-obj.sel{ outline:2px solid var(--accent,#d97757); cursor:grab; }
+body:not(.ed-on) .ed-obj{ cursor:default; }
+
+/* resize handle + delete button on selected obj (edit mode only) */
+.ed-handle{
+ position:absolute; right:-7px; bottom:-7px; width:14px; height:14px;
+ background:var(--accent,#d97757); border:2px solid #fff; border-radius:50%;
+ cursor:nwse-resize; display:none; z-index:6;
+}
+.ed-del{
+ position:absolute; right:-10px; top:-12px; width:22px; height:22px;
+ background:#e06c5e; color:#fff; border:none; border-radius:50%;
+ font-size:13px; line-height:1; cursor:pointer; display:none; z-index:6;
+}
+body.ed-on .ed-obj.sel .ed-handle,
+body.ed-on .ed-obj.sel .ed-del{ display:block; }
+
+/* toast */
+#ed-toast{
+ position:fixed; bottom:70px; right:18px; z-index:9001;
+ background:rgba(28,25,23,.95); color:var(--text-1,#f5efe9);
+ border:1px solid var(--accent,#d97757); border-radius:10px;
+ padding:10px 16px; font-family:var(--font-sans,sans-serif); font-size:13px;
+ opacity:0; transform:translateY(8px); transition:opacity .2s, transform .2s;
+ pointer-events:none;
+}
+#ed-toast.show{ opacity:1; transform:translateY(0); }
+
+/* hide all editor chrome in presenter-preview iframes */
+body.is-preview #ed-launch, body.is-preview #ed-bar{ display:none !important; }
diff --git a/editable-html-slides/assets/editor.js b/editable-html-slides/assets/editor.js
new file mode 100644
index 0000000..db085ca
--- /dev/null
+++ b/editable-html-slides/assets/editor.js
@@ -0,0 +1,387 @@
+/* editor.js — self-contained WYSIWYG layer for claude-edit-skill
+ * No dependencies. Coexists with html-ppt runtime.js (presenter mode).
+ *
+ * Inline text edit · add image (data URL) / text box · drag / resize / delete ·
+ * UNDO / REDO (snapshot stack) · FILMSTRIP reorder (slot-fixed, content-swap so
+ * the presenter runtime's cached page order is never disturbed) ·
+ * localStorage autosave + restore · export clean standalone HTML.
+ * Toggle with E or the ✎ button. */
+(function () {
+ "use strict";
+
+ var params = new URLSearchParams(location.search);
+ if (params.has("preview")) { document.body.classList.add("is-preview"); return; }
+
+ var TEXT_SEL = "h1,h2,h3,h4,p,li,blockquote,.kicker,.lede,.t,.d";
+ var KEY = "cesk2::" + location.pathname; // v2 storage (payload snapshots)
+ var MAX_HISTORY = 80;
+
+ var slots = []; // .slide elements, FIXED DOM order (never moved)
+ var sel = null; // selected .ed-obj
+ var drag = null;
+ var saveTimer = null;
+ var hist = []; // array of JSON snapshot strings
+ var hi = -1; // history pointer
+
+ /* ---------- tiny helpers ---------- */
+ function $(s, c) { return (c || document).querySelector(s); }
+ function activeSlot() { return $(".slide.is-active") || slots[0]; }
+ function slotPos(el) { return slots.indexOf(el); }
+ function clsClean(node) {
+ return (node.className || "").split(/\s+/)
+ .filter(function (c) { return c && c !== "is-active" && c !== "sel"; }).join(" ");
+ }
+
+ function sanitizeHTML(slot) {
+ var c = slot.cloneNode(true);
+ c.querySelectorAll("[contenteditable]").forEach(function (n) { n.removeAttribute("contenteditable"); });
+ c.querySelectorAll("[data-ed-text]").forEach(function (n) { n.removeAttribute("data-ed-text"); });
+ c.querySelectorAll(".ed-handle,.ed-del").forEach(function (n) { n.remove(); });
+ c.querySelectorAll(".ed-obj").forEach(function (n) { n.classList.remove("sel", "editing-text"); n.removeAttribute("contenteditable"); });
+ return c.innerHTML;
+ }
+ function payloadOf(slot) {
+ return { cls: clsClean(slot), title: slot.getAttribute("data-title") || "", html: sanitizeHTML(slot) };
+ }
+ function applyPayload(slot, p) {
+ var active = slot.classList.contains("is-active");
+ slot.className = p.cls + (active ? " is-active" : "");
+ if (p.title != null) slot.setAttribute("data-title", p.title);
+ slot.innerHTML = p.html;
+ markSlide(slot);
+ if (isOn()) setEditableIn(slot, true);
+ }
+ function snapshot() { return slots.map(payloadOf); }
+
+ /* ---------- history ---------- */
+ function record() {
+ var s = JSON.stringify(snapshot());
+ if (s === hist[hi]) return; // no real change
+ hist = hist.slice(0, hi + 1);
+ hist.push(s); hi++;
+ if (hist.length > MAX_HISTORY) { hist.shift(); hi--; }
+ persist(s);
+ updateUndoBtns();
+ }
+ function restore(jsonStr) {
+ var arr = JSON.parse(jsonStr);
+ arr.forEach(function (p, k) { if (slots[k]) applyPayload(slots[k], p); });
+ deselect();
+ if (filmOpen) buildFilm();
+ updateUndoBtns();
+ }
+ function undo() { if (hi > 0) { hi--; restore(hist[hi]); persist(hist[hi]); toast("已撤销"); } }
+ function redo() { if (hi < hist.length - 1) { hi++; restore(hist[hi]); persist(hist[hi]); toast("已重做"); } }
+ function persist(s) { try { localStorage.setItem(KEY, s); } catch (e) {} }
+ function recordDebounced() { clearTimeout(saveTimer); saveTimer = setTimeout(record, 700); }
+
+ /* ---------- text marking ---------- */
+ function markSlide(slot) {
+ slot.querySelectorAll(TEXT_SEL).forEach(function (el) {
+ if (el.closest("aside.notes")) return;
+ if (el.closest(".ed-obj")) return;
+ el.setAttribute("data-ed-text", "");
+ });
+ }
+ function setEditableIn(slot, on) {
+ slot.querySelectorAll("[data-ed-text]").forEach(function (el) {
+ if (on) el.setAttribute("contenteditable", "true");
+ else el.removeAttribute("contenteditable");
+ });
+ }
+ function setEditable(on) { slots.forEach(function (s) { setEditableIn(s, on); }); }
+
+ /* ---------- edit mode ---------- */
+ function isOn() { return document.body.classList.contains("ed-on"); }
+ function toggleEdit(force) {
+ var on = (force === undefined) ? !isOn() : force;
+ document.body.classList.toggle("ed-on", on);
+ setEditable(on);
+ if (!on) { deselect(); closeFilm(); }
+ launch.textContent = on ? "✓ 完成编辑" : "✎ 编辑";
+ if (on) toast("编辑模式:点字改 · 加图/拖动 · ⌘Z 撤销 · 页序可拖");
+ }
+
+ /* ---------- objects ---------- */
+ function ensureRelative(slot) { if (getComputedStyle(slot).position === "static") slot.style.position = "relative"; }
+ function placeCenter(slot, w) {
+ return { left: Math.max(20, (slot.clientWidth - w) / 2), top: Math.max(20, (slot.clientHeight - w * 0.62) / 2) };
+ }
+ function addImage(dataURL) {
+ var slot = activeSlot(); ensureRelative(slot);
+ var w = Math.min(460, slot.clientWidth * 0.4), p = placeCenter(slot, w);
+ var o = document.createElement("div");
+ o.className = "ed-obj ed-img";
+ o.style.left = p.left + "px"; o.style.top = p.top + "px"; o.style.width = w + "px";
+ o.innerHTML = ' ';
+ slot.appendChild(o); selectObj(o); record(); toast("已添加图片,可拖动 / 拖右下角缩放");
+ }
+ function addTextBox() {
+ var slot = activeSlot(); ensureRelative(slot);
+ var p = placeCenter(slot, 280);
+ var o = document.createElement("div");
+ o.className = "ed-obj ed-textbox";
+ o.style.left = p.left + "px"; o.style.top = p.top + "px"; o.style.width = "280px";
+ o.textContent = "双击编辑文字";
+ slot.appendChild(o); selectObj(o); record();
+ }
+ function selectObj(o) {
+ deselect(); sel = o; o.classList.add("sel");
+ var h = document.createElement("div"); h.className = "ed-handle"; o.appendChild(h);
+ var d = document.createElement("button"); d.className = "ed-del"; d.textContent = "✕"; o.appendChild(d);
+ }
+ function deselect() {
+ if (!sel) return;
+ sel.querySelectorAll(".ed-handle,.ed-del").forEach(function (n) { n.remove(); });
+ sel.classList.remove("sel"); sel = null;
+ }
+ function removeSel() { if (!sel) { return; } sel.remove(); sel = null; record(); }
+
+ /* ---------- pointer drag / resize ---------- */
+ document.addEventListener("mousedown", function (e) {
+ if (!isOn()) return;
+ var obj = e.target.closest(".ed-obj");
+ if (!obj) { if (!e.target.closest("#ed-bar") && !e.target.closest("#ed-film")) deselect(); return; }
+ if (obj.classList.contains("editing-text")) return;
+ if (e.target.classList.contains("ed-del")) { removeSel(); e.preventDefault(); return; }
+ selectObj(obj);
+ var resizing = e.target.classList.contains("ed-handle");
+ var r = obj.getBoundingClientRect();
+ drag = { obj: obj, resizing: resizing, x: e.clientX, y: e.clientY,
+ left: parseFloat(obj.style.left) || 0, top: parseFloat(obj.style.top) || 0, w: r.width };
+ e.preventDefault();
+ });
+ document.addEventListener("mousemove", function (e) {
+ if (!drag) return;
+ var dx = e.clientX - drag.x, dy = e.clientY - drag.y;
+ if (drag.resizing) drag.obj.style.width = Math.max(40, drag.w + dx) + "px";
+ else { drag.obj.style.left = (drag.left + dx) + "px"; drag.obj.style.top = (drag.top + dy) + "px"; }
+ });
+ document.addEventListener("mouseup", function () { if (drag) { drag = null; record(); } });
+
+ document.addEventListener("dblclick", function (e) {
+ if (!isOn()) return;
+ var tb = e.target.closest(".ed-obj.ed-textbox");
+ if (!tb) return;
+ tb.classList.add("editing-text"); tb.setAttribute("contenteditable", "true"); tb.focus();
+ });
+ document.addEventListener("focusout", function (e) {
+ var tb = e.target.closest && e.target.closest(".ed-obj.ed-textbox");
+ if (tb) { tb.classList.remove("editing-text"); tb.removeAttribute("contenteditable"); record(); }
+ });
+ document.addEventListener("input", function (e) {
+ if (!isOn()) return;
+ if (e.target.closest("[data-ed-text]") || e.target.closest(".ed-obj")) recordDebounced();
+ });
+ document.addEventListener("paste", function (e) {
+ if (!isOn()) return;
+ if (document.activeElement && document.activeElement.isContentEditable) return;
+ var items = (e.clipboardData || {}).items || [];
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].type.indexOf("image") === 0) {
+ var rd = new FileReader(); rd.onload = function () { addImage(rd.result); };
+ rd.readAsDataURL(items[i].getAsFile()); e.preventDefault(); break;
+ }
+ }
+ });
+
+ /* ---------- keyboard: shield runtime + shortcuts ---------- */
+ window.addEventListener("keydown", function (e) {
+ if (!isOn()) return;
+ var k = e.key.toLowerCase();
+ if ((e.metaKey || e.ctrlKey) && k === "z") { e.preventDefault(); e.stopImmediatePropagation(); if (e.shiftKey) redo(); else undo(); return; }
+ if ((e.metaKey || e.ctrlKey) && k === "y") { e.preventDefault(); e.stopImmediatePropagation(); redo(); return; }
+ if ((e.metaKey || e.ctrlKey) && k === "s") { e.preventDefault(); e.stopImmediatePropagation(); exportHTML(); return; }
+ if (e.key === "Escape") { if (sel) deselect(); else toggleEdit(false); e.stopImmediatePropagation(); return; }
+ var typing = document.activeElement && document.activeElement.isContentEditable;
+ if ((e.key === "Delete" || (e.key === "Backspace" && !typing)) && sel) {
+ removeSel(); e.preventDefault(); e.stopImmediatePropagation(); return;
+ }
+ var nav = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", " ", "PageUp", "PageDown", "Home", "End"];
+ var letters = ["f", "s", "t", "a", "o", "n", "r"];
+ if (nav.indexOf(e.key) >= 0 || letters.indexOf(k) >= 0) e.stopImmediatePropagation();
+ }, true);
+
+ /* ---------- navigation ---------- */
+ function curN() { return slotPos(activeSlot()) + 1; }
+ function goto(n) {
+ n = Math.max(1, Math.min(slots.length, n));
+ location.hash = "#/" + n;
+ setTimeout(function () { deselect(); updatePage(); if (filmOpen) buildFilm(); }, 60);
+ }
+ function updatePage() { page.textContent = curN() + " / " + slots.length; }
+
+ /* ---------- filmstrip (reorder by content-swap) ---------- */
+ var filmOpen = false, filmEl = null, dragFrom = -1;
+ function titleOf(slot) {
+ var t = slot.getAttribute("data-title");
+ var h = slot.querySelector("h1,h2,.h1,.h2");
+ var txt = (h ? h.textContent : (t || "")).trim().replace(/\s+/g, " ");
+ return txt.length > 22 ? txt.slice(0, 22) + "…" : (txt || t || "");
+ }
+ function toggleFilm() { filmOpen ? closeFilm() : openFilm(); }
+ function openFilm() { filmOpen = true; buildFilm(); }
+ function closeFilm() { filmOpen = false; if (filmEl) filmEl.style.display = "none"; }
+ function buildFilm() {
+ if (!filmEl) { filmEl = document.createElement("div"); filmEl.id = "ed-film"; document.body.appendChild(filmEl); }
+ filmEl.style.display = "flex";
+ filmEl.innerHTML = "";
+ slots.forEach(function (slot, i) {
+ var chip = document.createElement("div");
+ chip.className = "ed-chip" + (i === curN() - 1 ? " cur" : "");
+ chip.draggable = true; chip.dataset.i = i;
+ chip.innerHTML = '' + (i + 1) + ' ' + (titleOf(slot) || "—") + " ";
+ chip.addEventListener("click", function () { goto(i + 1); });
+ chip.addEventListener("dragstart", function () { dragFrom = i; chip.classList.add("dragging"); });
+ chip.addEventListener("dragend", function () { chip.classList.remove("dragging"); });
+ chip.addEventListener("dragover", function (ev) { ev.preventDefault(); chip.classList.add("over"); });
+ chip.addEventListener("dragleave", function () { chip.classList.remove("over"); });
+ chip.addEventListener("drop", function (ev) { ev.preventDefault(); chip.classList.remove("over"); reorder(dragFrom, i); });
+ filmEl.appendChild(chip);
+ });
+ }
+ function reorder(from, to) {
+ if (from < 0 || to < 0 || from === to) return;
+ var arr = snapshot();
+ var moved = arr.splice(from, 1)[0];
+ arr.splice(to, 0, moved);
+ arr.forEach(function (p, k) { applyPayload(slots[k], p); });
+ record();
+ goto(to + 1);
+ toast("第 " + (from + 1) + " 页 → 第 " + (to + 1) + " 页");
+ }
+
+ /* ---------- export ---------- */
+ function buildExportHTML() {
+ var root = document.documentElement.cloneNode(true);
+ root.querySelectorAll("#ed-launch,#ed-bar,#ed-toast,#ed-film").forEach(function (n) { n.remove(); });
+ root.querySelectorAll('link[href$="editor.css"],script[src$="editor.js"]').forEach(function (n) { n.remove(); });
+ root.querySelector("body").classList.remove("ed-on", "is-preview");
+ root.querySelectorAll(".slide").forEach(function (s) {
+ s.querySelectorAll("[contenteditable]").forEach(function (n) { n.removeAttribute("contenteditable"); });
+ s.querySelectorAll("[data-ed-text]").forEach(function (n) { n.removeAttribute("data-ed-text"); });
+ s.querySelectorAll(".ed-handle,.ed-del").forEach(function (n) { n.remove(); });
+ s.querySelectorAll(".ed-obj").forEach(function (n) { n.classList.remove("sel", "editing-text"); });
+ });
+ return "\n" + root.outerHTML;
+ }
+ function exportHTML() {
+ var blob = new Blob([buildExportHTML()], { type: "text/html" });
+ var a = document.createElement("a");
+ a.href = URL.createObjectURL(blob); a.download = "claude-edit-skill.html";
+ document.body.appendChild(a); a.click(); a.remove();
+ toast("已导出 claude-edit-skill.html(含修改/图片/页序)");
+ }
+ function resetAll() {
+ try { localStorage.removeItem(KEY); } catch (e) {}
+ location.reload();
+ }
+
+ /* ---------- toast ---------- */
+ var toastEl, toastTimer;
+ function toast(msg) {
+ if (!toastEl) { toastEl = document.createElement("div"); toastEl.id = "ed-toast"; document.body.appendChild(toastEl); }
+ toastEl.textContent = msg; toastEl.classList.add("show");
+ clearTimeout(toastTimer); toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2400);
+ }
+
+ /* ---------- chrome (launcher + toolbar) ---------- */
+ var launch = document.createElement("button");
+ launch.id = "ed-launch"; launch.textContent = "✎ 编辑";
+ launch.addEventListener("click", function () { toggleEdit(); });
+
+ var bar = document.createElement("div"); bar.id = "ed-bar";
+ var page = document.createElement("span"); page.className = "ed-page";
+ var undoBtn, redoBtn;
+ function mkBtn(label, fn, cls) {
+ var b = document.createElement("button"); b.textContent = label; if (cls) b.className = cls;
+ b.addEventListener("click", fn); return b;
+ }
+ function sep() { var s = document.createElement("span"); s.className = "ed-sep"; return s; }
+ function updateUndoBtns() {
+ if (undoBtn) undoBtn.disabled = hi <= 0;
+ if (redoBtn) redoBtn.disabled = hi >= hist.length - 1;
+ }
+ var fileInput = document.createElement("input");
+ fileInput.type = "file"; fileInput.accept = "image/*"; fileInput.style.display = "none";
+ fileInput.addEventListener("change", function () {
+ var f = fileInput.files && fileInput.files[0]; if (!f) return;
+ var rd = new FileReader(); rd.onload = function () { addImage(rd.result); };
+ rd.readAsDataURL(f); fileInput.value = "";
+ });
+
+ bar.appendChild(mkBtn("‹", function () { goto(curN() - 1); }));
+ bar.appendChild(page);
+ bar.appendChild(mkBtn("›", function () { goto(curN() + 1); }));
+ bar.appendChild(sep());
+ undoBtn = mkBtn("↶ 撤销", undo); redoBtn = mkBtn("↷ 重做", redo);
+ bar.appendChild(undoBtn); bar.appendChild(redoBtn);
+ bar.appendChild(sep());
+ bar.appendChild(mkBtn("🖼 加图片", function () { fileInput.click(); }));
+ bar.appendChild(mkBtn("T 文本框", function () { addTextBox(); }));
+ bar.appendChild(mkBtn("▦ 页序", toggleFilm));
+ bar.appendChild(sep());
+ bar.appendChild(mkBtn("↺ 还原全部", resetAll));
+ bar.appendChild(mkBtn("⬇ 导出", exportHTML));
+ bar.appendChild(mkBtn("✓ 完成", function () { toggleEdit(false); }, "primary"));
+
+ /* ---------- init ---------- */
+ function init() {
+ // Scope to the real deck only — runtime.js clones every slide into a
+ // hidden .overview grid, so a bare '.slide' selector would double-count.
+ var deck = document.querySelector(".deck");
+ slots = Array.prototype.slice.call((deck || document).querySelectorAll(".slide"))
+ .filter(function (s) { return !s.closest(".overview"); });
+ // restore saved snapshot (v2)
+ var saved = null; try { saved = localStorage.getItem(KEY); } catch (e) {}
+ if (saved) { try { JSON.parse(saved).forEach(function (p, k) { if (slots[k]) applyPayload(slots[k], p); }); } catch (e) { saved = null; } }
+ slots.forEach(markSlide);
+ document.body.appendChild(launch);
+ document.body.appendChild(bar);
+ document.body.appendChild(fileInput);
+ hist = [JSON.stringify(snapshot())]; hi = 0; updateUndoBtns();
+ updatePage();
+ document.addEventListener("keydown", function (e) {
+ if (e.key !== "e" && e.key !== "E") return;
+ var t = document.activeElement;
+ if (t && (t.isContentEditable || t.tagName === "INPUT" || t.tagName === "TEXTAREA")) return;
+ if (!isOn()) { toggleEdit(true); e.preventDefault(); }
+ });
+ window.addEventListener("hashchange", function () { setTimeout(updatePage, 50); });
+ document.addEventListener("keyup", function () { if (isOn()) updatePage(); });
+ if (params.has("edtest")) setTimeout(runSelfTest, 200);
+ if (params.has("edopen")) setTimeout(function () { toggleEdit(true); openFilm(); }, 150);
+ }
+
+ /* ---------- headless self-test (?edtest=1) ---------- */
+ function runSelfTest() {
+ var px = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
+ try {
+ toggleEdit(true);
+ var slot0Title = titleOf(slots[0]);
+ addTextBox(); sel.textContent = "自测文本"; record();
+ addImage(px);
+ sel.style.left = "300px"; sel.style.top = "200px"; record();
+ var objs = activeSlot().querySelectorAll(".ed-obj").length;
+ var lenAfterAdds = hist.length;
+ undo(); // undo image move? actually undo last record
+ var afterUndo = hi;
+ redo();
+ reorder(0, 2); // move slide 1 -> position 3
+ var movedOk = titleOf(slots[2]) === slot0Title;
+ var exp = buildExportHTML();
+ var stored = localStorage.getItem(KEY) || "";
+ var hasEditorScript = /
+
+
+