@grahlnn/comps 0.1.7 → 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,7 +3282,7 @@ 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) {
@@ -3083,6 +3295,169 @@ function readFlowInlineSize(node) {
3083
3295
  }
3084
3296
  return node.getBoundingClientRect().width;
3085
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
+ }
3086
3461
  function getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize) {
3087
3462
  const backend = getPretextMorphMeasurementBackend(text, layoutContext);
3088
3463
  if (backend !== "probe") {
@@ -3123,16 +3498,48 @@ function shouldMeasureUsingContentInlineSize(layoutContext, layoutHint) {
3123
3498
  }
3124
3499
  return nearlyEqual(layoutHint.layoutInlineSize, resolveContentWidthLockInlineSize(layoutHint), MORPH.contentWidthLockEpsilon);
3125
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
+ });
3528
+ }
3126
3529
  function createMorphMeasurementRequest({
3127
3530
  text,
3128
3531
  layoutContext,
3129
- layoutHint
3532
+ layoutHint,
3533
+ forceContentInlineSize = false
3130
3534
  }) {
3131
3535
  if (layoutContext === null) {
3132
3536
  return null;
3133
3537
  }
3134
3538
  const renderText = getPretextMorphRenderedText(text, layoutContext);
3135
- const useContentInlineSize = shouldMeasureUsingContentInlineSize(layoutContext, layoutHint);
3539
+ let useContentInlineSize = shouldMeasureUsingContentInlineSize(layoutContext, layoutHint);
3540
+ if (forceContentInlineSize) {
3541
+ useContentInlineSize = true;
3542
+ }
3136
3543
  const measurementBackend = getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize);
3137
3544
  let segments = readCachedMorphSegments(renderText);
