@cyber-dash-tech/revela 0.14.0 → 0.15.1

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.
package/lib/qa/measure.ts CHANGED
@@ -49,6 +49,19 @@ export interface ElementInfo {
49
49
  children: ElementInfo[]
50
50
  /** all CSS class names on this element */
51
51
  classList: string[]
52
+ /** visible text excerpt for text overflow diagnostics */
53
+ text?: string
54
+ /** whether text content is clipped inside this element */
55
+ textOverflow?: boolean
56
+ }
57
+
58
+ export interface SlideContentStats {
59
+ /** Non-title effective text points: English words + CJK characters. */
60
+ bodyTextPoints: number
61
+ /** Recognizable semantic content units such as boxes, cards, evidence, charts, tables, media, metrics, bullets. */
62
+ contentUnits: number
63
+ /** Evidence/source/caveat-like references visible on the slide. */
64
+ supportReferences: number
52
65
  }
53
66
 
54
67
  export interface SlideMetrics {
@@ -65,10 +78,16 @@ export interface SlideMetrics {
65
78
  slideQa: boolean
66
79
  /** bounding box of the slide-canvas element itself (post-scale) */
67
80
  canvasRect: Rect
81
+ /** bounding box of the .slide element itself (post-scale) */
82
+ slideRect: Rect
83
+ /** whether document/body/slide has scrollbars at 1920x1080 */
84
+ hasScrollbars: boolean
68
85
  /** top-level visible children of .slide-canvas */
69
86
  elements: ElementInfo[]
70
87
  /** union bounding box of all visible leaf elements */
71
88
  contentRect: Rect
89
+ /** text/content-density signals for content slides */
90
+ contentStats: SlideContentStats
72
91
  }
73
92
 
74
93
  /**
@@ -207,6 +226,29 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
207
226
  visible: boolean
208
227
  children: EI[]
209
228
  classList: string[]
229
+ text?: string
230
+ textOverflow?: boolean
231
+ }
232
+
233
+ function textPoints(text: string): number {
234
+ const normalized = text.replace(/\s+/g, " ").trim()
235
+ if (!normalized) return 0
236
+ const cjk = (normalized.match(/[\u3400-\u9fff\uf900-\ufaff]/g) || []).length
237
+ const words = (normalized.replace(/[\u3400-\u9fff\uf900-\ufaff]/g, " ").match(/[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*/g) || []).length
238
+ return cjk + words
239
+ }
240
+
241
+ function isSemanticContentUnit(el: Element): boolean {
242
+ const tag = el.tagName.toLowerCase()
243
+ if (["li", "table", "figure", "img", "svg", "canvas", "blockquote"].includes(tag)) return true
244
+ const cls = Array.from(el.classList).join(" ")
245
+ return /\b(box|card|claim|evidence|source|caveat|metric|stat|quote|media|chart|echart|table|step|roadmap|toc-item|bullet)\b/i.test(cls)
246
+ }
247
+
248
+ function isSupportReference(el: Element): boolean {
249
+ const text = (el.textContent || "").replace(/\s+/g, " ").trim()
250
+ const cls = Array.from(el.classList).join(" ")
251
+ return /\b(evidence|source|caveat|claim|support|citation|note)\b/i.test(cls) || /\b(source|evidence|caveat|claim|来源|证据|出处|风险|假设)\b/i.test(text)
210
252
  }
211
253
 
212
254
  function collectChildren(
@@ -220,6 +262,11 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
220
262
  for (const child of Array.from(el.children)) {
221
263
  if (!isVisible(child)) continue
222
264
  const rawR = child.getBoundingClientRect()
265
+ const text = (child.textContent || "").replace(/\s+/g, " ").trim()
266
+ const textOverflow = textPoints(text) > 0 && (
267
+ (child as HTMLElement).scrollHeight > (child as HTMLElement).clientHeight + 2 ||
268
+ (child as HTMLElement).scrollWidth > (child as HTMLElement).clientWidth + 2
269
+ )
223
270
  const cls = child.className || ""
224
271
  if (
225
272
  typeof cls === "string" &&
@@ -235,6 +282,8 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
235
282
  rect: relR,
236
283
  visible: true,
237
284
  classList: Array.from(child.classList),
285
+ text: text.slice(0, 160),
286
+ textOverflow,
238
287
  children: collectChildren(child, offsetTop, offsetLeft, depth + 1),
239
288
  })
240
289
  }
@@ -273,6 +322,7 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
273
322
  if (!canvas) return null
