@archie/devtools 0.0.47 → 0.1.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.
@@ -1725,10 +1725,96 @@
1725
1725
  // asking blindly (and treating silence as "still unknown", never
1726
1726
  // as "reported empty") is the correct posture.
1727
1727
  capabilitiesQuery: true,
1728
+ // vid-identity migration (protocol 1.x, `@8base-archie/editor-protocol`):
1729
+ // this script reads `data-vid`/`meta.vid` as the node's identity
1730
+ // (`resolveNodeVid` below) and assembles a `NodeMeta` object for
1731
+ // `ELEMENT_SELECTED`, and it emits `REFUSED` instead of silently
1732
+ // accepting a drag/drop this package's own policy would reject.
1733
+ // Hosts MUST feature-detect all three: a frozen pre-migration
1734
+ // inspector has none of them, and its `ELEMENT_SELECTED` has no
1735
+ // `vid`/`nodeMeta` fields at all.
1736
+ vidIdentity: true,
1737
+ nodeMeta: true,
1738
+ refusals: true,
1728
1739
  },
1729
1740
  }
1730
1741
  }
1731
1742
 
1743
+ // `@8base-archie/editor-protocol`'s `PROTOCOL_VERSION` (semver,
1744
+ // major-only compatibility). This script cannot `import` the shared
1745
+ // package (vanilla IIFE, CDN-served) — keep this literal in sync BY
1746
+ // HAND with that package's `src/version.ts` whenever the wire shape of
1747
+ // `RuntimeMessage`/`NodeMeta`/`Op` changes. Reported as a SEPARATE key
1748
+ // from the existing numeric `protocolVersion` above (never overwritten
1749
+ // — archie-app's `runtime-compatibility.ts` parses that field as a
1750
+ // number and compares it with strict equality; colliding the two would
1751
+ // silently break every existing compatibility check).
1752
+ var EDITOR_PROTOCOL_VERSION = "1.0.0"
1753
+
1754
+ // vid-identity migration: `/^[a-z0-9]{6,8}$/`, mirrors
1755
+ // `@8base-archie/editor-protocol`'s `VID_PATTERN` — kept in parity by
1756
+ // hand for the same reason as `EDITOR_PROTOCOL_VERSION` above.
1757
+ var VID_PATTERN = /^[a-z0-9]{6,8}$/
1758
+
1759
+ /**
1760
+ * This node's `vid` — the fiber-resolved (usage-projected) meta's
1761
+ * `vid` first, the DOM `data-vid` attribute next, or null. Mirrors
1762
+ * `node-key.ts`'s `getNodeKeyFromElement` precedence for the vid-only
1763
+ * portion of it (this script has no legacy-nodeId/runtime-key
1764
+ * fallback to offer here — callers that need a key for every element,
1765
+ * vid or not, keep using their own `inspectorSource` resolution).
1766
+ */
1767
+ function resolveNodeVid(el, meta) {
1768
+ if (meta && typeof meta.vid === "string" && VID_PATTERN.test(meta.vid)) {
1769
+ return meta.vid
1770
+ }
1771
+ if (el && typeof el.getAttribute === "function") {
1772
+ var domVid = el.getAttribute("data-vid")
1773
+ if (domVid && VID_PATTERN.test(domVid)) return domVid
1774
+ }
1775
+ return null
1776
+ }
1777
+
1778
+ /**
1779
+ * Assemble this node's `NodeMeta` (`@8base-archie/editor-protocol`)
1780
+ * from its `EditorMeta`, or null when there is no `vid` to key it by.
1781
+ * Mirrors `fiber-bridge.ts`'s `getNodeMeta` — kept in parity by hand,
1782
+ * same reason as `EDITOR_PROTOCOL_VERSION` above (this script cannot
1783
+ * import the TS module or run its `NodeMetaSchema.safeParse`).
1784
+ */
1785
+ function resolveNodeMeta(el, meta) {
1786
+ var vid = resolveNodeVid(el, meta)
1787
+ if (!vid) return null
1788
+ var nodeMeta = {
1789
+ vid: vid,
1790
+ file: (meta && meta.file) || "",
1791
+ dynamicType: (meta && meta.dynamicType) || "STATIC",
1792
+ isKitOrigin: !!(meta && (meta.isKitOrigin || isTemplateFrame(meta))),
1793
+ isChrome: !!(meta && meta.isChrome),
1794
+ containerKind: (meta && meta.containerKind) || "none",
1795
+ }
1796
+ if (meta && meta.componentName) nodeMeta.component = meta.componentName
1797
+ if (meta && meta.lockReason) nodeMeta.lockReason = meta.lockReason
1798
+ return nodeMeta
1799
+ }
1800
+
1801
+ /**
1802
+ * Emit `REFUSED` (vid-identity migration) instead of letting a
1803
+ * drag/drop the runtime's own policy would reject proceed silently —
1804
+ * the gap `docs/deploy-order.md`-adjacent decision this capability bit
1805
+ * documents: "today the drop is accepted and the host discards it
1806
+ * after the fact; that must stop."
1807
+ */
1808
+ function refuseNode(code, cause, vid) {
1809
+ postMessageToParent({
1810
+ source: "archie-devtools",
1811
+ type: "REFUSED",
1812
+ code: code,
1813
+ cause: cause,
1814
+ vid: vid || undefined,
1815
+ })
1816
+ }
1817
+
1732
1818
  /**
1733
1819
  * Emit the capabilities payload as `CAPABILITIES_RESULT`.
1734
1820
  *
@@ -1745,6 +1831,7 @@
1745
1831
  reason: typeof reason === "string" && reason !== "" ? reason : null,
1746
1832
  version: handshake.version,
1747
1833
  protocolVersion: handshake.protocolVersion,
1834
+ editorProtocolVersion: EDITOR_PROTOCOL_VERSION,
1748
1835
  capabilities: handshake.capabilities,
1749
1836
  })
1750
1837
  }
@@ -1755,6 +1842,7 @@
1755
1842
  timestamp: Date.now(),
1756
1843
  version: initialHandshake.version,
1757
1844
  protocolVersion: initialHandshake.protocolVersion,
1845
+ editorProtocolVersion: EDITOR_PROTOCOL_VERSION,
1758
1846
  capabilities: initialHandshake.capabilities,
1759
1847
  })
1760
1848
 
@@ -3069,25 +3157,254 @@
3069
3157
  return result
3070
3158
  }
3071
3159
 
3072
- /** Same-origin CSS text, concatenated. Cross-origin sheets throw on
3073
- * `cssRules` and are skipped — their styling is simply absent from the
3074
- * capture, which is why the host treats it as best-effort. */
3075
- function collectSameOriginCss() {
3076
- var chunks = []
3077
- for (var i = 0; i < document.styleSheets.length; i++) {
3078
- var sheet = document.styleSheets[i]
3079
- var rules = null
3080
- try {
3081
- rules = sheet.cssRules
3082
- } catch (error) {
3083
- continue
3084
- }
3085
- if (!rules) continue
3086
- for (var j = 0; j < rules.length; j++) {
3087
- chunks.push(rules[j].cssText)
3088
- }
3160
+ var CSS_INLINE_TIMEOUT_MS = 2000
3161
+ var CSS_URL_RE = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g
3162
+
3163
+ /** Resolves once `promise` settles, or once `ms` elapses — whichever
3164
+ * first — falling back to `fallbackValue` instead of rejecting. Used
3165
+ * to bound best-effort work (font/url inlining) that must never block
3166
+ * the capture past a short budget. */
3167
+ function withTimeout(promise, ms, fallbackValue) {
3168
+ return new Promise(function (resolve) {
3169
+ var settled = false
3170
+ var timer = setTimeout(function () {
3171
+ if (settled) return
3172
+ settled = true
3173
+ resolve(fallbackValue)
3174
+ }, ms)
3175
+ promise.then(
3176
+ function (value) {
3177
+ if (settled) return
3178
+ settled = true
3179
+ clearTimeout(timer)
3180
+ resolve(value)
3181
+ },
3182
+ function () {
3183
+ if (settled) return
3184
+ settled = true
3185
+ clearTimeout(timer)
3186
+ resolve(fallbackValue)
3187
+ }
3188
+ )
3189
+ })
3190
+ }
3191
+
3192
+ // Duck-typed against CSSOM's numeric `type` (MEDIA_RULE=4, IMPORT_RULE=3,
3193
+ // FONT_FACE_RULE=5) because CSSMediaRule/CSSImportRule/CSSFontFaceRule
3194
+ // are not guaranteed to exist in every test DOM (e.g. jsdom).
3195
+ function isMediaRule(rule) {
3196
+ if (typeof CSSMediaRule !== "undefined" && rule instanceof CSSMediaRule) {
3197
+ return true
3198
+ }
3199
+ return !!(rule && rule.media && rule.cssRules && rule.type === 4)
3200
+ }
3201
+
3202
+ function isImportRule(rule) {
3203
+ if (typeof CSSImportRule !== "undefined" && rule instanceof CSSImportRule) {
3204
+ return true
3089
3205
  }
3090
- return chunks.join("\n")
3206
+ return !!(rule && rule.styleSheet && rule.type === 3)
3207
+ }
3208
+
3209
+ function isFontFaceRule(rule) {
3210
+ if (typeof CSSFontFaceRule !== "undefined" && rule instanceof CSSFontFaceRule) {
3211
+ return true
3212
+ }
3213
+ return !!(rule && rule.type === 5)
3214
+ }
3215
+
3216
+ /** The at-rule keyword of `rule.cssText` (e.g. "layer", "supports"),
3217
+ * lowercased. Used where a rule's numeric `.type` is unreliable — the
3218
+ * CSSOM spec never assigned legacy type constants to rules added
3219
+ * after `@supports` (`@layer`, `@container`, `@starting-style`, ...),
3220
+ * so they all report `type === 0`. */
3221
+ function ruleAtKeyword(rule) {
3222
+ var text = rule && rule.cssText ? rule.cssText : ""
3223
+ var match = /^\s*@(?:-webkit-|-moz-|-ms-|-o-)?([a-zA-Z-]+)/.exec(text)
3224
+ return match ? match[1].toLowerCase() : ""
3225
+ }
3226
+
3227
+ /** A rule that carries its own declarations/content and must always be
3228
+ * pushed verbatim, never treated as a group to recurse into — even
3229
+ * though some of these also expose `.cssRules`:
3230
+ * - `CSSStyleRule` (type 1) exposes `.cssRules` in real browsers for
3231
+ * CSS nesting (an empty list on an un-nested rule); treating that
3232
+ * as a grouping rule renders `.foo {}` with the declarations
3233
+ * dropped. Nested `@media` inside a nested style rule is a rare
3234
+ * case and is left as-is inside `cssText` rather than recursed.
3235
+ * - `@keyframes` / `@page` also expose `.cssRules`, but their nested
3236
+ * rules are keyframe selectors / margin boxes, not media-sensitive
3237
+ * content.
3238
+ * `.style` existing is a reliable signal across all of these (and
3239
+ * `CSSKeyframeRule`, type 8) even where `.type` duck-typing can't be
3240
+ * trusted. */
3241
+ function isStyleLikeLeaf(rule) {
3242
+ if (typeof CSSStyleRule !== "undefined" && rule instanceof CSSStyleRule) {
3243
+ return true
3244
+ }
3245
+ if (
3246
+ rule &&
3247
+ (rule.type === 1 || rule.type === 6 || rule.type === 7 || rule.type === 8)
3248
+ ) {
3249
+ return true
3250
+ }
3251
+ return !!(rule && rule.style)
3252
+ }
3253
+
3254
+ var GROUPING_AT_KEYWORDS = {
3255
+ layer: true,
3256
+ supports: true,
3257
+ container: true,
3258
+ scope: true,
3259
+ "starting-style": true,
3260
+ document: true,
3261
+ }
3262
+
3263
+ /** Positive test for "safe to recurse into and rebuild": only at-rules
3264
+ * whose sole job is grouping other rules under a condition/namespace
3265
+ * (`@layer`, `@supports`, `@container`, `@scope`, `@starting-style`,
3266
+ * `@document`), and only when they actually carry nested rules — a
3267
+ * `@layer a, b;` statement form has no `cssRules` and falls through
3268
+ * to the verbatim leaf branch instead. Everything else defaults to
3269
+ * verbatim, rather than recursing by default, precisely because
3270
+ * `CSSStyleRule` also exposes `cssRules` (see `isStyleLikeLeaf`). */
3271
+ function isGroupingRule(rule) {
3272
+ if (!rule || !rule.cssRules) return false
3273
+ if (typeof CSSStyleRule !== "undefined" && rule instanceof CSSStyleRule) {
3274
+ return false
3275
+ }
3276
+ return !!GROUPING_AT_KEYWORDS[ruleAtKeyword(rule)]
3277
+ }
3278
+
3279
+ /** The rule's header text up to (not including) its opening brace,
3280
+ * e.g. `@layer utilities ` or `@supports (display: grid) `. Used to
3281
+ * rebuild a grouping at-rule around its flattened children. */
3282
+ function headerTextOf(rule) {
3283
+ var text = rule && rule.cssText ? rule.cssText : ""
3284
+ var braceIndex = text.indexOf("{")
3285
+ return braceIndex === -1 ? text : text.slice(0, braceIndex)
3286
+ }
3287
+
3288
+ /** Renders a node produced by `flattenStyleRules` back into CSS text.
3289
+ * A "slot" defers to `pieces[index]`, which a font-face/cross-origin
3290
+ * inlining job may still be mutating when this runs — see
3291
+ * `collectCaptureCss`'s `CSS_INLINE_TIMEOUT_MS` race. */
3292
+ function renderNode(node, pieces) {
3293
+ if (typeof node === "string") return node
3294
+ if (node.kind === "slot") return pieces[node.index]
3295
+ if (node.kind === "group") {
3296
+ return node.header + "{" + renderNodes(node.children, pieces) + "}"
3297
+ }
3298
+ return ""
3299
+ }
3300
+
3301
+ function renderNodes(nodes, pieces) {
3302
+ var texts = []
3303
+ for (var i = 0; i < nodes.length; i++) {
3304
+ texts.push(renderNode(nodes[i], pieces))
3305
+ }
3306
+ return texts.join("\n")
3307
+ }
3308
+
3309
+ /**
3310
+ * Flattens `rules` into a tree of plain-string leaves, font-face "slot"
3311
+ * placeholders (resolved later by URL-inlining — see `pieces` in
3312
+ * `collectCaptureCss`), and group nodes that preserve a non-media
3313
+ * at-rule's own semantics while still flattening any `@media` nested
3314
+ * inside it. This last case matters in practice: Tailwind v4 emits
3315
+ * responsive utilities as `@layer utilities { @media (min-width: ...)
3316
+ * { ... } }`, so a `@layer`/`@supports`/`@container` wrapper has to be
3317
+ * rebuilt around its recursed children rather than pushed as opaque
3318
+ * `cssText` — otherwise the nested `@media` inside it would still be
3319
+ * evaluated against the tiny rasterized SVG instead of the live
3320
+ * viewport.
3321
+ *
3322
+ * In a rasterized SVG, `@media` is evaluated against the SVG image's
3323
+ * own (tiny) intrinsic size, not the live viewport — so a non-matching
3324
+ * query is dropped up front and a matching one is inlined (unwrapped),
3325
+ * using the live `window.matchMedia` as the source of truth. `@import`
3326
+ * is followed into its target sheet the same way; a cross-origin
3327
+ * import throws on `.styleSheet.cssRules` and is dropped, same as any
3328
+ * other unreachable cross-origin CSS. A rule with no `cssRules` at all
3329
+ * (e.g. a plain style rule, or `@layer a, b;`'s statement form) is
3330
+ * pushed as a plain string leaf.
3331
+ */
3332
+ function flattenStyleRules(rules, sheetHref, fetchCache, enhancementJobs, pieces) {
3333
+ var nodes = []
3334
+ if (!rules) return nodes
3335
+ for (var i = 0; i < rules.length; i++) {
3336
+ ;(function (rule) {
3337
+ if (isMediaRule(rule)) {
3338
+ var conditionText =
3339
+ rule.conditionText || (rule.media && rule.media.mediaText) || ""
3340
+ var matches = true
3341
+ try {
3342
+ matches = window.matchMedia(conditionText).matches
3343
+ } catch (error) {
3344
+ matches = true
3345
+ }
3346
+ if (matches) {
3347
+ var mediaChildren = flattenStyleRules(
3348
+ rule.cssRules,
3349
+ sheetHref,
3350
+ fetchCache,
3351
+ enhancementJobs,
3352
+ pieces
3353
+ )
3354
+ for (var m = 0; m < mediaChildren.length; m++) {
3355
+ nodes.push(mediaChildren[m])
3356
+ }
3357
+ }
3358
+ } else if (isImportRule(rule)) {
3359
+ try {
3360
+ var importedChildren = flattenStyleRules(
3361
+ rule.styleSheet.cssRules,
3362
+ rule.href || sheetHref,
3363
+ fetchCache,
3364
+ enhancementJobs,
3365
+ pieces
3366
+ )
3367
+ for (var k = 0; k < importedChildren.length; k++) {
3368
+ nodes.push(importedChildren[k])
3369
+ }
3370
+ } catch (error) {
3371
+ // cross-origin @import — best-effort, same as other
3372
+ // unreachable cross-origin CSS.
3373
+ }
3374
+ } else if (isFontFaceRule(rule)) {
3375
+ var index = pieces.length
3376
+ pieces.push(rule.cssText)
3377
+ enhancementJobs.push(
3378
+ inlineCssUrls(
3379
+ rule.cssText,
3380
+ sheetHref || document.baseURI,
3381
+ fetchCache
3382
+ ).then(function (text) {
3383
+ pieces[index] = text
3384
+ })
3385
+ )
3386
+ nodes.push({ kind: "slot", index: index })
3387
+ } else if (isStyleLikeLeaf(rule)) {
3388
+ nodes.push(rule.cssText)
3389
+ } else if (isGroupingRule(rule)) {
3390
+ var groupChildren = flattenStyleRules(
3391
+ rule.cssRules,
3392
+ sheetHref,
3393
+ fetchCache,
3394
+ enhancementJobs,
3395
+ pieces
3396
+ )
3397
+ nodes.push({
3398
+ kind: "group",
3399
+ header: headerTextOf(rule),
3400
+ children: groupChildren,
3401
+ })
3402
+ } else {
3403
+ nodes.push(rule.cssText)
3404
+ }
3405
+ })(rules[i])
3406
+ }
3407
+ return nodes
3091
3408
  }
3092
3409
 
3093
3410
  function fetchAsDataUrl(url) {
@@ -3108,6 +3425,168 @@
3108
3425
  })
3109
3426
  }