3138
3545
  if (measurementBackend === "pretext") {
@@ -3348,6 +3755,8 @@ function buildMorphPlan(previous, next, visualBridge = ZERO_BRIDGE) {
3348
3755
  frameHeight: frame.height,
3349
3756
  layoutInlineSizeFrom: previous.layoutInlineSize,
3350
3757
  layoutInlineSizeTo: next.layoutInlineSize,
3758
+ sourceRenderText: previous.snapshot.renderText,
3759
+ targetRenderText: next.snapshot.renderText,
3351
3760
  visualBridge,
3352
3761
  liveItems: next.snapshot.graphemes.map((grapheme) => {
3353
3762
  const move = movesByDestinationKey.get(grapheme.key);
@@ -3466,6 +3875,7 @@ function commitStaticMeasurement(session, measurement, setState) {
3466
3875
  function scheduleMorphTimeline({
3467
3876
  session,
3468
3877
  timeline,
3878
+ finalizeMeasurement,
3469
3879
  measurement,
3470
3880
  plan,
3471
3881
  setState
@@ -3486,12 +3896,13 @@ function scheduleMorphTimeline({
3486
3896
  timeline.animateFrame = null;
3487
3897
  timeline.finalizeTimer = window.setTimeout(() => {
3488
3898
  timeline.finalizeTimer = null;
3489
- commitStaticMeasurement(session, session.target ?? measurement, setState);
3899
+ commitStaticMeasurement(session, finalizeMeasurement(session.target ?? measurement), setState);
3490
3900
  }, MORPH.durationMs);
3491
3901
  });
3492
3902
  });
3493
3903
  }
3494
3904
  function startMorph({
3905
+ finalizeMeasurement,
3495
3906
  nextMeasurement,
3496
3907
  session,
3497
3908
  timeline,
@@ -3518,12 +3929,14 @@ function startMorph({
3518
3929
  scheduleMorphTimeline({
3519
3930
  session,
3520
3931
  timeline,
3932
+ finalizeMeasurement,
3521
3933
  measurement: nextMeasurement,
3522
3934
  plan,
3523
3935
  setState
3524
3936
  });
3525
3937
  }
3526
3938
  function reconcileMorphChange({
3939
+ finalizeMeasurement,
3527
3940
  root,
3528
3941
  measurementLayer,
3529
3942
  measurementBackend,
@@ -3578,6 +3991,7 @@ function reconcileMorphChange({
3578
3991
  return nextMeasurement;
3579
3992
  }
3580
3993
  startMorph({
3994
+ finalizeMeasurement,
3581
3995
  nextMeasurement,
3582
3996
  session,
3583
3997
  timeline,
@@ -3708,7 +4122,7 @@ function getMeasurementLayerStyle(layoutContext, useContentInlineSize = false) {
3708
4122
  };
3709
4123
  }
3710
4124
  function resolveFlowText(committedMeasurement, stateMeasurement, text) {
3711
- return committedMeasurement?.snapshot.text ?? stateMeasurement?.snapshot.text ?? text;
4125
+ return stateMeasurement?.snapshot.text ?? committedMeasurement?.snapshot.text ?? text;
3712
4126
  }
3713
4127
  function getOverlayStyle(plan) {
3714
4128
  return {
@@ -3736,7 +4150,6 @@ function getLiveGlyphStyle(item, stage, visualBridge) {
3736
4150
  top: item.top,
3737
4151
  width: item.width,
3738
4152
  height: item.height,
3739
- lineHeight: `${item.height}px`,
3740
4153
  opacity: getLiveOpacity(item, stage),
3741
4154
  transform: getLiveTransform(item, stage, visualBridge),
3742
4155
  transition: getLiveTransition(item, stage)
@@ -3749,28 +4162,52 @@ function getExitGlyphStyle(item, stage, visualBridge) {
3749
4162
  top: item.top,
3750
4163
  width: item.width,
3751
4164
  height: item.height,
3752
- lineHeight: `${item.height}px`,
3753
4165
  opacity: getExitOpacity(stage),
3754
4166
  transform: getExitTransform(visualBridge),
3755
4167
  transition: getExitTransition(stage)
3756
4168
  };
3757
4169
  }
3758
- 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
+ }) {
3759
4183
  let exitItems = [];
3760
4184
  if (stage !== "idle") {
3761
4185
  exitItems = plan.exitItems;
3762
4186
  }
3763
4187
  return /* @__PURE__ */ jsxDEV("div", {
4188
+ ref: overlayRef,
3764
4189
  "aria-hidden": "true",
3765
4190
  style: getOverlayStyle(plan),
3766
4191
  children: [
3767
4192
  exitItems.map((item) => /* @__PURE__ */ jsxDEV("span", {
4193
+ "data-morph-role": "exit",
4194
+ "data-morph-key": item.key,
4195
+ "data-morph-glyph": item.glyph,
3768
4196
  style: getExitGlyphStyle(item, stage, plan.visualBridge),
3769
- children: item.glyph
4197
+ children: /* @__PURE__ */ jsxDEV("span", {
4198
+ style: getContextSliceStyle(plan.layoutInlineSizeFrom, item),
4199
+ children: plan.sourceRenderText
4200
+ }, undefined, false, undefined, this)
3770
4201
  }, `exit-${item.key}`, false, undefined, this)),
3771
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,
3772
4206
  style: getLiveGlyphStyle(item, stage, plan.visualBridge),
3773
- children: item.glyph
4207
+ children: /* @__PURE__ */ jsxDEV("span", {
4208
+ style: getContextSliceStyle(plan.layoutInlineSizeTo, item),
4209
+ children: plan.targetRenderText
4210
+ }, undefined, false, undefined, this)
3774
4211
  }, item.key, false, undefined, this))
3775
4212
  ]
3776
4213
  }, undefined, true, undefined, this);
@@ -3779,55 +4216,63 @@ function MeasurementLayer({
3779
4216
  layerRef,
3780
4217
  layoutContext,
3781
4218
  text,
3782
- segments,
3783
4219
  useContentInlineSize
3784
4220
  }) {
3785
- let glyphs = segments;
3786
- if (text.length === 0) {
3787
- glyphs = EMPTY_SEGMENTS;
3788
- }
3789
4221
  return /* @__PURE__ */ jsxDEV("span", {
3790
4222
  ref: layerRef,
3791
4223
  "aria-hidden": "true",
3792
4224
  style: getMeasurementLayerStyle(layoutContext, useContentInlineSize),
3793
- children: glyphs.map((segment) => /* @__PURE__ */ jsxDEV("span", {
3794
- "data-morph-key": segment.key,
3795
- style: MEASUREMENT_GLYPH_STYLE,
3796
- children: segment.glyph
3797
- }, segment.key, false, undefined, this))
4225
+ children: text
3798
4226
  }, undefined, false, undefined, this);
3799
4227
  }
3800
4228
  function useMorphTransition(text, className) {
3801
4229
  const { ref, layoutContext } = useObservedLayoutContext([className]);
4230
+ const debugInstanceIdRef = useRef(null);
3802
4231
  const flowTextRef = useRef(null);
3803
4232
  const measurementLayerRef = useRef(null);
3804
4233
  const completedDomMeasurementKeyRef = useRef(null);
3805
4234
  const domMeasurementSnapshotCacheRef = useRef(new Map);
3806
4235
  const sessionRef = useRef({ ...EMPTY_SESSION });
3807
4236
  const timelineRef = useRef({ ...EMPTY_TIMELINE });
4237
+ const debugDriftSignatureRef = useRef(null);
3808
4238
  const [domMeasurementRequestKey, setDomMeasurementRequestKey] = useState(null);
4239
+ const [forceContentMeasurementText, setForceContentMeasurementText] = useState(null);
3809
4240
  const [state, setState] = useState(EMPTY_STATE);
4241
+ if (debugInstanceIdRef.current === null) {
4242
+ debugInstanceIdRef.current = nextTorphDebugInstanceId();
4243
+ }
3810
4244
  let measurementHint = sessionRef.current.committed;
3811
4245
  if (sessionRef.current.animating) {
3812
4246
  measurementHint = sessionRef.current.target ?? sessionRef.current.committed;
3813
4247
  }
4248
+ const forceContentInlineSize = forceContentMeasurementText === text;
3814
4249
  const measurementRequest = useMemo(() => createMorphMeasurementRequest({
3815
4250
  text,
3816
4251
  layoutContext,
3817
- layoutHint: measurementHint
3818
- }), [text, layoutContext, measurementHint]);
4252
+ layoutHint: measurementHint,
4253
+ forceContentInlineSize
4254
+ }), [text, layoutContext, measurementHint, forceContentInlineSize]);
3819
4255
  const renderText = measurementRequest?.renderText ?? text;
3820
4256
  const useContentInlineSize = measurementRequest?.useContentInlineSize ?? false;
3821
4257
  const measurementBackend = measurementRequest?.measurementBackend ?? null;
3822
4258
  const segments = measurementRequest?.segments ?? EMPTY_SEGMENTS;
3823
4259
  const domMeasurementKey = measurementRequest?.domMeasurementKey ?? null;
3824
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);
3825
4269
  if (ref.current === null || layoutContext === null) {
3826
4270
  completedDomMeasurementKeyRef.current = null;
3827
4271
  if (domMeasurementRequestKey !== null) {
3828
4272
  setDomMeasurementRequestKey(null);
3829
4273
  }
3830
4274
  reconcileMorphChange({
4275
+ finalizeMeasurement,
3831
4276
  root: ref.current,
3832
4277
  measurementLayer: measurementLayerRef.current,
3833
4278
  measurementBackend,
@@ -3853,6 +4298,7 @@ function useMorphTransition(text, className) {
3853
4298
  setDomMeasurementRequestKey(null);
3854
4299
  }
3855
4300
  reconcileMorphChange({
4301
+ finalizeMeasurement,
3856
4302
  root: ref.current,
3857
4303
  measurementLayer: null,
3858
4304
  measurementBackend,
@@ -3875,7 +4321,8 @@ function useMorphTransition(text, className) {
3875
4321
  if (measurementLayerRef.current === null) {
3876
4322
  return;
3877
4323
  }
3878
- const nextMeasurement = reconcileMorphChange({
4324
+ const nextMeasurement2 = reconcileMorphChange({
4325
+ finalizeMeasurement,
3879
4326
  root: ref.current,
3880
4327
  measurementLayer: measurementLayerRef.current,
3881
4328
  measurementBackend,
@@ -3888,9 +4335,9 @@ function useMorphTransition(text, className) {
3888
4335
  timeline: timelineRef.current,
3889
4336
  setState
3890
4337
  });
3891
- if (nextMeasurement !== null) {
4338
+ if (nextMeasurement2 !== null) {
3892
4339
  if (canCacheMeasurementLayerSnapshot(measurementBackend)) {
3893
- rememberCachedMorphSnapshot(domMeasurementSnapshotCacheRef.current, domMeasurementKey, nextMeasurement.snapshot);
4340
+ rememberCachedMorphSnapshot(domMeasurementSnapshotCacheRef.current, domMeasurementKey, nextMeasurement2.snapshot);
3894
4341
  }
3895
4342
  }
3896
4343
  completedDomMeasurementKeyRef.current = domMeasurementKey;
@@ -3908,7 +4355,8 @@ function useMorphTransition(text, className) {
3908
4355
  if (domMeasurementRequestKey !== null) {
3909
4356
  setDomMeasurementRequestKey(null);
3910
4357
  }
3911
- reconcileMorphChange({
4358
+ const nextMeasurement = reconcileMorphChange({
4359
+ finalizeMeasurement,
3912
4360
  root: ref.current,
3913
4361
  measurementLayer: measurementLayerRef.current,
3914
4362
  measurementBackend,
@@ -3940,12 +4388,76 @@ function useMorphTransition(text, className) {
3940
4388
  session: sessionRef.current
3941
4389
  });
3942
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]);
3943
4454
  useLayoutEffect(() => {
3944
4455
  return () => {
3945
4456
  cancelTimeline(timelineRef.current);
3946
4457
  };
3947
4458
  }, []);
3948
4459
  return {
4460
+ debugInstanceId: debugInstanceIdRef.current,
3949
4461
  committedMeasurement: sessionRef.current.committed,
3950
4462
  domMeasurementRequestKey,
3951
4463
  flowTextRef,
@@ -3962,7 +4474,16 @@ function ActiveTorph({
3962
4474
  text,
3963
4475
  className
3964
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);
3965
4485
  const {
4486
+ debugInstanceId,
3966
4487
  committedMeasurement,
3967
4488
  domMeasurementRequestKey,
3968
4489
  flowTextRef,
@@ -3978,19 +4499,338 @@ function ActiveTorph({
3978
4499
  const shouldRenderOverlay = state.stage !== "idle" && plan !== null;
3979
4500
  const shouldRenderMeasurementLayer = domMeasurementRequestKey !== null;
3980
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]);
3981
4821
  let measurementLayer = null;
3982
4822
  if (shouldRenderMeasurementLayer) {
3983
4823
  measurementLayer = /* @__PURE__ */ jsxDEV(MeasurementLayer, {
3984
4824
  layerRef: measurementLayerRef,
3985
4825
  layoutContext,
3986
4826
  text: renderText,
3987
- segments,
3988
4827
  useContentInlineSize
3989
4828
  }, undefined, false, undefined, this);
3990
4829
  }
3991
4830
  let overlay = null;
3992
4831
  if (shouldRenderOverlay) {
3993
4832
  overlay = /* @__PURE__ */ jsxDEV(MorphOverlay, {
4833
+ overlayRef,
3994
4834
  stage: state.stage,
3995
4835
  plan
3996
4836
  }, undefined, false, undefined, this);
@@ -4028,9 +4868,11 @@ function Torph({
4028
4868
  }
4029
4869
  export {
4030
4870
  supportsIntrinsicWidthLock,
4871
+ shouldHealIdleMeasurementFromFlow,
4031
4872
  resolveMorphFrameBounds,
4032
4873
  resolveFlowText,
4033
4874
  resolveContentWidthLockInlineSize,
4875
+ refineMeasurementWithLiveGeometry,
4034
4876
  pairMorphCharacters,
4035
4877
  needsMeasurementLayer,
4036
4878
  measureMorphSnapshotFromLayer,
@@ -40,6 +40,8 @@ export type MorphRenderPlan = {
40
40
  frameHeight: number;
41
41
  layoutInlineSizeFrom: number;
42
42
  layoutInlineSizeTo: number;
43
+ sourceRenderText: string;
44
+ targetRenderText: string;
43
45
  visualBridge: MorphVisualBridge;
44
46
  liveItems: MorphLiveItem[];
45
47
  exitItems: MorphCharacterLayout[];
@@ -81,6 +83,14 @@ type GlyphPairing = GlyphMove | GlyphEnter | GlyphExit;
81
83
  export declare function needsMeasurementLayer(measurementBackend: PretextMorphMeasurementBackend, renderText: string): boolean;
82
84
  export declare function measureMorphSnapshotFromLayer(text: string, renderText: string, segments: readonly MorphSegment[], layer: HTMLElement | null): MorphSnapshot;
83
85
  export declare function resolveContentWidthLockInlineSize(layoutHint: MorphMeasurement): number;
86
+ export declare function shouldHealIdleMeasurementFromFlow(measurement: MorphMeasurement, flowLineCount: number | null): boolean;
87
+ export declare function refineMeasurementWithLiveGeometry(measurement: MorphMeasurement, liveGeometry: {
88
+ flowInlineSize: number | null;
89
+ rootOrigin: {
90
+ left: number;
91
+ top: number;
92
+ };
93
+ }): MorphMeasurement;
84
94
  export declare function pairMorphCharacters(previous: MorphCharacterLayout[], next: MorphCharacterLayout[]): GlyphPairing[];
85
95
  export declare function resolveMorphFrameBounds(previous: MorphSnapshot, next: MorphSnapshot): {
86
96
  width: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grahlnn/comps",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "React components from grahlnn/comps.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",