@21stware/rpui 0.4.4 → 0.5.5

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.
@@ -1,3 +1,6 @@
1
1
  export declare class RpPage extends HTMLElement {
2
+ private _handlers?;
2
3
  connectedCallback(): void;
4
+ private wireRouting;
5
+ disconnectedCallback(): void;
3
6
  }
@@ -0,0 +1,11 @@
1
+ export interface LiveRenderOpts {
2
+ scroller?: Element | null;
3
+ preserve?: boolean;
4
+ onError?: (msg: string | null) => void;
5
+ }
6
+ export interface DocRenderer {
7
+ render(source: string): void;
8
+ destroy(): void;
9
+ }
10
+ export declare function createDocRenderer(host: HTMLElement, opts?: Pick<LiveRenderOpts, 'scroller' | 'onError'>): DocRenderer;
11
+ export declare function liveRender(host: HTMLElement, source: string, opts?: LiveRenderOpts): void;
package/dist/gallery.d.ts CHANGED
@@ -2,4 +2,8 @@ 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
+ destroy(): void;
8
+ }
9
+ 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; }
@@ -1031,8 +1031,13 @@ function activateSection(path, pane) {
1031
1031
  setTimeout(() => target.classList.remove("rp-section-focus"), 3e3);
1032
1032
  }
1033
1033
  class RpPage extends HTMLElement {
1034
+ constructor() {
1035
+ super(...arguments);
1036
+ __publicField(this, "_handlers");
1037
+ }
1034
1038
  connectedCallback() {
1035
1039
  injectStyle();
1040
+ this.wireRouting();
1036
1041
  if (this.dataset.rpReady) return;
1037
1042
  this.dataset.rpReady = "true";
1038
1043
  const pageTitle = attr(this, "title", "Untitled");
@@ -1069,13 +1074,29 @@ class RpPage extends HTMLElement {
1069
1074
  const mv = body.querySelector("main-view, main-view");
1070
1075
  if (mv) header.style.maxWidth = `${mv.offsetWidth}px`;
1071
1076
  });
1077
+ this.wireRouting();
1078
+ requestAnimationFrame(this._handlers[0]);
1079
+ }
1080
+ /** Section routing: handle URL and rp-section events. Re-queries the pane
1081
+ * lazily so listeners don't capture build-scope DOM (survives reconnect). */
1082
+ wireRouting() {
1083
+ if (this._handlers) return;
1084
+ const pane = () => this.querySelector(".annotation-el-pane");
1072
1085
  const go = () => {
1073
1086
  const sec = new URLSearchParams(location.search).get("section");
1074
- if (sec) activateSection(sec, pane);
1087
+ if (sec) activateSection(sec, pane());
1075
1088
  };
1089
+ const onSection = (e) => activateSection(e.detail, pane());
1090
+ this._handlers = [go, onSection];
1076
1091
  window.addEventListener("popstate", go);
1077
- window.addEventListener("rp-section", (e) => activateSection(e.detail, pane));
1078
- requestAnimationFrame(go);
1092
+ window.addEventListener("rp-section", onSection);
1093
+ }
1094
+ disconnectedCallback() {
1095
+ if (this._handlers) {
1096
+ window.removeEventListener("popstate", this._handlers[0]);
1097
+ window.removeEventListener("rp-section", this._handlers[1]);
1098
+ this._handlers = void 0;
1099
+ }
1079
1100
  }
1080
1101
  }
1081
1102
  class GenericElement extends HTMLElement {
@@ -99584,7 +99605,30 @@ function registerAll() {
99584
99605
  define(toComponentTag(suffix), ctor);
99585
99606
  }
99586
99607
  }
