@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/README.md +65 -46
- package/README.zh-CN.md +65 -46
- package/designs/starter/DESIGN.md +168 -171
- package/designs/starter/preview.html +2 -2
- package/designs/summit/DESIGN.md +283 -129
- package/lib/command-intent.ts +59 -0
- package/lib/commands/brief.ts +1 -1
- package/lib/commands/designs.ts +1 -1
- package/lib/commands/domains.ts +1 -1
- package/lib/commands/edit.ts +2 -21
- package/lib/commands/enable.ts +6 -6
- package/lib/commands/help.ts +16 -8
- package/lib/commands/init.ts +1 -1
- package/lib/commands/narrative.ts +26 -0
- package/lib/commands/research.ts +66 -0
- package/lib/commands/review.ts +52 -15
- package/lib/decks-state.ts +127 -8
- package/lib/design/designs.ts +1 -2
- package/lib/edit/prompt.ts +6 -5
- package/lib/edit/resolve-deck.ts +1 -1
- package/lib/narrative-state/render-plan.ts +10 -1
- package/lib/qa/artifact.ts +77 -0
- package/lib/qa/checks.ts +100 -10
- package/lib/qa/index.ts +8 -6
- package/lib/qa/measure.ts +85 -0
- package/lib/refine/open.ts +21 -1
- package/lib/refine/server.ts +127 -4
- package/lib/workspace-state/types.ts +1 -0
- package/package.json +1 -1
- package/plugin.ts +283 -178
- package/skill/NARRATIVE_SKILL.md +103 -25
- package/skill/SKILL.md +6 -11
- package/tools/decks.ts +29 -3
- package/tools/narrative-view.ts +1 -1
- package/tools/qa.ts +17 -11
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
|
package/lib/refine/open.ts
CHANGED
|
@@ -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
|
}
|
package/lib/refine/server.ts
CHANGED
|
@@ -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')
|
|
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);
|