3110
3427
 
3428
+ function fetchAsDataUrlCors(url) {
3429
+ return fetch(url, { mode: "cors" })
3430
+ .then(function (response) {
3431
+ if (!response.ok) throw new Error("bad status")
3432
+ return response.blob()
3433
+ })
3434
+ .then(function (blob) {
3435
+ return new Promise(function (resolve, reject) {
3436
+ var reader = new FileReader()
3437
+ reader.onload = function () {
3438
+ resolve(String(reader.result))
3439
+ }
3440
+ reader.onerror = reject
3441
+ reader.readAsDataURL(blob)
3442
+ })
3443
+ })
3444
+ .catch(function () {
3445
+ return null
3446
+ })
3447
+ }
3448
+
3449
+ function collectCssUrls(cssText) {
3450
+ var urls = []
3451
+ var seen = {}
3452
+ var match
3453
+ CSS_URL_RE.lastIndex = 0
3454
+ while ((match = CSS_URL_RE.exec(cssText))) {
3455
+ var raw = match[2]
3456
+ if (!raw || raw.indexOf("data:") === 0 || seen[raw]) continue
3457
+ seen[raw] = true
3458
+ urls.push(raw)
3459
+ }
3460
+ return urls
3461
+ }
3462
+
3463
+ /** Replaces every `url(...)` in `cssText` with a `data:` URL, fetched
3464
+ * cross-origin and deduped via `fetchCache` (shared across calls so
3465
+ * the same font file is only ever fetched once). A URL that fails to
3466
+ * resolve or fetch is left as-is rather than aborting the rest. */
3467
+ function inlineCssUrls(cssText, baseHref, fetchCache) {
3468
+ var urls = collectCssUrls(cssText)
3469
+ if (!urls.length) return Promise.resolve(cssText)
3470
+
3471
+ var resolved = {}
3472
+ var jobs = []
3473
+ for (var i = 0; i < urls.length; i++) {
3474
+ ;(function (raw) {
3475
+ var absolute
3476
+ try {
3477
+ absolute = new URL(raw, baseHref).href
3478
+ } catch (error) {
3479
+ return
3480
+ }
3481
+ if (!fetchCache[absolute]) {
3482
+ fetchCache[absolute] = fetchAsDataUrlCors(absolute)
3483
+ }
3484
+ jobs.push(
3485
+ fetchCache[absolute].then(function (dataUrl) {
3486
+ resolved[absolute] = dataUrl
3487
+ })
3488
+ )
3489
+ })(urls[i])
3490
+ }
3491
+
3492
+ return Promise.all(jobs).then(function () {
3493
+ return cssText.replace(CSS_URL_RE, function (full, quote, raw) {
3494
+ if (!raw || raw.indexOf("data:") === 0) return full
3495
+ var absolute
3496
+ try {
3497
+ absolute = new URL(raw, baseHref).href
3498
+ } catch (error) {
3499
+ return full
3500
+ }
3501
+ var dataUrl = resolved[absolute]
3502
+ return dataUrl ? "url(" + dataUrl + ")" : full
3503
+ })
3504
+ })
3505
+ }
3506
+
3507
+ /**
3508
+ * CSS text for the capture clone: same-origin sheets have their
3509
+ * `@media` flattened against the LIVE viewport (see `flattenStyleRules`)
3510
+ * and `:root` rewritten to `html`, because inside the rasterized SVG
3511
+ * the root element is the `<svg>`, not `<html>`. Cross-origin sheets
3512
+ * throw on `cssRules`, so their raw text is fetched instead and every
3513
+ * `url(...)` inlined as a `data:` URL (same-origin `@font-face` rules
3514
+ * get the same treatment); this fetching work is best-effort and
3515
+ * bounded by `CSS_INLINE_TIMEOUT_MS` so a slow or unreachable font
3516
+ * never delays the capture — rules still pending when the budget runs
3517
+ * out simply keep their original (uninlined) `url(...)`.
3518
+ */
3519
+ function collectCaptureCss() {
3520
+ var fetchCache = {}
3521
+ var pieces = []
3522
+ var enhancementJobs = []
3523
+ var topNodes = []
3524
+
3525
+ for (var i = 0; i < document.styleSheets.length; i++) {
3526
+ ;(function (sheet) {
3527
+ var rules = null
3528
+ try {
3529
+ rules = sheet.cssRules
3530
+ } catch (error) {
3531
+ rules = null
3532
+ }
3533
+
3534
+ if (rules) {
3535
+ var nodes = flattenStyleRules(
3536
+ rules,
3537
+ sheet.href,
3538
+ fetchCache,
3539
+ enhancementJobs,
3540
+ pieces
3541
+ )
3542
+ for (var n = 0; n < nodes.length; n++) topNodes.push(nodes[n])
3543
+ return
3544
+ }
3545
+
3546
+ if (!sheet.href) return
3547
+
3548
+ var index = pieces.length
3549
+ pieces.push("")
3550
+ topNodes.push({ kind: "slot", index: index })
3551
+ enhancementJobs.push(
3552
+ fetch(sheet.href, { mode: "cors" })
3553
+ .then(function (response) {
3554
+ if (!response.ok) throw new Error("bad status")
3555
+ return response.text()
3556
+ })
3557
+ .then(function (text) {
3558
+ return inlineCssUrls(text, sheet.href, fetchCache)
3559
+ })
3560
+ .then(function (text) {
3561
+ // Best-effort only: this raw fetched text is not run back
3562
+ // through flattenStyleRules, so any @media/@layer nesting
3563
+ // inside a blocked cross-origin sheet is left unflattened.
3564
+ pieces[index] = text
3565
+ })
3566
+ .catch(function () {
3567
+ // cross-origin sheet unreachable — left blank, same as
3568
+ // the previous best-effort behavior.
3569
+ })
3570
+ )
3571
+ })(document.styleSheets[i])
3572
+ }
3573
+
3574
+ return withTimeout(
3575
+ Promise.all(enhancementJobs),
3576
+ CSS_INLINE_TIMEOUT_MS,
3577
+ null
3578
+ ).then(function () {
3579
+ return renderNodes(topNodes, pieces).replace(/:root\b/g, "html")
3580
+ })
3581
+ }
3582
+
3583
+ // Test hook only — lets inspector.annotation.test.ts assert on the
3584
+ // collected CSS text directly, since the full capture pipeline needs a
3585
+ // browser Image decoder no test DOM provides.
3586
+ if (window.__ARCHIE_INSPECTOR__) {
3587
+ window.__ARCHIE_INSPECTOR__.collectCaptureCss = collectCaptureCss
3588
+ }
3589
+
3111
3590
  /** Rasterizing an SVG cannot fetch subresources, so same-origin <img>
3112
3591
  * sources are inlined first. Failures leave the image blank rather
3113
3592
  * than aborting the whole capture. */
