@cyber-dash-tech/revela 0.15.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 +6 -7
- package/README.zh-CN.md +6 -7
- package/designs/starter/DESIGN.md +168 -171
- package/designs/starter/preview.html +2 -2
- package/designs/summit/DESIGN.md +283 -129
- package/lib/commands/edit.ts +2 -21
- package/lib/commands/help.ts +1 -2
- package/lib/commands/narrative.ts +26 -0
- package/lib/commands/review.ts +49 -12
- package/lib/decks-state.ts +122 -3
- 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 +36 -130
- package/skill/NARRATIVE_SKILL.md +1 -1
- package/skill/SKILL.md +5 -10
- package/tools/decks.ts +29 -3
- package/tools/narrative-view.ts +1 -1
- package/tools/qa.ts +17 -11
package/lib/qa/checks.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/qa/checks.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Browser-measured slide quality checks. The active default path checks hard
|
|
5
|
+
* artifact failures plus a content-substance warning; older soft visual
|
|
6
|
+
* heuristics are kept here for future opt-in use.
|
|
6
7
|
*
|
|
7
|
-
* Dimension 1:
|
|
8
|
-
* Dimension 2:
|
|
9
|
-
* Dimension 3:
|
|
8
|
+
* Dimension 1: Canvas — exact 1920x1080 slide/canvas size
|
|
9
|
+
* Dimension 2: Overflow — scrollbars, element overflow, and text clipping
|
|
10
|
+
* Dimension 3: Density — claim/evidence/source substance warnings
|
|
10
11
|
* Dimension 4: Compliance — CSS classes match the active design's vocabulary
|
|
11
12
|
*
|
|
12
13
|
* All checks operate on SlideMetrics produced by measure.ts.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* Design component compliance requires an allowedClasses vocabulary from the
|
|
15
|
+
* design system and is run by combined artifact QA.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import type { SlideMetrics, ElementInfo, Rect } from "./measure"
|
|
@@ -22,9 +23,10 @@ import { CANVAS_W, CANVAS_H } from "./measure"
|
|
|
22
23
|
export type IssueSeverity = "error" | "warning" | "info"
|
|
23
24
|
|
|
24
25
|
export interface LayoutIssue {
|
|
25
|
-
type: "overflow" | "balance" | "symmetry" | "rhythm" | "compliance"
|
|
26
|
+
type: "canvas" | "scrollbar" | "overflow" | "text_overflow" | "density" | "balance" | "symmetry" | "rhythm" | "compliance"
|
|
26
27
|
/** Sub-category within the dimension */
|
|
27
|
-
sub?: "
|
|
28
|
+
sub?: "size_mismatch" | "page_scroll" | "text_clipped" | "thin_content"
|
|
29
|
+
| "centroid_offset" | "bottom_gap" | "sparse"
|
|
28
30
|
| "height_mismatch" | "density_mismatch"
|
|
29
31
|
| "gap_variance"
|
|
30
32
|
| "unknown_class" | "novel_css_rule"
|
|
@@ -75,6 +77,9 @@ const T = {
|
|
|
75
77
|
GAP_MIN_MEAN: 10,
|
|
76
78
|
// Rhythm — min children count to check gap variance
|
|
77
79
|
GAP_MIN_CHILDREN: 3,
|
|
80
|
+
CANVAS_TOLERANCE: 1,
|
|
81
|
+
DENSITY_MIN_TEXT_POINTS: 70,
|
|
82
|
+
DENSITY_MIN_UNITS: 2,
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
// ── Geometry helpers ──────────────────────────────────────────────────────────
|
|
@@ -172,6 +177,42 @@ function collectLeaves(el: ElementInfo): ElementInfo[] {
|
|
|
172
177
|
|
|
173
178
|
// ── Dimension 1: Overflow ─────────────────────────────────────────────────────
|
|
174
179
|
|
|
180
|
+
function checkCanvas(metrics: SlideMetrics): LayoutIssue[] {
|
|
181
|
+
const issues: LayoutIssue[] = []
|
|
182
|
+
const tol = T.CANVAS_TOLERANCE
|
|
183
|
+
const canvasBad = Math.abs(metrics.canvasRect.width - CANVAS_W) > tol || Math.abs(metrics.canvasRect.height - CANVAS_H) > tol
|
|
184
|
+
const slideBad = Math.abs(metrics.slideRect.width - CANVAS_W) > tol || Math.abs(metrics.slideRect.height - CANVAS_H) > tol
|
|
185
|
+
|
|
186
|
+
if (canvasBad || slideBad) {
|
|
187
|
+
issues.push({
|
|
188
|
+
type: "canvas",
|
|
189
|
+
sub: "size_mismatch",
|
|
190
|
+
severity: "error",
|
|
191
|
+
detail: `Slide and canvas must render exactly ${CANVAS_W}x${CANVAS_H}px. Measured slide ${Math.round(metrics.slideRect.width)}x${Math.round(metrics.slideRect.height)}px, canvas ${Math.round(metrics.canvasRect.width)}x${Math.round(metrics.canvasRect.height)}px.`,
|
|
192
|
+
data: {
|
|
193
|
+
expectedWidth: CANVAS_W,
|
|
194
|
+
expectedHeight: CANVAS_H,
|
|
195
|
+
slideWidth: Math.round(metrics.slideRect.width),
|
|
196
|
+
slideHeight: Math.round(metrics.slideRect.height),
|
|
197
|
+
canvasWidth: Math.round(metrics.canvasRect.width),
|
|
198
|
+
canvasHeight: Math.round(metrics.canvasRect.height),
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return issues
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function checkScrollbars(metrics: SlideMetrics): LayoutIssue[] {
|
|
207
|
+
if (!metrics.hasScrollbars) return []
|
|
208
|
+
return [{
|
|
209
|
+
type: "scrollbar",
|
|
210
|
+
sub: "page_scroll",
|
|
211
|
+
severity: "error",
|
|
212
|
+
detail: "Rendered slide/page has scrollbars at 1920x1080. Deck slides must fit the fixed canvas without document, body, or slide scrolling.",
|
|
213
|
+
}]
|
|
214
|
+
}
|
|
215
|
+
|
|
175
216
|
/**
|
|
176
217
|
* Check 1: Overflow — elements extending beyond canvas boundaries.
|
|
177
218
|
* Hard correctness check; applies to all slide types.
|
|
@@ -205,6 +246,45 @@ function checkOverflow(metrics: SlideMetrics): LayoutIssue[] {
|
|
|
205
246
|
return issues
|
|
206
247
|
}
|
|
207
248
|
|
|
249
|
+
function checkTextOverflow(metrics: SlideMetrics): LayoutIssue[] {
|
|
250
|
+
const issues: LayoutIssue[] = []
|
|
251
|
+
|
|
252
|
+
function walk(els: ElementInfo[]) {
|
|
253
|
+
for (const el of els) {
|
|
254
|
+
if (!el.visible) continue
|
|
255
|
+
if (el.textOverflow) {
|
|
256
|
+
issues.push({
|
|
257
|
+
type: "text_overflow",
|
|
258
|
+
sub: "text_clipped",
|
|
259
|
+
severity: "error",
|
|
260
|
+
detail: `Text appears clipped inside \`${el.selector}\`${el.text ? `: "${el.text}"` : ""}. Increase container size, reduce copy, or adjust font/line-height.`,
|
|
261
|
+
data: { selector: el.selector, text: el.text ?? "" },
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
if (el.children.length > 0) walk(el.children)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
walk(metrics.elements)
|
|
269
|
+
return issues
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function checkContentDensity(metrics: SlideMetrics): LayoutIssue[] {
|
|
273
|
+
if (!metrics.slideQa) return []
|
|
274
|
+
const { bodyTextPoints, contentUnits, supportReferences } = metrics.contentStats
|
|
275
|
+
const thinText = bodyTextPoints < T.DENSITY_MIN_TEXT_POINTS
|
|
276
|
+
const thinUnits = contentUnits < T.DENSITY_MIN_UNITS
|
|
277
|
+
if (!thinText && !thinUnits) return []
|
|
278
|
+
|
|
279
|
+
return [{
|
|
280
|
+
type: "density",
|
|
281
|
+
sub: "thin_content",
|
|
282
|
+
severity: "warning",
|
|
283
|
+
detail: `Content slide may not have enough claim/evidence substance: ${bodyTextPoints} non-title text point(s), ${contentUnits} recognizable content unit(s), ${supportReferences} evidence/source/claim reference(s). Add concrete claim points, evidence, metrics, chart/table support, or source/caveat text if this is not a deliberate focus slide.`,
|
|
284
|
+
data: { bodyTextPoints, contentUnits, supportReferences },
|
|
285
|
+
}]
|
|
286
|
+
}
|
|
287
|
+
|
|
208
288
|
// ── Dimension 2: Balance ──────────────────────────────────────────────────────
|
|
209
289
|
|
|
210
290
|
/**
|
|
@@ -542,7 +622,13 @@ export function runChecks(
|
|
|
542
622
|
const slides: SlideReport[] = []
|
|
543
623
|
|
|
544
624
|
for (const metrics of allMetrics) {
|
|
545
|
-
const issues: LayoutIssue[] = [
|
|
625
|
+
const issues: LayoutIssue[] = [
|
|
626
|
+
...checkCanvas(metrics),
|
|
627
|
+
...checkScrollbars(metrics),
|
|
628
|
+
...checkOverflow(metrics),
|
|
629
|
+
...checkTextOverflow(metrics),
|
|
630
|
+
...checkContentDensity(metrics),
|
|
631
|
+
]
|
|
546
632
|
|
|
547
633
|
slides.push({ index: metrics.index, title: metrics.title, issues })
|
|
548
634
|
}
|
|
@@ -605,7 +691,11 @@ export function formatReport(report: QAReport): string {
|
|
|
605
691
|
`### Action Required`,
|
|
606
692
|
``,
|
|
607
693
|
`Please fix the above hard-error issues in the HTML file. For each issue type:`,
|
|
694
|
+
`- **canvas**: ensure each slide and .slide-canvas render exactly 1920x1080px, not merely any 16:9 size.`,
|
|
695
|
+
`- **scrollbar**: remove document/body/slide scrolling; content must fit inside the fixed 1920x1080 canvas.`,
|
|
608
696
|
`- **overflow**: reduce font size, padding, or content amount for the affected element.`,
|
|
697
|
+
`- **text_overflow**: increase the text container size, reduce copy, or adjust font/line-height so text is not clipped.`,
|
|
698
|
+
`- **density/thin_content**: add concrete claim/evidence points, metrics, chart/table support, or source/caveat text. This is a warning for content substance, not a blank-space failure.`,
|
|
609
699
|
`- **compliance/unknown_class**: an HTML element uses a CSS class not defined in the active design. Replace it with a class from the Component Index or Layout Index. Fetch the component/layout details with the \`revela-designs\` tool if needed.`,
|
|
610
700
|
`- **compliance/novel_css_rule**: \`<style>\` defines a CSS class that is not part of the active design. Remove the custom rule and use the design's existing component styles. For minor spacing/sizing adjustments, use inline \`style=""\` instead.`,
|
|
611
701
|
)
|
package/lib/qa/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/qa/index.ts
|
|
3
3
|
*
|
|
4
|
-
* Public entry point for
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Public entry point for browser-rendered slide QA.
|
|
5
|
+
* Combined artifact QA, including contract and component compliance, lives in
|
|
6
|
+
* `lib/qa/artifact.ts`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { measureSlides } from "./measure"
|
|
@@ -18,12 +18,14 @@ export type { RunChecksOptions } from "./checks"
|
|
|
18
18
|
* Run hard-error QA on `htmlFilePath`.
|
|
19
19
|
*
|
|
20
20
|
* 1. Opens the file in headless Chrome (puppeteer-core)
|
|
21
|
-
* 2. Measures each .slide element's geometry
|
|
22
|
-
*
|
|
21
|
+
* 2. Measures each .slide element's geometry, scroll state, text clipping,
|
|
22
|
+
* content-density signals, and CSS class definitions
|
|
23
|
+
* 3. Runs browser QA checks for exact 1920x1080 slides, scrollbars, overflow,
|
|
24
|
+
* text clipping, and claim/evidence density warnings
|
|
23
25
|
* 4. Returns a structured QAReport
|
|
24
26
|
*
|
|
25
27
|
* The optional `vocabulary` argument is retained for backward compatibility;
|
|
26
|
-
* compliance is
|
|
28
|
+
* design compliance is handled by combined artifact QA.
|
|
27
29
|
*
|
|
28
30
|
* Throws if the file cannot be opened or Chrome is not found.
|
|
29
31
|
*/
|
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);
|