@grahlnn/comps 0.1.6 → 0.1.8

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/dist/index.js CHANGED
@@ -2600,20 +2600,273 @@ var SHARED_GLYPH_TYPOGRAPHY_STYLE = {
2600
2600
  wordSpacing: "inherit",
2601
2601
  direction: "inherit"
2602
2602
  };
2603
- var MEASUREMENT_GLYPH_STYLE = {
2604
- ...SHARED_GLYPH_TYPOGRAPHY_STYLE,
2605
- display: "inline"
2606
- };
2607
2603
  var ABSOLUTE_GLYPH_STYLE = {
2604
+ position: "absolute",
2605
+ display: "block",
2606
+ overflow: "hidden",
2607
+ transformOrigin: "left top"
2608
+ };
2609
+ var CONTEXT_SLICE_TEXT_STYLE = {
2608
2610
  ...SHARED_GLYPH_TYPOGRAPHY_STYLE,
2609
2611
  position: "absolute",
2610
2612
  display: "block",
2611
- overflow: "visible",
2612
- transformOrigin: "left top",
2613
- whiteSpace: "pre"
2613
+ minWidth: 0,
2614
+ whiteSpace: "inherit"
2614
2615
  };
2615
2616
  var graphemeSegmenter = null;
2616
2617
  var domMeasurementService = null;