274
323
 
275
324
  const canvasRaw = canvas.getBoundingClientRect()
325
+ const slideRaw = (slide as HTMLElement).getBoundingClientRect()
276
326
  // Use canvas top-left as the coordinate origin
277
327
  const offsetTop = canvasRaw.top
278
328
  const offsetLeft = canvasRaw.left
@@ -286,8 +336,40 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
286
336
  height: canvasRaw.height,
287
337
  }
288
338
 
339
+ const slideRect = {
340
+ left: 0,
341
+ top: 0,
342
+ right: slideRaw.width,
343
+ bottom: slideRaw.height,
344
+ width: slideRaw.width,
345
+ height: slideRaw.height,
346
+ }
347
+
289
348
  const elements = collectChildren(canvas, offsetTop, offsetLeft)
290
349
 
350
+ let bodyTextPoints = 0
351
+ let contentUnits = 0
352
+ let supportReferences = 0
353
+ for (const el of Array.from(canvas.querySelectorAll("*"))) {
354
+ if (!isVisible(el)) continue
355
+ if (/^H[1-2]$/.test(el.tagName)) continue
356
+ const text = (el.textContent || "").replace(/\s+/g, " ").trim()
357
+ if (text) bodyTextPoints += textPoints(text)
358
+ if (isSemanticContentUnit(el)) contentUnits++
359
+ if (isSupportReference(el)) supportReferences++
360
+ }
361
+
362
+ const doc = document.documentElement
363
+ const body = document.body
364
+ const slideEl = slide as HTMLElement
365
+ const hasScrollbars =
366
+ doc.scrollWidth > window.innerWidth + 2 ||
367
+ doc.scrollHeight > window.innerHeight + 2 ||
368
+ body.scrollWidth > window.innerWidth + 2 ||
369
+ body.scrollHeight > window.innerHeight + 2 ||
370
+ slideEl.scrollWidth > slideEl.clientWidth + 2 ||
371
+ slideEl.scrollHeight > slideEl.clientHeight + 2
372
+
291
373
  const titleEl = canvas.querySelector("h1, h2")
292
374
  const title = titleEl
293
375
  ? (titleEl.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80)
@@ -298,8 +380,11 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
298
380
  title,
299
381
  slideQa,
300
382
  canvasRect,
383
+ slideRect,
384
+ hasScrollbars,
301
385
  elements,
302
386
  contentRect: unionRect(elements),
387
+ contentStats: { bodyTextPoints, contentUnits, supportReferences },
303
388
  }
304
389
  },
305
390
  idx
@@ -22,6 +22,10 @@ export interface OpenRefineDeckResult {
22
22
  mode: RefineMode
23
23
  }
24
24
 
25
+ export interface EnsureRefineDeckOpenResult extends OpenRefineDeckResult {
26
+ skippedReason?: "live-session"
27
+ }
28
+
25
29
  export interface OpenRefineDeckOptions {
26
30
  client: any
27
31
  sessionID: string
@@ -32,6 +36,21 @@ export interface OpenRefineDeckOptions {
32
36
  }
33
37
 
34
38
  export function openRefineDeck(target: string, options: OpenRefineDeckOptions): OpenRefineDeckResult {
39
+ return openRefineDeckInternal(target, options, { skipLiveSession: false })
40
+ }
41
+
42
+ export function ensureRefineDeckOpenForChange(
43
+ target: string,
44
+ options: OpenRefineDeckOptions,
45
+ ): EnsureRefineDeckOpenResult {
46
+ return openRefineDeckInternal(target, options, { skipLiveSession: true })
47
+ }
48
+
49
+ function openRefineDeckInternal(
50
+ target: string,
51
+ options: OpenRefineDeckOptions,
52
+ behavior: { skipLiveSession: boolean },
53
+ ): EnsureRefineDeckOpenResult {
35
54
  const deck = resolveEditableDeck(options.workspaceRoot, target)
36
55
  const preflight = ensureEditableDeckState(options.workspaceRoot, deck)
37
56
  assertDeckHtmlContractValid(options.workspaceRoot, deck.absoluteFile)
@@ -53,7 +72,7 @@ export function openRefineDeck(target: string, options: OpenRefineDeckOptions):
53
72
  mode,
54
73
  })
55
74
  const url = `${refineServer.baseUrl}/refine?token=${encodeURIComponent(session.token)}`
56
- const shouldOpen = options.openBrowser !== false
75
+ const shouldOpen = options.openBrowser !== false && !(behavior.skipLiveSession && session.live)
57
76
  if (shouldOpen) (options.openUrl ?? openUrl)(url)
58
77
 
59
78
  return {
@@ -66,5 +85,6 @@ export function openRefineDeck(target: string, options: OpenRefineDeckOptions):
66
85
  liveSession: session.live,
67
86
  openedBrowser: shouldOpen,
68
87
  mode,
88
+ skippedReason: behavior.skipLiveSession && session.live ? "live-session" : undefined,
69
89
  }
70
90
  }
@@ -634,6 +634,11 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
634
634
  .resize-handle:hover::before, body.resizing .resize-handle::before { height: 52px; background: #94a3b8; box-shadow: 0 0 0 4px rgba(148,163,184,.16); }
635
635
  iframe { display: block; width: 100%; height: 100%; border: 0; background: #fff; }
636
636
  .hitbox { position: absolute; inset: 0; z-index: 2; cursor: crosshair; background: transparent; }
637
+ .deck-nav { position: absolute; left: 50%; bottom: 18px; z-index: 4; display: inline-flex; align-items: center; gap: 8px; transform: translateX(-50%); padding: 7px; border: 1px solid rgba(148,163,184,.42); border-radius: 999px; background: rgba(15,23,42,.76); box-shadow: 0 16px 44px rgba(15,23,42,.24); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); pointer-events: auto; }
638
+ .deck-nav button { width: auto; min-width: 84px; padding: 8px 12px; border-radius: 999px; background: rgba(255,255,255,.12); color: #fff; box-shadow: none; font-size: 12px; font-weight: 900; }
639
+ .deck-nav button:hover:not(:disabled) { background: rgba(255,255,255,.22); }
640
+ .deck-nav button:disabled { opacity: .38; }
641
+ .deck-nav-status { min-width: 76px; color: #e2e8f0; font-size: 12px; font-weight: 900; text-align: center; font-variant-numeric: tabular-nums; }
637
642
  aside { display: flex; flex-direction: column; gap: 16px; padding: 20px; background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); overflow: auto; }
638
643
  h1 { margin: 0; font-size: 18px; line-height: 1.2; letter-spacing: -.01em; color: #0f172a; }
639
644
  .wordmark { font-family: Garamond, "Iowan Old Style", Georgia, serif; font-size: 21px; letter-spacing: .08em; font-weight: 600; }
@@ -681,12 +686,12 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
681
686
  button { width: 100%; padding: 12px 14px; border: 0; border-radius: 12px; background: #2563eb; color: #ffffff; font-weight: 800; cursor: pointer; box-shadow: 0 10px 24px rgba(37,99,235,.22); }
682
687
  button:disabled { cursor: not-allowed; opacity: .5; }
683
688
  .status { min-height: 20px; color: #475569; font-size: 13px; line-height: 1.45; }
684
- @media (max-width: 900px) { .app { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; } .resize-handle { display: none; } aside { max-height: 48vh; } }
689
+ @media (max-width: 900px) { .app { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; } .resize-handle { display: none; } aside { max-height: 48vh; } .deck-nav { bottom: 10px; } }
685
690
  </style>
686
691
  </head>
687
692
  <body>
688
693
  <main class="app">
689
- <section class="preview"><iframe id="deck" src="/deck?token=${encodeURIComponent(token)}"></iframe><div id="hitbox" class="hitbox" aria-label="Deck element selection layer"></div></section>
694
+ <section class="preview"><iframe id="deck" src="/deck?token=${encodeURIComponent(token)}"></iframe><div id="hitbox" class="hitbox" aria-label="Deck element selection layer"></div><nav class="deck-nav" aria-label="Deck navigation"><button id="deckPrev" type="button" title="Previous slide (ArrowLeft / ArrowUp / PageUp)">Previous</button><div id="deckCounter" class="deck-nav-status" aria-live="polite">-- / --</div><button id="deckNext" type="button" title="Next slide (ArrowRight / ArrowDown / Space / PageDown)">Next</button></nav></section>
690
695
  <div id="resizeHandle" class="resize-handle" role="separator" aria-label="Resize editor panel" aria-orientation="vertical" title="Drag to resize editor. Double-click to reset."></div>
691
696
  <aside>
692
697
  <div>
@@ -752,6 +757,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
752
757
  bound: false,
753
758
  commentRange: null,
754
759
  resizeDrag: null,
760
+ deckSlideIndex: 0,
761
+ deckSlideCount: 0,
755
762
  mode: defaultMode === 'inspect' ? 'inspect' : 'edit',
756
763
  inspecting: false,
757
764
  activeInspectRequestId: '',
@@ -762,6 +769,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
762
769
  frame: null,
763
770
  hitbox: null,
764
771
  resizeHandle: null,
772
+ deckPrev: null,
773
+ deckNext: null,
774
+ deckCounter: null,
765
775
  selectionSummary: null,
766
776
  selectionChips: null,
767
777
  editTab: null,
@@ -792,6 +802,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
792
802
  els.frame = document.getElementById('deck');
793
803
  els.hitbox = document.getElementById('hitbox');
794
804
  els.resizeHandle = document.getElementById('resizeHandle');
805
+ els.deckPrev = document.getElementById('deckPrev');
806
+ els.deckNext = document.getElementById('deckNext');
807
+ els.deckCounter = document.getElementById('deckCounter');
795
808
  els.selectionSummary = document.getElementById('selectionSummary');
796
809
  els.selectionChips = document.getElementById('selectionChips');
797
810
  els.editTab = document.getElementById('editTab');
@@ -808,7 +821,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
808
821
 
809
822
  els.inspectLanguage = document.getElementById('inspectLanguage');
810
823
 
811
- if (!els.frame || !els.hitbox || !els.resizeHandle || !els.selectionSummary || !els.selectionChips || !els.editTab || !els.inspectTab || !els.editPanel || !els.inspectPanel || !els.comment || !els.commentThread || !els.send || !els.inspectButton || !els.inspectLanguage || !els.inspectCards || !els.inspectStale || !els.status) {
824
+ if (!els.frame || !els.hitbox || !els.resizeHandle || !els.deckPrev || !els.deckNext || !els.deckCounter || !els.selectionSummary || !els.selectionChips || !els.editTab || !els.inspectTab || !els.editPanel || !els.inspectPanel || !els.comment || !els.commentThread || !els.send || !els.inspectButton || !els.inspectLanguage || !els.inspectCards || !els.inspectStale || !els.status) {
812
825
  throw new Error('Editor boot failed: required DOM nodes are missing.');
813
826
  }
814
827
 
@@ -828,7 +841,18 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
828
841
  state.bound = true;
829
842
  els.frame.addEventListener('load', initFrame);
830
843
  document.addEventListener('keydown', (event) => {
831
- if (event.key === 'Escape') clearHover();
844
+ if (event.key === 'Escape') {
845
+ clearHover();
846
+ return;
847
+ }
848
+ if (isTextInputTarget(event.target) || event.metaKey || event.ctrlKey || event.altKey) return;
849
+ if (['ArrowDown', 'ArrowRight', ' ', 'PageDown'].includes(event.key)) {
850
+ event.preventDefault();
851
+ nextDeckSlide();
852
+ } else if (['ArrowUp', 'ArrowLeft', 'PageUp'].includes(event.key)) {
853
+ event.preventDefault();
854
+ prevDeckSlide();
855
+ }
832
856
  });
833
857
  els.comment.addEventListener('input', () => {
834
858
  saveCommentRange();
@@ -854,6 +878,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
854
878
  }, { passive: false });
855
879
  els.resizeHandle.addEventListener('pointerdown', startEditorResize);
856
880
  els.resizeHandle.addEventListener('dblclick', resetEditorWidth);
881
+ els.deckPrev.addEventListener('click', prevDeckSlide);
882
+ els.deckNext.addEventListener('click', nextDeckSlide);
857
883
  els.send.addEventListener('click', sendComment);
858
884
  els.inspectButton.addEventListener('click', inspectCurrentSelection);
859
885
  els.inspectLanguage.addEventListener('change', () => {
@@ -940,6 +966,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
940
966
  renderReferenceOutlines();
941
967
  }, true);
942
968
  const slides = getSlides(doc);
969
+ syncDeckNavigation();
943
970
  updateSendState();
944
971
  if (state.pendingRefreshMessage) {
945
972
  state.pendingRefreshMessage = false;
@@ -952,6 +979,102 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
952
979
  }
953
980
  }
954
981
 
982
+ function isTextInputTarget(target) {
983
+ if (!target || !(target instanceof Element)) return false;
984
+ const tag = target.tagName.toLowerCase();
985
+ return tag === 'input' || tag === 'textarea' || tag === 'select' || target.isContentEditable || Boolean(target.closest('[contenteditable="true"]'));
986
+ }
987
+
988
+ function syncDeckNavigation() {
989
+ try {
990
+ const doc = els.frame.contentDocument;
991
+ const slides = doc ? getSlides(doc) : [];
992
+ state.deckSlideCount = slides.length;
993
+ state.deckSlideIndex = Math.max(0, Math.min(state.deckSlideIndex, Math.max(0, slides.length - 1)));
994
+ updateDeckNavControls();
995
+ } catch {
996
+ state.deckSlideCount = 0;
997
+ state.deckSlideIndex = 0;
998
+ updateDeckNavControls();
999
+ }
1000
+ }
1001
+
1002
+ function updateDeckNavControls() {
1003
+ const total = state.deckSlideCount;
1004
+ const current = total > 0 ? state.deckSlideIndex + 1 : 0;
1005
+ els.deckCounter.textContent = total > 0 ? current + ' / ' + total : '-- / --';
1006
+ els.deckPrev.disabled = total <= 1 || state.deckSlideIndex <= 0;
1007
+ els.deckNext.disabled = total <= 1 || state.deckSlideIndex >= total - 1;
1008
+ }
1009
+
1010
+ function prevDeckSlide() {
1011
+ goToDeckSlide(state.deckSlideIndex - 1);
1012
+ }
1013
+
1014
+ function nextDeckSlide() {
1015
+ goToDeckSlide(state.deckSlideIndex + 1);
1016
+ }
1017
+
1018
+ function goToDeckSlide(index) {
1019
+ try {
1020
+ const doc = els.frame.contentDocument;
1021
+ const win = els.frame.contentWindow;
1022
+ if (!doc || !win) return;
1023
+ const slides = getSlides(doc);
1024
+ if (!slides.length) {
1025
+ syncDeckNavigation();
1026
+ return;
1027
+ }
1028
+ const clamped = Math.max(0, Math.min(slides.length - 1, index));
1029
+ const nav = win.RevelaDeckNav;
1030
+ let handled = false;
1031
+ if (nav && typeof nav.goTo === 'function') {
1032
+ try {
1033
+ nav.goTo(clamped);
1034
+ handled = true;
1035
+ } catch {}
1036
+ } else if (nav && clamped > state.deckSlideIndex && typeof nav.next === 'function') {
1037
+ try {
1038
+ nav.next();
1039
+ handled = true;
1040
+ } catch {}
1041
+ } else if (nav && clamped < state.deckSlideIndex && typeof nav.prev === 'function') {
1042
+ try {
1043
+ nav.prev();
1044
+ handled = true;
1045
+ } catch {}
1046
+ }
1047
+ if (!handled) applyFallbackDeckNavigation(win, doc, slides, clamped);
1048
+ state.deckSlideIndex = clamped;
1049
+ updateDeckNavControls();
1050
+ renderHoverOutline(state.hoverEl);
1051
+ renderReferenceOutlines();
1052
+ } catch (error) {
1053
+ reportError(error);
1054
+ }
1055
+ }
1056
+
1057
+ function applyFallbackDeckNavigation(win, doc, slides, index) {
1058
+ const target = slides[index];
1059
+ const usesOverlaySlides = slides.some((slide) => {
1060
+ const style = win.getComputedStyle(slide);
1061
+ return style.position === 'absolute' || style.position === 'fixed' || style.opacity === '0' || slide.style.opacity !== '';
1062
+ });
1063
+ if (usesOverlaySlides) {
1064
+ slides.forEach((slide, i) => {
1065
+ slide.style.opacity = i === index ? '1' : '0';
1066
+ slide.style.pointerEvents = i === index ? 'auto' : 'none';
1067
+ });
1068
+ win.scrollTo?.(0, 0);
1069
+ return;
1070
+ }
1071
+ if (target && typeof target.scrollIntoView === 'function') {
1072
+ target.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'auto' });
1073
+ return;
1074
+ }
1075
+ doc.defaultView?.scrollTo?.(0, index * win.innerHeight);
1076
+ }
1077
+
955
1078
  function startDeckVersionPolling() {
956
1079
  pollDeckVersion();
957
1080
  window.setInterval(pollDeckVersion, 2000);
@@ -98,6 +98,7 @@ export type WorkspaceActionType =
98
98
  | "research.gap_closed"
99
99
  | "narrative.upserted"
100
100
  | "deck.plan_compiled"
101
+ | "deck.plan_confirmed"
101
102
  | "artifact.coverage_backfilled"
102
103
  | "evidence.candidate_generated"
103
104
  | "evidence.binding_applied"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",