99587
- registerAll();
99608
+ function liveRender(host, source, opts = {}) {
99609
+ const { scroller, preserve = true, onError } = opts;
99610
+ const sc = scroller ?? document.scrollingElement ?? document.documentElement;
99611
+ const pane = preserve ? host.querySelector(".annotation-el-pane") : null;
99612
+ 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;
99613
+ try {
99614
+ host.replaceChildren(parseToPage(source));
99615
+ onError == null ? void 0 : onError(null);
99616
+ } catch (e) {
99617
+ onError == null ? void 0 : onError(e.message ?? String(e));
99618
+ return;
99619
+ }
99620
+ if (pos) {
99621
+ requestAnimationFrame(() => requestAnimationFrame(() => {
99622
+ sc.scrollLeft = pos.x;
99623
+ sc.scrollTop = pos.y;
99624
+ const newPane = host.querySelector(".annotation-el-pane");
99625
+ if (newPane) {
99626
+ newPane.scrollLeft = pos.px;
99627
+ newPane.scrollTop = pos.py;
99628
+ }
99629
+ }));
99630
+ }
99631
+ }
99588
99632
  const THEME_STYLE_ID = "rpml-theme-style";
99589
99633
  const ATTR = "data-rpml-theme";
99590
99634
  const THEME_CSS = `
@@ -99721,14 +99765,13 @@ function resolveAnchorTarget(to, fromPath, paths) {
99721
99765
  }
99722
99766
  function mountGallery(docs, host = document.body) {
99723
99767
  injectChrome();
99724
- const byPath = new Map(docs.map((d) => [d.path, d]));
99725
- const tree = buildTree(docs);
99768
+ let byPath = new Map(docs.map((d) => [d.path, d]));
99769
+ let tree = buildTree(docs);
99726
99770
  const root = document.createElement("div");
99727
99771
  root.className = "rpml-gallery";
99728
99772
  const side = document.createElement("aside");
99729
99773
  side.className = "rpml-gx-side";
99730
- const count = docs.length;
99731
- side.innerHTML = `<div class="rpml-gx-head"><span>RPML 文档<small>${count} 个文件</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>`;
99774
+ 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>`;
99732
99775
  const nav = document.createElement("nav");
99733
99776
  nav.className = "rpml-gx-nav";
99734
99777
  side.appendChild(nav);
@@ -99781,25 +99824,34 @@ function mountGallery(docs, host = document.body) {
99781
99824
  }
99782
99825
  }
99783
99826
  renderNav(tree, 0);
99827
+ function rebuildNav(newDocs) {
99828
+ nav.innerHTML = "";
99829
+ links.clear();
99830
+ tree = buildTree(newDocs);
99831
+ renderNav(tree, 0);
99832
+ side.querySelector(".rpml-gx-head small").textContent = `${newDocs.length} 个文件`;
99833
+ }
99784
99834
  function pickDefault() {
99785
- const idx = docs.find((d) => basename(d.path).replace(/\.rpml$/i, "").toLowerCase() === "index");
99786
- return (idx ?? docs[0]).path;
99835
+ const cur = [...byPath.values()];
99836
+ const idx = cur.find((d) => basename(d.path).replace(/\.rpml$/i, "").toLowerCase() === "index");
99837
+ return (idx ?? cur[0]).path;
99787
99838
  }
99788
99839
  let currentPath = "";
99789
- function show(path, section) {
99840
+ function show(path, section, preserve = false) {
99790
99841
  const doc = byPath.get(path);
99791
99842
  if (!doc) {
99792
99843
  main.innerHTML = `<div class="rpml-gx-err">未找到文档:${path}</div>`;
99793
99844
  return;
99794
99845
  }
99795
- try {
99796
- main.innerHTML = "";
99797
- main.appendChild(parseToPage(doc.source));
99798
- currentPath = path;
99799
- } catch (e) {
99800
- main.innerHTML = `<div class="rpml-gx-err">RPML 解析错误:${e.message}</div>`;
99801
- }
99802
- links.forEach((a, p) => a.classList.toggle("active", p === path));
99846
+ liveRender(main, doc.source, {
99847
+ scroller: main,
99848
+ preserve,
99849
+ onError: (msg) => {
99850
+ if (msg) main.innerHTML = `<div class="rpml-gx-err">RPML 解析错误:${msg}</div>`;
99851
+ }
99852
+ });
99853
+ currentPath = path;
99854
+ links.forEach((row, p) => row.classList.toggle("active", p === path));
99803
99855
  if (section) {
99804
99856
  requestAnimationFrame(() => requestAnimationFrame(() => window.dispatchEvent(new CustomEvent("rp-section", { detail: section }))));
99805
99857
  }
@@ -99808,14 +99860,17 @@ function mountGallery(docs, host = document.body) {
99808
99860
  const path = decodeURIComponent(location.hash.slice(1)) || pickDefault();
99809
99861
  show(path);
99810
99862
  }
99811
- window.addEventListener("rp-anchor", (e) => {
99863
+ const onAnchor = (e) => {
99812
99864
  const { to, section } = e.detail;
99813
99865
  const target = resolveAnchorTarget(to, currentPath, new Set(byPath.keys()));
99814
99866
  if (!target) return;
99815
99867
  e.preventDefault();
99816
99868
  history.pushState(null, "", `#${target}`);
99817
99869
  show(target, section);
99818
- });
99870
+ };
99871
+ const onPopstate = () => route();
99872
+ window.addEventListener("rp-anchor", onAnchor);
99873
+ window.addEventListener("popstate", onPopstate);
99819
99874
  nav.addEventListener("click", (e) => {
99820
99875
  var _a2;
99821
99876
  const el = e.target;
@@ -99841,13 +99896,57 @@ function mountGallery(docs, host = document.body) {
99841
99896
  history.pushState(null, "", a.hash);
99842
99897
  show(path);
99843
99898
  });
99844
- window.addEventListener("popstate", route);
99845
99899
  route();
99900
+ const controller = {
99901
+ update(newDocs) {
99902
+ var _a2;
99903
+ const prevSource = (_a2 = byPath.get(currentPath)) == null ? void 0 : _a2.source;
99904
+ byPath = new Map(newDocs.map((d) => [d.path, d]));
99905
+ const newPaths = newDocs.map((d) => d.path).sort().join("\0");
99906
+ const oldPaths = [...links.keys()].sort().join("\0");
99907
+ if (newPaths !== oldPaths) rebuildNav(newDocs);
99908
+ const curr = byPath.get(currentPath);
99909
+ if (curr) {
99910
+ if (curr.source !== prevSource) show(currentPath, void 0, true);
99911
+ else links.forEach((row, p) => row.classList.toggle("active", p === currentPath));
99912
+ } else if (newDocs.length) {
99913
+ const def = pickDefault();
99914
+ history.replaceState(null, "", `#${def}`);
99915
+ show(def);
99916
+ }
99917
+ },
99918
+ destroy() {
99919
+ window.removeEventListener("rp-anchor", onAnchor);
99920
+ window.removeEventListener("popstate", onPopstate);
99921
+ host.innerHTML = "";
99922
+ const g = globalThis;
99923
+ if (g.__RPML_GALLERY__ === controller) delete g.__RPML_GALLERY__;
99924
+ }
99925
+ };
99926
+ globalThis.__RPML_GALLERY__ = controller;
99927
+ return controller;
99846
99928
  }
99847
99929
  const inlined = globalThis.__RPML_DOCS__;
99848
99930
  if (inlined && Array.isArray(inlined) && inlined.length) {
99849
- if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => mountGallery(inlined));
99850
- else mountGallery(inlined);
99931
+ const mount = () => {
99932
+ const gallery = mountGallery(inlined);
99933
+ if (globalThis.__RPML_LIVE__) {
99934
+ const es = new EventSource("/~live");
99935
+ es.onmessage = (ev) => {
99936
+ try {
99937
+ gallery.update(JSON.parse(ev.data));
99938
+ } catch {
99939
+ }
99940
+ };
99941
+ const origDestroy = gallery.destroy.bind(gallery);
99942
+ gallery.destroy = () => {
99943
+ es.close();
99944
+ origDestroy();
99945
+ };
99946
+ }
99947
+ };
99948
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", mount);
99949
+ else mount();
99851
99950
  }
99852
99951
  export {
99853
99952
  mountGallery