@21stware/rpui 0.4.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ export interface LiveRenderOpts {
2
+ scroller?: Element | null;
3
+ preserve?: boolean;
4
+ onError?: (msg: string | null) => void;
5
+ }
6
+ export declare function liveRender(host: HTMLElement, source: string, opts?: LiveRenderOpts): void;
@@ -0,0 +1,6 @@
1
+ export declare const THEME_CSS = "\n:root {\n --rpml-gx-border:#e5e7eb; --rpml-gx-side-bg:#fff; --rpml-gx-main-bg:#f4f6f8;\n --rpml-gx-fg:#111827; --rpml-gx-muted:#6b7280; --rpml-gx-group:#9ca3af;\n --rpml-gx-hover:#f3f4f6; --rpml-gx-item:#374151; --rpml-gx-active-bg:#eff6ff;\n --rpml-gx-active-fg:#1d4ed8; --rpml-gx-copy-hover:#e5e7eb; --rpml-gx-ok:#059669;\n}\nhtml[data-rpml-theme=\"dark\"] {\n --rpml-gx-border:#2a3344; --rpml-gx-side-bg:#0f172a; --rpml-gx-main-bg:#0b1120;\n --rpml-gx-fg:#e2e8f0; --rpml-gx-muted:#94a3b8; --rpml-gx-group:#64748b;\n --rpml-gx-hover:#1e293b; --rpml-gx-item:#cbd5e1; --rpml-gx-active-bg:#1e3a5f;\n --rpml-gx-active-fg:#93c5fd; --rpml-gx-copy-hover:#334155; --rpml-gx-ok:#34d399;\n}\nhtml[data-rpml-theme=\"dark\"] body { background:#0b1120; }\n/* Invert the rendered prototype as a whole; hue-rotate keeps colors recognizable. */\nhtml[data-rpml-theme=\"dark\"] page-el { filter:invert(0.92) hue-rotate(180deg); }\n\n.rpml-theme-fab {\n position:fixed; top:12px; right:12px; z-index:50; display:flex; align-items:center;\n justify-content:center; width:34px; height:34px; padding:0; border:1px solid var(--rpml-gx-border);\n border-radius:9px; background:var(--rpml-gx-side-bg); color:var(--rpml-gx-fg); font-size:16px;\n line-height:1; cursor:pointer; box-shadow:0 1px 3px rgba(0,0,0,.12);\n}\n.rpml-theme-fab:hover { background:var(--rpml-gx-hover); }\n";
2
+ export declare function injectThemeStyle(): void;
3
+ export declare function currentTheme(): 'light' | 'dark';
4
+ export declare function setTheme(theme: 'light' | 'dark'): void;
5
+ export declare function initTheme(): void;
6
+ export declare function mountThemeFab(host?: HTMLElement): void;
package/dist/gallery.d.ts CHANGED
@@ -2,4 +2,7 @@ export interface RpmlDoc {
2
2
  path: string;
3
3
  source: string;
4
4
  }
5
- export declare function mountGallery(docs: RpmlDoc[], host?: HTMLElement): void;
5
+ export interface GalleryController {
6
+ update(newDocs: RpmlDoc[]): void;
7
+ }
8
+ export declare function mountGallery(docs: RpmlDoc[], host?: HTMLElement): GalleryController;
package/dist/gallery.js CHANGED
@@ -1,6 +1,59 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ function attr(el, name, fallback = "") {
5
+ return el.getAttribute(name) ?? fallback;
6
+ }
7
+ function intAttr(el, name, fallback) {
8
+ const raw = el.getAttribute(name);
9
+ const value = raw === null || raw === "" ? NaN : Number(raw);
10
+ return Number.isFinite(value) ? value : fallback;
11
+ }
12
+ function escapeHtml(value) {
13
+ return value.replace(/[&<>'"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;" })[c] || c);
14
+ }
15
+ function csv(el, name, fallback) {
16
+ return attr(el, name, fallback).split(",").map((s) => s.trim()).filter(Boolean);
17
+ }
18
+ const deviceWidths = { web: 1440, ipad: 834, mobile: 390 };
19
+ function resolveWidth(el, fallback) {
20
+ const raw = el.getAttribute("width");
21
+ const width = raw === null || raw === "" ? NaN : Number(raw);
22
+ if (Number.isFinite(width)) return width;
23
+ return deviceWidths[attr(el, "device")] ?? fallback;
24
+ }
25
+ function hasExplicitNumericHeight(el) {
26
+ const raw = el.getAttribute("height");
27
+ return raw !== null && raw !== "" && Number.isFinite(Number(raw));
28
+ }
29
+ function usesAutoHeight(el) {
30
+ const raw = el.getAttribute("height");
31
+ return raw === "auto" || el.hasAttribute("auto-height") || !!el.getAttribute("device") && !hasExplicitNumericHeight(el);
32
+ }
33
+ function resolveHeight(el, fallback) {
34
+ const raw = el.getAttribute("height");
35
+ const height = raw === null || raw === "" ? NaN : Number(raw);
36
+ return Number.isFinite(height) ? height : fallback;
37
+ }
38
+ function isTopAnnotation(node) {
39
+ if (!(node instanceof HTMLElement)) return false;
40
+ const tag = node.tagName.toLowerCase();
41
+ return tag === "annotation-el" || tag === "annotation-el";
42
+ }
43
+ function isGlobalAnnotation(node) {
44
+ return node instanceof HTMLElement && node.tagName.toLowerCase() === "annotation-global-el";
45
+ }
46
+ function isViewportNode(node) {
47
+ if (!(node instanceof HTMLElement)) return false;
48
+ const tag = node.tagName.toLowerCase();
49
+ return tag === "viewport-el" || tag === "viewport-el";
50
+ }
51
+ function define(name, ctor) {
52
+ if (customElements.get(name)) return;
53
+ const Alias = class extends ctor {
54
+ };
55
+ customElements.define(name, Alias);
56
+ }
4
57
  const PRIMITIVES = [
5
58
  // layout
6
59
  "viewport",
@@ -187,59 +240,6 @@ function parseToPage(source) {
187
240
  if (!root) throw new Error("RPML parse error: no <page> root element found");
188
241
  return root;
189
242
  }
190
- function attr(el, name, fallback = "") {
191
- return el.getAttribute(name) ?? fallback;
192
- }
193
- function intAttr(el, name, fallback) {
194
- const raw = el.getAttribute(name);
195
- const value = raw === null || raw === "" ? NaN : Number(raw);
196
- return Number.isFinite(value) ? value : fallback;
197
- }
198
- function escapeHtml(value) {
199
- return value.replace(/[&<>'"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;" })[c] || c);
200
- }
201
- function csv(el, name, fallback) {
202
- return attr(el, name, fallback).split(",").map((s) => s.trim()).filter(Boolean);
203
- }
204
- const deviceWidths = { web: 1440, ipad: 834, mobile: 390 };
205
- function resolveWidth(el, fallback) {
206
- const raw = el.getAttribute("width");
207
- const width = raw === null || raw === "" ? NaN : Number(raw);
208
- if (Number.isFinite(width)) return width;
209
- return deviceWidths[attr(el, "device")] ?? fallback;
210
- }
211
- function hasExplicitNumericHeight(el) {
212
- const raw = el.getAttribute("height");
213
- return raw !== null && raw !== "" && Number.isFinite(Number(raw));
214
- }
215
- function usesAutoHeight(el) {
216
- const raw = el.getAttribute("height");
217
- return raw === "auto" || el.hasAttribute("auto-height") || !!el.getAttribute("device") && !hasExplicitNumericHeight(el);
218
- }
219
- function resolveHeight(el, fallback) {
220
- const raw = el.getAttribute("height");
221
- const height = raw === null || raw === "" ? NaN : Number(raw);
222
- return Number.isFinite(height) ? height : fallback;
223
- }
224
- function isTopAnnotation(node) {
225
- if (!(node instanceof HTMLElement)) return false;
226
- const tag = node.tagName.toLowerCase();
227
- return tag === "annotation-el" || tag === "annotation-el";
228
- }
229
- function isGlobalAnnotation(node) {
230
- return node instanceof HTMLElement && node.tagName.toLowerCase() === "annotation-global-el";
231
- }
232
- function isViewportNode(node) {
233
- if (!(node instanceof HTMLElement)) return false;
234
- const tag = node.tagName.toLowerCase();
235
- return tag === "viewport-el" || tag === "viewport-el";
236
- }
237
- function define(name, ctor) {
238
- if (customElements.get(name)) return;
239
- const Alias = class extends ctor {
240
- };
241
- customElements.define(name, Alias);
242
- }
243
243
  const RPUI_STYLE_ID = "rpui-runtime-style";
244
244
  const style = `
245
245
  :root { --rp-bg:#f0f2f5; --rp-surface:#fff; --rp-surface-soft:#f9fafb; --rp-text:#111827; --rp-muted:#6b7280; --rp-border:#e5e7eb; --rp-border-strong:#d1d5db; --rp-primary:#2563eb; --rp-success:#059669; --rp-warning:#d97706; --rp-danger:#dc2626; --rp-purple:#7c3aed; --rp-radius-sm:4px; --rp-radius-md:8px; --rp-radius-lg:12px; --rp-shadow:0 8px 28px rgba(15,23,42,.08); --rp-font:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
@@ -868,6 +868,10 @@ class RpAnnotationGlobal extends HTMLElement {
868
868
  this.dataset.rpReady = "true";
869
869
  const existing = Array.from(this.childNodes);
870
870
  const label = attr(this, "label", "全局说明");
871
+ const globalSiblings = this.parentElement ? Array.from(this.parentElement.children).filter(
872
+ (el) => el.tagName.toLowerCase() === "annotation-global-el"
873
+ ) : [];
874
+ this.dataset.rpSection = `global-${globalSiblings.indexOf(this) + 1}`;
871
875
  const marker = document.createElement("span");
872
876
  marker.className = "annotation-el-marker global";
873
877
  marker.innerHTML = "<span>★</span>";
@@ -99580,30 +99584,118 @@ function registerAll() {
99580
99584
  define(toComponentTag(suffix), ctor);
99581
99585
  }
99582
99586
  }
99587
+ function liveRender(host, source, opts = {}) {
99588
+ const { scroller, preserve = true, onError } = opts;
99589
+ const sc = scroller ?? document.scrollingElement ?? document.documentElement;
99590
+ const pane = preserve ? host.querySelector(".annotation-el-pane") : null;
99591
+ const pos = preserve ? { x: sc.scrollLeft, y: sc.scrollTop, px: (pane == null ? void 0 : pane.scrollLeft) ?? 0, py: (pane == null ? void 0 : pane.scrollTop) ?? 0 } : null;
99592
+ try {
99593
+ host.replaceChildren(parseToPage(source));
99594
+ onError == null ? void 0 : onError(null);
99595
+ } catch (e) {
99596
+ onError == null ? void 0 : onError(e.message ?? String(e));
99597
+ return;
99598
+ }
99599
+ if (pos) {
99600
+ requestAnimationFrame(() => requestAnimationFrame(() => {
99601
+ sc.scrollLeft = pos.x;
99602
+ sc.scrollTop = pos.y;
99603
+ const newPane = host.querySelector(".annotation-el-pane");
99604
+ if (newPane) {
99605
+ newPane.scrollLeft = pos.px;
99606
+ newPane.scrollTop = pos.py;
99607
+ }
99608
+ }));
99609
+ }
99610
+ }
99583
99611
  registerAll();
99612
+ const THEME_STYLE_ID = "rpml-theme-style";
99613
+ const ATTR = "data-rpml-theme";
99614
+ const THEME_CSS = `
99615
+ :root {
99616
+ --rpml-gx-border:#e5e7eb; --rpml-gx-side-bg:#fff; --rpml-gx-main-bg:#f4f6f8;
99617
+ --rpml-gx-fg:#111827; --rpml-gx-muted:#6b7280; --rpml-gx-group:#9ca3af;
99618
+ --rpml-gx-hover:#f3f4f6; --rpml-gx-item:#374151; --rpml-gx-active-bg:#eff6ff;
99619
+ --rpml-gx-active-fg:#1d4ed8; --rpml-gx-copy-hover:#e5e7eb; --rpml-gx-ok:#059669;
99620
+ }
99621
+ html[${ATTR}="dark"] {
99622
+ --rpml-gx-border:#2a3344; --rpml-gx-side-bg:#0f172a; --rpml-gx-main-bg:#0b1120;
99623
+ --rpml-gx-fg:#e2e8f0; --rpml-gx-muted:#94a3b8; --rpml-gx-group:#64748b;
99624
+ --rpml-gx-hover:#1e293b; --rpml-gx-item:#cbd5e1; --rpml-gx-active-bg:#1e3a5f;
99625
+ --rpml-gx-active-fg:#93c5fd; --rpml-gx-copy-hover:#334155; --rpml-gx-ok:#34d399;
99626
+ }
99627
+ html[${ATTR}="dark"] body { background:#0b1120; }
99628
+ /* Invert the rendered prototype as a whole; hue-rotate keeps colors recognizable. */
99629
+ html[${ATTR}="dark"] page-el { filter:invert(0.92) hue-rotate(180deg); }
99630
+
99631
+ .rpml-theme-fab {
99632
+ position:fixed; top:12px; right:12px; z-index:50; display:flex; align-items:center;
99633
+ justify-content:center; width:34px; height:34px; padding:0; border:1px solid var(--rpml-gx-border);
99634
+ border-radius:9px; background:var(--rpml-gx-side-bg); color:var(--rpml-gx-fg); font-size:16px;
99635
+ line-height:1; cursor:pointer; box-shadow:0 1px 3px rgba(0,0,0,.12);
99636
+ }
99637
+ .rpml-theme-fab:hover { background:var(--rpml-gx-hover); }
99638
+ `;
99639
+ function injectThemeStyle() {
99640
+ if (document.getElementById(THEME_STYLE_ID)) return;
99641
+ const s = document.createElement("style");
99642
+ s.id = THEME_STYLE_ID;
99643
+ s.textContent = THEME_CSS;
99644
+ document.head.appendChild(s);
99645
+ }
99646
+ function themeFromUrl() {
99647
+ const t = new URLSearchParams(location.search).get("theme");
99648
+ return t === "dark" || t === "light" ? t : null;
99649
+ }
99650
+ function currentTheme() {
99651
+ return document.documentElement.getAttribute(ATTR) === "dark" ? "dark" : "light";
99652
+ }
99653
+ function setTheme(theme) {
99654
+ document.documentElement.setAttribute(ATTR, theme);
99655
+ const url = new URL(location.href);
99656
+ url.searchParams.set("theme", theme);
99657
+ history.replaceState(history.state, "", url);
99658
+ }
99659
+ function initTheme() {
99660
+ const fromUrl = themeFromUrl();
99661
+ if (fromUrl) {
99662
+ document.documentElement.setAttribute(ATTR, fromUrl);
99663
+ return;
99664
+ }
99665
+ const prefersDark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
99666
+ document.documentElement.setAttribute(ATTR, prefersDark ? "dark" : "light");
99667
+ }
99584
99668
  registerAll();
99585
99669
  const CHROME_CSS = `
99586
99670
  .rpml-gallery { display:grid; grid-template-columns:260px 1fr; height:100vh; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
99587
99671
  .rpml-gallery.collapsed { grid-template-columns:1fr; }
99588
99672
  .rpml-gallery.collapsed .rpml-gx-side { display:none; }
99589
- .rpml-gx-side { display:flex; flex-direction:column; border-right:1px solid #e5e7eb; background:#fff; min-height:0; }
99590
- .rpml-gx-head { display:flex; align-items:flex-start; justify-content:space-between; gap:8px; padding:16px; border-bottom:1px solid #e5e7eb; font-size:14px; font-weight:700; color:#111827; }
99591
- .rpml-gx-head small { display:block; margin-top:3px; font-size:11px; font-weight:400; color:#6b7280; }
99592
- .rpml-gx-toggle { flex:none; display:flex; align-items:center; justify-content:center; width:26px; height:26px; margin:-3px -3px 0 0; padding:0; border:1px solid #e5e7eb; border-radius:7px; background:#fff; color:#6b7280; font-size:15px; line-height:1; cursor:pointer; }
99593
- .rpml-gx-toggle:hover { background:#f3f4f6; color:#111827; }
99594
- .rpml-gx-fab { position:fixed; top:12px; left:12px; z-index:50; display:none; align-items:center; justify-content:center; width:34px; height:34px; padding:0; border:1px solid #e5e7eb; border-radius:9px; background:#fff; color:#374151; font-size:17px; line-height:1; cursor:pointer; box-shadow:0 1px 3px rgba(0,0,0,.12); }
99595
- .rpml-gx-fab:hover { background:#f3f4f6; color:#111827; }
99673
+ .rpml-gx-side { display:flex; flex-direction:column; border-right:1px solid var(--rpml-gx-border,#e5e7eb); background:var(--rpml-gx-side-bg,#fff); min-height:0; }
99674
+ .rpml-gx-head { display:flex; align-items:flex-start; justify-content:space-between; gap:8px; padding:16px; border-bottom:1px solid var(--rpml-gx-border,#e5e7eb); font-size:14px; font-weight:700; color:var(--rpml-gx-fg,#111827); }
99675
+ .rpml-gx-head small { display:block; margin-top:3px; font-size:11px; font-weight:400; color:var(--rpml-gx-muted,#6b7280); }
99676
+ .rpml-gx-head-actions { flex:none; display:flex; gap:6px; margin:-3px -3px 0 0; }
99677
+ .rpml-gx-btn { display:flex; align-items:center; justify-content:center; width:26px; height:26px; padding:0; border:1px solid var(--rpml-gx-border,#e5e7eb); border-radius:7px; background:var(--rpml-gx-side-bg,#fff); color:var(--rpml-gx-muted,#6b7280); font-size:15px; line-height:1; cursor:pointer; }
99678
+ .rpml-gx-btn:hover { background:var(--rpml-gx-hover,#f3f4f6); color:var(--rpml-gx-fg,#111827); }
99679
+ .rpml-gx-fab { position:fixed; top:12px; left:12px; z-index:50; display:none; align-items:center; justify-content:center; width:34px; height:34px; padding:0; border:1px solid var(--rpml-gx-border,#e5e7eb); border-radius:9px; background:var(--rpml-gx-side-bg,#fff); color:var(--rpml-gx-fg,#374151); font-size:17px; line-height:1; cursor:pointer; box-shadow:0 1px 3px rgba(0,0,0,.12); }
99680
+ .rpml-gx-fab:hover { background:var(--rpml-gx-hover,#f3f4f6); }
99596
99681
  .rpml-gallery.collapsed .rpml-gx-fab { display:flex; }
99597
99682
  .rpml-gx-nav { flex:1; overflow-y:auto; padding:8px; }
99598
- .rpml-gx-group { padding:10px 8px 3px; font-size:11px; font-weight:700; color:#9ca3af; text-transform:uppercase; letter-spacing:.04em; }
99599
- .rpml-gx-item { display:block; padding:6px 10px; border-radius:7px; font-size:13px; color:#374151; text-decoration:none; cursor:pointer; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
99600
- .rpml-gx-item:hover { background:#f3f4f6; }
99601
- .rpml-gx-item.active { background:#eff6ff; color:#1d4ed8; font-weight:650; }
99602
- .rpml-gx-indent { padding-left:22px; }
99603
- .rpml-gx-main { overflow:auto; min-height:0; background:#f4f6f8; }
99683
+ .rpml-gx-group { padding:10px 8px 3px; font-size:11px; font-weight:700; color:var(--rpml-gx-group,#9ca3af); text-transform:uppercase; letter-spacing:.04em; }
99684
+ .rpml-gx-row { display:flex; align-items:center; border-radius:7px; }
99685
+ .rpml-gx-row:hover { background:var(--rpml-gx-hover,#f3f4f6); }
99686
+ .rpml-gx-row.active { background:var(--rpml-gx-active-bg,#eff6ff); }
99687
+ .rpml-gx-item { flex:1; min-width:0; display:block; padding:6px 10px; font-size:13px; color:var(--rpml-gx-item,#374151); text-decoration:none; cursor:pointer; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
99688
+ .rpml-gx-row.active .rpml-gx-item { color:var(--rpml-gx-active-fg,#1d4ed8); font-weight:650; }
99689
+ .rpml-gx-indent .rpml-gx-item { padding-left:22px; }
99690
+ .rpml-gx-copy { flex:none; margin-right:6px; padding:3px 7px; border:1px solid var(--rpml-gx-border,#e5e7eb); border-radius:6px; background:transparent; color:var(--rpml-gx-muted,#9ca3af); font-size:11px; line-height:1.4; cursor:pointer; opacity:0; transition:opacity .12s; }
99691
+ .rpml-gx-row:hover .rpml-gx-copy { opacity:1; }
99692
+ .rpml-gx-copy:hover { background:var(--rpml-gx-copy-hover,#e5e7eb); color:var(--rpml-gx-fg,#111827); }
99693
+ .rpml-gx-copy.copied { opacity:1; color:var(--rpml-gx-ok,#059669); border-color:var(--rpml-gx-ok,#059669); }
99694
+ .rpml-gx-main { overflow:auto; min-height:0; background:var(--rpml-gx-main-bg,#f4f6f8); }
99604
99695
  .rpml-gx-err { padding:40px; color:#dc2626; font-family:ui-monospace,Menlo,monospace; }
99605
99696
  `;
99606
99697
  function injectChrome() {
99698
+ injectThemeStyle();
99607
99699
  if (document.getElementById("rpml-gallery-style")) return;
99608
99700
  const s = document.createElement("style");
99609
99701
  s.id = "rpml-gallery-style";
@@ -99653,14 +99745,13 @@ function resolveAnchorTarget(to, fromPath, paths) {
99653
99745
  }
99654
99746
  function mountGallery(docs, host = document.body) {
99655
99747
  injectChrome();
99656
- const byPath = new Map(docs.map((d) => [d.path, d]));
99657
- const tree = buildTree(docs);
99748
+ let byPath = new Map(docs.map((d) => [d.path, d]));
99749
+ let tree = buildTree(docs);
99658
99750
  const root = document.createElement("div");
99659
99751
  root.className = "rpml-gallery";
99660
99752
  const side = document.createElement("aside");
99661
99753
  side.className = "rpml-gx-side";
99662
- const count = docs.length;
99663
- side.innerHTML = `<div class="rpml-gx-head"><span>RPML 文档<small>${count} 个文件</small></span><button class="rpml-gx-toggle" type="button" title="收起侧边栏" aria-label="收起侧边栏">«</button></div>`;
99754
+ side.innerHTML = `<div class="rpml-gx-head"><span>RPML 文档<small>${docs.length} 个文件</small></span><div class="rpml-gx-head-actions"><button class="rpml-gx-btn rpml-gx-theme" type="button" title="切换亮色/暗色" aria-label="切换亮色/暗色">◑</button><button class="rpml-gx-btn rpml-gx-toggle" type="button" title="收起侧边栏" aria-label="收起侧边栏">«</button></div></div>`;
99664
99755
  const nav = document.createElement("nav");
99665
99756
  nav.className = "rpml-gx-nav";
99666
99757
  side.appendChild(nav);
@@ -99677,18 +99768,32 @@ function mountGallery(docs, host = document.body) {
99677
99768
  host.appendChild(root);
99678
99769
  side.querySelector(".rpml-gx-toggle").addEventListener("click", () => root.classList.add("collapsed"));
99679
99770
  fab.addEventListener("click", () => root.classList.remove("collapsed"));
99771
+ initTheme();
99772
+ side.querySelector(".rpml-gx-theme").addEventListener("click", () => {
99773
+ setTheme(currentTheme() === "dark" ? "light" : "dark");
99774
+ });
99680
99775
  const links = /* @__PURE__ */ new Map();
99681
99776
  function renderNav(node, depth) {
99682
99777
  const entries = [...node.children.values()];
99683
99778
  const leaves = entries.filter((e) => e.path).sort((a, b) => a.name.localeCompare(b.name));
99684
99779
  const folders = entries.filter((e) => !e.path).sort((a, b) => a.name.localeCompare(b.name));
99685
99780
  for (const leaf of leaves) {
99781
+ const row = document.createElement("div");
99782
+ row.className = depth > 0 ? "rpml-gx-row rpml-gx-indent" : "rpml-gx-row";
99686
99783
  const a = document.createElement("a");
99687
- a.className = depth > 0 ? "rpml-gx-item rpml-gx-indent" : "rpml-gx-item";
99784
+ a.className = "rpml-gx-item";
99688
99785
  a.textContent = leaf.title || leaf.name;
99689
99786
  a.href = `#${leaf.path}`;
99690
- links.set(leaf.path, a);
99691
- nav.appendChild(a);
99787
+ const copy = document.createElement("button");
99788
+ copy.className = "rpml-gx-copy";
99789
+ copy.type = "button";
99790
+ copy.title = "复制此页全部内容";
99791
+ copy.setAttribute("aria-label", "复制此页全部内容");
99792
+ copy.dataset.copyPath = leaf.path;
99793
+ copy.textContent = "copy";
99794
+ row.append(a, copy);
99795
+ links.set(leaf.path, row);
99796
+ nav.appendChild(row);
99692
99797
  }
99693
99798
  for (const folder of folders) {
99694
99799
  const g = document.createElement("div");
@@ -99699,25 +99804,34 @@ function mountGallery(docs, host = document.body) {
99699
99804
  }
99700
99805
  }
99701
99806
  renderNav(tree, 0);
99807
+ function rebuildNav(newDocs) {
99808
+ nav.innerHTML = "";
99809
+ links.clear();
99810
+ tree = buildTree(newDocs);
99811
+ renderNav(tree, 0);
99812
+ side.querySelector(".rpml-gx-head small").textContent = `${newDocs.length} 个文件`;
99813
+ }
99702
99814
  function pickDefault() {
99703
- const idx = docs.find((d) => basename(d.path).replace(/\.rpml$/i, "").toLowerCase() === "index");
99704
- return (idx ?? docs[0]).path;
99815
+ const cur = [...byPath.values()];
99816
+ const idx = cur.find((d) => basename(d.path).replace(/\.rpml$/i, "").toLowerCase() === "index");
99817
+ return (idx ?? cur[0]).path;
99705
99818
  }
99706
99819
  let currentPath = "";
99707
- function show(path, section) {
99820
+ function show(path, section, preserve = false) {
99708
99821
  const doc = byPath.get(path);
99709
99822
  if (!doc) {
99710
99823
  main.innerHTML = `<div class="rpml-gx-err">未找到文档:${path}</div>`;
99711
99824
  return;
99712
99825
  }
99713
- try {
99714
- main.innerHTML = "";
99715
- main.appendChild(parseToPage(doc.source));
99716
- currentPath = path;
99717
- } catch (e) {
99718
- main.innerHTML = `<div class="rpml-gx-err">RPML 解析错误:${e.message}</div>`;
99719
- }
99720
- links.forEach((a, p) => a.classList.toggle("active", p === path));
99826
+ liveRender(main, doc.source, {
99827
+ scroller: main,
99828
+ preserve,
99829
+ onError: (msg) => {
99830
+ if (msg) main.innerHTML = `<div class="rpml-gx-err">RPML 解析错误:${msg}</div>`;
99831
+ }
99832
+ });
99833
+ currentPath = path;
99834
+ links.forEach((row, p) => row.classList.toggle("active", p === path));
99721
99835
  if (section) {
99722
99836
  requestAnimationFrame(() => requestAnimationFrame(() => window.dispatchEvent(new CustomEvent("rp-section", { detail: section }))));
99723
99837
  }
@@ -99735,7 +99849,24 @@ function mountGallery(docs, host = document.body) {
99735
99849
  show(target, section);
99736
99850
  });
99737
99851
  nav.addEventListener("click", (e) => {
99738
- const a = e.target.closest("a.rpml-gx-item");
99852
+ var _a2;
99853
+ const el = e.target;
99854
+ const copyBtn = el.closest(".rpml-gx-copy");
99855
+ if (copyBtn) {
99856
+ e.preventDefault();
99857
+ const doc = byPath.get(copyBtn.dataset.copyPath);
99858
+ if (!doc) return;
99859
+ void ((_a2 = navigator.clipboard) == null ? void 0 : _a2.writeText(doc.source).then(() => {
99860
+ copyBtn.classList.add("copied");
99861
+ copyBtn.textContent = "已复制";
99862
+ setTimeout(() => {
99863
+ copyBtn.classList.remove("copied");
99864
+ copyBtn.textContent = "copy";
99865
+ }, 1500);
99866
+ }));
99867
+ return;
99868
+ }
99869
+ const a = el.closest("a.rpml-gx-item");
99739
99870
  if (!a) return;
99740
99871
  e.preventDefault();
99741
99872
  const path = decodeURIComponent(a.hash.slice(1));
@@ -99744,11 +99875,46 @@ function mountGallery(docs, host = document.body) {
99744
99875
  });
99745
99876
  window.addEventListener("popstate", route);
99746
99877
  route();
99878
+ const controller = {
99879
+ update(newDocs) {
99880
+ var _a2;
99881
+ const prevSource = (_a2 = byPath.get(currentPath)) == null ? void 0 : _a2.source;
99882
+ byPath = new Map(newDocs.map((d) => [d.path, d]));
99883
+ const newPaths = newDocs.map((d) => d.path).sort().join("\0");
99884
+ const oldPaths = [...links.keys()].sort().join("\0");
99885
+ if (newPaths !== oldPaths) rebuildNav(newDocs);
99886
+ const curr = byPath.get(currentPath);
99887
+ if (curr) {
99888
+ if (curr.source !== prevSource) show(currentPath, void 0, true);
99889
+ else links.forEach((row, p) => row.classList.toggle("active", p === currentPath));
99890
+ } else if (newDocs.length) {
99891
+ const def = pickDefault();
99892
+ history.replaceState(null, "", `#${def}`);
99893
+ show(def);
99894
+ }
99895
+ }
99896
+ };
99897
+ globalThis.__RPML_GALLERY__ = controller;
99898
+ return controller;
99747
99899
  }
99748
99900
  const inlined = globalThis.__RPML_DOCS__;
99749
99901
  if (inlined && Array.isArray(inlined) && inlined.length) {
99750
- if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => mountGallery(inlined));
99751
- else mountGallery(inlined);
99902
+ const mount = () => {
99903
+ mountGallery(inlined);
99904
+ if (globalThis.__RPML_LIVE__) {
99905
+ const es = new EventSource("/~live");
99906
+ es.onmessage = (ev) => {
99907
+ var _a2;
99908
+ try {
99909
+ const docs = JSON.parse(ev.data);
99910
+ (_a2 = globalThis.__RPML_GALLERY__) == null ? void 0 : _a2.update(docs);
99911
+ } catch {
99912
+ }
99913
+ };
99914
+ }
99915
+ };
99916
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", mount);
99917
+ else mount();
99752
99918
  }
99753
99919
  export {
99754
99920
  mountGallery