2618
+ var torphDebugInstanceOrdinal = 0;
2619
+ var TORPH_TRACE_MAX_BYTES = 4 * 1024 * 1024;
2620
+ var TORPH_TRACE_MAX_LINES = 4000;
2621
+ var TORPH_TRACE_SCHEMA_VERSION = 2;
2622
+ function nextTorphDebugInstanceId() {
2623
+ torphDebugInstanceOrdinal += 1;
2624
+ return torphDebugInstanceOrdinal;
2625
+ }
2626
+ function readTorphDebugConfig() {
2627
+ const scope = globalThis;
2628
+ return scope.__TORPH_DEBUG__ ?? null;
2629
+ }
2630
+ function shouldCaptureTorphTrace(config) {
2631
+ if (config === null) {
2632
+ return true;
2633
+ }
2634
+ if (typeof config === "boolean") {
2635
+ return config;
2636
+ }
2637
+ if (config.capture === false) {
2638
+ return false;
2639
+ }
2640
+ return true;
2641
+ }
2642
+ function isTorphDebugEnabled(config) {
2643
+ if (config === null) {
2644
+ return false;
2645
+ }
2646
+ if (typeof config === "boolean") {
2647
+ return config;
2648
+ }
2649
+ if (config.console !== undefined) {
2650
+ return config.console;
2651
+ }
2652
+ return config.enabled !== false;
2653
+ }
2654
+ function shouldRunTorphInstrumentation(config) {
2655
+ if (shouldCaptureTorphTrace(config)) {
2656
+ return true;
2657
+ }
2658
+ return isTorphDebugEnabled(config);
2659
+ }
2660
+ function getTorphTraceStore() {
2661
+ const scope = globalThis;
2662
+ let store = scope.__TORPH_TRACE_STORE__;
2663
+ if (store !== undefined) {
2664
+ return store;
2665
+ }
2666
+ store = {
2667
+ lines: [],
2668
+ nextSeq: 1,
2669
+ totalBytes: 0
2670
+ };
2671
+ scope.__TORPH_TRACE_STORE__ = store;
2672
+ return store;
2673
+ }
2674
+ function getTorphTraceText() {
2675
+ return getTorphTraceStore().lines.join("");
2676
+ }
2677
+ function clearTorphTrace() {
2678
+ const store = getTorphTraceStore();
2679
+ store.lines = [];
2680
+ store.nextSeq = 1;
2681
+ store.totalBytes = 0;
2682
+ }
2683
+ function downloadTorphTrace(filename) {
2684
+ if (typeof document === "undefined") {
2685
+ return null;
2686
+ }
2687
+ const text = getTorphTraceText();
2688
+ const blob = new Blob([text], { type: "application/x-ndjson;charset=utf-8" });
2689
+ const href = URL.createObjectURL(blob);
2690
+ const anchor = document.createElement("a");
2691
+ let resolvedFilename = filename;
2692
+ if (resolvedFilename === undefined) {
2693
+ resolvedFilename = `torph-trace-${new Date().toISOString().replaceAll(":", "-")}.jsonl`;
2694
+ }
2695
+ anchor.href = href;
2696
+ anchor.download = resolvedFilename;
2697
+ anchor.click();
2698
+ window.setTimeout(() => {
2699
+ URL.revokeObjectURL(href);
2700
+ }, 0);
2701
+ return resolvedFilename;
2702
+ }
2703
+ function ensureTorphTraceApi() {
2704
+ const scope = globalThis;
2705
+ if (scope.__TORPH_TRACE__ !== undefined) {
2706
+ return scope.__TORPH_TRACE__;
2707
+ }
2708
+ const api = {
2709
+ clear: clearTorphTrace,
2710
+ count: () => getTorphTraceStore().lines.length,
2711
+ download: downloadTorphTrace,
2712
+ text: getTorphTraceText
2713
+ };
2714
+ scope.__TORPH_TRACE__ = api;
2715
+ return api;
2716
+ }
2717
+ function appendTorphTrace(instanceId, event, payload) {
2718
+ ensureTorphTraceApi();
2719
+ const store = getTorphTraceStore();
2720
+ const entry = {
2721
+ instanceId,
2722
+ event,
2723
+ payload,
2724
+ seq: store.nextSeq,
2725
+ time: new Date().toISOString()
2726
+ };
2727
+ store.nextSeq += 1;
2728
+ const line = `${JSON.stringify(entry)}
2729
+ `;
2730
+ store.lines.push(line);
2731
+ store.totalBytes += line.length;
2732
+ while (store.lines.length > TORPH_TRACE_MAX_LINES || store.totalBytes > TORPH_TRACE_MAX_BYTES) {
2733
+ const removed = store.lines.shift();
2734
+ if (removed === undefined) {
2735
+ break;
2736
+ }
2737
+ store.totalBytes = Math.max(0, store.totalBytes - removed.length);
2738
+ }
2739
+ }
2740
+ function roundDebugValue(value) {
2741
+ if (value === null || value === undefined) {
2742
+ return value;
2743
+ }
2744
+ return Math.round(value * 100) / 100;
2745
+ }
2746
+ function summarizeDebugSnapshot(snapshot) {
2747
+ if (snapshot === null) {
2748
+ return null;
2749
+ }
2750
+ return {
2751
+ text: snapshot.text,
2752
+ renderText: snapshot.renderText,
2753
+ width: roundDebugValue(snapshot.width),
2754
+ height: roundDebugValue(snapshot.height),
2755
+ graphemes: snapshot.graphemes.length
2756
+ };
2757
+ }
2758
+ function summarizeDebugMeasurement(measurement) {
2759
+ if (measurement === null) {
2760
+ return null;
2761
+ }
2762
+ return {
2763
+ layoutInlineSize: roundDebugValue(measurement.layoutInlineSize),
2764
+ reservedInlineSize: roundDebugValue(measurement.reservedInlineSize),
2765
+ flowInlineSize: roundDebugValue(measurement.flowInlineSize),
2766
+ rootOrigin: {
2767
+ left: roundDebugValue(measurement.rootOrigin.left),
2768
+ top: roundDebugValue(measurement.rootOrigin.top)
2769
+ },
2770
+ snapshot: summarizeDebugSnapshot(measurement.snapshot)
2771
+ };
2772
+ }
2773
+ function summarizeDebugRect(rect) {
2774
+ if (rect === null) {
2775
+ return null;
2776
+ }
2777
+ return {
2778
+ left: roundDebugValue(rect.left),
2779
+ top: roundDebugValue(rect.top),
2780
+ width: roundDebugValue(rect.width),
2781
+ height: roundDebugValue(rect.height)
2782
+ };
2783
+ }
2784
+ function collectDebugAnchorIndices(length) {
2785
+ const indices = new Set;
2786
+ if (length <= 0) {
2787
+ return [];
2788
+ }
2789
+ indices.add(0);
2790
+ if (length > 1) {
2791
+ indices.add(1);
2792
+ indices.add(length - 2);
2793
+ }
2794
+ if (length > 2) {
2795
+ indices.add(Math.floor((length - 1) / 2));
2796
+ }
2797
+ indices.add(length - 1);
2798
+ return Array.from(indices).sort((left, right) => left - right);
2799
+ }
2800
+ function summarizeDebugViewportAnchors(snapshot, rootRect) {
2801
+ if (snapshot === null || rootRect === null) {
2802
+ return null;
2803
+ }
2804
+ const anchors = [];
2805
+ const anchorIndices = collectDebugAnchorIndices(snapshot.graphemes.length);
2806
+ for (const index of anchorIndices) {
2807
+ const grapheme = snapshot.graphemes[index];
2808
+ if (grapheme === undefined) {
2809
+ continue;
2810
+ }
2811
+ anchors.push({
2812
+ index,
2813
+ glyph: grapheme.glyph,
2814
+ left: roundDebugValue(rootRect.left + grapheme.left),
2815
+ top: roundDebugValue(rootRect.top + grapheme.top),
2816
+ width: roundDebugValue(grapheme.width),
2817
+ height: roundDebugValue(grapheme.height)
2818
+ });
2819
+ }
2820
+ return anchors;
2821
+ }
2822
+ function summarizeDebugRootOriginDrift(measurement, rootRect) {
2823
+ if (measurement === null || rootRect === null) {
2824
+ return null;
2825
+ }
2826
+ return {
2827
+ expectedLeft: roundDebugValue(measurement.rootOrigin.left),
2828
+ expectedTop: roundDebugValue(measurement.rootOrigin.top),
2829
+ actualLeft: roundDebugValue(rootRect.left),
2830
+ actualTop: roundDebugValue(rootRect.top),
2831
+ deltaLeft: roundDebugValue(rootRect.left - measurement.rootOrigin.left),
2832
+ deltaTop: roundDebugValue(rootRect.top - measurement.rootOrigin.top)
2833
+ };
2834
+ }
2835
+ function summarizeSnapshotDrift(drift) {
2836
+ return {
2837
+ comparedGlyphs: drift.comparedGlyphs,
2838
+ expectedGlyphs: drift.expectedGlyphs,
2839
+ actualGlyphs: drift.actualGlyphs,
2840
+ snapshotWidthDelta: roundDebugValue(drift.snapshotWidthDelta),
2841
+ snapshotHeightDelta: roundDebugValue(drift.snapshotHeightDelta),
2842
+ maxAbsLeftDelta: roundDebugValue(drift.maxAbsLeftDelta),
2843
+ maxAbsTopDelta: roundDebugValue(drift.maxAbsTopDelta),
2844
+ maxAbsWidthDelta: roundDebugValue(drift.maxAbsWidthDelta),
2845
+ maxAbsHeightDelta: roundDebugValue(drift.maxAbsHeightDelta),
2846
+ mismatches: drift.mismatches.map((mismatch) => ({
2847
+ index: mismatch.index,
2848
+ glyph: mismatch.glyph,
2849
+ leftDelta: roundDebugValue(mismatch.leftDelta),
2850
+ topDelta: roundDebugValue(mismatch.topDelta),
2851
+ widthDelta: roundDebugValue(mismatch.widthDelta),
2852
+ heightDelta: roundDebugValue(mismatch.heightDelta)
2853
+ }))
2854
+ };
2855
+ }
2856
+ function logTorphDebug(instanceId, event, payload) {
2857
+ const config = readTorphDebugConfig();
2858
+ const captureTrace = shouldCaptureTorphTrace(config);
2859
+ const logToConsole = isTorphDebugEnabled(config);
2860
+ if (!captureTrace && !logToConsole) {
2861
+ return;
2862
+ }
2863
+ if (captureTrace) {
2864
+ appendTorphTrace(instanceId, event, payload);
2865
+ }
2866
+ if (logToConsole) {
2867
+ console.log(`[Torph#${instanceId}] ${event}`, payload);
2868
+ }
2869
+ }
2617
2870
  function parsePx(value) {
2618
2871
  const parsed = Number.parseFloat(value);
2619
2872
  if (Number.isFinite(parsed)) {
@@ -2701,6 +2954,21 @@ function isSingleLineSnapshot(snapshot) {
2701
2954
  const firstTop = snapshot.graphemes[0].top;
2702
2955
  return snapshot.graphemes.every((grapheme) => nearlyEqual(grapheme.top, firstTop, MORPH.lineGroupingEpsilon));
2703
2956
  }
2957
+ function countSnapshotLines(snapshot) {
2958
+ if (snapshot.graphemes.length === 0) {
2959
+ return 0;
2960
+ }
2961
+ let lineCount = 1;
2962
+ let currentTop = snapshot.graphemes[0].top;
2963
+ for (let index = 1;index < snapshot.graphemes.length; index += 1) {
2964
+ const top = snapshot.graphemes[index].top;
2965
+ if (!nearlyEqual(top, currentTop, MORPH.lineGroupingEpsilon)) {
2966
+ currentTop = top;
2967
+ lineCount += 1;
2968
+ }
2969
+ }
2970
+ return lineCount;
2971
+ }
2704
2972
  function acquireMorphMeasurementInvalidationListeners() {
2705
2973
  activeMorphMeasurementConsumers += 1;
2706
2974
  if (detachMorphMeasurementInvalidationListeners === null) {
@@ -2791,25 +3059,6 @@ function getSegmenter() {
2791
3059
  });
2792
3060
  return graphemeSegmenter;
2793
3061
  }
2794
- function createMeasurementGlyphNode() {
2795
- const node = document.createElement("span");
2796
- node.style.font = "inherit";
2797
- node.style.fontKerning = "inherit";
2798
- node.style.fontFeatureSettings = "inherit";
2799
- node.style.fontOpticalSizing = "inherit";
2800
- node.style.fontStretch = "inherit";
2801
- node.style.fontStyle = "inherit";
2802
- node.style.fontVariant = "inherit";
2803
- node.style.fontVariantNumeric = "inherit";
2804
- node.style.fontVariationSettings = "inherit";
2805
- node.style.fontWeight = "inherit";
2806
- node.style.letterSpacing = "inherit";
2807
- node.style.textTransform = "inherit";
2808
- node.style.wordSpacing = "inherit";
2809
- node.style.direction = "inherit";
2810
- node.style.display = "inline";
2811
- return node;
2812
- }
2813
3062
  function getDomMeasurementService() {
2814
3063
  if (domMeasurementService !== null) {
2815
3064
  return domMeasurementService;
@@ -2831,25 +3080,10 @@ function getDomMeasurementService() {
2831
3080
  document.body.appendChild(root);
2832
3081
  domMeasurementService = {
2833
3082
  root,
2834
- host,
2835
- glyphNodes: []
3083
+ host
2836
3084
  };
2837
3085
  return domMeasurementService;
2838
3086
  }
2839
- function syncMeasurementGlyphNodes(service, segments) {
2840
- while (service.glyphNodes.length < segments.length) {
2841
- const node = createMeasurementGlyphNode();
2842
- service.host.appendChild(node);
2843
- service.glyphNodes.push(node);
2844
- }
2845
- while (service.glyphNodes.length > segments.length) {
2846
- const node = service.glyphNodes.pop();
2847
- node?.remove();
2848
- }
2849
- for (let index = 0;index < segments.length; index += 1) {
2850
- service.glyphNodes[index].textContent = segments[index].glyph;
2851
- }
2852
- }
2853
3087
  function applyMeasurementHostStyle({
2854
3088
  host,
2855
3089
  root,
@@ -2959,51 +3193,38 @@ function rememberCachedMorphSnapshot(cache, cacheKey, snapshot) {
2959
3193
  }
2960
3194
  }
2961
3195
  }
2962
- function assertMeasurementLayer(layer, segments) {
3196
+ function assertMeasurementLayer(layer) {
2963
3197
  if (layer === null) {
2964
3198
  throw new Error("Torph measurement layer is missing.");
2965
3199
  }
2966
- if (layer.children.length !== segments.length) {
2967
- throw new Error(`Torph measurement layer is out of sync. Expected ${segments.length} glyph nodes, received ${layer.children.length}.`);
2968
- }
2969
3200
  return layer;
2970
3201
  }
2971
3202
  function readMeasuredGlyphLayouts(layer, layerRect, segments) {
2972
3203
  const measuredGlyphs = [];
2973
- const layerOffsetTop = layer.offsetTop;
3204
+ const textNode = readFirstTextNode(layer);
3205
+ if (textNode === null) {
3206
+ throw new Error("Torph measurement layer text node is missing.");
3207
+ }
3208
+ const range = document.createRange();
3209
+ let offset = 0;
2974
3210
  for (let index = 0;index < segments.length; index += 1) {
2975
3211
  const segment = segments[index];
2976
- const child = layer.children[index];
2977
- if (!(child instanceof HTMLElement)) {
2978
- throw new Error(`Torph glyph node ${index} is not an HTMLElement.`);
2979
- }
2980
- const rect = child.getBoundingClientRect();
3212
+ const nextOffset = offset + segment.glyph.length;
3213
+ range.setStart(textNode, offset);
3214
+ range.setEnd(textNode, nextOffset);
3215
+ const rect = range.getBoundingClientRect();
2981
3216
  measuredGlyphs.push({
2982
3217
  glyph: segment.glyph,
2983
3218
  key: segment.key,
2984
3219
  left: rect.left - layerRect.left,
2985
- top: child.offsetTop - layerOffsetTop,
2986
- width: rect.width
3220
+ top: rect.top - layerRect.top,
3221
+ width: rect.width,
3222
+ height: rect.height
2987
3223
  });
3224
+ offset = nextOffset;
2988
3225
  }
2989
3226
  return measuredGlyphs;
2990
3227
  }
2991
- function assignMeasuredGlyphLineIndices(measuredGlyphs) {
2992
- const lineIndices = [];
2993
- let lineCount = 0;
2994
- let currentLineTop = null;
2995
- for (const glyph of measuredGlyphs) {
2996
- if (currentLineTop === null || Math.abs(glyph.top - currentLineTop) > MORPH.lineGroupingEpsilon) {
2997
- currentLineTop = glyph.top;
2998
- lineCount += 1;
2999
- }
3000
- lineIndices.push(lineCount - 1);
3001
- }
3002
- return {
3003
- lineCount,
3004
- lineIndices
3005
- };
3006
- }
3007
3228
  function measureMorphSnapshotFromLayer(text, renderText, segments, layer) {
3008
3229
  if (renderText.length === 0) {
3009
3230
  return {
@@ -3014,28 +3235,19 @@ function measureMorphSnapshotFromLayer(text, renderText, segments, layer) {
3014
3235
  graphemes: []
3015
3236
  };
3016
3237
  }
3017
- const measurementLayer = assertMeasurementLayer(layer, segments);
3238
+ const measurementLayer = assertMeasurementLayer(layer);
3018
3239
  const layerRect = measurementLayer.getBoundingClientRect();
3019
3240
  const measuredGlyphs = readMeasuredGlyphLayouts(measurementLayer, layerRect, segments);
3020
- const { lineCount, lineIndices } = assignMeasuredGlyphLineIndices(measuredGlyphs);
3021
- let lineHeight = 0;
3022
- if (lineCount !== 0) {
3023
- lineHeight = layerRect.height / lineCount;
3024
- }
3025
3241
  let width = 0;
3026
- const graphemes = measuredGlyphs.map((glyph, index) => {
3027
- const lineIndex = lineIndices[index];
3028
- if (lineIndex === undefined) {
3029
- throw new Error("Torph failed to assign a line index.");
3030
- }
3242
+ const graphemes = measuredGlyphs.map((glyph) => {
3031
3243
  width = Math.max(width, glyph.left + glyph.width);
3032
3244
  return {
3033
3245
  glyph: glyph.glyph,
3034
3246
  key: glyph.key,
3035
3247
  left: glyph.left,
3036
- top: lineIndex * lineHeight,
3248
+ top: glyph.top,
3037
3249
  width: glyph.width,
3038
- height: lineHeight
3250
+ height: glyph.height
3039
3251
  };
3040
3252
  });
3041
3253
  return {
@@ -3070,13 +3282,182 @@ function measureMorphSnapshotWithDomService({
3070
3282
  layoutContext,
3071
3283
  useContentInlineSize
3072
3284
  });
3073
- syncMeasurementGlyphNodes(service, segments);
3285
+ service.host.textContent = renderText;
3074
3286
  return measureMorphSnapshotFromLayer(text, renderText, segments, service.host);
3075
3287
  }
3076
3288
  function readRootOrigin(node) {
3077
3289
  const rect = node.getBoundingClientRect();
3078
3290
  return { left: rect.left, top: rect.top };
3079
3291
  }
3292
+ function readFlowInlineSize(node) {
3293
+ if (node === null) {
3294
+ return null;
3295
+ }
3296
+ return node.getBoundingClientRect().width;
3297
+ }
3298
+ function readFlowLineCount(node) {
3299
+ if (node === null) {
3300
+ return null;
3301
+ }
3302
+ const rects = Array.from(node.getClientRects());
3303
+ if (rects.length === 0) {
3304
+ if ((node.textContent ?? "").length === 0) {
3305
+ return 0;
3306
+ }
3307
+ return 1;
3308
+ }
3309
+ let lineCount = 0;
3310
+ let currentTop = null;
3311
+ for (const rect of rects) {
3312
+ if (currentTop === null || !nearlyEqual(rect.top, currentTop, MORPH.lineGroupingEpsilon)) {
3313
+ currentTop = rect.top;
3314
+ lineCount += 1;
3315
+ }
3316
+ }
3317
+ return lineCount;
3318
+ }
3319
+ function readFirstTextNode(node) {
3320
+ if (node === null) {
3321
+ return null;
3322
+ }
3323
+ for (const childNode of node.childNodes) {
3324
+ if (childNode.nodeType === Node.TEXT_NODE) {
3325
+ return childNode;
3326
+ }
3327
+ }
3328
+ return null;
3329
+ }
3330
+ function measureLiveFlowSnapshot(root, flowTextNode) {
3331
+ const textNode = readFirstTextNode(flowTextNode);
3332
+ if (textNode === null) {
3333
+ return null;
3334
+ }
3335
+ const text = textNode.data;
3336
+ const renderText = text;
3337
+ if (renderText.length === 0) {
3338
+ return {
3339
+ text,
3340
+ renderText,
3341
+ width: 0,
3342
+ height: 0,
3343
+ graphemes: []
3344
+ };
3345
+ }
3346
+ const rootRect = root.getBoundingClientRect();
3347
+ const range = document.createRange();
3348
+ const graphemes = [];
3349
+ let width = 0;
3350
+ let height = 0;
3351
+ let offset = 0;
3352
+ const segments = readCachedMorphSegments(renderText);
3353
+ for (const segment of segments) {
3354
+ const nextOffset = offset + segment.glyph.length;
3355
+ range.setStart(textNode, offset);
3356
+ range.setEnd(textNode, nextOffset);
3357
+ const rect = range.getBoundingClientRect();
3358
+ graphemes.push({
3359
+ glyph: segment.glyph,
3360
+ key: segment.key,
3361
+ left: rect.left - rootRect.left,
3362
+ top: rect.top - rootRect.top,
3363
+ width: rect.width,
3364
+ height: rect.height
3365
+ });
3366
+ width = Math.max(width, rect.right - rootRect.left);
3367
+ height = Math.max(height, rect.bottom - rootRect.top);
3368
+ offset = nextOffset;
3369
+ }
3370
+ return {
3371
+ text,
3372
+ renderText,
3373
+ width,
3374
+ height,
3375
+ graphemes
3376
+ };
3377
+ }
3378
+ function measureSnapshotDrift(expected, actual) {
3379
+ const comparedGlyphs = Math.min(expected.graphemes.length, actual.graphemes.length);
3380
+ const mismatches = [];
3381
+ let maxAbsLeftDelta = 0;
3382
+ let maxAbsTopDelta = 0;
3383
+ let maxAbsWidthDelta = 0;
3384
+ let maxAbsHeightDelta = 0;
3385
+ for (let index = 0;index < comparedGlyphs; index += 1) {
3386
+ const expectedGlyph = expected.graphemes[index];
3387
+ const actualGlyph = actual.graphemes[index];
3388
+ const leftDelta = actualGlyph.left - expectedGlyph.left;
3389
+ const topDelta = actualGlyph.top - expectedGlyph.top;
3390
+ const widthDelta = actualGlyph.width - expectedGlyph.width;
3391
+ const heightDelta = actualGlyph.height - expectedGlyph.height;
3392
+ maxAbsLeftDelta = Math.max(maxAbsLeftDelta, Math.abs(leftDelta));
3393
+ maxAbsTopDelta = Math.max(maxAbsTopDelta, Math.abs(topDelta));
3394
+ maxAbsWidthDelta = Math.max(maxAbsWidthDelta, Math.abs(widthDelta));
3395
+ maxAbsHeightDelta = Math.max(maxAbsHeightDelta, Math.abs(heightDelta));
3396
+ if (mismatches.length < 8 && (Math.abs(leftDelta) > MORPH.geometryEpsilon || Math.abs(topDelta) > MORPH.geometryEpsilon || Math.abs(widthDelta) > MORPH.geometryEpsilon || Math.abs(heightDelta) > MORPH.geometryEpsilon)) {
3397
+ mismatches.push({
3398
+ index,
3399
+ glyph: expectedGlyph.glyph,
3400
+ leftDelta,
3401
+ topDelta,
3402
+ widthDelta,
3403
+ heightDelta
3404
+ });
3405
+ }
3406
+ }
3407
+ return {
3408
+ comparedGlyphs,
3409
+ expectedGlyphs: expected.graphemes.length,
3410
+ actualGlyphs: actual.graphemes.length,
3411
+ maxAbsLeftDelta,
3412
+ maxAbsTopDelta,
3413
+ maxAbsWidthDelta,
3414
+ maxAbsHeightDelta,
3415
+ snapshotWidthDelta: actual.width - expected.width,
3416
+ snapshotHeightDelta: actual.height - expected.height,
3417
+ mismatches
3418
+ };
3419
+ }
3420
+ function measureOverlayBoxSnapshot(root, overlayRoot, role) {
3421
+ const nodes = overlayRoot.querySelectorAll(`[data-morph-role='${role}']`);
3422
+ if (nodes.length === 0) {
3423
+ return null;
3424
+ }
3425
+ const rootRect = root.getBoundingClientRect();
3426
+ const graphemes = [];
3427
+ let width = 0;
3428
+ let height = 0;
3429
+ let renderText = "";
3430
+ for (const node of nodes) {
3431
+ const key = node.dataset.morphKey;
3432
+ const glyph = node.dataset.morphGlyph;
3433
+ if (key === undefined || glyph === undefined) {
3434
+ return null;
3435
+ }
3436
+ const rect = node.getBoundingClientRect();
3437
+ const left = rect.left - rootRect.left;
3438
+ const top = rect.top - rootRect.top;
3439
+ const boxWidth = rect.width;
3440
+ const boxHeight = rect.height;
3441
+ graphemes.push({
3442
+ glyph,
3443
+ key,
3444
+ left,
3445
+ top,
3446
+ width: boxWidth,
3447
+ height: boxHeight
3448
+ });
3449
+ renderText += glyph;
3450
+ width = Math.max(width, left + boxWidth);
3451
+ height = Math.max(height, top + boxHeight);
3452
+ }
3453
+ return {
3454
+ text: renderText,
3455
+ renderText,
3456
+ width,
3457
+ height,
3458
+ graphemes
3459
+ };
3460
+ }
3080
3461
  function getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize) {
3081
3462
  const backend = getPretextMorphMeasurementBackend(text, layoutContext);
3082
3463
  if (backend !== "probe") {
@@ -3099,6 +3480,12 @@ function getTrustedPretextMeasurementBackend(text, renderText, layoutContext, us
3099
3480
  }
3100
3481
  return "dom";
3101
3482
  }
3483
+ function resolveContentWidthLockInlineSize(layoutHint) {
3484
+ if (layoutHint.flowInlineSize !== null) {
3485
+ return layoutHint.flowInlineSize;
3486
+ }
3487
+ return layoutHint.snapshot.width;
3488
+ }
3102
3489
  function shouldMeasureUsingContentInlineSize(layoutContext, layoutHint) {
3103
3490
  if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
3104
3491
  return true;
@@ -3109,18 +3496,50 @@ function shouldMeasureUsingContentInlineSize(layoutContext, layoutHint) {
3109
3496
  if (layoutHint === null || !isSingleLineSnapshot(layoutHint.snapshot)) {
3110
3497
  return false;
3111
3498
  }
3112
- return nearlyEqual(layoutHint.layoutInlineSize, layoutHint.snapshot.width, MORPH.contentWidthLockEpsilon);
3499
+ return nearlyEqual(layoutHint.layoutInlineSize, resolveContentWidthLockInlineSize(layoutHint), MORPH.contentWidthLockEpsilon);
3500
+ }
3501
+ function shouldHealIdleMeasurementFromFlow(measurement, flowLineCount) {
3502
+ if (flowLineCount !== 1) {
3503
+ return false;
3504
+ }
3505
+ return countSnapshotLines(measurement.snapshot) > 1;
3506
+ }
3507
+ function refineMeasurementWithLiveGeometry(measurement, liveGeometry) {
3508
+ const sameFlowInlineSize = measurement.flowInlineSize === null && liveGeometry.flowInlineSize === null || measurement.flowInlineSize !== null && liveGeometry.flowInlineSize !== null && nearlyEqual(measurement.flowInlineSize, liveGeometry.flowInlineSize);
3509
+ if (sameFlowInlineSize && nearlyEqual(measurement.rootOrigin.left, liveGeometry.rootOrigin.left) && nearlyEqual(measurement.rootOrigin.top, liveGeometry.rootOrigin.top)) {
3510
+ return measurement;
3511
+ }
3512
+ return {
3513
+ snapshot: measurement.snapshot,
3514
+ layoutInlineSize: measurement.layoutInlineSize,
3515
+ reservedInlineSize: measurement.reservedInlineSize,
3516
+ flowInlineSize: liveGeometry.flowInlineSize,
3517
+ rootOrigin: liveGeometry.rootOrigin
3518
+ };
3519
+ }
3520
+ function refineMeasurementFromLiveNodes(measurement, root, flowTextNode) {
3521
+ if (root === null) {
3522
+ return measurement;
3523
+ }
3524
+ return refineMeasurementWithLiveGeometry(measurement, {
3525
+ flowInlineSize: readFlowInlineSize(flowTextNode),
3526
+ rootOrigin: readRootOrigin(root)
3527
+ });
3113
3528
  }
3114
3529
  function createMorphMeasurementRequest({
3115
3530
  text,
3116
3531
  layoutContext,
3117
- layoutHint
3532
+ layoutHint,
3533
+ forceContentInlineSize = false
3118
3534
  }) {
3119
3535
  if (layoutContext === null) {
3120
3536
  return null;
3121
3537
  }
3122
3538
  const renderText = getPretextMorphRenderedText(text, layoutContext);
3123
- const useContentInlineSize = shouldMeasureUsingContentInlineSize(layoutContext, layoutHint);
3539
+ let useContentInlineSize = shouldMeasureUsingContentInlineSize(layoutContext, layoutHint);
3540
+ if (forceContentInlineSize) {
3541
+ useContentInlineSize = true;
3542
+ }
3124
3543
  const measurementBackend = getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize);
3125
3544
  let segments = readCachedMorphSegments(renderText);
3126
3545
  if (measurementBackend === "pretext") {
@@ -3244,6 +3663,7 @@ function measureFromNodes({
3244
3663
  snapshot,
3245
3664
  layoutInlineSize,
3246
3665
  reservedInlineSize,
3666
+ flowInlineSize: null,
3247
3667
  rootOrigin: readRootOrigin(root)
3248
3668
  };
3249
3669
  }
@@ -3255,6 +3675,7 @@ function pinMeasurementToCurrentOrigin(measurement, origin) {
3255
3675
  snapshot: measurement.snapshot,
3256
3676
  layoutInlineSize: measurement.layoutInlineSize,
3257
3677
  reservedInlineSize: measurement.reservedInlineSize,
3678
+ flowInlineSize: measurement.flowInlineSize,
3258
3679
  rootOrigin: origin
3259
3680
  };
3260
3681
  }
@@ -3334,6 +3755,8 @@ function buildMorphPlan(previous, next, visualBridge = ZERO_BRIDGE) {
3334
3755
  frameHeight: frame.height,
3335
3756
  layoutInlineSizeFrom: previous.layoutInlineSize,
3336
3757
  layoutInlineSizeTo: next.layoutInlineSize,
3758
+ sourceRenderText: previous.snapshot.renderText,
3759
+ targetRenderText: next.snapshot.renderText,
3337
3760
  visualBridge,
3338
3761
  liveItems: next.snapshot.graphemes.map((grapheme) => {
3339
3762
  const move = movesByDestinationKey.get(grapheme.key);
@@ -3384,7 +3807,7 @@ function sameMeasurement(a, b) {
3384
3807
  if (a === b) {
3385
3808
  return true;
3386
3809
  }
3387
- return sameSnapshot(a.snapshot, b.snapshot) && nearlyEqual(a.layoutInlineSize, b.layoutInlineSize) && (a.reservedInlineSize === null && b.reservedInlineSize === null || a.reservedInlineSize !== null && b.reservedInlineSize !== null && nearlyEqual(a.reservedInlineSize, b.reservedInlineSize)) && nearlyEqual(a.rootOrigin.left, b.rootOrigin.left) && nearlyEqual(a.rootOrigin.top, b.rootOrigin.top);
3810
+ return sameSnapshot(a.snapshot, b.snapshot) && nearlyEqual(a.layoutInlineSize, b.layoutInlineSize) && (a.reservedInlineSize === null && b.reservedInlineSize === null || a.reservedInlineSize !== null && b.reservedInlineSize !== null && nearlyEqual(a.reservedInlineSize, b.reservedInlineSize)) && (a.flowInlineSize === null && b.flowInlineSize === null || a.flowInlineSize !== null && b.flowInlineSize !== null && nearlyEqual(a.flowInlineSize, b.flowInlineSize)) && nearlyEqual(a.rootOrigin.left, b.rootOrigin.left) && nearlyEqual(a.rootOrigin.top, b.rootOrigin.top);
3388
3811
  }
3389
3812
  function refreshAnimatingTarget(activeTarget, measurement) {
3390
3813
  if (sameMeasurement(activeTarget, measurement)) {
@@ -3394,6 +3817,7 @@ function refreshAnimatingTarget(activeTarget, measurement) {
3394
3817
  snapshot: activeTarget.snapshot,
3395
3818
  layoutInlineSize: measurement.layoutInlineSize,
3396
3819
  reservedInlineSize: measurement.reservedInlineSize,
3820
+ flowInlineSize: activeTarget.flowInlineSize,
3397
3821
  rootOrigin: measurement.rootOrigin
3398
3822
  };
3399
3823
  }
@@ -3451,6 +3875,7 @@ function commitStaticMeasurement(session, measurement, setState) {
3451
3875
  function scheduleMorphTimeline({
3452
3876
  session,
3453
3877
  timeline,
3878
+ finalizeMeasurement,
3454
3879
  measurement,
3455
3880
  plan,
3456
3881
  setState
@@ -3471,12 +3896,13 @@ function scheduleMorphTimeline({
3471
3896
  timeline.animateFrame = null;
3472
3897
  timeline.finalizeTimer = window.setTimeout(() => {
3473
3898
  timeline.finalizeTimer = null;
3474
- commitStaticMeasurement(session, session.target ?? measurement, setState);
3899
+ commitStaticMeasurement(session, finalizeMeasurement(session.target ?? measurement), setState);
3475
3900
  }, MORPH.durationMs);
3476
3901
  });
3477
3902
  });
3478
3903
  }
3479
3904
  function startMorph({
3905
+ finalizeMeasurement,
3480
3906
  nextMeasurement,
3481
3907
  session,
3482
3908
  timeline,
@@ -3503,12 +3929,14 @@ function startMorph({
3503
3929
  scheduleMorphTimeline({
3504
3930
  session,
3505
3931
  timeline,
3932
+ finalizeMeasurement,
3506
3933
  measurement: nextMeasurement,
3507
3934
  plan,
3508
3935
  setState
3509
3936
  });
3510
3937
  }
3511
3938
  function reconcileMorphChange({
3939
+ finalizeMeasurement,
3512
3940
  root,
3513
3941
  measurementLayer,
3514
3942
  measurementBackend,
@@ -3563,6 +3991,7 @@ function reconcileMorphChange({
3563
3991
  return nextMeasurement;
3564
3992
  }
3565
3993
  startMorph({
3994
+ finalizeMeasurement,
3566
3995
  nextMeasurement,
3567
3996
  session,
3568
3997
  timeline,
@@ -3572,6 +4001,7 @@ function reconcileMorphChange({
3572
4001
  }
3573
4002
  function syncCommittedRootOriginWhenIdle({
3574
4003
  root,
4004
+ flowTextRef,
3575
4005
  layoutContext,
3576
4006
  state,
3577
4007
  session
@@ -3583,8 +4013,9 @@ function syncCommittedRootOriginWhenIdle({
3583
4013
  return;
3584
4014
  }
3585
4015
  const nextRootOrigin = readRootOrigin(root);
4016
+ const nextFlowInlineSize = readFlowInlineSize(flowTextRef.current);
3586
4017
  const committedMeasurement = state.measurement;
3587
- if (nearlyEqual(committedMeasurement.rootOrigin.left, nextRootOrigin.left) && nearlyEqual(committedMeasurement.rootOrigin.top, nextRootOrigin.top)) {
4018
+ if (nearlyEqual(committedMeasurement.rootOrigin.left, nextRootOrigin.left) && nearlyEqual(committedMeasurement.rootOrigin.top, nextRootOrigin.top) && (committedMeasurement.flowInlineSize === null && nextFlowInlineSize === null || committedMeasurement.flowInlineSize !== null && nextFlowInlineSize !== null && nearlyEqual(committedMeasurement.flowInlineSize, nextFlowInlineSize))) {
3588
4019
  session.committed = committedMeasurement;
3589
4020
  return;
3590
4021
  }
@@ -3592,6 +4023,7 @@ function syncCommittedRootOriginWhenIdle({
3592
4023
  snapshot: committedMeasurement.snapshot,
3593
4024
  layoutInlineSize: committedMeasurement.layoutInlineSize,
3594
4025
  reservedInlineSize: committedMeasurement.reservedInlineSize,
4026
+ flowInlineSize: nextFlowInlineSize,
3595
4027
  rootOrigin: nextRootOrigin
3596
4028
  };
3597
4029
  }
@@ -3690,7 +4122,7 @@ function getMeasurementLayerStyle(layoutContext, useContentInlineSize = false) {
3690
4122
  };
3691
4123
  }
3692
4124
  function resolveFlowText(committedMeasurement, stateMeasurement, text) {
3693
- return committedMeasurement?.snapshot.text ?? stateMeasurement?.snapshot.text ?? text;
4125
+ return stateMeasurement?.snapshot.text ?? committedMeasurement?.snapshot.text ?? text;
3694
4126
  }
3695
4127
  function getOverlayStyle(plan) {
3696
4128
  return {
@@ -3718,7 +4150,6 @@ function getLiveGlyphStyle(item, stage, visualBridge) {
3718
4150
  top: item.top,
3719
4151
  width: item.width,
3720
4152
  height: item.height,
3721
- lineHeight: `${item.height}px`,
3722
4153
  opacity: getLiveOpacity(item, stage),
3723
4154
  transform: getLiveTransform(item, stage, visualBridge),
3724
4155
  transition: getLiveTransition(item, stage)
@@ -3731,28 +4162,52 @@ function getExitGlyphStyle(item, stage, visualBridge) {
3731
4162
  top: item.top,
3732
4163
  width: item.width,
3733
4164
  height: item.height,
3734
- lineHeight: `${item.height}px`,
3735
4165
  opacity: getExitOpacity(stage),
3736
4166
  transform: getExitTransform(visualBridge),
3737
4167
  transition: getExitTransition(stage)
3738
4168
  };
3739
4169
  }
3740
- function MorphOverlay({ stage, plan }) {
4170
+ function getContextSliceStyle(layoutInlineSize, item) {
4171
+ return {
4172
+ ...CONTEXT_SLICE_TEXT_STYLE,
4173
+ left: -item.left,
4174
+ top: -item.top,
4175
+ width: layoutInlineSize
4176
+ };
4177
+ }
4178
+ function MorphOverlay({
4179
+ overlayRef,
4180
+ stage,
4181
+ plan
4182
+ }) {
3741
4183
  let exitItems = [];
3742
4184
  if (stage !== "idle") {
3743
4185
  exitItems = plan.exitItems;
3744
4186
  }
3745
4187
  return /* @__PURE__ */ jsxDEV("div", {
4188
+ ref: overlayRef,
3746
4189
  "aria-hidden": "true",
3747
4190
  style: getOverlayStyle(plan),
3748
4191
  children: [
3749
4192
  exitItems.map((item) => /* @__PURE__ */ jsxDEV("span", {
4193
+ "data-morph-role": "exit",
4194
+ "data-morph-key": item.key,
4195
+ "data-morph-glyph": item.glyph,
3750
4196
  style: getExitGlyphStyle(item, stage, plan.visualBridge),
3751
- children: item.glyph
4197
+ children: /* @__PURE__ */ jsxDEV("span", {
4198
+ style: getContextSliceStyle(plan.layoutInlineSizeFrom, item),
4199
+ children: plan.sourceRenderText
4200
+ }, undefined, false, undefined, this)
3752
4201
  }, `exit-${item.key}`, false, undefined, this)),
3753
4202
  plan.liveItems.map((item) => /* @__PURE__ */ jsxDEV("span", {
4203
+ "data-morph-role": "live",
4204
+ "data-morph-key": item.key,
4205
+ "data-morph-glyph": item.glyph,
3754
4206
  style: getLiveGlyphStyle(item, stage, plan.visualBridge),
3755
- children: item.glyph
4207
+ children: /* @__PURE__ */ jsxDEV("span", {
4208
+ style: getContextSliceStyle(plan.layoutInlineSizeTo, item),
4209
+ children: plan.targetRenderText
4210
+ }, undefined, false, undefined, this)
3756
4211
  }, item.key, false, undefined, this))
3757
4212
  ]
3758
4213
  }, undefined, true, undefined, this);
@@ -3761,54 +4216,63 @@ function MeasurementLayer({
3761
4216
  layerRef,
3762
4217
  layoutContext,
3763
4218
  text,
3764
- segments,
3765
4219
  useContentInlineSize
3766
4220
  }) {
3767
- let glyphs = segments;
3768
- if (text.length === 0) {
3769
- glyphs = EMPTY_SEGMENTS;
3770
- }
3771
4221
  return /* @__PURE__ */ jsxDEV("span", {
3772
4222
  ref: layerRef,
3773
4223
  "aria-hidden": "true",
3774
4224
  style: getMeasurementLayerStyle(layoutContext, useContentInlineSize),
3775
- children: glyphs.map((segment) => /* @__PURE__ */ jsxDEV("span", {
3776
- "data-morph-key": segment.key,
3777
- style: MEASUREMENT_GLYPH_STYLE,
3778
- children: segment.glyph
3779
- }, segment.key, false, undefined, this))
4225
+ children: text
3780
4226
  }, undefined, false, undefined, this);
3781
4227
  }
3782
4228
  function useMorphTransition(text, className) {
3783
4229
  const { ref, layoutContext } = useObservedLayoutContext([className]);
4230
+ const debugInstanceIdRef = useRef(null);
4231
+ const flowTextRef = useRef(null);
3784
4232
  const measurementLayerRef = useRef(null);
3785
4233
  const completedDomMeasurementKeyRef = useRef(null);
3786
4234
  const domMeasurementSnapshotCacheRef = useRef(new Map);
3787
4235
  const sessionRef = useRef({ ...EMPTY_SESSION });
3788
4236
  const timelineRef = useRef({ ...EMPTY_TIMELINE });
4237
+ const debugDriftSignatureRef = useRef(null);
3789
4238
  const [domMeasurementRequestKey, setDomMeasurementRequestKey] = useState(null);
4239
+ const [forceContentMeasurementText, setForceContentMeasurementText] = useState(null);
3790
4240
  const [state, setState] = useState(EMPTY_STATE);
4241
+ if (debugInstanceIdRef.current === null) {
4242
+ debugInstanceIdRef.current = nextTorphDebugInstanceId();
4243
+ }
3791
4244
  let measurementHint = sessionRef.current.committed;
3792
4245
  if (sessionRef.current.animating) {
3793
4246
  measurementHint = sessionRef.current.target ?? sessionRef.current.committed;
3794
4247
  }
4248
+ const forceContentInlineSize = forceContentMeasurementText === text;
3795
4249
  const measurementRequest = useMemo(() => createMorphMeasurementRequest({
3796
4250
  text,
3797
4251
  layoutContext,
3798
- layoutHint: measurementHint
3799
- }), [text, layoutContext, measurementHint]);
4252
+ layoutHint: measurementHint,
4253
+ forceContentInlineSize
4254
+ }), [text, layoutContext, measurementHint, forceContentInlineSize]);
3800
4255
  const renderText = measurementRequest?.renderText ?? text;
3801
4256
  const useContentInlineSize = measurementRequest?.useContentInlineSize ?? false;
3802
4257
  const measurementBackend = measurementRequest?.measurementBackend ?? null;
3803
4258
  const segments = measurementRequest?.segments ?? EMPTY_SEGMENTS;
3804
4259
  const domMeasurementKey = measurementRequest?.domMeasurementKey ?? null;
3805
4260
  useLayoutEffect(() => {
4261
+ const config = readTorphDebugConfig();
4262
+ if (!shouldCaptureTorphTrace(config)) {
4263
+ return;
4264
+ }
4265
+ ensureTorphTraceApi();
4266
+ }, []);
4267
+ useLayoutEffect(() => {
4268
+ const finalizeMeasurement = (measurement) => refineMeasurementFromLiveNodes(measurement, ref.current, flowTextRef.current);
3806
4269
  if (ref.current === null || layoutContext === null) {
3807
4270
  completedDomMeasurementKeyRef.current = null;
3808
4271
  if (domMeasurementRequestKey !== null) {
3809
4272
  setDomMeasurementRequestKey(null);
3810
4273
  }
3811
4274
  reconcileMorphChange({
4275
+ finalizeMeasurement,
3812
4276
  root: ref.current,
3813
4277
  measurementLayer: measurementLayerRef.current,
3814
4278
  measurementBackend,
@@ -3834,6 +4298,7 @@ function useMorphTransition(text, className) {
3834
4298
  setDomMeasurementRequestKey(null);
3835
4299
  }
3836
4300
  reconcileMorphChange({
4301
+ finalizeMeasurement,
3837
4302
  root: ref.current,
3838
4303
  measurementLayer: null,
3839
4304
  measurementBackend,
@@ -3856,7 +4321,8 @@ function useMorphTransition(text, className) {
3856
4321
  if (measurementLayerRef.current === null) {
3857
4322
  return;
3858
4323
  }
3859
- const nextMeasurement = reconcileMorphChange({
4324
+ const nextMeasurement2 = reconcileMorphChange({
4325
+ finalizeMeasurement,
3860
4326
  root: ref.current,
3861
4327
  measurementLayer: measurementLayerRef.current,
3862
4328
  measurementBackend,
@@ -3869,9 +4335,9 @@ function useMorphTransition(text, className) {
3869
4335
  timeline: timelineRef.current,
3870
4336
  setState
3871
4337
  });
3872
- if (nextMeasurement !== null) {
4338
+ if (nextMeasurement2 !== null) {
3873
4339
  if (canCacheMeasurementLayerSnapshot(measurementBackend)) {
3874
- rememberCachedMorphSnapshot(domMeasurementSnapshotCacheRef.current, domMeasurementKey, nextMeasurement.snapshot);
4340
+ rememberCachedMorphSnapshot(domMeasurementSnapshotCacheRef.current, domMeasurementKey, nextMeasurement2.snapshot);
3875
4341
  }
3876
4342
  }
3877
4343
  completedDomMeasurementKeyRef.current = domMeasurementKey;
@@ -3889,7 +4355,8 @@ function useMorphTransition(text, className) {
3889
4355
  if (domMeasurementRequestKey !== null) {
3890
4356
  setDomMeasurementRequestKey(null);
3891
4357
  }
3892
- reconcileMorphChange({
4358
+ const nextMeasurement = reconcileMorphChange({
4359
+ finalizeMeasurement,
3893
4360
  root: ref.current,
3894
4361
  measurementLayer: measurementLayerRef.current,
3895
4362
  measurementBackend,
@@ -3915,19 +4382,85 @@ function useMorphTransition(text, className) {
3915
4382
  useLayoutEffect(() => {
3916
4383
  syncCommittedRootOriginWhenIdle({
3917
4384
  root: ref.current,
4385
+ flowTextRef,
3918
4386
  layoutContext,
3919
4387
  state,
3920
4388
  session: sessionRef.current
3921
4389
  });
3922
4390
  }, [layoutContext, state]);
4391
+ useLayoutEffect(() => {
4392
+ if (forceContentMeasurementText !== null && forceContentMeasurementText !== text) {
4393
+ setForceContentMeasurementText(null);
4394
+ return;
4395
+ }
4396
+ if (state.stage !== "idle" || state.measurement === null) {
4397
+ return;
4398
+ }
4399
+ const flowLineCount = readFlowLineCount(flowTextRef.current);
4400
+ const shouldHeal = shouldHealIdleMeasurementFromFlow(state.measurement, flowLineCount);
4401
+ if (shouldHeal) {
4402
+ if (forceContentMeasurementText !== text) {
4403
+ setForceContentMeasurementText(text);
4404
+ }
4405
+ return;
4406
+ }
4407
+ if (forceContentMeasurementText === text) {
4408
+ setForceContentMeasurementText(null);
4409
+ }
4410
+ }, [forceContentMeasurementText, state, text]);
4411
+ useLayoutEffect(() => {
4412
+ const config = readTorphDebugConfig();
4413
+ if (!shouldRunTorphInstrumentation(config)) {
4414
+ debugDriftSignatureRef.current = null;
4415
+ return;
4416
+ }
4417
+ if (state.stage !== "idle" || state.measurement === null) {
4418
+ debugDriftSignatureRef.current = null;
4419
+ return;
4420
+ }
4421
+ const root = ref.current;
4422
+ const flowTextNode = flowTextRef.current;
4423
+ if (root === null || flowTextNode === null) {
4424
+ debugDriftSignatureRef.current = null;
4425
+ return;
4426
+ }
4427
+ const liveSnapshot = measureLiveFlowSnapshot(root, flowTextNode);
4428
+ if (liveSnapshot === null) {
4429
+ debugDriftSignatureRef.current = null;
4430
+ return;
4431
+ }
4432
+ const drift = measureSnapshotDrift(state.measurement.snapshot, liveSnapshot);
4433
+ const hasDrift = drift.expectedGlyphs !== drift.actualGlyphs || Math.abs(drift.snapshotWidthDelta) > MORPH.geometryEpsilon || drift.maxAbsLeftDelta > MORPH.geometryEpsilon || drift.maxAbsTopDelta > MORPH.geometryEpsilon || drift.maxAbsWidthDelta > MORPH.geometryEpsilon || drift.maxAbsHeightDelta > MORPH.geometryEpsilon;
4434
+ if (!hasDrift) {
4435
+ debugDriftSignatureRef.current = null;
4436
+ return;
4437
+ }
4438
+ const signature = JSON.stringify({
4439
+ text,
4440
+ renderText: state.measurement.snapshot.renderText,
4441
+ drift: summarizeSnapshotDrift(drift)
4442
+ });
4443
+ if (debugDriftSignatureRef.current === signature) {
4444
+ return;
4445
+ }
4446
+ debugDriftSignatureRef.current = signature;
4447
+ logTorphDebug(debugInstanceIdRef.current, "effect:idle-flow-drift", {
4448
+ text,
4449
+ expected: summarizeDebugSnapshot(state.measurement.snapshot),
4450
+ actual: summarizeDebugSnapshot(liveSnapshot),
4451
+ drift: summarizeSnapshotDrift(drift)
4452
+ });
4453
+ }, [state, text]);
3923
4454
  useLayoutEffect(() => {
3924
4455
  return () => {
3925
4456
  cancelTimeline(timelineRef.current);
3926
4457
  };
3927
4458
  }, []);
3928
4459
  return {
4460
+ debugInstanceId: debugInstanceIdRef.current,
3929
4461
  committedMeasurement: sessionRef.current.committed,
3930
4462
  domMeasurementRequestKey,
4463
+ flowTextRef,
3931
4464
  ref,
3932
4465
  measurementLayerRef,
3933
4466
  renderText,
@@ -3941,9 +4474,19 @@ function ActiveTorph({
3941
4474
  text,
3942
4475
  className
3943
4476
  }) {
4477
+ const overlayRef = useRef(null);
4478
+ const debugFinalizeSignatureRef = useRef(null);
4479
+ const debugFrameHandleRef = useRef(null);
4480
+ const debugFrameOrdinalRef = useRef(0);
4481
+ const debugIdlePostFrameHandleRef = useRef(null);
4482
+ const debugIdlePostFrameOrdinalRef = useRef(0);
4483
+ const debugPendingIdlePostFramesRef = useRef(false);
4484
+ const debugPreviousStageRef = useRef(null);
3944
4485
  const {
4486
+ debugInstanceId,
3945
4487
  committedMeasurement,
3946
4488
  domMeasurementRequestKey,
4489
+ flowTextRef,
3947
4490
  ref,
3948
4491
  measurementLayerRef,
3949
4492
  renderText,
@@ -3956,19 +4499,338 @@ function ActiveTorph({
3956
4499
  const shouldRenderOverlay = state.stage !== "idle" && plan !== null;
3957
4500
  const shouldRenderMeasurementLayer = domMeasurementRequestKey !== null;
3958
4501
  const flowText = resolveFlowText(committedMeasurement, state.measurement, text);
4502
+ useLayoutEffect(() => {
4503
+ const config = readTorphDebugConfig();
4504
+ if (!shouldRunTorphInstrumentation(config)) {
4505
+ return;
4506
+ }
4507
+ logTorphDebug(debugInstanceId, "effect:trace-meta", {
4508
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
4509
+ includesIdlePostFrame: true,
4510
+ includesViewportAnchors: true,
4511
+ includesRootOriginRefine: true
4512
+ });
4513
+ }, [debugInstanceId]);
4514
+ useLayoutEffect(() => {
4515
+ const config = readTorphDebugConfig();
4516
+ if (!shouldRunTorphInstrumentation(config)) {
4517
+ debugPendingIdlePostFramesRef.current = false;
4518
+ debugPreviousStageRef.current = state.stage;
4519
+ return;
4520
+ }
4521
+ const previousStage = debugPreviousStageRef.current;
4522
+ if (previousStage !== state.stage) {
4523
+ if (state.stage === "idle") {
4524
+ debugPendingIdlePostFramesRef.current = true;
4525
+ }
4526
+ logTorphDebug(debugInstanceId, "effect:stage-transition", {
4527
+ text,
4528
+ fromStage: previousStage,
4529
+ toStage: state.stage,
4530
+ committed: summarizeDebugMeasurement(committedMeasurement),
4531
+ stateMeasurement: summarizeDebugMeasurement(state.measurement),
4532
+ flowText
4533
+ });
4534
+ debugPreviousStageRef.current = state.stage;
4535
+ }
4536
+ }, [committedMeasurement, debugInstanceId, flowText, state, text]);
4537
+ useLayoutEffect(() => {
4538
+ const config = readTorphDebugConfig();
4539
+ if (!shouldRunTorphInstrumentation(config)) {
4540
+ if (debugFrameHandleRef.current !== null) {
4541
+ cancelAnimationFrame(debugFrameHandleRef.current);
4542
+ debugFrameHandleRef.current = null;
4543
+ }
4544
+ return;
4545
+ }
4546
+ if (state.stage === "idle" || state.measurement === null) {
4547
+ if (debugFrameHandleRef.current !== null) {
4548
+ cancelAnimationFrame(debugFrameHandleRef.current);
4549
+ debugFrameHandleRef.current = null;
4550
+ }
4551
+ debugFrameOrdinalRef.current = 0;
4552
+ return;
4553
+ }
4554
+ debugFrameOrdinalRef.current = 0;
4555
+ const measurement = state.measurement;
4556
+ let cancelled = false;
4557
+ const captureFrame = () => {
4558
+ if (cancelled) {
4559
+ return;
4560
+ }
4561
+ const root = ref.current;
4562
+ const flowNode = flowTextRef.current;
4563
+ const overlayNode = overlayRef.current;
4564
+ let rootRect = null;
4565
+ if (root !== null) {
4566
+ rootRect = root.getBoundingClientRect();
4567
+ }
4568
+ let flowRect = null;
4569
+ if (flowNode !== null) {
4570
+ flowRect = flowNode.getBoundingClientRect();
4571
+ }
4572
+ let overlayRect = null;
4573
+ if (overlayNode !== null) {
4574
+ overlayRect = overlayNode.getBoundingClientRect();
4575
+ }
4576
+ let overlayLiveSnapshot = null;
4577
+ if (root !== null && overlayNode !== null) {
4578
+ overlayLiveSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "live");
4579
+ }
4580
+ let overlayExitSnapshot = null;
4581
+ if (root !== null && overlayNode !== null) {
4582
+ overlayExitSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "exit");
4583
+ }
4584
+ let flowSnapshot = null;
4585
+ if (root !== null && flowNode !== null) {
4586
+ flowSnapshot = measureLiveFlowSnapshot(root, flowNode);
4587
+ }
4588
+ let overlayLiveDrift = null;
4589
+ if (overlayLiveSnapshot !== null) {
4590
+ overlayLiveDrift = summarizeSnapshotDrift(measureSnapshotDrift(measurement.snapshot, overlayLiveSnapshot));
4591
+ }
4592
+ let flowDrift = null;
4593
+ if (flowSnapshot !== null) {
4594
+ flowDrift = summarizeSnapshotDrift(measureSnapshotDrift(measurement.snapshot, flowSnapshot));
4595
+ }
4596
+ let planSummary = null;
4597
+ if (plan !== null) {
4598
+ planSummary = {
4599
+ frameWidth: roundDebugValue(plan.frameWidth),
4600
+ frameHeight: roundDebugValue(plan.frameHeight),
4601
+ layoutInlineSizeFrom: roundDebugValue(plan.layoutInlineSizeFrom),
4602
+ layoutInlineSizeTo: roundDebugValue(plan.layoutInlineSizeTo),
4603
+ sourceRenderText: plan.sourceRenderText,
4604
+ targetRenderText: plan.targetRenderText,
4605
+ visualBridge: {
4606
+ offsetX: roundDebugValue(plan.visualBridge.offsetX),
4607
+ offsetY: roundDebugValue(plan.visualBridge.offsetY)
4608
+ },
4609
+ liveItems: plan.liveItems.length,
4610
+ exitItems: plan.exitItems.length
4611
+ };
4612
+ }
4613
+ logTorphDebug(debugInstanceId, "effect:frame-snapshot", {
4614
+ text,
4615
+ frame: debugFrameOrdinalRef.current,
4616
+ stateStage: state.stage,
4617
+ propText: text,
4618
+ flowText,
4619
+ committed: summarizeDebugMeasurement(committedMeasurement),
4620
+ stateMeasurement: summarizeDebugMeasurement(measurement),
4621
+ plan: planSummary,
4622
+ rootBox: summarizeDebugRect(rootRect),
4623
+ overlayBox: summarizeDebugRect(overlayRect),
4624
+ flowBox: summarizeDebugRect(flowRect),
4625
+ rootOriginDrift: summarizeDebugRootOriginDrift(measurement, rootRect),
4626
+ overlayLive: summarizeDebugSnapshot(overlayLiveSnapshot),
4627
+ overlayLiveViewportAnchors: summarizeDebugViewportAnchors(overlayLiveSnapshot, rootRect),
4628
+ overlayLiveDrift,
4629
+ overlayExit: summarizeDebugSnapshot(overlayExitSnapshot),
4630
+ overlayExitViewportAnchors: summarizeDebugViewportAnchors(overlayExitSnapshot, rootRect),
4631
+ flow: summarizeDebugSnapshot(flowSnapshot),
4632
+ flowViewportAnchors: summarizeDebugViewportAnchors(flowSnapshot, rootRect),
4633
+ flowDrift
4634
+ });
4635
+ debugFrameOrdinalRef.current += 1;
4636
+ debugFrameHandleRef.current = requestAnimationFrame(captureFrame);
4637
+ };
4638
+ debugFrameHandleRef.current = requestAnimationFrame(captureFrame);
4639
+ return () => {
4640
+ cancelled = true;
4641
+ if (debugFrameHandleRef.current !== null) {
4642
+ cancelAnimationFrame(debugFrameHandleRef.current);
4643
+ debugFrameHandleRef.current = null;
4644
+ }
4645
+ };
4646
+ }, [committedMeasurement, debugInstanceId, flowText, plan, ref, state, text]);
4647
+ useLayoutEffect(() => {
4648
+ const config = readTorphDebugConfig();
4649
+ if (!shouldRunTorphInstrumentation(config)) {
4650
+ debugPendingIdlePostFramesRef.current = false;
4651
+ if (debugIdlePostFrameHandleRef.current !== null) {
4652
+ cancelAnimationFrame(debugIdlePostFrameHandleRef.current);
4653
+ debugIdlePostFrameHandleRef.current = null;
4654
+ }
4655
+ return;
4656
+ }
4657
+ if (!debugPendingIdlePostFramesRef.current) {
4658
+ return;
4659
+ }
4660
+ if (state.stage !== "idle" || state.measurement === null) {
4661
+ return;
4662
+ }
4663
+ debugPendingIdlePostFramesRef.current = false;
4664
+ debugIdlePostFrameOrdinalRef.current = 0;
4665
+ const measurement = state.measurement;
4666
+ let remainingFrames = 3;
4667
+ let cancelled = false;
4668
+ const captureIdlePostFrame = () => {
4669
+ if (cancelled) {
4670
+ return;
4671
+ }
4672
+ const root = ref.current;
4673
+ const flowNode = flowTextRef.current;
4674
+ if (root === null || flowNode === null) {
4675
+ return;
4676
+ }
4677
+ const rootRect = root.getBoundingClientRect();
4678
+ const flowRect = flowNode.getBoundingClientRect();
4679
+ const flowSnapshot = measureLiveFlowSnapshot(root, flowNode);
4680
+ let flowDrift = null;
4681
+ if (flowSnapshot !== null) {
4682
+ flowDrift = summarizeSnapshotDrift(measureSnapshotDrift(measurement.snapshot, flowSnapshot));
4683
+ }
4684
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame", {
4685
+ text,
4686
+ frame: debugIdlePostFrameOrdinalRef.current,
4687
+ stateStage: state.stage,
4688
+ propText: text,
4689
+ flowText,
4690
+ committed: summarizeDebugMeasurement(committedMeasurement),
4691
+ stateMeasurement: summarizeDebugMeasurement(measurement),
4692
+ rootBox: summarizeDebugRect(rootRect),
4693
+ flowBox: summarizeDebugRect(flowRect),
4694
+ rootOriginDrift: summarizeDebugRootOriginDrift(measurement, rootRect),
4695
+ flow: summarizeDebugSnapshot(flowSnapshot),
4696
+ flowViewportAnchors: summarizeDebugViewportAnchors(flowSnapshot, rootRect),
4697
+ flowDrift
4698
+ });
4699
+ debugIdlePostFrameOrdinalRef.current += 1;
4700
+ remainingFrames -= 1;
4701
+ if (remainingFrames <= 0) {
4702
+ debugIdlePostFrameHandleRef.current = null;
4703
+ return;
4704
+ }
4705
+ debugIdlePostFrameHandleRef.current = requestAnimationFrame(captureIdlePostFrame);
4706
+ };
4707
+ debugIdlePostFrameHandleRef.current = requestAnimationFrame(captureIdlePostFrame);
4708
+ return () => {
4709
+ cancelled = true;
4710
+ if (debugIdlePostFrameHandleRef.current !== null) {
4711
+ cancelAnimationFrame(debugIdlePostFrameHandleRef.current);
4712
+ debugIdlePostFrameHandleRef.current = null;
4713
+ }
4714
+ };
4715
+ }, [committedMeasurement, debugInstanceId, flowText, ref, state, text]);
4716
+ useLayoutEffect(() => {
4717
+ return () => {
4718
+ if (debugIdlePostFrameHandleRef.current !== null) {
4719
+ cancelAnimationFrame(debugIdlePostFrameHandleRef.current);
4720
+ debugIdlePostFrameHandleRef.current = null;
4721
+ }
4722
+ };
4723
+ }, []);
4724
+ useLayoutEffect(() => {
4725
+ const config = readTorphDebugConfig();
4726
+ if (!shouldRunTorphInstrumentation(config)) {
4727
+ debugFinalizeSignatureRef.current = null;
4728
+ return;
4729
+ }
4730
+ if (state.stage !== "animate" || state.measurement === null || plan === null) {
4731
+ debugFinalizeSignatureRef.current = null;
4732
+ return;
4733
+ }
4734
+ const root = ref.current;
4735
+ const overlayNode = overlayRef.current;
4736
+ const flowNode = flowTextRef.current;
4737
+ if (root === null || overlayNode === null || flowNode === null) {
4738
+ debugFinalizeSignatureRef.current = null;
4739
+ return;
4740
+ }
4741
+ const measurement = state.measurement;
4742
+ let handled = false;
4743
+ const capture = (event) => {
4744
+ if (handled) {
4745
+ return;
4746
+ }
4747
+ if (event.propertyName !== "transform") {
4748
+ return;
4749
+ }
4750
+ const target = event.target;
4751
+ if (!(target instanceof HTMLElement)) {
4752
+ return;
4753
+ }
4754
+ if (target.dataset.morphRole !== "live") {
4755
+ return;
4756
+ }
4757
+ handled = true;
4758
+ const overlayLiveSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "live");
4759
+ const overlayExitSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "exit");
4760
+ const flowSnapshot = measureLiveFlowSnapshot(root, flowNode);
4761
+ if (overlayLiveSnapshot === null || flowSnapshot === null) {
4762
+ return;
4763
+ }
4764
+ const overlayLiveDrift = measureSnapshotDrift(measurement.snapshot, overlayLiveSnapshot);
4765
+ const flowDrift = measureSnapshotDrift(measurement.snapshot, flowSnapshot);
4766
+ const rootRect = root.getBoundingClientRect();
4767
+ const flowRect = flowNode.getBoundingClientRect();
4768
+ const overlayRect = overlayNode.getBoundingClientRect();
4769
+ const signature = JSON.stringify({
4770
+ text,
4771
+ renderText: measurement.snapshot.renderText,
4772
+ overlayLiveDrift: summarizeSnapshotDrift(overlayLiveDrift),
4773
+ flowDrift: summarizeSnapshotDrift(flowDrift),
4774
+ overlayWidth: roundDebugValue(overlayRect.width),
4775
+ rootWidth: roundDebugValue(rootRect.width)
4776
+ });
4777
+ if (debugFinalizeSignatureRef.current === signature) {
4778
+ return;
4779
+ }
4780
+ debugFinalizeSignatureRef.current = signature;
4781
+ logTorphDebug(debugInstanceId, "effect:animate-finalize-snapshot", {
4782
+ text,
4783
+ target: {
4784
+ layoutInlineSize: roundDebugValue(measurement.layoutInlineSize),
4785
+ reservedInlineSize: roundDebugValue(measurement.reservedInlineSize),
4786
+ flowInlineSize: roundDebugValue(measurement.flowInlineSize),
4787
+ rootOrigin: {
4788
+ left: roundDebugValue(measurement.rootOrigin.left),
4789
+ top: roundDebugValue(measurement.rootOrigin.top)
4790
+ },
4791
+ snapshot: summarizeDebugSnapshot(measurement.snapshot)
4792
+ },
4793
+ overlayLive: summarizeDebugSnapshot(overlayLiveSnapshot),
4794
+ overlayLiveDrift: summarizeSnapshotDrift(overlayLiveDrift),
4795
+ overlayExit: summarizeDebugSnapshot(overlayExitSnapshot),
4796
+ flow: summarizeDebugSnapshot(flowSnapshot),
4797
+ flowDrift: summarizeSnapshotDrift(flowDrift),
4798
+ rootOriginDrift: summarizeDebugRootOriginDrift(measurement, rootRect),
4799
+ overlayLiveViewportAnchors: summarizeDebugViewportAnchors(overlayLiveSnapshot, rootRect),
4800
+ overlayExitViewportAnchors: summarizeDebugViewportAnchors(overlayExitSnapshot, rootRect),
4801
+ flowViewportAnchors: summarizeDebugViewportAnchors(flowSnapshot, rootRect),
4802
+ rootBox: {
4803
+ width: roundDebugValue(rootRect.width),
4804
+ height: roundDebugValue(rootRect.height)
4805
+ },
4806
+ overlayBox: {
4807
+ width: roundDebugValue(overlayRect.width),
4808
+ height: roundDebugValue(overlayRect.height)
4809
+ },
4810
+ flowBox: {
4811
+ width: roundDebugValue(flowRect.width),
4812
+ height: roundDebugValue(flowRect.height)
4813
+ }
4814
+ });
4815
+ };
4816
+ overlayNode.addEventListener("transitionend", capture);
4817
+ return () => {
4818
+ overlayNode.removeEventListener("transitionend", capture);
4819
+ };
4820
+ }, [debugInstanceId, plan, ref, state, text]);
3959
4821
  let measurementLayer = null;
3960
4822
  if (shouldRenderMeasurementLayer) {
3961
4823
  measurementLayer = /* @__PURE__ */ jsxDEV(MeasurementLayer, {
3962
4824
  layerRef: measurementLayerRef,
3963
4825
  layoutContext,
3964
4826
  text: renderText,
3965
- segments,
3966
4827
  useContentInlineSize
3967
4828
  }, undefined, false, undefined, this);
3968
4829
  }
3969
4830
  let overlay = null;
3970
4831
  if (shouldRenderOverlay) {
3971
4832
  overlay = /* @__PURE__ */ jsxDEV(MorphOverlay, {
4833
+ overlayRef,
3972
4834
  stage: state.stage,
3973
4835
  plan
3974
4836
  }, undefined, false, undefined, this);
@@ -3985,7 +4847,10 @@ function ActiveTorph({
3985
4847
  /* @__PURE__ */ jsxDEV("span", {
3986
4848
  "aria-hidden": "true",
3987
4849
  style: getFallbackTextStyle(shouldRenderOverlay),
3988
- children: flowText
4850
+ children: /* @__PURE__ */ jsxDEV("span", {
4851
+ ref: flowTextRef,
4852
+ children: flowText
4853
+ }, undefined, false, undefined, this)
3989
4854
  }, undefined, false, undefined, this),
3990
4855
  measurementLayer,
3991
4856
  overlay
@@ -4003,8 +4868,11 @@ function Torph({
4003
4868
  }
4004
4869
  export {
4005
4870
  supportsIntrinsicWidthLock,
4871
+ shouldHealIdleMeasurementFromFlow,
4006
4872
  resolveMorphFrameBounds,
4007
4873
  resolveFlowText,
4874
+ resolveContentWidthLockInlineSize,
4875
+ refineMeasurementWithLiveGeometry,
4008
4876
  pairMorphCharacters,
4009
4877
  needsMeasurementLayer,
4010
4878
  measureMorphSnapshotFromLayer,