@@ -3141,15 +3620,22 @@
3141
3620
 
3142
3621
  /**
3143
3622
  * Rasterizes the CURRENT viewport into a PNG data URL via an
3144
- * `<svg><foreignObject>` round-trip. Best-effort by construction: web
3145
- * fonts, cross-origin images and <canvas> pixels do not survive it. The
3146
- * host composes the result with the user's drawing, and falls back to
3147
- * the drawing alone when this rejects.
3623
+ * `<svg><foreignObject>` round-trip. Best-effort by construction:
3624
+ * cross-origin images and <canvas> pixels do not survive it, and web
3625
+ * fonts survive only within `CSS_INLINE_TIMEOUT_MS`. The host composes
3626
+ * the result with the user's drawing, and falls back to the drawing
3627
+ * alone when this rejects.
3148
3628
  */
3149
3629
  function captureViewportDataUrl(pixelRatio) {
3150
3630
  var width = window.innerWidth
3151
3631
  var height = window.innerHeight
3152
3632
  var ratio = Math.min(Math.max(Number(pixelRatio) || 1, 1), 2)
3633
+ // The rasterized SVG's root element is the `<svg>` itself, not
3634
+ // `<html>`, so `rem` would otherwise resolve against the UA default
3635
+ // instead of the live page's root font-size.
3636
+ var rootFontSize = window.getComputedStyle(
3637
+ document.documentElement
3638
+ ).fontSize
3153
3639
 
3154
3640
  var clone = document.documentElement.cloneNode(true)
3155
3641
  var strip = clone.querySelectorAll(
@@ -3159,75 +3645,79 @@
3159
3645
  if (strip[i].parentNode) strip[i].parentNode.removeChild(strip[i])
3160
3646
  }
3161
3647
 
3162
- var styleEl = document.createElement("style")
3163
- styleEl.textContent = collectSameOriginCss()
3164
- var head = clone.querySelector("head")
3165
- if (head) head.appendChild(styleEl)
3166
- else clone.insertBefore(styleEl, clone.firstChild)
3167
-
3168
- return inlineImages(clone).then(function () {
3169
- var markup = new XMLSerializer().serializeToString(clone)
3170
- var svg =
3171
- '<svg xmlns="http://www.w3.org/2000/svg" width="' +
3172
- width +
3173
- '" height="' +
3174
- height +
3175
- '">' +
3176
- '<foreignObject x="0" y="0" width="' +
3177
- width +
3178
- '" height="' +
3179
- height +
3180
- '">' +
3181
- '<div xmlns="http://www.w3.org/1999/xhtml" style="width:' +
3182
- width +
3183
- "px;transform:translate(" +
3184
- -window.scrollX +
3185
- "px," +
3186
- -window.scrollY +
3187
- 'px)">' +
3188
- markup +
3189
- "</div></foreignObject></svg>"
3190
-
3191
- return new Promise(function (resolve, reject) {
3192
- var image = new Image()
3193
- // An SVG the browser refuses to decode can fire NEITHER onload nor
3194
- // onerror (malformed serialization, blocked subresource). Without
3195
- // this guard the host would sit on the request until its own
3196
- // timeout with no diagnostic; the deadline turns that into an
3197
- // explicit VIEWPORT_CAPTURE_FAILED.
3198
- var settled = false
3199
- var deadline = setTimeout(function () {
3200
- if (settled) return
3201
- settled = true
3202
- reject(new Error("svg rasterization timed out"))
3203
- }, ANNOTATION_CAPTURE_TIMEOUT_MS)
3204
- var finish = function (fn, value) {
3205
- if (settled) return
3206
- settled = true
3207
- clearTimeout(deadline)
3208
- fn(value)
3209
- }
3210
- image.onload = function () {
3211
- try {
3212
- var canvas = document.createElement("canvas")
3213
- canvas.width = Math.round(width * ratio)
3214
- canvas.height = Math.round(height * ratio)
3215
- var context = canvas.getContext("2d")
3216
- context.fillStyle = "#ffffff"
3217
- context.fillRect(0, 0, canvas.width, canvas.height)
3218
- context.drawImage(image, 0, 0, canvas.width, canvas.height)
3219
- finish(resolve, canvas.toDataURL("image/png"))
3220
- } catch (error) {
3221
- finish(reject, error)
3648
+ return Promise.all([collectCaptureCss(), inlineImages(clone)]).then(
3649
+ function (results) {
3650
+ var styleEl = document.createElement("style")
3651
+ styleEl.textContent = results[0]
3652
+ var head = clone.querySelector("head")
3653
+ if (head) head.appendChild(styleEl)
3654
+ else clone.insertBefore(styleEl, clone.firstChild)
3655
+
3656
+ var markup = new XMLSerializer().serializeToString(clone)
3657
+ var svg =
3658
+ '<svg xmlns="http://www.w3.org/2000/svg" width="' +
3659
+ width +
3660
+ '" height="' +
3661
+ height +
3662
+ '" style="font-size:' +
3663
+ rootFontSize +
3664
+ '">' +
3665
+ '<foreignObject x="0" y="0" width="' +
3666
+ width +
3667
+ '" height="' +
3668
+ height +
3669
+ '">' +
3670
+ '<div xmlns="http://www.w3.org/1999/xhtml" style="width:' +
3671
+ width +
3672
+ "px;transform:translate(" +
3673
+ -window.scrollX +
3674
+ "px," +
3675
+ -window.scrollY +
3676
+ 'px)">' +
3677
+ markup +
3678
+ "</div></foreignObject></svg>"
3679
+
3680
+ return new Promise(function (resolve, reject) {
3681
+ var image = new Image()
3682
+ // An SVG the browser refuses to decode can fire NEITHER onload nor
3683
+ // onerror (malformed serialization, blocked subresource). Without
3684
+ // this guard the host would sit on the request until its own
3685
+ // timeout with no diagnostic; the deadline turns that into an
3686
+ // explicit VIEWPORT_CAPTURE_FAILED.
3687
+ var settled = false
3688
+ var deadline = setTimeout(function () {
3689
+ if (settled) return
3690
+ settled = true
3691
+ reject(new Error("svg rasterization timed out"))
3692
+ }, ANNOTATION_CAPTURE_TIMEOUT_MS)
3693
+ var finish = function (fn, value) {
3694
+ if (settled) return
3695
+ settled = true
3696
+ clearTimeout(deadline)
3697
+ fn(value)
3222
3698
  }
3223
- }
3224
- image.onerror = function () {
3225
- finish(reject, new Error("svg rasterization failed"))
3226
- }
3227
- image.src =
3228
- "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg)
3229
- })
3230
- })
3699
+ image.onload = function () {
3700
+ try {
3701
+ var canvas = document.createElement("canvas")
3702
+ canvas.width = Math.round(width * ratio)
3703
+ canvas.height = Math.round(height * ratio)
3704
+ var context = canvas.getContext("2d")
3705
+ context.fillStyle = "#ffffff"
3706
+ context.fillRect(0, 0, canvas.width, canvas.height)
3707
+ context.drawImage(image, 0, 0, canvas.width, canvas.height)
3708
+ finish(resolve, canvas.toDataURL("image/png"))
3709
+ } catch (error) {
3710
+ finish(reject, error)
3711
+ }
3712
+ }
3713
+ image.onerror = function () {
3714
+ finish(reject, new Error("svg rasterization failed"))
3715
+ }
3716
+ image.src =
3717
+ "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg)
3718
+ })
3719
+ }
3720
+ )
3231
3721
  }
3232
3722
 
3233
3723
  function extractMetadata(element) {
@@ -3279,6 +3769,12 @@
3279
3769
  // this script's version. The slot string is opaque here; validating it
3280
3770
  // against kit-runtime's `slot-vocabulary` is the host's job.
3281
3771
  editorMeta: editorMeta ? editorMeta.meta : null,
3772
+ // vid-identity migration — `vid`/`nodeMeta` derived from the SAME
3773
+ // meta as `editorMeta` above (`resolveNodeVid`/`resolveNodeMeta`,
3774
+ // mirroring `node-key.ts`/`fiber-bridge.ts`'s `getNodeMeta`). Both
3775
+ // are null for a legacy node with no `data-vid`.
3776
+ vid: resolveNodeVid(element, editorMeta ? editorMeta.meta : null),
3777
+ nodeMeta: resolveNodeMeta(element, editorMeta ? editorMeta.meta : null),
3282
3778
  // ARCH-883 F2 — the two things the host cannot derive from
3283
3779
  // `editorMeta` alone (SDD §2.9):
3284
3780
  //
@@ -3817,6 +4313,45 @@
3817
4313
  return
3818
4314
  }
3819
4315
 
4316
+ // `data-vid` identity migration — the DnD runtime's `drag-started`/
4317
+ // `drag-refused` events (`useDragLifecycle.ts`, `host-bridge.ts`)
4318
+ // otherwise die inside the iframe: nothing relayed them to the host.
4319
+ // Reuses `refuseNode` for `REFUSED` so the wire shape is the SAME
4320
+ // one produced by every other refusal path in this file (never a
4321
+ // second, drifting literal) and validates against
4322
+ // `RuntimeMessageSchema` (`@8base-archie/editor-protocol`).
4323
+ if (detail.type === "drag-refused") {
4324
+ refuseNode(detail.code, detail.cause, detail.vid)
4325
+ return
4326
+ }
4327
+
4328
+ if (detail.type === "drag-started") {
4329
+ // `DragStartMessageSchema.targetVid` is REQUIRED (not optional) —
4330
+ // unlike `REFUSED.vid`. A legacy (no `data-vid`) page's dragged
4331
+ // node has no vid to report, so the message cannot be made to
4332
+ // satisfy the schema; skip the relay rather than send a shape
4333
+ // that fails validation on the host. `droppable` is filtered to
4334
+ // well-formed vids for the same reason — the projection's own ids
4335
+ // can be legacy `file:line:col:componentName` strings, which the
4336
+ // schema's `z.array(Vid)` would reject.
4337
+ if (typeof detail.targetVid !== "string" || !VID_PATTERN.test(detail.targetVid)) {
4338
+ veTrace("runtime-event:drag-started:skipped-no-vid", {})
4339
+ return
4340
+ }
4341
+ var droppableVids = Array.isArray(detail.droppable)
4342
+ ? detail.droppable.filter(function (id) {
4343
+ return typeof id === "string" && VID_PATTERN.test(id)
4344
+ })
4345
+ : []
4346
+ postMessageToParent({
4347
+ source: "archie-devtools",
4348
+ type: "DRAG_START",
4349
+ targetVid: detail.targetVid,
4350
+ droppable: droppableVids,
4351
+ })
4352
+ return
4353
+ }
4354
+
3820
4355
  // ARCH-1367 F6 — attach the drop container's editor meta chain to a
3821
4356
  // container descriptor ({selector, nodeId, …}). Shared by the
3822
4357
  // `element-inserted` relay AND the `arrange-session-changed` relay: