@storystick/preview 0.0.0-stage → 0.2.0-rc.0
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/LICENSE +112 -0
- package/PUBLIC-API.md +55 -0
- package/README.md +42 -2
- package/dist/.source-hash +1 -0
- package/dist/authoring/runtime-contract.d.ts +89 -0
- package/dist/authoring/runtime-contract.js +28 -0
- package/dist/channel-taxonomy.d.ts +17 -0
- package/dist/channel-taxonomy.js +46 -0
- package/dist/context-command.d.ts +18 -0
- package/dist/context-command.js +38 -0
- package/dist/dev/suspicious-codeword-detector.d.ts +15 -0
- package/dist/dev/suspicious-codeword-detector.js +220 -0
- package/dist/dev/suspicious-codeword-detector.noop.d.ts +3 -0
- package/dist/dev/suspicious-codeword-detector.noop.js +4 -0
- package/dist/framework/browser-context.d.ts +2 -0
- package/dist/framework/browser-context.js +2 -0
- package/dist/framework/generation-refresh.d.ts +23 -0
- package/dist/framework/generation-refresh.js +82 -0
- package/dist/framework/generation-signal.d.ts +8 -0
- package/dist/framework/generation-signal.js +17 -0
- package/dist/framework/projection-generation-watch.d.ts +5 -0
- package/dist/framework/projection-generation-watch.js +53 -0
- package/dist/framework/render-receipt.d.ts +8 -0
- package/dist/framework/render-receipt.js +10 -0
- package/dist/framework/runtime-adapter-store.d.ts +30 -0
- package/dist/framework/runtime-adapter-store.js +81 -0
- package/dist/grant-coordinator.d.ts +53 -0
- package/dist/grant-coordinator.js +329 -0
- package/dist/grant-lifecycle.d.ts +52 -0
- package/dist/grant-lifecycle.js +74 -0
- package/dist/identity/__test-adapter.d.ts +34 -0
- package/dist/identity/__test-adapter.js +59 -0
- package/dist/identity/browser-context-gate.d.ts +4 -0
- package/dist/identity/browser-context-gate.js +22 -0
- package/dist/identity/browser-context.d.ts +13 -0
- package/dist/identity/browser-context.js +22 -0
- package/dist/identity/carriers/postmessage.d.ts +84 -0
- package/dist/identity/carriers/postmessage.js +177 -0
- package/dist/identity/carriers/storage-state.d.ts +41 -0
- package/dist/identity/carriers/storage-state.js +99 -0
- package/dist/identity/carriers/storage.d.ts +39 -0
- package/dist/identity/carriers/storage.js +182 -0
- package/dist/identity/carriers/url.d.ts +19 -0
- package/dist/identity/carriers/url.js +38 -0
- package/dist/identity/identity-state-machine.d.ts +70 -0
- package/dist/identity/identity-state-machine.js +244 -0
- package/dist/identity/identity-types.d.ts +192 -0
- package/dist/identity/identity-types.js +22 -0
- package/dist/identity/index.d.ts +18 -0
- package/dist/identity/index.js +16 -0
- package/dist/identity/preview-cookie.d.ts +9 -0
- package/dist/identity/preview-cookie.js +12 -0
- package/dist/identity/preview-draft.d.ts +6 -0
- package/dist/identity/preview-draft.js +25 -0
- package/dist/identity/preview-identity.d.ts +28 -0
- package/dist/identity/preview-identity.js +84 -0
- package/dist/identity/preview-scope.d.ts +28 -0
- package/dist/identity/preview-scope.js +47 -0
- package/dist/identity/resolver.d.ts +110 -0
- package/dist/identity/resolver.js +334 -0
- package/dist/identity/session-query.d.ts +4 -0
- package/dist/identity/session-query.js +19 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +788 -0
- package/dist/interaction-mode.d.ts +58 -0
- package/dist/interaction-mode.js +93 -0
- package/dist/nav-handler.d.ts +95 -0
- package/dist/nav-handler.js +84 -0
- package/dist/overlay-label.d.ts +79 -0
- package/dist/overlay-label.js +73 -0
- package/dist/provenance/anchor-map.d.ts +95 -0
- package/dist/provenance/anchor-map.js +177 -0
- package/dist/provenance/carrier-plan.d.ts +51 -0
- package/dist/provenance/carrier-plan.js +90 -0
- package/dist/provenance/coded-text.d.ts +20 -0
- package/dist/provenance/coded-text.js +302 -0
- package/dist/provenance/direct-edit-locator.d.ts +29 -0
- package/dist/provenance/direct-edit-locator.js +218 -0
- package/dist/provenance/editable-leaf.d.ts +134 -0
- package/dist/provenance/editable-leaf.js +483 -0
- package/dist/provenance/editor-target.d.ts +29 -0
- package/dist/provenance/editor-target.js +46 -0
- package/dist/provenance/field-leaf-walker.d.ts +21 -0
- package/dist/provenance/field-leaf-walker.js +98 -0
- package/dist/provenance/frame-dependencies.d.ts +212 -0
- package/dist/provenance/frame-dependencies.js +550 -0
- package/dist/provenance/index.d.ts +26 -0
- package/dist/provenance/index.js +26 -0
- package/dist/provenance/local-events.d.ts +1 -0
- package/dist/provenance/local-events.js +1 -0
- package/dist/provenance/markers.d.ts +21 -0
- package/dist/provenance/markers.js +70 -0
- package/dist/provenance/multi-frame-anchor-map.d.ts +54 -0
- package/dist/provenance/multi-frame-anchor-map.js +109 -0
- package/dist/provenance/normalize.d.ts +2 -0
- package/dist/provenance/normalize.js +19 -0
- package/dist/provenance/overlay-focus-echo.d.ts +17 -0
- package/dist/provenance/overlay-focus-echo.js +16 -0
- package/dist/provenance/overlay-projection.d.ts +65 -0
- package/dist/provenance/overlay-projection.js +201 -0
- package/dist/provenance/owner-map.d.ts +55 -0
- package/dist/provenance/owner-map.js +239 -0
- package/dist/provenance/read-carrier.d.ts +22 -0
- package/dist/provenance/read-carrier.js +92 -0
- package/dist/provenance/recorder.d.ts +38 -0
- package/dist/provenance/recorder.js +82 -0
- package/dist/provenance/render-context.d.ts +106 -0
- package/dist/provenance/render-context.js +338 -0
- package/dist/provenance/reveal-contract.d.ts +44 -0
- package/dist/provenance/reveal-contract.js +1 -0
- package/dist/provenance/richtext-slice-embed-address.d.ts +20 -0
- package/dist/provenance/richtext-slice-embed-address.js +35 -0
- package/dist/provenance/runtime-script.d.ts +1 -0
- package/dist/provenance/runtime-script.js +2217 -0
- package/dist/provenance/sidecar-store.d.ts +63 -0
- package/dist/provenance/sidecar-store.js +151 -0
- package/dist/provenance/sidecar.d.ts +34 -0
- package/dist/provenance/sidecar.js +227 -0
- package/dist/provenance/slice-model.d.ts +28 -0
- package/dist/provenance/slice-model.js +86 -0
- package/dist/provenance/source-identity.d.ts +3 -0
- package/dist/provenance/source-identity.js +7 -0
- package/dist/provenance/source-map-materializer.d.ts +76 -0
- package/dist/provenance/source-map-materializer.js +286 -0
- package/dist/provenance/source-registry.d.ts +103 -0
- package/dist/provenance/source-registry.js +562 -0
- package/dist/provenance/transformed-output-gate.d.ts +161 -0
- package/dist/provenance/transformed-output-gate.js +171 -0
- package/dist/provenance/types.d.ts +102 -0
- package/dist/provenance/types.js +1 -0
- package/dist/refresh-policy.d.ts +11 -0
- package/dist/refresh-policy.js +14 -0
- package/dist/reveal-target.d.ts +8 -0
- package/dist/reveal-target.js +47 -0
- package/dist/route-paths.d.ts +11 -0
- package/dist/route-paths.js +14 -0
- package/dist/router-subscriber.d.ts +92 -0
- package/dist/router-subscriber.js +44 -0
- package/dist/runtime/anchor-types.d.ts +5 -0
- package/dist/runtime/anchor-types.js +1 -0
- package/dist/runtime/authority-store.d.ts +31 -0
- package/dist/runtime/authority-store.js +56 -0
- package/dist/runtime/boot.d.ts +52 -0
- package/dist/runtime/boot.js +379 -0
- package/dist/runtime/content-refresh.d.ts +30 -0
- package/dist/runtime/content-refresh.js +68 -0
- package/dist/runtime/emit-runtime-error.d.ts +51 -0
- package/dist/runtime/emit-runtime-error.js +64 -0
- package/dist/runtime/field-proxy.d.ts +112 -0
- package/dist/runtime/field-proxy.js +471 -0
- package/dist/runtime/form-contract-reporter.d.ts +99 -0
- package/dist/runtime/form-contract-reporter.js +391 -0
- package/dist/runtime/invalidation-adapter.d.ts +17 -0
- package/dist/runtime/invalidation-adapter.js +42 -0
- package/dist/runtime/live-query-overlay.d.ts +39 -0
- package/dist/runtime/live-query-overlay.js +345 -0
- package/dist/runtime/momentary-navigate.d.ts +16 -0
- package/dist/runtime/momentary-navigate.js +106 -0
- package/dist/runtime/observation-scheduler.d.ts +15 -0
- package/dist/runtime/observation-scheduler.js +42 -0
- package/dist/runtime/perf-metrics.d.ts +28 -0
- package/dist/runtime/perf-metrics.js +90 -0
- package/dist/runtime/preview-interaction-policy.d.ts +1 -0
- package/dist/runtime/preview-interaction-policy.js +103 -0
- package/dist/runtime/preview-overlay-painter.d.ts +27 -0
- package/dist/runtime/preview-overlay-painter.js +1732 -0
- package/dist/runtime/preview-overlay-sidecar.d.ts +26 -0
- package/dist/runtime/preview-overlay-sidecar.js +45 -0
- package/dist/runtime/preview-perspective-alignment.d.ts +15 -0
- package/dist/runtime/preview-perspective-alignment.js +40 -0
- package/dist/runtime/preview-perspective-intent-state.d.ts +23 -0
- package/dist/runtime/preview-perspective-intent-state.js +5 -0
- package/dist/runtime/preview-transition-state.d.ts +142 -0
- package/dist/runtime/preview-transition-state.js +257 -0
- package/dist/runtime/render-mode-state.d.ts +51 -0
- package/dist/runtime/render-mode-state.js +76 -0
- package/dist/runtime/request-id.d.ts +4 -0
- package/dist/runtime/request-id.js +8 -0
- package/dist/runtime/resolve-schema-field.d.ts +99 -0
- package/dist/runtime/resolve-schema-field.js +356 -0
- package/dist/runtime/runtime-error-dispatch.d.ts +23 -0
- package/dist/runtime/runtime-error-dispatch.js +75 -0
- package/dist/runtime/schema-store.d.ts +121 -0
- package/dist/runtime/schema-store.js +667 -0
- package/dist/runtime/snapshot-assembly.d.ts +104 -0
- package/dist/runtime/snapshot-assembly.js +553 -0
- package/dist/runtime/snapshot-reporter.d.ts +55 -0
- package/dist/runtime/snapshot-reporter.js +375 -0
- package/dist/runtime/standalone-overlay-controller.d.ts +33 -0
- package/dist/runtime/standalone-overlay-controller.js +281 -0
- package/dist/runtime/store.d.ts +307 -0
- package/dist/runtime/store.js +1026 -0
- package/dist/runtime/windowed-minimap-projection.d.ts +28 -0
- package/dist/runtime/windowed-minimap-projection.js +189 -0
- package/dist/runtime/workspace-shortcuts.d.ts +30 -0
- package/dist/runtime/workspace-shortcuts.js +188 -0
- package/dist/sdk-error-copy.d.ts +48 -0
- package/dist/sdk-error-copy.js +182 -0
- package/dist/select-cursor.d.ts +2 -0
- package/dist/select-cursor.js +23 -0
- package/dist/server/admission-cookie.d.ts +38 -0
- package/dist/server/admission-cookie.js +122 -0
- package/dist/server/control-client.d.ts +29 -0
- package/dist/server/control-client.js +22 -0
- package/dist/server/control-errors.d.ts +22 -0
- package/dist/server/control-errors.js +165 -0
- package/dist/server/draft-route.d.ts +12 -0
- package/dist/server/draft-route.js +45 -0
- package/dist/server/extract-token.d.ts +30 -0
- package/dist/server/extract-token.js +98 -0
- package/dist/server/grant.d.ts +128 -0
- package/dist/server/grant.js +395 -0
- package/dist/server/ordinary-read-provenance.d.ts +11 -0
- package/dist/server/ordinary-read-provenance.js +124 -0
- package/dist/server/preview-read-metadata.d.ts +10 -0
- package/dist/server/preview-read-metadata.js +27 -0
- package/dist/server/provenance-frame-builder.d.ts +53 -0
- package/dist/server/provenance-frame-builder.js +102 -0
- package/dist/server/provenance-runtime.d.ts +70 -0
- package/dist/server/provenance-runtime.js +46 -0
- package/dist/server/read-context-command.d.ts +11 -0
- package/dist/server/read-context-command.js +53 -0
- package/dist/server/read-identity.d.ts +3 -0
- package/dist/server/read-identity.js +63 -0
- package/dist/server/read-lifecycle.d.ts +17 -0
- package/dist/server/read-lifecycle.js +52 -0
- package/dist/server/render-frame-provenance.d.ts +133 -0
- package/dist/server/render-frame-provenance.js +676 -0
- package/dist/server/render-id.d.ts +15 -0
- package/dist/server/render-id.js +22 -0
- package/dist/server/route-document-provenance.d.ts +77 -0
- package/dist/server/route-document-provenance.js +1327 -0
- package/dist/server/route-session-cookie.d.ts +33 -0
- package/dist/server/route-session-cookie.js +54 -0
- package/dist/session-projections.d.ts +33 -0
- package/dist/session-projections.js +43 -0
- package/dist/session-subscriber.d.ts +124 -0
- package/dist/session-subscriber.js +79 -0
- package/dist/sha256-fallback.d.ts +30 -0
- package/dist/sha256-fallback.js +126 -0
- package/dist/types.d.ts +446 -0
- package/dist/types.js +85 -0
- package/dist/validators.d.ts +61 -0
- package/dist/validators.js +110 -0
- package/package.json +206 -3
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-129 — atomic preview-snapshot assembly (pure).
|
|
3
|
+
*
|
|
4
|
+
* The atomic snapshot the Studio overlay layer consumes carries every
|
|
5
|
+
* overlay item for one render — slices + explicitly bound fields.
|
|
6
|
+
* Fields are discovered through `[data-ss-bind="<fieldId>"]` attributes
|
|
7
|
+
* on slice subtrees.
|
|
8
|
+
*
|
|
9
|
+
* Multi-SliceZone support — chantier S4 (2026-05-27).
|
|
10
|
+
*
|
|
11
|
+
* Per-render `renderId` is fresh per `<SliceZone>` call, so a page with
|
|
12
|
+
* multiple SliceZones (e.g. chrome zone with header slices + body zone
|
|
13
|
+
* with page slices ; or recursive zone-container sub-SliceZones) emits
|
|
14
|
+
* slices with N distinct renderIds. Pre-S4 this triggered a global
|
|
15
|
+
* `return null` degrade. Post-S4 :
|
|
16
|
+
*
|
|
17
|
+
* - DOM-scan entries are grouped by `renderId` ; each group is one
|
|
18
|
+
* zone.
|
|
19
|
+
* - Per-zone emit (slice + explicit-binding fields) runs in DOM order.
|
|
20
|
+
* - `pageDocId` heuristic — picks the most-frequent `model.doc` among
|
|
21
|
+
* top-level slices (tiebreak first-in-DOM-order). This is a single-
|
|
22
|
+
* string best-effort signal ; the wire shape stays a single `string`
|
|
23
|
+
* (the brief carves "single is safer ; array touches the gate
|
|
24
|
+
* contract" — wire-shape changes here would require owner
|
|
25
|
+
* arbitration via the ADR-137 trigger path).
|
|
26
|
+
*
|
|
27
|
+
* The Studio gate's strict `pageDocId === commandedDocId` resolves
|
|
28
|
+
* correctly for :
|
|
29
|
+
* (i) single-zone pages (one zone = one doc),
|
|
30
|
+
* (ii) single-doc multi-zone (every zone shares the commanded
|
|
31
|
+
* doc — recursive zone-containers' inner zones are skipped,
|
|
32
|
+
* so only top-level zones count),
|
|
33
|
+
* (iii) multi-doc multi-zone with the body zone dominant (e.g.
|
|
34
|
+
* 1 chrome slice + N body slices, N ≥ 1 — body wins on
|
|
35
|
+
* count).
|
|
36
|
+
*
|
|
37
|
+
* Two residual edges that the heuristic CAN'T disambiguate on a
|
|
38
|
+
* single string without a Studio-provided hint :
|
|
39
|
+
* (a) 1-1 tiebreak — exactly 1 chrome slice + 1 body slice with
|
|
40
|
+
* chrome-first DOM order. Heuristic picks chrome ; strict
|
|
41
|
+
* gate drops the snapshot ; the page paints no overlays.
|
|
42
|
+
* Product-rare (asymmetric counts dominate).
|
|
43
|
+
* (b) Embedded-other-doc-larger-than-host — an OLD page has 1
|
|
44
|
+
* own slice + a top-level SliceZone surfacing 2+ slices from
|
|
45
|
+
* the (newly commanded) doc B. Heuristic picks doc B ; gate
|
|
46
|
+
* passes ; a stale pre-nav snapshot might paint briefly
|
|
47
|
+
* until the iframe completes navigation. Product-bizarre
|
|
48
|
+
* (customers rarely render another page's SliceZone at the
|
|
49
|
+
* same level as their own).
|
|
50
|
+
*
|
|
51
|
+
* Both edges are documented as known V1 limitations tied to a
|
|
52
|
+
* future ADR for `pageDocId` cardinality (Studio-provided commanded
|
|
53
|
+
* -doc hint via render-mode, OR `pageDocId: string[]` ; both are
|
|
54
|
+
* wire-shape changes deferred to a separate chantier). Strict
|
|
55
|
+
* equality at the gate is non-negotiable per codex S4 R2 P1 — an
|
|
56
|
+
* epoch-fresh pre-nav snapshot can surface the commanded doc via
|
|
57
|
+
* a chrome/linked SliceZone, so softer "any commanded-doc item"
|
|
58
|
+
* checks at the gate admit stale snapshots that the strict check
|
|
59
|
+
* correctly drops.
|
|
60
|
+
* - Aggregate `renderId` is the joined `|`-separated list of every
|
|
61
|
+
* zone's renderId (sorted DOM-order). Any zone re-render produces
|
|
62
|
+
* a different aggregate, so downstream renderId-keyed equality
|
|
63
|
+
* checks still see a fresh tag on any render.
|
|
64
|
+
*
|
|
65
|
+
* C4 anchored path support: new SDK models carry the rendered zone
|
|
66
|
+
* field plus ordered parent anchors, so nested zone-container slices
|
|
67
|
+
* emit visual paths such as `sections[0].sidebar[1]` while Studio
|
|
68
|
+
* navigation gets the mounted path (`sidebar[1]`). Legacy/defaulted
|
|
69
|
+
* models keep the older single-zone `sections` fallback and same-doc
|
|
70
|
+
* guard, so older SDKs degrade instead of fabricating nested paths.
|
|
71
|
+
*/
|
|
72
|
+
import type { SnapshotItem } from '../types.js';
|
|
73
|
+
import { type PreviewOverlayRegistryDraft } from './preview-overlay-sidecar.js';
|
|
74
|
+
export interface PlainRect {
|
|
75
|
+
x: number;
|
|
76
|
+
y: number;
|
|
77
|
+
width: number;
|
|
78
|
+
height: number;
|
|
79
|
+
}
|
|
80
|
+
/** Injected geometry source — `el.getBoundingClientRect()` in the
|
|
81
|
+
* live reporter, a deterministic stub in unit tests. */
|
|
82
|
+
export type Measure = (el: Element) => PlainRect;
|
|
83
|
+
export interface AssembledSnapshot {
|
|
84
|
+
renderId: string;
|
|
85
|
+
pageDocId: string;
|
|
86
|
+
/**
|
|
87
|
+
* ADR-139 §2.1 + §5 — renamed from `visibleDocumentIds`. Iframe-side
|
|
88
|
+
* "what model islands declared this render" — debug hint only. See
|
|
89
|
+
* `MsgPreviewSnapshot.iframeDeclaredDocumentIdsDebugHint` doc-comment
|
|
90
|
+
* for the trust-boundary rationale ; the field is NEVER consulted by
|
|
91
|
+
* the Studio anti-bleed gate.
|
|
92
|
+
*/
|
|
93
|
+
iframeDeclaredDocumentIdsDebugHint: string[];
|
|
94
|
+
items: SnapshotItem[];
|
|
95
|
+
}
|
|
96
|
+
export interface SnapshotAssemblyOptions {
|
|
97
|
+
overlayRegistry?: PreviewOverlayRegistryDraft;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Synchronous assemble path — Tier-1 explicit only. Pre-C2 behaviour
|
|
101
|
+
* preserved verbatim. Returns null when there is no preview seam OR
|
|
102
|
+
* when the multi-SliceZone guard fires.
|
|
103
|
+
*/
|
|
104
|
+
export declare function assembleSnapshot(doc: Document, measure?: Measure, scrollX?: number, scrollY?: number, options?: SnapshotAssemblyOptions): AssembledSnapshot | null;
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-129 — atomic preview-snapshot assembly (pure).
|
|
3
|
+
*
|
|
4
|
+
* The atomic snapshot the Studio overlay layer consumes carries every
|
|
5
|
+
* overlay item for one render — slices + explicitly bound fields.
|
|
6
|
+
* Fields are discovered through `[data-ss-bind="<fieldId>"]` attributes
|
|
7
|
+
* on slice subtrees.
|
|
8
|
+
*
|
|
9
|
+
* Multi-SliceZone support — chantier S4 (2026-05-27).
|
|
10
|
+
*
|
|
11
|
+
* Per-render `renderId` is fresh per `<SliceZone>` call, so a page with
|
|
12
|
+
* multiple SliceZones (e.g. chrome zone with header slices + body zone
|
|
13
|
+
* with page slices ; or recursive zone-container sub-SliceZones) emits
|
|
14
|
+
* slices with N distinct renderIds. Pre-S4 this triggered a global
|
|
15
|
+
* `return null` degrade. Post-S4 :
|
|
16
|
+
*
|
|
17
|
+
* - DOM-scan entries are grouped by `renderId` ; each group is one
|
|
18
|
+
* zone.
|
|
19
|
+
* - Per-zone emit (slice + explicit-binding fields) runs in DOM order.
|
|
20
|
+
* - `pageDocId` heuristic — picks the most-frequent `model.doc` among
|
|
21
|
+
* top-level slices (tiebreak first-in-DOM-order). This is a single-
|
|
22
|
+
* string best-effort signal ; the wire shape stays a single `string`
|
|
23
|
+
* (the brief carves "single is safer ; array touches the gate
|
|
24
|
+
* contract" — wire-shape changes here would require owner
|
|
25
|
+
* arbitration via the ADR-137 trigger path).
|
|
26
|
+
*
|
|
27
|
+
* The Studio gate's strict `pageDocId === commandedDocId` resolves
|
|
28
|
+
* correctly for :
|
|
29
|
+
* (i) single-zone pages (one zone = one doc),
|
|
30
|
+
* (ii) single-doc multi-zone (every zone shares the commanded
|
|
31
|
+
* doc — recursive zone-containers' inner zones are skipped,
|
|
32
|
+
* so only top-level zones count),
|
|
33
|
+
* (iii) multi-doc multi-zone with the body zone dominant (e.g.
|
|
34
|
+
* 1 chrome slice + N body slices, N ≥ 1 — body wins on
|
|
35
|
+
* count).
|
|
36
|
+
*
|
|
37
|
+
* Two residual edges that the heuristic CAN'T disambiguate on a
|
|
38
|
+
* single string without a Studio-provided hint :
|
|
39
|
+
* (a) 1-1 tiebreak — exactly 1 chrome slice + 1 body slice with
|
|
40
|
+
* chrome-first DOM order. Heuristic picks chrome ; strict
|
|
41
|
+
* gate drops the snapshot ; the page paints no overlays.
|
|
42
|
+
* Product-rare (asymmetric counts dominate).
|
|
43
|
+
* (b) Embedded-other-doc-larger-than-host — an OLD page has 1
|
|
44
|
+
* own slice + a top-level SliceZone surfacing 2+ slices from
|
|
45
|
+
* the (newly commanded) doc B. Heuristic picks doc B ; gate
|
|
46
|
+
* passes ; a stale pre-nav snapshot might paint briefly
|
|
47
|
+
* until the iframe completes navigation. Product-bizarre
|
|
48
|
+
* (customers rarely render another page's SliceZone at the
|
|
49
|
+
* same level as their own).
|
|
50
|
+
*
|
|
51
|
+
* Both edges are documented as known V1 limitations tied to a
|
|
52
|
+
* future ADR for `pageDocId` cardinality (Studio-provided commanded
|
|
53
|
+
* -doc hint via render-mode, OR `pageDocId: string[]` ; both are
|
|
54
|
+
* wire-shape changes deferred to a separate chantier). Strict
|
|
55
|
+
* equality at the gate is non-negotiable per codex S4 R2 P1 — an
|
|
56
|
+
* epoch-fresh pre-nav snapshot can surface the commanded doc via
|
|
57
|
+
* a chrome/linked SliceZone, so softer "any commanded-doc item"
|
|
58
|
+
* checks at the gate admit stale snapshots that the strict check
|
|
59
|
+
* correctly drops.
|
|
60
|
+
* - Aggregate `renderId` is the joined `|`-separated list of every
|
|
61
|
+
* zone's renderId (sorted DOM-order). Any zone re-render produces
|
|
62
|
+
* a different aggregate, so downstream renderId-keyed equality
|
|
63
|
+
* checks still see a fresh tag on any render.
|
|
64
|
+
*
|
|
65
|
+
* C4 anchored path support: new SDK models carry the rendered zone
|
|
66
|
+
* field plus ordered parent anchors, so nested zone-container slices
|
|
67
|
+
* emit visual paths such as `sections[0].sidebar[1]` while Studio
|
|
68
|
+
* navigation gets the mounted path (`sidebar[1]`). Legacy/defaulted
|
|
69
|
+
* models keep the older single-zone `sections` fallback and same-doc
|
|
70
|
+
* guard, so older SDKs degrade instead of fabricating nested paths.
|
|
71
|
+
*/
|
|
72
|
+
import { attachPreviewOverlayRegistryTarget, } from './preview-overlay-sidecar.js';
|
|
73
|
+
import { readWindowedMinimapGeometry } from './windowed-minimap-projection.js';
|
|
74
|
+
// ─── Wire contract (mirrors @storystick/react) ───────────────
|
|
75
|
+
/** Per-slice model attribute — the serialized `SliceModel` JSON, on the
|
|
76
|
+
* slice's own root element (ADR-129 D9 — no `<script>` node). */
|
|
77
|
+
const SS_MODEL_ATTR = 'data-ss-model';
|
|
78
|
+
/** Slice-root binding attribute — carries the slice `_key`. */
|
|
79
|
+
const SS_SLICE_ATTR = 'data-ss-slice';
|
|
80
|
+
/** Field binding attribute — carries the schema-relative field path. */
|
|
81
|
+
const SS_BIND_ATTR = 'data-ss-bind';
|
|
82
|
+
/** Legacy/default SliceZone field used only when the producer did not
|
|
83
|
+
* prove a concrete zone field. */
|
|
84
|
+
const LEGACY_DEFAULT_ZONE_FIELD = 'sections';
|
|
85
|
+
/**
|
|
86
|
+
* Read + validate ONE slice element's `data-ss-model` attribute.
|
|
87
|
+
*/
|
|
88
|
+
function readSliceModel(el) {
|
|
89
|
+
const raw = el.getAttribute(SS_MODEL_ATTR);
|
|
90
|
+
if (raw === null || raw.length === 0)
|
|
91
|
+
return null;
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(raw);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
100
|
+
return null;
|
|
101
|
+
const m = parsed;
|
|
102
|
+
if (m.v !== 129 ||
|
|
103
|
+
typeof m.renderId !== 'string' ||
|
|
104
|
+
m.renderId.length === 0 ||
|
|
105
|
+
typeof m.doc !== 'string' ||
|
|
106
|
+
typeof m.i !== 'number' ||
|
|
107
|
+
typeof m.type !== 'string' ||
|
|
108
|
+
m.fields === null ||
|
|
109
|
+
typeof m.fields !== 'object' ||
|
|
110
|
+
Array.isArray(m.fields)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (m.zoneField !== undefined && typeof m.zoneField !== 'string')
|
|
114
|
+
return null;
|
|
115
|
+
if (m.zoneFieldSource !== undefined &&
|
|
116
|
+
m.zoneFieldSource !== 'explicit' &&
|
|
117
|
+
m.zoneFieldSource !== 'default') {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (m.parentAnchors !== undefined && !isPathAnchorArray(m.parentAnchors)) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (m.source !== undefined) {
|
|
124
|
+
if (m.source === null || typeof m.source !== 'object')
|
|
125
|
+
return null;
|
|
126
|
+
const source = m.source;
|
|
127
|
+
if (typeof source.path !== 'string' || source.path.length === 0 ||
|
|
128
|
+
!isPathAnchorArray(source.anchors) || source.anchors.length === 0 ||
|
|
129
|
+
source.anchors.at(-1)?.key !== el.getAttribute(SS_SLICE_ATTR) ||
|
|
130
|
+
m.zoneFieldSource !== 'explicit')
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
function isPathAnchorArray(value) {
|
|
136
|
+
return (Array.isArray(value) &&
|
|
137
|
+
value.every((anchor) => anchor !== null &&
|
|
138
|
+
typeof anchor === 'object' &&
|
|
139
|
+
typeof anchor.pathPrefix === 'string' &&
|
|
140
|
+
typeof anchor.key === 'string'));
|
|
141
|
+
}
|
|
142
|
+
function pathAnchorSignature(documentId, anchors) {
|
|
143
|
+
return `${documentId}\x01${anchors
|
|
144
|
+
.map((anchor) => `${anchor.pathPrefix}\x02${anchor.key}`)
|
|
145
|
+
.join('\x03')}`;
|
|
146
|
+
}
|
|
147
|
+
function anchorsEqual(left, right) {
|
|
148
|
+
if (left.length !== right.length)
|
|
149
|
+
return false;
|
|
150
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
151
|
+
const a = left[index];
|
|
152
|
+
const b = right[index];
|
|
153
|
+
if (a.pathPrefix !== b.pathPrefix || a.key !== b.key)
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
function classifyAnchorState(model) {
|
|
159
|
+
const hasZoneField = model.zoneField !== undefined;
|
|
160
|
+
const hasSource = model.zoneFieldSource !== undefined;
|
|
161
|
+
const hasParentAnchors = model.parentAnchors !== undefined;
|
|
162
|
+
if (!hasZoneField && !hasSource && !hasParentAnchors)
|
|
163
|
+
return 'legacy';
|
|
164
|
+
if (hasZoneField && hasSource && hasParentAnchors) {
|
|
165
|
+
return model.zoneFieldSource === 'explicit' ? 'explicit' : 'default';
|
|
166
|
+
}
|
|
167
|
+
return 'partial';
|
|
168
|
+
}
|
|
169
|
+
function nearestSliceAncestor(el) {
|
|
170
|
+
const parent = el.parentElement;
|
|
171
|
+
if (parent === null)
|
|
172
|
+
return null;
|
|
173
|
+
try {
|
|
174
|
+
return parent.closest(`[${SS_SLICE_ATTR}]`);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Internal explicit-binding DOM scan.
|
|
182
|
+
*
|
|
183
|
+
* Deliberate invariant: this is the single Tier-1 emission path for
|
|
184
|
+
* authored `data-ss-bind` annotations. Do not add a second resolver or
|
|
185
|
+
* direct emission path beside it.
|
|
186
|
+
*
|
|
187
|
+
* Multi-SliceZone (S4) : entries are grouped by `renderId` (one zone =
|
|
188
|
+
* one renderId) ; each zone contributes its slice/field items in DOM
|
|
189
|
+
* order. `pageDocId` uses a most-frequent-doc heuristic with
|
|
190
|
+
* first-in-DOM-order tiebreak ; the aggregate `renderId` is the joined
|
|
191
|
+
* list (DOM-order, deduped, `|`-separated) so any zone's fresh re-render
|
|
192
|
+
* produces a distinct snapshot identity.
|
|
193
|
+
*/
|
|
194
|
+
function assembleTier1(doc, measure, scrollX, scrollY, options = {}) {
|
|
195
|
+
let sliceEls;
|
|
196
|
+
try {
|
|
197
|
+
sliceEls = doc.querySelectorAll(`[${SS_SLICE_ATTR}]`);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (sliceEls.length === 0)
|
|
203
|
+
return null; // no preview seam on this page
|
|
204
|
+
// DOM-order pass — read every entry, grouping by renderId while
|
|
205
|
+
// refusing legacy/incomplete nested models before they can fabricate
|
|
206
|
+
// a visible path.
|
|
207
|
+
const zones = new Map();
|
|
208
|
+
let entryCount = 0;
|
|
209
|
+
let nestedSkipped = false;
|
|
210
|
+
for (let i = 0; i < sliceEls.length; i++) {
|
|
211
|
+
const el = sliceEls[i];
|
|
212
|
+
const key = el.getAttribute(SS_SLICE_ATTR);
|
|
213
|
+
if (key === null || key.length === 0)
|
|
214
|
+
continue;
|
|
215
|
+
const model = readSliceModel(el);
|
|
216
|
+
if (model === null)
|
|
217
|
+
continue;
|
|
218
|
+
const anchorState = classifyAnchorState(model);
|
|
219
|
+
const nested = nearestSliceAncestor(el) !== null;
|
|
220
|
+
if (anchorState === 'partial') {
|
|
221
|
+
nestedSkipped ||= nested;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (nested && anchorState !== 'explicit') {
|
|
225
|
+
nestedSkipped = true;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const zoneField = anchorState === 'legacy' ? LEGACY_DEFAULT_ZONE_FIELD : model.zoneField;
|
|
229
|
+
const parentAnchors = anchorState === 'legacy' ? [] : model.parentAnchors;
|
|
230
|
+
entryCount++;
|
|
231
|
+
const bucket = zones.get(model.renderId);
|
|
232
|
+
const entry = {
|
|
233
|
+
el,
|
|
234
|
+
key,
|
|
235
|
+
model,
|
|
236
|
+
anchorState,
|
|
237
|
+
zoneField,
|
|
238
|
+
parentAnchors,
|
|
239
|
+
nested,
|
|
240
|
+
};
|
|
241
|
+
if (bucket === undefined)
|
|
242
|
+
zones.set(model.renderId, [entry]);
|
|
243
|
+
else
|
|
244
|
+
bucket.push(entry);
|
|
245
|
+
}
|
|
246
|
+
if (nestedSkipped)
|
|
247
|
+
warnNestedZone();
|
|
248
|
+
if (entryCount === 0)
|
|
249
|
+
return null;
|
|
250
|
+
const items = [];
|
|
251
|
+
const docIds = new Set();
|
|
252
|
+
// Count `model.doc` occurrences for the pageDocId heuristic + remember
|
|
253
|
+
// first-in-DOM-order per doc for the tiebreak. `firstOrder` indexes
|
|
254
|
+
// distinct docs by their FIRST appearance across all zones — a stable
|
|
255
|
+
// tiebreak that doesn't depend on Map iteration semantics for ties.
|
|
256
|
+
const docCounts = new Map();
|
|
257
|
+
const docFirstOrder = new Map();
|
|
258
|
+
let firstSeenIdx = 0;
|
|
259
|
+
// Source identity and visual occurrence are separate. Equal, explicitly
|
|
260
|
+
// proven sources may have several DOM roots; contradictory paths/keys still
|
|
261
|
+
// fail closed. The overlay registry assigns each root its own snapshot key.
|
|
262
|
+
const emittedPathsByDoc = new Map();
|
|
263
|
+
const emittedAnchorSignatures = new Map();
|
|
264
|
+
// Legacy/defaulted top-level namespaces keep the old same-doc guard:
|
|
265
|
+
// a second renderId on the same doc can fabricate a non-colliding
|
|
266
|
+
// `sections[i]` path unless the producer proved the real field.
|
|
267
|
+
const firstDefaultTopLevelRenderIdByDoc = new Map();
|
|
268
|
+
const emittedInfoByElement = new Map();
|
|
269
|
+
let pathCollisionSkipped = false;
|
|
270
|
+
let duplicateKeySkipped = false;
|
|
271
|
+
for (const [renderId, entries] of zones) {
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
const { el, key, model, zoneField } = entry;
|
|
274
|
+
let slicePath;
|
|
275
|
+
let anchors;
|
|
276
|
+
let navigationPath;
|
|
277
|
+
let navigationAnchors;
|
|
278
|
+
if (model.source) {
|
|
279
|
+
slicePath = model.source.path;
|
|
280
|
+
anchors = model.source.anchors;
|
|
281
|
+
if (entry.parentAnchors.length) {
|
|
282
|
+
navigationPath = `${zoneField}[${model.i}]`;
|
|
283
|
+
navigationAnchors = [{ pathPrefix: zoneField, key }];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else if (entry.nested) {
|
|
287
|
+
const parentEl = nearestSliceAncestor(el);
|
|
288
|
+
const parentInfo = parentEl === null ? undefined : emittedInfoByElement.get(parentEl);
|
|
289
|
+
if (parentInfo === undefined ||
|
|
290
|
+
!anchorsEqual(entry.parentAnchors, parentInfo.fromAnchors)) {
|
|
291
|
+
pathCollisionSkipped = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const currentPrefix = `${parentInfo.path}.${zoneField}`;
|
|
295
|
+
slicePath = `${currentPrefix}[${model.i}]`;
|
|
296
|
+
anchors = [...parentInfo.fromAnchors, { pathPrefix: currentPrefix, key }];
|
|
297
|
+
navigationPath = `${zoneField}[${model.i}]`;
|
|
298
|
+
navigationAnchors = [{ pathPrefix: zoneField, key }];
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
if (entry.anchorState !== 'explicit') {
|
|
302
|
+
const firstRenderId = firstDefaultTopLevelRenderIdByDoc.get(model.doc);
|
|
303
|
+
if (firstRenderId !== undefined && firstRenderId !== renderId) {
|
|
304
|
+
pathCollisionSkipped = true;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const currentPrefix = zoneField;
|
|
309
|
+
slicePath = `${currentPrefix}[${model.i}]`;
|
|
310
|
+
anchors = [{ pathPrefix: currentPrefix, key }];
|
|
311
|
+
}
|
|
312
|
+
const anchorSignature = pathAnchorSignature(model.doc, anchors);
|
|
313
|
+
if (emittedAnchorSignatures.has(anchorSignature) && (!model.source || emittedAnchorSignatures.get(anchorSignature) !== slicePath)) {
|
|
314
|
+
duplicateKeySkipped = true;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
let emittedPaths = emittedPathsByDoc.get(model.doc);
|
|
318
|
+
if (emittedPaths === undefined) {
|
|
319
|
+
emittedPaths = new Map();
|
|
320
|
+
emittedPathsByDoc.set(model.doc, emittedPaths);
|
|
321
|
+
}
|
|
322
|
+
if (emittedPaths.has(slicePath) && (!model.source || emittedPaths.get(slicePath) !== anchorSignature)) {
|
|
323
|
+
pathCollisionSkipped = true;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
emittedPaths.set(slicePath, anchorSignature);
|
|
327
|
+
emittedAnchorSignatures.set(anchorSignature, slicePath);
|
|
328
|
+
if (!entry.nested && entry.anchorState !== 'explicit') {
|
|
329
|
+
firstDefaultTopLevelRenderIdByDoc.set(model.doc, renderId);
|
|
330
|
+
}
|
|
331
|
+
docIds.add(model.doc);
|
|
332
|
+
docCounts.set(model.doc, (docCounts.get(model.doc) ?? 0) + 1);
|
|
333
|
+
if (!docFirstOrder.has(model.doc)) {
|
|
334
|
+
docFirstOrder.set(model.doc, firstSeenIdx++);
|
|
335
|
+
}
|
|
336
|
+
items.push(attachPreviewOverlayRegistryTarget({
|
|
337
|
+
kind: 'slice',
|
|
338
|
+
method: 'explicit',
|
|
339
|
+
documentId: model.doc,
|
|
340
|
+
sliceKey: key,
|
|
341
|
+
path: slicePath,
|
|
342
|
+
fromAnchors: anchors,
|
|
343
|
+
...(navigationPath !== undefined ? { navigationPath } : {}),
|
|
344
|
+
...(navigationAnchors !== undefined ? { navigationAnchors } : {}),
|
|
345
|
+
field: '',
|
|
346
|
+
documentRect: toDocumentRect(measure(el), scrollX, scrollY),
|
|
347
|
+
tier: { kind: 'explicit' },
|
|
348
|
+
}, el, options.overlayRegistry));
|
|
349
|
+
let bindEls;
|
|
350
|
+
try {
|
|
351
|
+
bindEls = Array.from(el.querySelectorAll(`[${SS_BIND_ATTR}]`));
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
bindEls = [];
|
|
355
|
+
}
|
|
356
|
+
if (el.hasAttribute(SS_BIND_ATTR))
|
|
357
|
+
bindEls.unshift(el);
|
|
358
|
+
for (let j = 0; j < bindEls.length; j++) {
|
|
359
|
+
const fieldEl = bindEls[j];
|
|
360
|
+
// Multi-SliceZone (codex S4 R1 P1#3) — `querySelectorAll` walks
|
|
361
|
+
// the FULL subtree, so a nested slice's `[data-ss-bind]` would
|
|
362
|
+
// pollute the parent scope and emit under the parent's slice path.
|
|
363
|
+
// The DOM ownership rule is "nearest `[data-ss-slice]` ancestor"
|
|
364
|
+
// — keep the binding ONLY when this slice element IS that
|
|
365
|
+
// ancestor. Skip silently otherwise (the binding belongs to the
|
|
366
|
+
// nested slice, which is itself currently skipped). The
|
|
367
|
+
// slice-root self-binding (added via `bindEls.unshift(el)`) is
|
|
368
|
+
// exempt because `el.closest('[data-ss-slice]')` matches `el`
|
|
369
|
+
// itself.
|
|
370
|
+
const owner = fieldEl.closest(`[${SS_SLICE_ATTR}]`);
|
|
371
|
+
if (owner !== el)
|
|
372
|
+
continue;
|
|
373
|
+
const field = fieldEl.getAttribute(SS_BIND_ATTR);
|
|
374
|
+
if (field === null || field.length === 0)
|
|
375
|
+
continue;
|
|
376
|
+
if (field.includes('[')) {
|
|
377
|
+
warnNestedBind(field);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
items.push(attachPreviewOverlayRegistryTarget({
|
|
381
|
+
kind: 'field',
|
|
382
|
+
method: 'explicit',
|
|
383
|
+
documentId: model.doc,
|
|
384
|
+
sliceKey: key,
|
|
385
|
+
path: `${slicePath}.${field}`,
|
|
386
|
+
fromAnchors: anchors,
|
|
387
|
+
...(navigationPath !== undefined
|
|
388
|
+
? { navigationPath: `${navigationPath}.${field}` }
|
|
389
|
+
: {}),
|
|
390
|
+
...(navigationAnchors !== undefined ? { navigationAnchors } : {}),
|
|
391
|
+
field,
|
|
392
|
+
documentRect: toDocumentRect(measure(fieldEl), scrollX, scrollY),
|
|
393
|
+
tier: { kind: 'explicit' },
|
|
394
|
+
}, fieldEl, options.overlayRegistry));
|
|
395
|
+
}
|
|
396
|
+
const emittedInfo = {
|
|
397
|
+
path: slicePath,
|
|
398
|
+
fromAnchors: anchors,
|
|
399
|
+
...(navigationPath !== undefined ? { navigationPath } : {}),
|
|
400
|
+
...(navigationAnchors !== undefined ? { navigationAnchors } : {}),
|
|
401
|
+
};
|
|
402
|
+
emittedInfoByElement.set(el, emittedInfo);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (pathCollisionSkipped)
|
|
406
|
+
warnPathCollision();
|
|
407
|
+
if (duplicateKeySkipped)
|
|
408
|
+
warnDuplicateSliceKey();
|
|
409
|
+
// pageDocId — most-frequent `model.doc` ; ties broken by first
|
|
410
|
+
// appearance in DOM order. Single-zone collapses to `firstEntry.doc`
|
|
411
|
+
// (identical to pre-S4). Multi-zone same-doc collapses identically.
|
|
412
|
+
let pageDocId = '';
|
|
413
|
+
let pageDocCount = -1;
|
|
414
|
+
let pageDocOrder = Number.POSITIVE_INFINITY;
|
|
415
|
+
for (const [doc, count] of docCounts) {
|
|
416
|
+
if (count > pageDocCount) {
|
|
417
|
+
pageDocId = doc;
|
|
418
|
+
pageDocCount = count;
|
|
419
|
+
pageDocOrder = docFirstOrder.get(doc) ?? 0;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (count === pageDocCount) {
|
|
423
|
+
const order = docFirstOrder.get(doc) ?? 0;
|
|
424
|
+
if (order < pageDocOrder) {
|
|
425
|
+
pageDocId = doc;
|
|
426
|
+
pageDocOrder = order;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Aggregate renderId — Map preserves insertion order so the zones
|
|
431
|
+
// iterate DOM-first-slice. Join with `|` ; single-zone collapses to
|
|
432
|
+
// the lone renderId (no separator), exactly preserving pre-S4 shape.
|
|
433
|
+
const renderId = [...zones.keys()].join('|');
|
|
434
|
+
return {
|
|
435
|
+
renderId,
|
|
436
|
+
pageDocId,
|
|
437
|
+
iframeDeclaredDocumentIdsDebugHint: [...docIds],
|
|
438
|
+
items,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Synchronous assemble path — Tier-1 explicit only. Pre-C2 behaviour
|
|
443
|
+
* preserved verbatim. Returns null when there is no preview seam OR
|
|
444
|
+
* when the multi-SliceZone guard fires.
|
|
445
|
+
*/
|
|
446
|
+
export function assembleSnapshot(doc, measure = liveMeasure, scrollX = 0, scrollY = 0, options = {}) {
|
|
447
|
+
const projection = readWindowedMinimapGeometry(doc);
|
|
448
|
+
if (!projection) {
|
|
449
|
+
return assembleTier1(doc, measure, scrollX, scrollY, options);
|
|
450
|
+
}
|
|
451
|
+
const inverse = 1 / projection.compression;
|
|
452
|
+
const unprojectedMeasure = (element) => {
|
|
453
|
+
const rect = measure(element);
|
|
454
|
+
return {
|
|
455
|
+
x: rect.x * inverse,
|
|
456
|
+
y: rect.y * inverse,
|
|
457
|
+
width: rect.width * inverse,
|
|
458
|
+
height: rect.height * inverse,
|
|
459
|
+
};
|
|
460
|
+
};
|
|
461
|
+
return assembleTier1(doc, unprojectedMeasure, scrollX * inverse, scrollY * inverse, options);
|
|
462
|
+
}
|
|
463
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
464
|
+
let warnedNestedZone = false;
|
|
465
|
+
/**
|
|
466
|
+
* Warn (once) that the previewed page contains a nested SliceZone
|
|
467
|
+
* emitted by a legacy/defaulted producer.
|
|
468
|
+
*
|
|
469
|
+
* C4 accepts nested zone-container slices only when the producer proves
|
|
470
|
+
* the zone field and parent anchor chain. Older payloads are degraded
|
|
471
|
+
* because their numeric index cannot be placed safely in the mounted
|
|
472
|
+
* Studio path.
|
|
473
|
+
*/
|
|
474
|
+
function warnNestedZone() {
|
|
475
|
+
if (warnedNestedZone)
|
|
476
|
+
return;
|
|
477
|
+
warnedNestedZone = true;
|
|
478
|
+
try {
|
|
479
|
+
console.warn(`[storystick] ADR-129 v1 supports TOP-LEVEL <SliceZone> instances ; ` +
|
|
480
|
+
`legacy/defaulted nested zone-container <SliceZone>s are degraded ` +
|
|
481
|
+
`(no overlay). Upgrade @storystick/react to emit anchored zone metadata.`);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// `console` absent — the warning is best-effort.
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
let warnedPathCollision = false;
|
|
488
|
+
/**
|
|
489
|
+
* Warn (once) that two SliceZones produced the same visual slice path
|
|
490
|
+
* on the SAME doc. The first safe path wins; the duplicate degrades
|
|
491
|
+
* rather than overwriting snapshot identity.
|
|
492
|
+
*/
|
|
493
|
+
function warnPathCollision() {
|
|
494
|
+
if (warnedPathCollision)
|
|
495
|
+
return;
|
|
496
|
+
warnedPathCollision = true;
|
|
497
|
+
try {
|
|
498
|
+
console.warn(`[storystick] Two <SliceZone>s referencing the SAME document collided ` +
|
|
499
|
+
`on at least one slice path — the first path wins and the duplicate ` +
|
|
500
|
+
`is degraded (no overlay).`);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
// best-effort
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
let warnedDuplicateSliceKey = false;
|
|
507
|
+
/**
|
|
508
|
+
* Warn (once) that two SliceZone slices share the same full anchor
|
|
509
|
+
* signature for one document.
|
|
510
|
+
*
|
|
511
|
+
* `_key`s are sibling-unique per ADR-051. C4's route lookup uses the
|
|
512
|
+
* full keyed-hop chain when it is present; an exact duplicate chain is
|
|
513
|
+
* still ambiguous and degrades.
|
|
514
|
+
*/
|
|
515
|
+
function warnDuplicateSliceKey() {
|
|
516
|
+
if (warnedDuplicateSliceKey)
|
|
517
|
+
return;
|
|
518
|
+
warnedDuplicateSliceKey = true;
|
|
519
|
+
try {
|
|
520
|
+
console.warn(`[storystick] Two <SliceZone> slices share the same full anchor ` +
|
|
521
|
+
`signature for one document — the duplicate is degraded (no overlay).`);
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// best-effort
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
let warnedNestedBind = false;
|
|
528
|
+
function warnNestedBind(field) {
|
|
529
|
+
if (warnedNestedBind)
|
|
530
|
+
return;
|
|
531
|
+
warnedNestedBind = true;
|
|
532
|
+
try {
|
|
533
|
+
console.warn(`[storystick] ADR-129 v1 binds simple field paths; the binding ` +
|
|
534
|
+
`"${field}" uses array syntax. Repeater / nested-array bindings ` +
|
|
535
|
+
`need the \`fieldPath\` helper + per-row \`_key\` anchors — a ` +
|
|
536
|
+
`tracked follow-up. No overlay is shown for it.`);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
// best-effort
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function liveMeasure(el) {
|
|
543
|
+
const r = el.getBoundingClientRect();
|
|
544
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
545
|
+
}
|
|
546
|
+
function toDocumentRect(r, scrollX, scrollY) {
|
|
547
|
+
return {
|
|
548
|
+
x: r.x + scrollX,
|
|
549
|
+
y: r.y + scrollY,
|
|
550
|
+
width: r.width,
|
|
551
|
+
height: r.height,
|
|
552
|
+
};
|
|
553
|
+
}
|