@grahlnn/comps 0.1.8 → 0.1.10

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
@@ -1,9 +1,9 @@
1
1
  // torph/src/components/Torph.tsx
2
2
  import {
3
- useLayoutEffect,
3
+ useLayoutEffect as useLayoutEffect2,
4
4
  useMemo,
5
- useRef,
6
- useState
5
+ useRef as useRef2,
6
+ useState as useState2
7
7
  } from "react";
8
8
 
9
9
  // torph/src/vendor/pretext/bidi.ts
@@ -2516,8 +2516,12 @@ function measureMorphSnapshotWithPretext(text, layoutContext) {
2516
2516
  };
2517
2517
  }
2518
2518
 
2519
- // torph/src/components/Torph.tsx
2520
- import { jsxDEV } from "react/jsx-dev-runtime";
2519
+ // torph/src/core/math.ts
2520
+ function nearlyEqual(a, b, epsilon = 0.5) {
2521
+ return Math.abs(a - b) <= epsilon;
2522
+ }
2523
+
2524
+ // torph/src/core/types.ts
2521
2525
  var MORPH = {
2522
2526
  durationMs: 280,
2523
2527
  maxFadeMs: 150,
@@ -2526,8 +2530,6 @@ var MORPH = {
2526
2530
  contentWidthLockEpsilon: 2,
2527
2531
  lineGroupingEpsilon: 1
2528
2532
  };
2529
- var MORPH_SEGMENT_CACHE_LIMIT = 256;
2530
- var DOM_MEASUREMENT_SNAPSHOT_CACHE_LIMIT = 8;
2531
2533
  var EMPTY_STATE = {
2532
2534
  stage: "idle",
2533
2535
  measurement: null,
@@ -2548,22 +2550,19 @@ var ZERO_BRIDGE = {
2548
2550
  offsetY: 0
2549
2551
  };
2550
2552
  var EMPTY_SEGMENTS = [];
2551
- var morphSegmentCache = new Map;
2552
- var pretextMorphTrustCache = new Map;
2553
- var morphMeasurementEpoch = 1;
2554
- var activeMorphMeasurementConsumers = 0;
2555
- var detachMorphMeasurementInvalidationListeners = null;
2556
- var SCREEN_READER_ONLY_STYLE = {
2553
+ function resolveContentWidthLockInlineSize(layoutHint) {
2554
+ if (layoutHint.flowInlineSize !== null) {
2555
+ return layoutHint.flowInlineSize;
2556
+ }
2557
+ return layoutHint.snapshot.width;
2558
+ }
2559
+
2560
+ // torph/src/core/render.ts
2561
+ var OVERLAY_STYLE = {
2557
2562
  position: "absolute",
2558
- width: "1px",
2559
- height: "1px",
2560
- margin: "-1px",
2561
- padding: 0,
2562
- border: 0,
2563
- clip: "rect(0 0 0 0)",
2564
- clipPath: "inset(50%)",
2565
- overflow: "hidden",
2566
- whiteSpace: "nowrap"
2563
+ inset: 0,
2564
+ minWidth: 0,
2565
+ pointerEvents: "none"
2567
2566
  };
2568
2567
  var MEASUREMENT_LAYER_STYLE = {
2569
2568
  pointerEvents: "none",
@@ -2574,299 +2573,721 @@ var MEASUREMENT_LAYER_STYLE = {
2574
2573
  right: 0,
2575
2574
  display: "block"
2576
2575
  };
2577
- var FALLBACK_TEXT_STYLE = {
2578
- display: "block",
2579
- gridArea: "1 / 1"
2580
- };
2581
- var OVERLAY_STYLE = {
2582
- position: "absolute",
2583
- inset: 0,
2584
- minWidth: 0,
2585
- pointerEvents: "none"
2586
- };
2587
- var SHARED_GLYPH_TYPOGRAPHY_STYLE = {
2588
- font: "inherit",
2589
- fontKerning: "inherit",
2590
- fontFeatureSettings: "inherit",
2591
- fontOpticalSizing: "inherit",
2592
- fontStretch: "inherit",
2593
- fontStyle: "inherit",
2594
- fontVariant: "inherit",
2595
- fontVariantNumeric: "inherit",
2596
- fontVariationSettings: "inherit",
2597
- fontWeight: "inherit",
2598
- letterSpacing: "inherit",
2599
- textTransform: "inherit",
2600
- wordSpacing: "inherit",
2601
- direction: "inherit"
2602
- };
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 = {
2610
- ...SHARED_GLYPH_TYPOGRAPHY_STYLE,
2611
- position: "absolute",
2612
- display: "block",
2613
- minWidth: 0,
2614
- whiteSpace: "inherit"
2615
- };
2616
- var graphemeSegmenter = null;
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;
2576
+ function getFadeDuration(fraction) {
2577
+ return Math.min(MORPH.durationMs * fraction, MORPH.maxFadeMs);
2629
2578
  }
2630
- function shouldCaptureTorphTrace(config) {
2631
- if (config === null) {
2632
- return true;
2579
+ function getOverlayStyle(stage, plan) {
2580
+ if (stage === "idle") {
2581
+ return OVERLAY_STYLE;
2633
2582
  }
2634
- if (typeof config === "boolean") {
2635
- return config;
2583
+ return {
2584
+ ...OVERLAY_STYLE,
2585
+ right: "auto",
2586
+ bottom: "auto",
2587
+ width: plan.frameWidth,
2588
+ height: plan.frameHeight
2589
+ };
2590
+ }
2591
+ function getLiveTransform(item, stage, visualBridge) {
2592
+ if (stage !== "prepare") {
2593
+ return "translate(0px, 0px)";
2636
2594
  }
2637
- if (config.capture === false) {
2638
- return false;
2595
+ if (item.kind === "move") {
2596
+ return `translate(${(item.fromLeft ?? item.left) - item.left + visualBridge.offsetX}px, ${(item.fromTop ?? item.top) - item.top + visualBridge.offsetY}px)`;
2639
2597
  }
2640
- return true;
2598
+ return `translate(${visualBridge.offsetX}px, ${visualBridge.offsetY}px)`;
2641
2599
  }
2642
- function isTorphDebugEnabled(config) {
2643
- if (config === null) {
2644
- return false;
2600
+ function getLiveTransition(item, stage) {
2601
+ if (stage !== "animate") {
2602
+ return;
2645
2603
  }
2646
- if (typeof config === "boolean") {
2647
- return config;
2604
+ if (item.kind === "enter") {
2605
+ return `opacity ${getFadeDuration(0.5)}ms linear ${getFadeDuration(0.25)}ms`;
2648
2606
  }
2649
- if (config.console !== undefined) {
2650
- return config.console;
2607
+ return `transform ${MORPH.durationMs}ms ${MORPH.ease}, opacity ${getFadeDuration(0.25)}ms linear`;
2608
+ }
2609
+ function getExitTransform(visualBridge) {
2610
+ return `translate(${visualBridge.offsetX}px, ${visualBridge.offsetY}px)`;
2611
+ }
2612
+ function getExitTransition(stage) {
2613
+ if (stage !== "animate") {
2614
+ return;
2651
2615
  }
2652
- return config.enabled !== false;
2616
+ return `transform ${MORPH.durationMs}ms ${MORPH.ease}, opacity ${getFadeDuration(0.25)}ms linear`;
2653
2617
  }
2654
- function shouldRunTorphInstrumentation(config) {
2655
- if (shouldCaptureTorphTrace(config)) {
2618
+ function supportsIntrinsicWidthLock(display, parentDisplay) {
2619
+ let parentNeedsReservation = false;
2620
+ if (parentDisplay === "flex" || parentDisplay === "inline-flex" || parentDisplay === "grid" || parentDisplay === "inline-grid") {
2621
+ parentNeedsReservation = true;
2622
+ }
2623
+ if (display === "inline" || display === "inline-block" || display === "inline-flex" || display === "inline-grid") {
2656
2624
  return true;
2657
2625
  }
2658
- return isTorphDebugEnabled(config);
2626
+ return parentNeedsReservation;
2659
2627
  }
2660
- function getTorphTraceStore() {
2661
- const scope = globalThis;
2662
- let store = scope.__TORPH_TRACE_STORE__;
2663
- if (store !== undefined) {
2664
- return store;
2628
+ function getRootDisplay(layoutContext) {
2629
+ if (layoutContext === null) {
2630
+ return "grid";
2665
2631
  }
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;
2632
+ if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
2633
+ return "inline-grid";
2634
+ }
2635
+ return "grid";
2682
2636
  }
2683
- function downloadTorphTrace(filename) {
2684
- if (typeof document === "undefined") {
2685
- return null;
2637
+ function getRootStyle(stage, plan, measurement, layoutContext) {
2638
+ let width = measurement?.reservedInlineSize;
2639
+ if (measurement !== null && measurement.flowInlineSize !== null) {
2640
+ width = measurement.layoutInlineSize;
2686
2641
  }
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`;
2642
+ if (plan !== null) {
2643
+ width = plan.layoutInlineSizeTo;
2694
2644
  }
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__;
2645
+ let height;
2646
+ if (plan !== null) {
2647
+ height = plan.frameHeight;
2707
2648
  }
2708
- const api = {
2709
- clear: clearTorphTrace,
2710
- count: () => getTorphTraceStore().lines.length,
2711
- download: downloadTorphTrace,
2712
- text: getTorphTraceText
2649
+ const style = {
2650
+ position: "relative",
2651
+ display: getRootDisplay(layoutContext)
2713
2652
  };
2714
- scope.__TORPH_TRACE__ = api;
2715
- return api;
2653
+ if (width !== null && width !== undefined) {
2654
+ style.width = width;
2655
+ }
2656
+ if (height !== undefined) {
2657
+ style.height = height;
2658
+ }
2659
+ return style;
2716
2660
  }
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;
2661
+ function getMeasurementLayerStyle(layoutContext, useContentInlineSize = false) {
2662
+ let intrinsicWidthLock = false;
2663
+ if (layoutContext !== null) {
2664
+ if (useContentInlineSize) {
2665
+ intrinsicWidthLock = true;
2666
+ } else if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
2667
+ intrinsicWidthLock = true;
2736
2668
  }
2737
- store.totalBytes = Math.max(0, store.totalBytes - removed.length);
2738
2669
  }
2670
+ if (!intrinsicWidthLock) {
2671
+ return MEASUREMENT_LAYER_STYLE;
2672
+ }
2673
+ return {
2674
+ ...MEASUREMENT_LAYER_STYLE,
2675
+ right: "auto",
2676
+ width: "max-content"
2677
+ };
2739
2678
  }
2740
- function roundDebugValue(value) {
2741
- if (value === null || value === undefined) {
2742
- return value;
2679
+ function resolveFlowText(committedMeasurement, stateMeasurement, text) {
2680
+ return stateMeasurement?.snapshot.text ?? committedMeasurement?.snapshot.text ?? text;
2681
+ }
2682
+ function shouldRenderGlyphLayer(stage, plan, measurement) {
2683
+ if (stage === "idle") {
2684
+ return measurement !== null;
2743
2685
  }
2744
- return Math.round(value * 100) / 100;
2686
+ return plan !== null;
2745
2687
  }
2746
- function summarizeDebugSnapshot(snapshot) {
2688
+ function resolveGlyphSliceWhiteSpace(snapshot) {
2747
2689
  if (snapshot === null) {
2748
- return null;
2690
+ return "inherit";
2749
2691
  }
2692
+ return "nowrap";
2693
+ }
2694
+ function toSteadyLiveItem(grapheme) {
2750
2695
  return {
2751
- text: snapshot.text,
2752
- renderText: snapshot.renderText,
2753
- width: roundDebugValue(snapshot.width),
2754
- height: roundDebugValue(snapshot.height),
2755
- graphemes: snapshot.graphemes.length
2696
+ ...grapheme,
2697
+ kind: "move",
2698
+ fromLeft: grapheme.left,
2699
+ fromTop: grapheme.top
2756
2700
  };
2757
2701
  }
2758
- function summarizeDebugMeasurement(measurement) {
2759
- if (measurement === null) {
2760
- return null;
2761
- }
2702
+ function createSteadyGlyphPlan(measurement) {
2703
+ const snapshot = measurement.snapshot;
2762
2704
  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)
2705
+ frameWidth: snapshot.width,
2706
+ frameHeight: snapshot.height,
2707
+ layoutInlineSizeFrom: measurement.layoutInlineSize,
2708
+ layoutInlineSizeTo: measurement.layoutInlineSize,
2709
+ sourceRenderText: snapshot.renderText,
2710
+ targetRenderText: snapshot.renderText,
2711
+ sourceRootOrigin: measurement.rootOrigin,
2712
+ visualBridge: ZERO_BRIDGE,
2713
+ liveItems: snapshot.graphemes.map(toSteadyLiveItem),
2714
+ exitItems: []
2771
2715
  };
2772
2716
  }
2773
- function summarizeDebugRect(rect) {
2774
- if (rect === null) {
2717
+
2718
+ // torph/src/core/snapshot.ts
2719
+ function isSingleLineSnapshot(snapshot) {
2720
+ if (snapshot.graphemes.length <= 1) {
2721
+ return true;
2722
+ }
2723
+ const firstTop = snapshot.graphemes[0].top;
2724
+ return snapshot.graphemes.every((grapheme) => nearlyEqual(grapheme.top, firstTop, MORPH.lineGroupingEpsilon));
2725
+ }
2726
+ function assertSingleLineSnapshot(snapshot) {
2727
+ if (isSingleLineSnapshot(snapshot)) {
2728
+ return snapshot;
2729
+ }
2730
+ throw new Error(`Torph only supports single-line text layout. Received wrapped text: "${snapshot.renderText}"`);
2731
+ }
2732
+
2733
+ // torph/src/core/measurement-policy.ts
2734
+ var MORPH_SEGMENT_CACHE_LIMIT = 256;
2735
+ var morphSegmentCache = new Map;
2736
+ var pretextMorphTrustCache = new Map;
2737
+ var morphMeasurementEpoch = 1;
2738
+ var graphemeSegmenter = null;
2739
+ function getGraphemeSegmenter() {
2740
+ if (graphemeSegmenter !== null) {
2741
+ return graphemeSegmenter;
2742
+ }
2743
+ graphemeSegmenter = new Intl.Segmenter(undefined, {
2744
+ granularity: "grapheme"
2745
+ });
2746
+ return graphemeSegmenter;
2747
+ }
2748
+ function clearPretextMorphTrustCache() {
2749
+ pretextMorphTrustCache.clear();
2750
+ }
2751
+ function clearMorphMeasurementCaches() {
2752
+ morphSegmentCache.clear();
2753
+ clearPretextMorphTrustCache();
2754
+ clearPretextMorphCaches();
2755
+ }
2756
+ function bumpMorphMeasurementEpoch() {
2757
+ morphMeasurementEpoch += 1;
2758
+ }
2759
+ function getMorphMeasurementEpoch() {
2760
+ return morphMeasurementEpoch;
2761
+ }
2762
+ function readCachedMorphSegments(text) {
2763
+ const cached = morphSegmentCache.get(text);
2764
+ if (cached !== undefined) {
2765
+ morphSegmentCache.delete(text);
2766
+ morphSegmentCache.set(text, cached);
2767
+ return cached;
2768
+ }
2769
+ const segments = Array.from(getGraphemeSegmenter().segment(text), (segment, index) => ({
2770
+ glyph: segment.segment,
2771
+ key: `${segment.segment}:${index}`
2772
+ }));
2773
+ morphSegmentCache.set(text, segments);
2774
+ if (morphSegmentCache.size > MORPH_SEGMENT_CACHE_LIMIT) {
2775
+ const oldest = morphSegmentCache.keys().next();
2776
+ if (!oldest.done) {
2777
+ morphSegmentCache.delete(oldest.value);
2778
+ }
2779
+ }
2780
+ return segments;
2781
+ }
2782
+ function shouldMeasureUsingContentInlineSize(layoutContext, layoutHint) {
2783
+ if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
2784
+ return true;
2785
+ }
2786
+ if (layoutContext.whiteSpace === "nowrap") {
2787
+ return true;
2788
+ }
2789
+ if (layoutHint === null) {
2790
+ return false;
2791
+ }
2792
+ if (!isSingleLineSnapshot(layoutHint.snapshot)) {
2793
+ return false;
2794
+ }
2795
+ return nearlyEqual(layoutHint.layoutInlineSize, resolveContentWidthLockInlineSize(layoutHint), MORPH.contentWidthLockEpsilon);
2796
+ }
2797
+ function getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize) {
2798
+ const backend = getPretextMorphMeasurementBackend(text, layoutContext);
2799
+ if (backend !== "probe") {
2800
+ return backend;
2801
+ }
2802
+ const signature = getPretextMorphTrustSignature({
2803
+ renderText,
2804
+ layoutContext,
2805
+ useContentInlineSize
2806
+ });
2807
+ if (signature === null) {
2808
+ return "dom";
2809
+ }
2810
+ const trusted = pretextMorphTrustCache.get(signature);
2811
+ if (trusted === undefined) {
2812
+ return "probe";
2813
+ }
2814
+ if (trusted) {
2815
+ return "pretext";
2816
+ }
2817
+ return "dom";
2818
+ }
2819
+ function getDomMeasurementRequestKey(text, renderText, layoutContext, useContentInlineSize) {
2820
+ let inlineSizeMode = "container";
2821
+ if (useContentInlineSize) {
2822
+ inlineSizeMode = "content";
2823
+ }
2824
+ return `dom\x00${inlineSizeMode}\x00${text}\x00${renderText}\x00${layoutContext.measurementVersion}\x00${getMorphMeasurementEpoch()}`;
2825
+ }
2826
+ function needsMeasurementLayer(measurementBackend, renderText) {
2827
+ if (measurementBackend === "pretext") {
2828
+ return false;
2829
+ }
2830
+ return renderText.length > 0;
2831
+ }
2832
+ function canCacheMeasurementLayerSnapshot(measurementBackend) {
2833
+ return measurementBackend === "dom";
2834
+ }
2835
+ function createMorphMeasurementRequest({
2836
+ text,
2837
+ layoutContext,
2838
+ layoutHint
2839
+ }) {
2840
+ if (layoutContext === null) {
2775
2841
  return null;
2776
2842
  }
2843
+ const renderText = getPretextMorphRenderedText(text, layoutContext);
2844
+ const useContentInlineSize = shouldMeasureUsingContentInlineSize(layoutContext, layoutHint);
2845
+ const measurementBackend = getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize);
2846
+ let segments = readCachedMorphSegments(renderText);
2847
+ if (measurementBackend === "pretext") {
2848
+ segments = EMPTY_SEGMENTS;
2849
+ }
2850
+ let domMeasurementKey = null;
2851
+ if (needsMeasurementLayer(measurementBackend, renderText)) {
2852
+ domMeasurementKey = getDomMeasurementRequestKey(text, renderText, layoutContext, useContentInlineSize);
2853
+ }
2777
2854
  return {
2778
- left: roundDebugValue(rect.left),
2779
- top: roundDebugValue(rect.top),
2780
- width: roundDebugValue(rect.width),
2781
- height: roundDebugValue(rect.height)
2855
+ text,
2856
+ renderText,
2857
+ segments,
2858
+ measurementBackend,
2859
+ useContentInlineSize,
2860
+ domMeasurementKey
2782
2861
  };
2783
2862
  }
2784
- function collectDebugAnchorIndices(length) {
2785
- const indices = new Set;
2786
- if (length <= 0) {
2787
- return [];
2863
+ function rememberPretextMeasurementTrust({
2864
+ renderText,
2865
+ layoutContext,
2866
+ useContentInlineSize,
2867
+ trusted
2868
+ }) {
2869
+ const signature = getPretextMorphTrustSignature({
2870
+ renderText,
2871
+ layoutContext,
2872
+ useContentInlineSize
2873
+ });
2874
+ if (signature === null) {
2875
+ return;
2788
2876
  }
2789
- indices.add(0);
2790
- if (length > 1) {
2791
- indices.add(1);
2792
- indices.add(length - 2);
2877
+ pretextMorphTrustCache.set(signature, trusted);
2878
+ }
2879
+ function areSnapshotsEquivalentForPretextTrust(left, right) {
2880
+ if (left.renderText !== right.renderText || left.graphemes.length !== right.graphemes.length) {
2881
+ return false;
2793
2882
  }
2794
- if (length > 2) {
2795
- indices.add(Math.floor((length - 1) / 2));
2883
+ if (Math.abs(left.width - right.width) > MORPH.geometryEpsilon) {
2884
+ return false;
2796
2885
  }
2797
- indices.add(length - 1);
2798
- return Array.from(indices).sort((left, right) => left - right);
2886
+ if (Math.abs(left.height - right.height) > MORPH.geometryEpsilon) {
2887
+ return false;
2888
+ }
2889
+ for (let index = 0;index < left.graphemes.length; index += 1) {
2890
+ const from = left.graphemes[index];
2891
+ const to = right.graphemes[index];
2892
+ if (from.glyph !== to.glyph) {
2893
+ return false;
2894
+ }
2895
+ if (Math.abs(from.left - to.left) > MORPH.geometryEpsilon) {
2896
+ return false;
2897
+ }
2898
+ if (Math.abs(from.top - to.top) > MORPH.geometryEpsilon) {
2899
+ return false;
2900
+ }
2901
+ if (Math.abs(from.width - to.width) > MORPH.geometryEpsilon) {
2902
+ return false;
2903
+ }
2904
+ if (Math.abs(from.height - to.height) > MORPH.geometryEpsilon) {
2905
+ return false;
2906
+ }
2907
+ }
2908
+ return true;
2799
2909
  }
2800
- function summarizeDebugViewportAnchors(snapshot, rootRect) {
2801
- if (snapshot === null || rootRect === null) {
2802
- return null;
2910
+
2911
+ // torph/src/core/finalize-barrier.ts
2912
+ function createMorphFinalizeBarrier(hasMoveTransitions) {
2913
+ return {
2914
+ waitForLiveTransform: hasMoveTransitions,
2915
+ sawLiveTransform: false
2916
+ };
2917
+ }
2918
+ function recordMorphFinalizeSignal(barrier, signal) {
2919
+ return {
2920
+ ...barrier,
2921
+ sawLiveTransform: signal === "live-transform"
2922
+ };
2923
+ }
2924
+ function isMorphFinalizeBarrierSatisfied(barrier) {
2925
+ if (barrier.waitForLiveTransform && !barrier.sawLiveTransform) {
2926
+ return false;
2803
2927
  }
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) {
2928
+ return true;
2929
+ }
2930
+ function summarizeMorphFinalizeBarrier(barrier) {
2931
+ return {
2932
+ waitForLiveTransform: barrier.waitForLiveTransform,
2933
+ sawLiveTransform: barrier.sawLiveTransform,
2934
+ satisfied: isMorphFinalizeBarrierSatisfied(barrier)
2935
+ };
2936
+ }
2937
+
2938
+ // torph/src/core/session.ts
2939
+ function bucketByGlyph(graphemes) {
2940
+ const buckets = new Map;
2941
+ for (const grapheme of graphemes) {
2942
+ const bucket = buckets.get(grapheme.glyph);
2943
+ if (bucket !== undefined) {
2944
+ bucket.push(grapheme);
2809
2945
  continue;
2810
2946
  }
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
- });
2947
+ buckets.set(grapheme.glyph, [grapheme]);
2819
2948
  }
2820
- return anchors;
2949
+ return buckets;
2821
2950
  }
2822
- function summarizeDebugRootOriginDrift(measurement, rootRect) {
2823
- if (measurement === null || rootRect === null) {
2824
- return null;
2951
+ function pairMorphCharacters(previous, next) {
2952
+ const previousBuckets = bucketByGlyph(previous);
2953
+ const nextBuckets = bucketByGlyph(next);
2954
+ const pairings = [];
2955
+ for (const [glyph, previousItems] of previousBuckets) {
2956
+ const nextItems = nextBuckets.get(glyph) ?? [];
2957
+ const shared = Math.min(previousItems.length, nextItems.length);
2958
+ for (let index = 0;index < shared; index += 1) {
2959
+ pairings.push({
2960
+ kind: "move",
2961
+ from: previousItems[index],
2962
+ to: nextItems[index]
2963
+ });
2964
+ }
2965
+ for (let index = shared;index < previousItems.length; index += 1) {
2966
+ pairings.push({
2967
+ kind: "exit",
2968
+ from: previousItems[index]
2969
+ });
2970
+ }
2971
+ }
2972
+ for (const [glyph, nextItems] of nextBuckets) {
2973
+ const previousItems = previousBuckets.get(glyph) ?? [];
2974
+ const shared = Math.min(previousItems.length, nextItems.length);
2975
+ for (let index = shared;index < nextItems.length; index += 1) {
2976
+ pairings.push({
2977
+ kind: "enter",
2978
+ to: nextItems[index]
2979
+ });
2980
+ }
2825
2981
  }
2982
+ return pairings;
2983
+ }
2984
+ function resolveMorphFrameBounds(previous, next) {
2826
2985
  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)
2986
+ width: Math.max(previous.width, next.width),
2987
+ height: Math.max(previous.height, next.height)
2833
2988
  };
2834
2989
  }
2835
- function summarizeSnapshotDrift(drift) {
2990
+ function buildMorphPlan(previous, next, visualBridge = ZERO_BRIDGE) {
2991
+ const pairings = pairMorphCharacters(previous.snapshot.graphemes, next.snapshot.graphemes);
2992
+ const movesByDestinationKey = new Map;
2993
+ const exitItems = [];
2994
+ for (const pairing of pairings) {
2995
+ if (pairing.kind === "move") {
2996
+ movesByDestinationKey.set(pairing.to.key, pairing);
2997
+ continue;
2998
+ }
2999
+ if (pairing.kind === "exit") {
3000
+ exitItems.push(pairing.from);
3001
+ }
3002
+ }
3003
+ const frame = resolveMorphFrameBounds(previous.snapshot, next.snapshot);
2836
3004
  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
- }))
3005
+ frameWidth: frame.width,
3006
+ frameHeight: frame.height,
3007
+ layoutInlineSizeFrom: previous.layoutInlineSize,
3008
+ layoutInlineSizeTo: next.layoutInlineSize,
3009
+ sourceRenderText: previous.snapshot.renderText,
3010
+ targetRenderText: next.snapshot.renderText,
3011
+ sourceRootOrigin: previous.rootOrigin,
3012
+ visualBridge,
3013
+ liveItems: next.snapshot.graphemes.map((grapheme) => {
3014
+ const move = movesByDestinationKey.get(grapheme.key);
3015
+ if (move !== undefined) {
3016
+ return {
3017
+ ...grapheme,
3018
+ kind: "move",
3019
+ fromLeft: move.from.left,
3020
+ fromTop: move.from.top
3021
+ };
3022
+ }
3023
+ return {
3024
+ ...grapheme,
3025
+ kind: "enter",
3026
+ fromLeft: null,
3027
+ fromTop: null
3028
+ };
3029
+ }),
3030
+ exitItems
3031
+ };
3032
+ }
3033
+ function sameSnapshot(a, b) {
3034
+ if (a === b) {
3035
+ return true;
3036
+ }
3037
+ if (a.text !== b.text || a.renderText !== b.renderText || a.graphemes.length !== b.graphemes.length) {
3038
+ return false;
3039
+ }
3040
+ if (!nearlyEqual(a.width, b.width, MORPH.geometryEpsilon)) {
3041
+ return false;
3042
+ }
3043
+ if (!nearlyEqual(a.height, b.height, MORPH.geometryEpsilon)) {
3044
+ return false;
3045
+ }
3046
+ for (let index = 0;index < a.graphemes.length; index += 1) {
3047
+ const left = a.graphemes[index];
3048
+ const right = b.graphemes[index];
3049
+ if (left.glyph !== right.glyph || left.key !== right.key) {
3050
+ return false;
3051
+ }
3052
+ if (!nearlyEqual(left.left, right.left, MORPH.geometryEpsilon)) {
3053
+ return false;
3054
+ }
3055
+ if (!nearlyEqual(left.top, right.top, MORPH.geometryEpsilon)) {
3056
+ return false;
3057
+ }
3058
+ if (!nearlyEqual(left.width, right.width, MORPH.geometryEpsilon)) {
3059
+ return false;
3060
+ }
3061
+ if (!nearlyEqual(left.height, right.height, MORPH.geometryEpsilon)) {
3062
+ return false;
3063
+ }
3064
+ }
3065
+ return true;
3066
+ }
3067
+ function sameMeasurement(a, b) {
3068
+ const sameReservedInlineSize = a.reservedInlineSize === null && b.reservedInlineSize === null || a.reservedInlineSize !== null && b.reservedInlineSize !== null && nearlyEqual(a.reservedInlineSize, b.reservedInlineSize, MORPH.geometryEpsilon);
3069
+ const sameFlowInlineSize = a.flowInlineSize === null && b.flowInlineSize === null || a.flowInlineSize !== null && b.flowInlineSize !== null && nearlyEqual(a.flowInlineSize, b.flowInlineSize, MORPH.geometryEpsilon);
3070
+ return sameSnapshot(a.snapshot, b.snapshot) && nearlyEqual(a.layoutInlineSize, b.layoutInlineSize, MORPH.geometryEpsilon) && sameReservedInlineSize && sameFlowInlineSize && nearlyEqual(a.rootOrigin.left, b.rootOrigin.left, MORPH.geometryEpsilon) && nearlyEqual(a.rootOrigin.top, b.rootOrigin.top, MORPH.geometryEpsilon);
3071
+ }
3072
+ function selectMorphLayoutHint(session) {
3073
+ if (session.animating && session.target !== null) {
3074
+ return session.target;
3075
+ }
3076
+ return session.committed;
3077
+ }
3078
+ function decideMorphSessionUpdate({
3079
+ committed,
3080
+ target,
3081
+ animating,
3082
+ nextMeasurement,
3083
+ fontsReady
3084
+ }) {
3085
+ let source = committed;
3086
+ if (animating && target !== null) {
3087
+ source = target;
3088
+ }
3089
+ if (source === null) {
3090
+ return {
3091
+ kind: "commit-static",
3092
+ measurement: nextMeasurement
3093
+ };
3094
+ }
3095
+ if (!fontsReady) {
3096
+ return {
3097
+ kind: "commit-static",
3098
+ measurement: nextMeasurement
3099
+ };
3100
+ }
3101
+ if (animating && target !== null) {
3102
+ if (nextMeasurement.snapshot.renderText === target.snapshot.renderText) {
3103
+ return {
3104
+ kind: "freeze-animating-target",
3105
+ target
3106
+ };
3107
+ }
3108
+ }
3109
+ if (committed !== null) {
3110
+ if (committed.snapshot.renderText === nextMeasurement.snapshot.renderText) {
3111
+ if (sameMeasurement(committed, nextMeasurement)) {
3112
+ return {
3113
+ kind: "commit-static",
3114
+ measurement: committed
3115
+ };
3116
+ }
3117
+ return {
3118
+ kind: "commit-static",
3119
+ measurement: nextMeasurement
3120
+ };
3121
+ }
3122
+ }
3123
+ return {
3124
+ kind: "start-morph",
3125
+ source,
3126
+ target: nextMeasurement
3127
+ };
3128
+ }
3129
+ function pinMeasurementToCurrentOrigin(measurement, origin) {
3130
+ if (nearlyEqual(measurement.rootOrigin.left, origin.left, MORPH.geometryEpsilon) && nearlyEqual(measurement.rootOrigin.top, origin.top, MORPH.geometryEpsilon)) {
3131
+ return measurement;
3132
+ }
3133
+ return {
3134
+ snapshot: measurement.snapshot,
3135
+ layoutInlineSize: measurement.layoutInlineSize,
3136
+ reservedInlineSize: measurement.reservedInlineSize,
3137
+ flowInlineSize: measurement.flowInlineSize,
3138
+ rootOrigin: origin
3139
+ };
3140
+ }
3141
+ function createStaticState(measurement) {
3142
+ return {
3143
+ stage: "idle",
3144
+ measurement,
3145
+ plan: null
2854
3146
  };
2855
3147
  }
2856
- function logTorphDebug(instanceId, event, payload) {
2857
- const config = readTorphDebugConfig();
2858
- const captureTrace = shouldCaptureTorphTrace(config);
2859
- const logToConsole = isTorphDebugEnabled(config);
2860
- if (!captureTrace && !logToConsole) {
3148
+ function areFontsReady() {
3149
+ return document.fonts.status === "loaded";
3150
+ }
3151
+ function cancelTimeline(timeline) {
3152
+ if (timeline.prepareFrame !== null) {
3153
+ cancelAnimationFrame(timeline.prepareFrame);
3154
+ timeline.prepareFrame = null;
3155
+ }
3156
+ if (timeline.animateFrame !== null) {
3157
+ cancelAnimationFrame(timeline.animateFrame);
3158
+ timeline.animateFrame = null;
3159
+ }
3160
+ if (timeline.finalizeTimer !== null) {
3161
+ window.clearTimeout(timeline.finalizeTimer);
3162
+ timeline.finalizeTimer = null;
3163
+ }
3164
+ }
3165
+ function resetMorph(session, timeline, setState) {
3166
+ cancelTimeline(timeline);
3167
+ session.committed = null;
3168
+ session.target = null;
3169
+ session.animating = false;
3170
+ setState(EMPTY_STATE);
3171
+ }
3172
+ function commitStaticMeasurement(session, measurement, setState) {
3173
+ if (!session.animating && session.target === null && session.committed !== null && sameMeasurement(session.committed, measurement)) {
2861
3174
  return;
2862
3175
  }
2863
- if (captureTrace) {
2864
- appendTorphTrace(instanceId, event, payload);
3176
+ session.committed = measurement;
3177
+ session.target = null;
3178
+ session.animating = false;
3179
+ setState((current) => {
3180
+ if (current.stage === "idle" && current.plan === null && current.measurement !== null && sameMeasurement(current.measurement, measurement)) {
3181
+ return current;
3182
+ }
3183
+ return createStaticState(measurement);
3184
+ });
3185
+ }
3186
+ function applyMorphSessionDecision({
3187
+ decision,
3188
+ session,
3189
+ timeline,
3190
+ setState
3191
+ }) {
3192
+ if (decision.kind === "freeze-animating-target") {
3193
+ return decision.target;
2865
3194
  }
2866
- if (logToConsole) {
2867
- console.log(`[Torph#${instanceId}] ${event}`, payload);
3195
+ cancelTimeline(timeline);
3196
+ if (decision.kind === "commit-static") {
3197
+ commitStaticMeasurement(session, decision.measurement, setState);
3198
+ return decision.measurement;
3199
+ }
3200
+ startMorph({
3201
+ source: decision.source,
3202
+ target: decision.target,
3203
+ session,
3204
+ timeline,
3205
+ setState
3206
+ });
3207
+ return decision.target;
3208
+ }
3209
+ function reconcileMorphSessionUpdate({
3210
+ session,
3211
+ timeline,
3212
+ nextMeasurement,
3213
+ fontsReady,
3214
+ setState
3215
+ }) {
3216
+ const decision = decideMorphSessionUpdate({
3217
+ committed: session.committed,
3218
+ target: session.target,
3219
+ animating: session.animating,
3220
+ nextMeasurement,
3221
+ fontsReady
3222
+ });
3223
+ return applyMorphSessionDecision({
3224
+ decision,
3225
+ session,
3226
+ timeline,
3227
+ setState
3228
+ });
3229
+ }
3230
+ function finalizeMorphTransition({
3231
+ session,
3232
+ timeline,
3233
+ measurement,
3234
+ setState
3235
+ }) {
3236
+ cancelTimeline(timeline);
3237
+ commitStaticMeasurement(session, session.target ?? measurement, setState);
3238
+ }
3239
+ function resolvePreparedMeasurementOrigin(measurement, origin) {
3240
+ return pinMeasurementToCurrentOrigin(measurement, origin);
3241
+ }
3242
+ function resolvePreparedPlanVisualBridge(plan, origin) {
3243
+ const offsetX = plan.sourceRootOrigin.left - origin.left;
3244
+ const offsetY = plan.sourceRootOrigin.top - origin.top;
3245
+ if (nearlyEqual(plan.visualBridge.offsetX, offsetX, MORPH.geometryEpsilon) && nearlyEqual(plan.visualBridge.offsetY, offsetY, MORPH.geometryEpsilon)) {
3246
+ return plan;
2868
3247
  }
3248
+ return {
3249
+ ...plan,
3250
+ visualBridge: {
3251
+ offsetX,
3252
+ offsetY
3253
+ }
3254
+ };
3255
+ }
3256
+ function startMorph({
3257
+ source,
3258
+ target,
3259
+ session,
3260
+ timeline,
3261
+ setState
3262
+ }) {
3263
+ const plan = buildMorphPlan(source, target, ZERO_BRIDGE);
3264
+ session.target = target;
3265
+ session.animating = true;
3266
+ setState({
3267
+ stage: "prepare",
3268
+ measurement: target,
3269
+ plan
3270
+ });
2869
3271
  }
3272
+
3273
+ // torph/src/core/layout-observer.ts
3274
+ import { useLayoutEffect, useRef, useState } from "react";
3275
+ var activeMorphMeasurementConsumers = 0;
3276
+ var detachMorphMeasurementInvalidationListeners = null;
3277
+ var morphMeasurementInvalidationSubscribers = new Set;
3278
+ var FONT_METRIC_TRANSITION_PROPERTIES = new Set([
3279
+ "font",
3280
+ "font-family",
3281
+ "font-size",
3282
+ "font-stretch",
3283
+ "font-style",
3284
+ "font-weight",
3285
+ "font-variation-settings",
3286
+ "letter-spacing",
3287
+ "line-height",
3288
+ "text-transform",
3289
+ "word-spacing"
3290
+ ]);
2870
3291
  function parsePx(value) {
2871
3292
  const parsed = Number.parseFloat(value);
2872
3293
  if (Number.isFinite(parsed)) {
@@ -2908,6 +3329,18 @@ function readContentWidth(node, styles) {
2908
3329
  const borderRight = parsePx(styles.borderRightWidth) ?? 0;
2909
3330
  return Math.max(0, rectWidth - paddingLeft - paddingRight - borderLeft - borderRight);
2910
3331
  }
3332
+ function isFontMetricTransitionProperty(propertyName) {
3333
+ return FONT_METRIC_TRANSITION_PROPERTIES.has(propertyName);
3334
+ }
3335
+ function doesTransitionTargetAffectNode(node, target) {
3336
+ if (target === node) {
3337
+ return true;
3338
+ }
3339
+ if (typeof target !== "object" || target === null || !("contains" in target) || typeof target.contains !== "function") {
3340
+ return false;
3341
+ }
3342
+ return target.contains(node);
3343
+ }
2911
3344
  function readLayoutContext(node, width) {
2912
3345
  const styles = getComputedStyle(node);
2913
3346
  const parentDisplay = (node.parentElement && getComputedStyle(node.parentElement).display) ?? "block";
@@ -2933,41 +3366,16 @@ function sameLayoutContext(a, b) {
2933
3366
  }
2934
3367
  return a.display === b.display && a.direction === b.direction && a.font === b.font && a.fontFeatureSettings === b.fontFeatureSettings && a.fontVariationSettings === b.fontVariationSettings && Math.abs(a.letterSpacingPx - b.letterSpacingPx) < MORPH.geometryEpsilon && Math.abs(a.lineHeightPx - b.lineHeightPx) < MORPH.geometryEpsilon && a.parentDisplay === b.parentDisplay && a.textTransform === b.textTransform && a.whiteSpace === b.whiteSpace && Math.abs(a.width - b.width) < MORPH.geometryEpsilon && Math.abs(a.wordSpacingPx - b.wordSpacingPx) < MORPH.geometryEpsilon;
2935
3368
  }
2936
- function clearPretextMorphTrustCache() {
2937
- pretextMorphTrustCache.clear();
2938
- }
2939
- function clearMorphMeasurementCaches() {
2940
- morphSegmentCache.clear();
2941
- clearPretextMorphTrustCache();
2942
- clearPretextMorphCaches();
2943
- }
2944
- function bumpMorphMeasurementEpoch() {
2945
- morphMeasurementEpoch += 1;
2946
- }
2947
- function getMorphMeasurementEpoch() {
2948
- return morphMeasurementEpoch;
2949
- }
2950
- function isSingleLineSnapshot(snapshot) {
2951
- if (snapshot.graphemes.length <= 1) {
2952
- return true;
3369
+ function notifyMorphMeasurementInvalidationSubscribers() {
3370
+ for (const subscriber of morphMeasurementInvalidationSubscribers) {
3371
+ subscriber();
2953
3372
  }
2954
- const firstTop = snapshot.graphemes[0].top;
2955
- return snapshot.graphemes.every((grapheme) => nearlyEqual(grapheme.top, firstTop, MORPH.lineGroupingEpsilon));
2956
3373
  }
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;
3374
+ function subscribeMorphMeasurementInvalidation(subscriber) {
3375
+ morphMeasurementInvalidationSubscribers.add(subscriber);
3376
+ return () => {
3377
+ morphMeasurementInvalidationSubscribers.delete(subscriber);
3378
+ };
2971
3379
  }
2972
3380
  function acquireMorphMeasurementInvalidationListeners() {
2973
3381
  activeMorphMeasurementConsumers += 1;
@@ -2975,6 +3383,7 @@ function acquireMorphMeasurementInvalidationListeners() {
2975
3383
  const handleFontChange = () => {
2976
3384
  clearMorphMeasurementCaches();
2977
3385
  bumpMorphMeasurementEpoch();
3386
+ notifyMorphMeasurementInvalidationSubscribers();
2978
3387
  };
2979
3388
  document.fonts.ready.then(handleFontChange);
2980
3389
  if (typeof document.fonts.addEventListener === "function") {
@@ -2997,6 +3406,7 @@ function releaseMorphMeasurementInvalidationListeners() {
2997
3406
  function useObservedLayoutContext(deps) {
2998
3407
  const ref = useRef(null);
2999
3408
  const [layoutContext, setLayoutContext] = useState(null);
3409
+ const syncLayoutContextRef = useRef(null);
3000
3410
  useLayoutEffect(() => {
3001
3411
  const node = ref.current;
3002
3412
  if (node === null) {
@@ -3025,9 +3435,88 @@ function useObservedLayoutContext(deps) {
3025
3435
  const next = readLayoutContext(node, width);
3026
3436
  commitLayoutContext(next, refreshMeasurements);
3027
3437
  };
3438
+ syncLayoutContextRef.current = syncLayoutContext;
3028
3439
  const initialLayoutContext = readLayoutContext(node);
3029
3440
  const shouldObserveWrappingWidth = initialLayoutContext.whiteSpace !== "nowrap" && !supportsIntrinsicWidthLock(initialLayoutContext.display, initialLayoutContext.parentDisplay);
3030
3441
  commitLayoutContext(initialLayoutContext, true);
3442
+ const unsubscribeInvalidation = subscribeMorphMeasurementInvalidation(() => {
3443
+ syncLayoutContext({
3444
+ refreshMeasurements: true
3445
+ });
3446
+ });
3447
+ const activeFontMetricTransitions = new Map;
3448
+ const ownerDocument = node.ownerDocument;
3449
+ const ownerWindow = ownerDocument.defaultView ?? window;
3450
+ let fontMetricTransitionFrame = null;
3451
+ const stopFontMetricTransitionPolling = () => {
3452
+ if (fontMetricTransitionFrame === null) {
3453
+ return;
3454
+ }
3455
+ ownerWindow.cancelAnimationFrame(fontMetricTransitionFrame);
3456
+ fontMetricTransitionFrame = null;
3457
+ };
3458
+ const pollFontMetricTransition = () => {
3459
+ syncLayoutContext({
3460
+ refreshMeasurements: true
3461
+ });
3462
+ fontMetricTransitionFrame = ownerWindow.requestAnimationFrame(pollFontMetricTransition);
3463
+ };
3464
+ const startFontMetricTransitionPolling = () => {
3465
+ if (fontMetricTransitionFrame !== null) {
3466
+ return;
3467
+ }
3468
+ syncLayoutContext({
3469
+ refreshMeasurements: true
3470
+ });
3471
+ fontMetricTransitionFrame = ownerWindow.requestAnimationFrame(pollFontMetricTransition);
3472
+ };
3473
+ const handleFontMetricTransitionStart = (event) => {
3474
+ if (!isFontMetricTransitionProperty(event.propertyName)) {
3475
+ return;
3476
+ }
3477
+ if (!doesTransitionTargetAffectNode(node, event.target)) {
3478
+ return;
3479
+ }
3480
+ const transitionTarget = event.target;
3481
+ if (transitionTarget === null) {
3482
+ return;
3483
+ }
3484
+ let activeProperties = activeFontMetricTransitions.get(transitionTarget);
3485
+ if (activeProperties === undefined) {
3486
+ activeProperties = new Set;
3487
+ activeFontMetricTransitions.set(transitionTarget, activeProperties);
3488
+ }
3489
+ activeProperties.add(`${event.propertyName}\x00${event.pseudoElement}`);
3490
+ startFontMetricTransitionPolling();
3491
+ };
3492
+ const handleFontMetricTransitionStop = (event) => {
3493
+ if (!isFontMetricTransitionProperty(event.propertyName)) {
3494
+ return;
3495
+ }
3496
+ if (!doesTransitionTargetAffectNode(node, event.target)) {
3497
+ return;
3498
+ }
3499
+ const transitionTarget = event.target;
3500
+ if (transitionTarget === null) {
3501
+ return;
3502
+ }
3503
+ const activeProperties = activeFontMetricTransitions.get(transitionTarget);
3504
+ if (activeProperties === undefined) {
3505
+ return;
3506
+ }
3507
+ activeProperties.delete(`${event.propertyName}\x00${event.pseudoElement}`);
3508
+ if (activeProperties.size > 0) {
3509
+ return;
3510
+ }
3511
+ activeFontMetricTransitions.delete(transitionTarget);
3512
+ if (activeFontMetricTransitions.size > 0) {
3513
+ return;
3514
+ }
3515
+ stopFontMetricTransitionPolling();
3516
+ syncLayoutContext({
3517
+ refreshMeasurements: true
3518
+ });
3519
+ };
3031
3520
  let resizeObserver = null;
3032
3521
  if (shouldObserveWrappingWidth) {
3033
3522
  resizeObserver = new ResizeObserver(([entry]) => {
@@ -3037,27 +3526,40 @@ function useObservedLayoutContext(deps) {
3037
3526
  });
3038
3527
  }
3039
3528
  resizeObserver?.observe(node);
3529
+ ownerDocument.addEventListener("transitionrun", handleFontMetricTransitionStart, true);
3530
+ ownerDocument.addEventListener("transitionstart", handleFontMetricTransitionStart, true);
3531
+ ownerDocument.addEventListener("transitionend", handleFontMetricTransitionStop, true);
3532
+ ownerDocument.addEventListener("transitioncancel", handleFontMetricTransitionStop, true);
3040
3533
  acquireMorphMeasurementInvalidationListeners();
3041
3534
  return () => {
3042
3535
  disposed = true;
3536
+ syncLayoutContextRef.current = null;
3537
+ unsubscribeInvalidation();
3043
3538
  resizeObserver?.disconnect();
3539
+ stopFontMetricTransitionPolling();
3540
+ activeFontMetricTransitions.clear();
3541
+ ownerDocument.removeEventListener("transitionrun", handleFontMetricTransitionStart, true);
3542
+ ownerDocument.removeEventListener("transitionstart", handleFontMetricTransitionStart, true);
3543
+ ownerDocument.removeEventListener("transitionend", handleFontMetricTransitionStop, true);
3544
+ ownerDocument.removeEventListener("transitioncancel", handleFontMetricTransitionStop, true);
3044
3545
  releaseMorphMeasurementInvalidationListeners();
3045
3546
  };
3046
3547
  }, deps);
3047
3548
  return { ref, layoutContext };
3048
3549
  }
3049
- function getSegmenter() {
3050
- if (graphemeSegmenter !== null) {
3051
- return graphemeSegmenter;
3550
+
3551
+ // torph/src/core/dom-measurement.ts
3552
+ var domMeasurementService = null;
3553
+ function readFirstTextNode(node) {
3554
+ if (node === null) {
3555
+ return null;
3052
3556
  }
3053
- const segmenterConstructor = Intl.Segmenter;
3054
- if (segmenterConstructor === undefined) {
3055
- throw new Error("Torph requires Intl.Segmenter for grapheme-safe pairing.");
3557
+ for (const childNode of node.childNodes) {
3558
+ if (childNode.nodeType === Node.TEXT_NODE) {
3559
+ return childNode;
3560
+ }
3056
3561
  }
3057
- graphemeSegmenter = new segmenterConstructor(undefined, {
3058
- granularity: "grapheme"
3059
- });
3060
- return graphemeSegmenter;
3562
+ return null;
3061
3563
  }
3062
3564
  function getDomMeasurementService() {
3063
3565
  if (domMeasurementService !== null) {
@@ -3129,51 +3631,6 @@ function applyMeasurementHostStyle({
3129
3631
  host.style.width = "max-content";
3130
3632
  }
3131
3633
  }
3132
- function segmentTorphText(text) {
3133
- const segments = [];
3134
- let ordinal = 0;
3135
- for (const segment of getSegmenter().segment(text)) {
3136
- segments.push({
3137
- glyph: segment.segment,
3138
- key: `${segment.segment}:${ordinal}`
3139
- });
3140
- ordinal += 1;
3141
- }
3142
- return segments;
3143
- }
3144
- function readCachedMorphSegments(text) {
3145
- const cached = morphSegmentCache.get(text);
3146
- if (cached !== undefined) {
3147
- morphSegmentCache.delete(text);
3148
- morphSegmentCache.set(text, cached);
3149
- return cached;
3150
- }
3151
- const segments = segmentTorphText(text);
3152
- morphSegmentCache.set(text, segments);
3153
- if (morphSegmentCache.size > MORPH_SEGMENT_CACHE_LIMIT) {
3154
- const oldest = morphSegmentCache.keys().next();
3155
- if (!oldest.done) {
3156
- morphSegmentCache.delete(oldest.value);
3157
- }
3158
- }
3159
- return segments;
3160
- }
3161
- function getDomMeasurementRequestKey(text, renderText, layoutContext, useContentInlineSize) {
3162
- let inlineSizeMode = "container";
3163
- if (useContentInlineSize) {
3164
- inlineSizeMode = "content";
3165
- }
3166
- return `dom\x00${inlineSizeMode}\x00${text}\x00${renderText}\x00${layoutContext.measurementVersion}\x00${getMorphMeasurementEpoch()}`;
3167
- }
3168
- function needsMeasurementLayer(measurementBackend, renderText) {
3169
- if (measurementBackend === "pretext") {
3170
- return false;
3171
- }
3172
- return renderText.length > 0;
3173
- }
3174
- function canCacheMeasurementLayerSnapshot(measurementBackend) {
3175
- return measurementBackend === "dom";
3176
- }
3177
3634
  function readCachedMorphSnapshot(cache, cacheKey) {
3178
3635
  const cached = cache.get(cacheKey);
3179
3636
  if (cached === undefined) {
@@ -3186,7 +3643,7 @@ function readCachedMorphSnapshot(cache, cacheKey) {
3186
3643
  function rememberCachedMorphSnapshot(cache, cacheKey, snapshot) {
3187
3644
  cache.delete(cacheKey);
3188
3645
  cache.set(cacheKey, snapshot);
3189
- if (cache.size > DOM_MEASUREMENT_SNAPSHOT_CACHE_LIMIT) {
3646
+ if (cache.size > 8) {
3190
3647
  const oldest = cache.keys().next();
3191
3648
  if (!oldest.done) {
3192
3649
  cache.delete(oldest.value);
@@ -3267,65 +3724,27 @@ function measureMorphSnapshotWithDomService({
3267
3724
  useContentInlineSize
3268
3725
  }) {
3269
3726
  if (renderText.length === 0) {
3270
- return {
3271
- text,
3272
- renderText,
3273
- width: 0,
3274
- height: 0,
3275
- graphemes: []
3276
- };
3277
- }
3278
- const service = getDomMeasurementService();
3279
- applyMeasurementHostStyle({
3280
- host: service.host,
3281
- root,
3282
- layoutContext,
3283
- useContentInlineSize
3284
- });
3285
- service.host.textContent = renderText;
3286
- return measureMorphSnapshotFromLayer(text, renderText, segments, service.host);
3287
- }
3288
- function readRootOrigin(node) {
3289
- const rect = node.getBoundingClientRect();
3290
- return { left: rect.left, top: rect.top };
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
- }
3727
+ return {
3728
+ text,
3729
+ renderText,
3730
+ width: 0,
3731
+ height: 0,
3732
+ graphemes: []
3733
+ };
3327
3734
  }
3328
- return null;
3735
+ const service = getDomMeasurementService();
3736
+ applyMeasurementHostStyle({
3737
+ host: service.host,
3738
+ root,
3739
+ layoutContext,
3740
+ useContentInlineSize
3741
+ });
3742
+ service.host.textContent = renderText;
3743
+ return measureMorphSnapshotFromLayer(text, renderText, segments, service.host);
3744
+ }
3745
+ function readRootOrigin(node) {
3746
+ const rect = node.getBoundingClientRect();
3747
+ return { left: rect.left, top: rect.top };
3329
3748
  }
3330
3749
  function measureLiveFlowSnapshot(root, flowTextNode) {
3331
3750
  const textNode = readFirstTextNode(flowTextNode);
@@ -3458,141 +3877,6 @@ function measureOverlayBoxSnapshot(root, overlayRoot, role) {
3458
3877
  graphemes
3459
3878
  };
3460
3879
  }
3461
- function getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize) {
3462
- const backend = getPretextMorphMeasurementBackend(text, layoutContext);
3463
- if (backend !== "probe") {
3464
- return backend;
3465
- }
3466
- const signature = getPretextMorphTrustSignature({
3467
- renderText,
3468
- layoutContext,
3469
- useContentInlineSize
3470
- });
3471
- if (signature === null) {
3472
- return "dom";
3473
- }
3474
- const trusted = pretextMorphTrustCache.get(signature);
3475
- if (trusted === undefined) {
3476
- return "probe";
3477
- }
3478
- if (trusted) {
3479
- return "pretext";
3480
- }
3481
- return "dom";
3482
- }
3483
- function resolveContentWidthLockInlineSize(layoutHint) {
3484
- if (layoutHint.flowInlineSize !== null) {
3485
- return layoutHint.flowInlineSize;
3486
- }
3487
- return layoutHint.snapshot.width;
3488
- }
3489
- function shouldMeasureUsingContentInlineSize(layoutContext, layoutHint) {
3490
- if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
3491
- return true;
3492
- }
3493
- if (layoutContext.whiteSpace === "nowrap") {
3494
- return true;
3495
- }
3496
- if (layoutHint === null || !isSingleLineSnapshot(layoutHint.snapshot)) {
3497
- return false;
3498
- }
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
- });
3528
- }
3529
- function createMorphMeasurementRequest({
3530
- text,
3531
- layoutContext,
3532
- layoutHint,
3533
- forceContentInlineSize = false
3534
- }) {
3535
- if (layoutContext === null) {
3536
- return null;
3537
- }
3538
- const renderText = getPretextMorphRenderedText(text, layoutContext);
3539
- let useContentInlineSize = shouldMeasureUsingContentInlineSize(layoutContext, layoutHint);
3540
- if (forceContentInlineSize) {
3541
- useContentInlineSize = true;
3542
- }
3543
- const measurementBackend = getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize);
3544
- let segments = readCachedMorphSegments(renderText);
3545
- if (measurementBackend === "pretext") {
3546
- segments = EMPTY_SEGMENTS;
3547
- }
3548
- let domMeasurementKey = null;
3549
- if (needsMeasurementLayer(measurementBackend, renderText)) {
3550
- domMeasurementKey = getDomMeasurementRequestKey(text, renderText, layoutContext, useContentInlineSize);
3551
- }
3552
- return {
3553
- text,
3554
- renderText,
3555
- segments,
3556
- measurementBackend,
3557
- useContentInlineSize,
3558
- domMeasurementKey
3559
- };
3560
- }
3561
- function rememberPretextMeasurementTrust({
3562
- renderText,
3563
- layoutContext,
3564
- useContentInlineSize,
3565
- trusted
3566
- }) {
3567
- const signature = getPretextMorphTrustSignature({
3568
- renderText,
3569
- layoutContext,
3570
- useContentInlineSize
3571
- });
3572
- if (signature === null) {
3573
- return;
3574
- }
3575
- pretextMorphTrustCache.set(signature, trusted);
3576
- }
3577
- function areSnapshotsEquivalentForPretextTrust(left, right) {
3578
- if (left.renderText !== right.renderText || left.graphemes.length !== right.graphemes.length) {
3579
- return false;
3580
- }
3581
- if (Math.abs(left.width - right.width) > MORPH.geometryEpsilon || Math.abs(left.height - right.height) > MORPH.geometryEpsilon) {
3582
- return false;
3583
- }
3584
- for (let index = 0;index < left.graphemes.length; index += 1) {
3585
- const from = left.graphemes[index];
3586
- const to = right.graphemes[index];
3587
- if (from.glyph !== to.glyph) {
3588
- return false;
3589
- }
3590
- if (Math.abs(from.left - to.left) > MORPH.geometryEpsilon || Math.abs(from.top - to.top) > MORPH.geometryEpsilon || Math.abs(from.width - to.width) > MORPH.geometryEpsilon || Math.abs(from.height - to.height) > MORPH.geometryEpsilon) {
3591
- return false;
3592
- }
3593
- }
3594
- return true;
3595
- }
3596
3880
  function measureFromNodes({
3597
3881
  root,
3598
3882
  layoutContext,
@@ -3612,7 +3896,7 @@ function measureFromNodes({
3612
3896
  width: Number.MAX_SAFE_INTEGER / 4
3613
3897
  };
3614
3898
  }
3615
- const snapshot = snapshotOverride ?? (() => {
3899
+ const snapshot = assertSingleLineSnapshot(snapshotOverride ?? (() => {
3616
3900
  let pretextSnapshot = null;
3617
3901
  if (measurementBackend !== "dom") {
3618
3902
  pretextSnapshot = measureMorphSnapshotWithPretext(text, measurementLayoutContext);
@@ -3649,294 +3933,378 @@ function measureFromNodes({
3649
3933
  if (resolvedSnapshot === null) {
3650
3934
  throw new Error("Torph failed to resolve a measurement snapshot.");
3651
3935
  }
3652
- return resolvedSnapshot;
3653
- })();
3654
- let layoutInlineSize = layoutContext.width;
3655
- if (useContentInlineSize) {
3656
- layoutInlineSize = snapshot.width;
3936
+ return resolvedSnapshot;
3937
+ })());
3938
+ let layoutInlineSize = layoutContext.width;
3939
+ if (useContentInlineSize) {
3940
+ layoutInlineSize = snapshot.width;
3941
+ }
3942
+ let reservedInlineSize = null;
3943
+ if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
3944
+ reservedInlineSize = snapshot.width;
3945
+ }
3946
+ let flowInlineSize = null;
3947
+ if (useContentInlineSize) {
3948
+ flowInlineSize = snapshot.width;
3949
+ }
3950
+ return {
3951
+ snapshot,
3952
+ layoutInlineSize,
3953
+ reservedInlineSize,
3954
+ flowInlineSize,
3955
+ rootOrigin: readRootOrigin(root)
3956
+ };
3957
+ }
3958
+
3959
+ // torph/src/debug/trace.ts
3960
+ var TORPH_TRACE_MAX_BYTES = 4 * 1024 * 1024;
3961
+ var TORPH_TRACE_MAX_LINES = 4000;
3962
+ var TORPH_TRACE_SCHEMA_VERSION = 7;
3963
+ var DEFAULT_TORPH_DEBUG_CONFIG = {
3964
+ capture: false,
3965
+ console: false
3966
+ };
3967
+ var torphDebugInstanceOrdinal = 0;
3968
+ function nextTorphDebugInstanceId() {
3969
+ torphDebugInstanceOrdinal += 1;
3970
+ return torphDebugInstanceOrdinal;
3971
+ }
3972
+ function readTorphDebugConfig() {
3973
+ const scope = globalThis;
3974
+ return scope.__TORPH_DEBUG__ ?? DEFAULT_TORPH_DEBUG_CONFIG;
3975
+ }
3976
+ function shouldCaptureTorphTrace(config) {
3977
+ if (config === null) {
3978
+ return false;
3979
+ }
3980
+ if (typeof config === "boolean") {
3981
+ return config;
3982
+ }
3983
+ if (config.capture === true) {
3984
+ return true;
3985
+ }
3986
+ return false;
3987
+ }
3988
+ function isTorphDebugEnabled(config) {
3989
+ if (config === null) {
3990
+ return false;
3991
+ }
3992
+ if (typeof config === "boolean") {
3993
+ return config;
3994
+ }
3995
+ if (config.console !== undefined) {
3996
+ return config.console;
3997
+ }
3998
+ if (config.enabled === true) {
3999
+ return true;
4000
+ }
4001
+ return false;
4002
+ }
4003
+ function shouldRunTorphInstrumentation(config) {
4004
+ if (shouldCaptureTorphTrace(config)) {
4005
+ return true;
4006
+ }
4007
+ return isTorphDebugEnabled(config);
4008
+ }
4009
+ function getTorphTraceStore() {
4010
+ const scope = globalThis;
4011
+ let store = scope.__TORPH_TRACE_STORE__;
4012
+ if (store !== undefined) {
4013
+ return store;
4014
+ }
4015
+ store = {
4016
+ lines: [],
4017
+ nextSeq: 1,
4018
+ totalBytes: 0
4019
+ };
4020
+ scope.__TORPH_TRACE_STORE__ = store;
4021
+ return store;
4022
+ }
4023
+ function getTorphTraceText() {
4024
+ return getTorphTraceStore().lines.join("");
4025
+ }
4026
+ function clearTorphTrace() {
4027
+ const store = getTorphTraceStore();
4028
+ store.lines = [];
4029
+ store.nextSeq = 1;
4030
+ store.totalBytes = 0;
4031
+ }
4032
+ function downloadTorphTrace(filename) {
4033
+ if (typeof document === "undefined") {
4034
+ return null;
4035
+ }
4036
+ const text = getTorphTraceText();
4037
+ const blob = new Blob([text], { type: "application/x-ndjson;charset=utf-8" });
4038
+ const href = URL.createObjectURL(blob);
4039
+ const anchor = document.createElement("a");
4040
+ let resolvedFilename = filename;
4041
+ if (resolvedFilename === undefined) {
4042
+ resolvedFilename = `torph-trace-${new Date().toISOString().replaceAll(":", "-")}.jsonl`;
4043
+ }
4044
+ anchor.href = href;
4045
+ anchor.download = resolvedFilename;
4046
+ anchor.click();
4047
+ window.setTimeout(() => {
4048
+ URL.revokeObjectURL(href);
4049
+ }, 0);
4050
+ return resolvedFilename;
4051
+ }
4052
+ function ensureTorphTraceApi() {
4053
+ const scope = globalThis;
4054
+ if (scope.__TORPH_TRACE__ !== undefined) {
4055
+ return scope.__TORPH_TRACE__;
4056
+ }
4057
+ const api = {
4058
+ clear: clearTorphTrace,
4059
+ count: () => getTorphTraceStore().lines.length,
4060
+ download: downloadTorphTrace,
4061
+ text: getTorphTraceText
4062
+ };
4063
+ scope.__TORPH_TRACE__ = api;
4064
+ return api;
4065
+ }
4066
+ function appendTorphTrace(instanceId, event, payload) {
4067
+ ensureTorphTraceApi();
4068
+ const store = getTorphTraceStore();
4069
+ const entry = {
4070
+ instanceId,
4071
+ event,
4072
+ payload,
4073
+ seq: store.nextSeq,
4074
+ time: new Date().toISOString()
4075
+ };
4076
+ store.nextSeq += 1;
4077
+ const line = `${JSON.stringify(entry)}
4078
+ `;
4079
+ store.lines.push(line);
4080
+ store.totalBytes += line.length;
4081
+ while (store.lines.length > TORPH_TRACE_MAX_LINES || store.totalBytes > TORPH_TRACE_MAX_BYTES) {
4082
+ const removed = store.lines.shift();
4083
+ if (removed === undefined) {
4084
+ break;
4085
+ }
4086
+ store.totalBytes = Math.max(0, store.totalBytes - removed.length);
3657
4087
  }
3658
- let reservedInlineSize = null;
3659
- if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
3660
- reservedInlineSize = snapshot.width;
4088
+ }
4089
+ function roundDebugValue(value) {
4090
+ if (value === null || value === undefined) {
4091
+ return value;
3661
4092
  }
3662
- return {
3663
- snapshot,
3664
- layoutInlineSize,
3665
- reservedInlineSize,
3666
- flowInlineSize: null,
3667
- rootOrigin: readRootOrigin(root)
3668
- };
4093
+ return Math.round(value * 1e4) / 1e4;
3669
4094
  }
3670
- function pinMeasurementToCurrentOrigin(measurement, origin) {
3671
- if (nearlyEqual(measurement.rootOrigin.left, origin.left) && nearlyEqual(measurement.rootOrigin.top, origin.top)) {
3672
- return measurement;
4095
+ function summarizeDebugSnapshot(snapshot) {
4096
+ if (snapshot === null) {
4097
+ return null;
3673
4098
  }
3674
4099
  return {
3675
- snapshot: measurement.snapshot,
3676
- layoutInlineSize: measurement.layoutInlineSize,
3677
- reservedInlineSize: measurement.reservedInlineSize,
3678
- flowInlineSize: measurement.flowInlineSize,
3679
- rootOrigin: origin
4100
+ text: snapshot.text,
4101
+ renderText: snapshot.renderText,
4102
+ width: roundDebugValue(snapshot.width),
4103
+ height: roundDebugValue(snapshot.height),
4104
+ graphemes: snapshot.graphemes.length
3680
4105
  };
3681
4106
  }
3682
- function bucketByGlyph(graphemes) {
3683
- const buckets = new Map;
3684
- for (const grapheme of graphemes) {
3685
- const bucket = buckets.get(grapheme.glyph);
3686
- if (bucket) {
3687
- bucket.push(grapheme);
3688
- } else {
3689
- buckets.set(grapheme.glyph, [grapheme]);
3690
- }
4107
+ function summarizeDebugGlyphs(snapshot) {
4108
+ if (snapshot === null) {
4109
+ return null;
3691
4110
  }
3692
- return buckets;
4111
+ return snapshot.graphemes.map((grapheme, index) => ({
4112
+ index,
4113
+ glyph: grapheme.glyph,
4114
+ key: grapheme.key,
4115
+ left: roundDebugValue(grapheme.left),
4116
+ top: roundDebugValue(grapheme.top),
4117
+ width: roundDebugValue(grapheme.width),
4118
+ height: roundDebugValue(grapheme.height)
4119
+ }));
3693
4120
  }
3694
- function pairMorphCharacters(previous, next) {
3695
- const previousBuckets = bucketByGlyph(previous);
3696
- const nextBuckets = bucketByGlyph(next);
3697
- const pairings = [];
3698
- for (const [glyph, previousItems] of previousBuckets) {
3699
- const nextItems = nextBuckets.get(glyph) ?? [];
3700
- const shared = Math.min(previousItems.length, nextItems.length);
3701
- for (let index = 0;index < shared; index += 1) {
3702
- pairings.push({
3703
- kind: "move",
3704
- from: previousItems[index],
3705
- to: nextItems[index]
3706
- });
3707
- }
3708
- for (let index = shared;index < previousItems.length; index += 1) {
3709
- pairings.push({
3710
- kind: "exit",
3711
- from: previousItems[index]
3712
- });
3713
- }
3714
- }
3715
- for (const [glyph, nextItems] of nextBuckets) {
3716
- const previousItems = previousBuckets.get(glyph) ?? [];
3717
- const shared = Math.min(previousItems.length, nextItems.length);
3718
- for (let index = shared;index < nextItems.length; index += 1) {
3719
- pairings.push({
3720
- kind: "enter",
3721
- to: nextItems[index]
3722
- });
3723
- }
4121
+ function summarizeDebugMeasurement(measurement) {
4122
+ if (measurement === null) {
4123
+ return null;
3724
4124
  }
3725
- return pairings;
3726
- }
3727
- function resolveMorphFrameBounds(previous, next) {
3728
4125
  return {
3729
- width: Math.max(previous.width, next.width),
3730
- height: Math.max(previous.height, next.height)
4126
+ layoutInlineSize: roundDebugValue(measurement.layoutInlineSize),
4127
+ reservedInlineSize: roundDebugValue(measurement.reservedInlineSize),
4128
+ flowInlineSize: roundDebugValue(measurement.flowInlineSize),
4129
+ rootOrigin: {
4130
+ left: roundDebugValue(measurement.rootOrigin.left),
4131
+ top: roundDebugValue(measurement.rootOrigin.top)
4132
+ },
4133
+ snapshot: summarizeDebugSnapshot(measurement.snapshot)
3731
4134
  };
3732
4135
  }
3733
- function buildMorphVisualBridge(previous, next) {
4136
+ function summarizeDebugLayoutContext(layoutContext) {
4137
+ if (layoutContext === null) {
4138
+ return null;
4139
+ }
3734
4140
  return {
3735
- offsetX: previous.rootOrigin.left - next.rootOrigin.left,
3736
- offsetY: previous.rootOrigin.top - next.rootOrigin.top
4141
+ display: layoutContext.display,
4142
+ parentDisplay: layoutContext.parentDisplay,
4143
+ whiteSpace: layoutContext.whiteSpace,
4144
+ width: roundDebugValue(layoutContext.width),
4145
+ measurementVersion: layoutContext.measurementVersion
3737
4146
  };
3738
4147
  }
3739
- function buildMorphPlan(previous, next, visualBridge = ZERO_BRIDGE) {
3740
- const pairings = pairMorphCharacters(previous.snapshot.graphemes, next.snapshot.graphemes);
3741
- const movesByDestinationKey = new Map;
3742
- const exitItems = [];
3743
- for (const pairing of pairings) {
3744
- if (pairing.kind === "move") {
3745
- movesByDestinationKey.set(pairing.to.key, pairing);
3746
- continue;
3747
- }
3748
- if (pairing.kind === "exit") {
3749
- exitItems.push(pairing.from);
3750
- }
4148
+ function summarizeDebugRect(rect) {
4149
+ if (rect === null) {
4150
+ return null;
3751
4151
  }
3752
- const frame = resolveMorphFrameBounds(previous.snapshot, next.snapshot);
3753
4152
  return {
3754
- frameWidth: frame.width,
3755
- frameHeight: frame.height,
3756
- layoutInlineSizeFrom: previous.layoutInlineSize,
3757
- layoutInlineSizeTo: next.layoutInlineSize,
3758
- sourceRenderText: previous.snapshot.renderText,
3759
- targetRenderText: next.snapshot.renderText,
3760
- visualBridge,
3761
- liveItems: next.snapshot.graphemes.map((grapheme) => {
3762
- const move = movesByDestinationKey.get(grapheme.key);
3763
- if (move) {
3764
- return {
3765
- ...grapheme,
3766
- kind: "move",
3767
- fromLeft: move.from.left,
3768
- fromTop: move.from.top
3769
- };
3770
- }
3771
- return {
3772
- ...grapheme,
3773
- kind: "enter",
3774
- fromLeft: null,
3775
- fromTop: null
3776
- };
3777
- }),
3778
- exitItems
4153
+ left: roundDebugValue(rect.left),
4154
+ top: roundDebugValue(rect.top),
4155
+ width: roundDebugValue(rect.width),
4156
+ height: roundDebugValue(rect.height)
3779
4157
  };
3780
4158
  }
3781
- function nearlyEqual(a, b, epsilon = MORPH.geometryEpsilon) {
3782
- return Math.abs(a - b) <= epsilon;
3783
- }
3784
- function sameSnapshot(a, b) {
3785
- if (a === b) {
3786
- return true;
3787
- }
3788
- if (a.text !== b.text || a.renderText !== b.renderText || a.graphemes.length !== b.graphemes.length) {
3789
- return false;
4159
+ function collectDebugAnchorIndices(length) {
4160
+ const indices = new Set;
4161
+ if (length <= 0) {
4162
+ return [];
3790
4163
  }
3791
- if (!nearlyEqual(a.width, b.width) || !nearlyEqual(a.height, b.height)) {
3792
- return false;
4164
+ indices.add(0);
4165
+ if (length > 1) {
4166
+ indices.add(1);
4167
+ indices.add(length - 2);
3793
4168
  }
3794
- for (let index = 0;index < a.graphemes.length; index += 1) {
3795
- const left = a.graphemes[index];
3796
- const right = b.graphemes[index];
3797
- if (left.glyph !== right.glyph || left.key !== right.key) {
3798
- return false;
3799
- }
3800
- if (!nearlyEqual(left.left, right.left) || !nearlyEqual(left.top, right.top) || !nearlyEqual(left.width, right.width) || !nearlyEqual(left.height, right.height)) {
3801
- return false;
3802
- }
4169
+ if (length > 2) {
4170
+ indices.add(Math.floor((length - 1) / 2));
3803
4171
  }
3804
- return true;
4172
+ indices.add(length - 1);
4173
+ return Array.from(indices).sort((left, right) => left - right);
3805
4174
  }
3806
- function sameMeasurement(a, b) {
3807
- if (a === b) {
3808
- return true;
4175
+ function summarizeDebugViewportAnchors(snapshot, rootRect) {
4176
+ if (snapshot === null || rootRect === null) {
4177
+ return null;
3809
4178
  }
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);
3811
- }
3812
- function refreshAnimatingTarget(activeTarget, measurement) {
3813
- if (sameMeasurement(activeTarget, measurement)) {
3814
- return activeTarget;
4179
+ const anchors = [];
4180
+ const anchorIndices = collectDebugAnchorIndices(snapshot.graphemes.length);
4181
+ for (const index of anchorIndices) {
4182
+ const grapheme = snapshot.graphemes[index];
4183
+ if (grapheme === undefined) {
4184
+ continue;
4185
+ }
4186
+ anchors.push({
4187
+ index,
4188
+ glyph: grapheme.glyph,
4189
+ left: roundDebugValue(rootRect.left + grapheme.left),
4190
+ top: roundDebugValue(rootRect.top + grapheme.top),
4191
+ width: roundDebugValue(grapheme.width),
4192
+ height: roundDebugValue(grapheme.height)
4193
+ });
3815
4194
  }
3816
- return {
3817
- snapshot: activeTarget.snapshot,
3818
- layoutInlineSize: measurement.layoutInlineSize,
3819
- reservedInlineSize: measurement.reservedInlineSize,
3820
- flowInlineSize: activeTarget.flowInlineSize,
3821
- rootOrigin: measurement.rootOrigin
3822
- };
4195
+ return anchors;
3823
4196
  }
3824
- function reuseCommittedMeasurement(committed, measurement) {
3825
- if (sameMeasurement(committed, measurement)) {
3826
- return committed;
4197
+ function summarizeDebugRootOriginDrift(measurement, rootRect) {
4198
+ if (measurement === null || rootRect === null) {
4199
+ return null;
3827
4200
  }
3828
- return measurement;
3829
- }
3830
- function createStaticState(measurement) {
3831
4201
  return {
3832
- stage: "idle",
3833
- measurement,
3834
- plan: null
4202
+ expectedLeft: roundDebugValue(measurement.rootOrigin.left),
4203
+ expectedTop: roundDebugValue(measurement.rootOrigin.top),
4204
+ actualLeft: roundDebugValue(rootRect.left),
4205
+ actualTop: roundDebugValue(rootRect.top),
4206
+ deltaLeft: roundDebugValue(rootRect.left - measurement.rootOrigin.left),
4207
+ deltaTop: roundDebugValue(rootRect.top - measurement.rootOrigin.top)
3835
4208
  };
3836
4209
  }
3837
- function areFontsReady() {
3838
- return document.fonts.status === "loaded";
3839
- }
3840
- function cancelTimeline(timeline) {
3841
- if (timeline.prepareFrame !== null) {
3842
- cancelAnimationFrame(timeline.prepareFrame);
3843
- timeline.prepareFrame = null;
3844
- }
3845
- if (timeline.animateFrame !== null) {
3846
- cancelAnimationFrame(timeline.animateFrame);
3847
- timeline.animateFrame = null;
3848
- }
3849
- if (timeline.finalizeTimer !== null) {
3850
- window.clearTimeout(timeline.finalizeTimer);
3851
- timeline.finalizeTimer = null;
3852
- }
3853
- }
3854
- function resetMorph(session, timeline, setState) {
3855
- cancelTimeline(timeline);
3856
- session.committed = null;
3857
- session.target = null;
3858
- session.animating = false;
3859
- setState(EMPTY_STATE);
4210
+ function summarizeSnapshotDrift(drift) {
4211
+ return {
4212
+ comparedGlyphs: drift.comparedGlyphs,
4213
+ expectedGlyphs: drift.expectedGlyphs,
4214
+ actualGlyphs: drift.actualGlyphs,
4215
+ snapshotWidthDelta: roundDebugValue(drift.snapshotWidthDelta),
4216
+ snapshotHeightDelta: roundDebugValue(drift.snapshotHeightDelta),
4217
+ maxAbsLeftDelta: roundDebugValue(drift.maxAbsLeftDelta),
4218
+ maxAbsTopDelta: roundDebugValue(drift.maxAbsTopDelta),
4219
+ maxAbsWidthDelta: roundDebugValue(drift.maxAbsWidthDelta),
4220
+ maxAbsHeightDelta: roundDebugValue(drift.maxAbsHeightDelta),
4221
+ mismatches: drift.mismatches.map((mismatch) => ({
4222
+ index: mismatch.index,
4223
+ glyph: mismatch.glyph,
4224
+ leftDelta: roundDebugValue(mismatch.leftDelta),
4225
+ topDelta: roundDebugValue(mismatch.topDelta),
4226
+ widthDelta: roundDebugValue(mismatch.widthDelta),
4227
+ heightDelta: roundDebugValue(mismatch.heightDelta)
4228
+ }))
4229
+ };
3860
4230
  }
3861
- function commitStaticMeasurement(session, measurement, setState) {
3862
- if (!session.animating && session.target === null && session.committed !== null && sameMeasurement(session.committed, measurement)) {
4231
+ function logTorphDebug(instanceId, event, payload) {
4232
+ const config = readTorphDebugConfig();
4233
+ const captureTrace = shouldCaptureTorphTrace(config);
4234
+ const logToConsole = isTorphDebugEnabled(config);
4235
+ if (!captureTrace && !logToConsole) {
3863
4236
  return;
3864
4237
  }
3865
- session.committed = measurement;
3866
- session.target = null;
3867
- session.animating = false;
3868
- setState((current) => {
3869
- if (current.stage === "idle" && current.plan === null && current.measurement !== null && sameMeasurement(current.measurement, measurement)) {
3870
- return current;
3871
- }
3872
- return createStaticState(measurement);
3873
- });
3874
- }
3875
- function scheduleMorphTimeline({
3876
- session,
3877
- timeline,
3878
- finalizeMeasurement,
3879
- measurement,
3880
- plan,
3881
- setState
3882
- }) {
3883
- timeline.prepareFrame = requestAnimationFrame(() => {
3884
- timeline.prepareFrame = null;
3885
- setState((current) => {
3886
- if (current.measurement !== measurement || current.plan !== plan) {
3887
- return current;
3888
- }
3889
- return {
3890
- stage: "animate",
3891
- measurement,
3892
- plan
3893
- };
3894
- });
3895
- timeline.animateFrame = requestAnimationFrame(() => {
3896
- timeline.animateFrame = null;
3897
- timeline.finalizeTimer = window.setTimeout(() => {
3898
- timeline.finalizeTimer = null;
3899
- commitStaticMeasurement(session, finalizeMeasurement(session.target ?? measurement), setState);
3900
- }, MORPH.durationMs);
3901
- });
3902
- });
4238
+ if (captureTrace) {
4239
+ appendTorphTrace(instanceId, event, payload);
4240
+ }
4241
+ if (logToConsole) {
4242
+ console.log(`[Torph#${instanceId}] ${event}`, payload);
4243
+ }
3903
4244
  }
3904
- function startMorph({
3905
- finalizeMeasurement,
3906
- nextMeasurement,
3907
- session,
3908
- timeline,
3909
- setState
3910
- }) {
3911
- let sourceMeasurement = session.committed;
3912
- if (session.animating && session.target) {
3913
- sourceMeasurement = session.target;
4245
+
4246
+ // torph/src/components/Torph.tsx
4247
+ import { jsxDEV } from "react/jsx-dev-runtime";
4248
+ var SCREEN_READER_ONLY_STYLE = {
4249
+ position: "absolute",
4250
+ width: "1px",
4251
+ height: "1px",
4252
+ margin: "-1px",
4253
+ padding: 0,
4254
+ border: 0,
4255
+ clip: "rect(0 0 0 0)",
4256
+ clipPath: "inset(50%)",
4257
+ overflow: "hidden",
4258
+ whiteSpace: "nowrap"
4259
+ };
4260
+ var FALLBACK_TEXT_STYLE = {
4261
+ display: "block",
4262
+ gridArea: "1 / 1"
4263
+ };
4264
+ var SHARED_GLYPH_TYPOGRAPHY_STYLE = {
4265
+ font: "inherit",
4266
+ fontKerning: "inherit",
4267
+ fontFeatureSettings: "inherit",
4268
+ fontOpticalSizing: "inherit",
4269
+ fontStretch: "inherit",
4270
+ fontStyle: "inherit",
4271
+ fontVariant: "inherit",
4272
+ fontVariantNumeric: "inherit",
4273
+ fontVariationSettings: "inherit",
4274
+ fontWeight: "inherit",
4275
+ letterSpacing: "inherit",
4276
+ textTransform: "inherit",
4277
+ wordSpacing: "inherit",
4278
+ direction: "inherit"
4279
+ };
4280
+ var ABSOLUTE_GLYPH_STYLE = {
4281
+ position: "absolute",
4282
+ display: "block",
4283
+ overflow: "hidden",
4284
+ transformOrigin: "left top"
4285
+ };
4286
+ var CONTEXT_SLICE_TEXT_STYLE = {
4287
+ ...SHARED_GLYPH_TYPOGRAPHY_STYLE,
4288
+ position: "absolute",
4289
+ display: "block",
4290
+ minWidth: 0,
4291
+ whiteSpace: "inherit"
4292
+ };
4293
+ var debugDomNodeIds = new WeakMap;
4294
+ var debugDomNodeOrdinal = 0;
4295
+ function getDebugDomNodeId(node) {
4296
+ if (node === null) {
4297
+ return null;
3914
4298
  }
3915
- if (sourceMeasurement === null) {
3916
- commitStaticMeasurement(session, nextMeasurement, setState);
3917
- return;
4299
+ const existing = debugDomNodeIds.get(node);
4300
+ if (existing !== undefined) {
4301
+ return existing;
3918
4302
  }
3919
- const previousMeasurement = pinMeasurementToCurrentOrigin(sourceMeasurement, nextMeasurement.rootOrigin);
3920
- const visualBridge = buildMorphVisualBridge(previousMeasurement, nextMeasurement);
3921
- const plan = buildMorphPlan(previousMeasurement, nextMeasurement, visualBridge);
3922
- session.target = nextMeasurement;
3923
- session.animating = true;
3924
- setState({
3925
- stage: "prepare",
3926
- measurement: nextMeasurement,
3927
- plan
3928
- });
3929
- scheduleMorphTimeline({
3930
- session,
3931
- timeline,
3932
- finalizeMeasurement,
3933
- measurement: nextMeasurement,
3934
- plan,
3935
- setState
3936
- });
4303
+ debugDomNodeOrdinal += 1;
4304
+ debugDomNodeIds.set(node, debugDomNodeOrdinal);
4305
+ return debugDomNodeOrdinal;
3937
4306
  }
3938
4307
  function reconcileMorphChange({
3939
- finalizeMeasurement,
3940
4308
  root,
3941
4309
  measurementLayer,
3942
4310
  measurementBackend,
@@ -3956,10 +4324,7 @@ function reconcileMorphChange({
3956
4324
  if (measurementBackend === null) {
3957
4325
  throw new Error("Torph measurement backend is missing.");
3958
4326
  }
3959
- let layoutHint = session.committed;
3960
- if (session.animating && session.target !== null) {
3961
- layoutHint = session.target;
3962
- }
4327
+ const layoutHint = selectMorphLayoutHint(session);
3963
4328
  const nextMeasurement = measureFromNodes({
3964
4329
  root,
3965
4330
  layoutContext,
@@ -3971,73 +4336,13 @@ function reconcileMorphChange({
3971
4336
  renderText,
3972
4337
  segments
3973
4338
  });
3974
- if (session.animating && session.target !== null) {
3975
- if (nextMeasurement.snapshot.renderText === session.target.snapshot.renderText) {
3976
- session.target = refreshAnimatingTarget(session.target, nextMeasurement);
3977
- return nextMeasurement;
3978
- }
3979
- }
3980
- cancelTimeline(timeline);
3981
- if (session.committed === null) {
3982
- commitStaticMeasurement(session, nextMeasurement, setState);
3983
- return nextMeasurement;
3984
- }
3985
- if (!areFontsReady()) {
3986
- commitStaticMeasurement(session, nextMeasurement, setState);
3987
- return nextMeasurement;
3988
- }
3989
- if (session.committed.snapshot.renderText === nextMeasurement.snapshot.renderText) {
3990
- commitStaticMeasurement(session, reuseCommittedMeasurement(session.committed, nextMeasurement), setState);
3991
- return nextMeasurement;
3992
- }
3993
- startMorph({
3994
- finalizeMeasurement,
3995
- nextMeasurement,
4339
+ return reconcileMorphSessionUpdate({
3996
4340
  session,
3997
4341
  timeline,
4342
+ nextMeasurement,
4343
+ fontsReady: areFontsReady(),
3998
4344
  setState
3999
4345
  });
4000
- return nextMeasurement;
4001
- }
4002
- function syncCommittedRootOriginWhenIdle({
4003
- root,
4004
- flowTextRef,
4005
- layoutContext,
4006
- state,
4007
- session
4008
- }) {
4009
- if (root === null || layoutContext === null) {
4010
- return;
4011
- }
4012
- if (state.stage !== "idle" || state.measurement === null) {
4013
- return;
4014
- }
4015
- const nextRootOrigin = readRootOrigin(root);
4016
- const nextFlowInlineSize = readFlowInlineSize(flowTextRef.current);
4017
- const committedMeasurement = state.measurement;
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))) {
4019
- session.committed = committedMeasurement;
4020
- return;
4021
- }
4022
- session.committed = {
4023
- snapshot: committedMeasurement.snapshot,
4024
- layoutInlineSize: committedMeasurement.layoutInlineSize,
4025
- reservedInlineSize: committedMeasurement.reservedInlineSize,
4026
- flowInlineSize: nextFlowInlineSize,
4027
- rootOrigin: nextRootOrigin
4028
- };
4029
- }
4030
- function getFadeDuration(fraction) {
4031
- return Math.min(MORPH.durationMs * fraction, MORPH.maxFadeMs);
4032
- }
4033
- function getLiveTransform(item, stage, visualBridge) {
4034
- if (stage !== "prepare") {
4035
- return "translate(0px, 0px)";
4036
- }
4037
- if (item.kind === "move") {
4038
- return `translate(${(item.fromLeft ?? item.left) - item.left + visualBridge.offsetX}px, ${(item.fromTop ?? item.top) - item.top + visualBridge.offsetY}px)`;
4039
- }
4040
- return `translate(${visualBridge.offsetX}px, ${visualBridge.offsetY}px)`;
4041
4346
  }
4042
4347
  function getLiveOpacity(item, stage) {
4043
4348
  if (stage === "prepare" && item.kind === "enter") {
@@ -4045,96 +4350,14 @@ function getLiveOpacity(item, stage) {
4045
4350
  }
4046
4351
  return 1;
4047
4352
  }
4048
- function getLiveTransition(item, stage) {
4049
- if (stage !== "animate") {
4050
- return;
4051
- }
4052
- if (item.kind === "enter") {
4053
- return `opacity ${getFadeDuration(0.5)}ms linear ${getFadeDuration(0.25)}ms`;
4054
- }
4055
- return `transform ${MORPH.durationMs}ms ${MORPH.ease}, opacity ${getFadeDuration(0.25)}ms linear`;
4056
- }
4057
4353
  function getExitOpacity(stage) {
4058
4354
  if (stage === "animate") {
4059
4355
  return 0;
4060
4356
  }
4061
4357
  return 1;
4062
4358
  }
4063
- function getExitTransform(visualBridge) {
4064
- return `translate(${visualBridge.offsetX}px, ${visualBridge.offsetY}px)`;
4065
- }
4066
- function getExitTransition(stage) {
4067
- if (stage !== "animate") {
4068
- return;
4069
- }
4070
- return `transform ${MORPH.durationMs}ms ${MORPH.ease}, opacity ${getFadeDuration(0.25)}ms linear`;
4071
- }
4072
- function supportsIntrinsicWidthLock(display, parentDisplay) {
4073
- const parentNeedsReservation = parentDisplay === "flex" || parentDisplay === "inline-flex" || parentDisplay === "grid" || parentDisplay === "inline-grid";
4074
- return display === "inline" || display === "inline-block" || display === "inline-flex" || display === "inline-grid" || parentNeedsReservation;
4075
- }
4076
- function getRootDisplay(layoutContext) {
4077
- if (layoutContext === null) {
4078
- return "grid";
4079
- }
4080
- if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
4081
- return "inline-grid";
4082
- }
4083
- return "grid";
4084
- }
4085
- function getRootStyle(stage, plan, measurement, layoutContext) {
4086
- let width = measurement?.reservedInlineSize ?? undefined;
4087
- if (plan !== null) {
4088
- width = plan.layoutInlineSizeTo;
4089
- if (stage === "prepare") {
4090
- width = plan.layoutInlineSizeFrom;
4091
- }
4092
- }
4093
- let height;
4094
- if (plan !== null) {
4095
- height = plan.frameHeight;
4096
- }
4097
- const shouldTransitionWidth = stage === "animate" && plan !== null && !nearlyEqual(plan.layoutInlineSizeFrom, plan.layoutInlineSizeTo);
4098
- const style = {
4099
- position: "relative",
4100
- display: getRootDisplay(layoutContext)
4101
- };
4102
- if (width !== undefined) {
4103
- style.width = width;
4104
- }
4105
- if (height !== undefined) {
4106
- style.height = height;
4107
- }
4108
- if (shouldTransitionWidth) {
4109
- style.transition = `width ${MORPH.durationMs}ms ${MORPH.ease}`;
4110
- }
4111
- return style;
4112
- }
4113
- function getMeasurementLayerStyle(layoutContext, useContentInlineSize = false) {
4114
- const intrinsicWidthLock = layoutContext !== null && (useContentInlineSize || supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay));
4115
- if (!intrinsicWidthLock) {
4116
- return MEASUREMENT_LAYER_STYLE;
4117
- }
4118
- return {
4119
- ...MEASUREMENT_LAYER_STYLE,
4120
- right: "auto",
4121
- width: "max-content"
4122
- };
4123
- }
4124
- function resolveFlowText(committedMeasurement, stateMeasurement, text) {
4125
- return stateMeasurement?.snapshot.text ?? committedMeasurement?.snapshot.text ?? text;
4126
- }
4127
- function getOverlayStyle(plan) {
4128
- return {
4129
- ...OVERLAY_STYLE,
4130
- right: "auto",
4131
- bottom: "auto",
4132
- width: plan.frameWidth,
4133
- height: plan.frameHeight
4134
- };
4135
- }
4136
- function getFallbackTextStyle(shouldRenderOverlay) {
4137
- if (!shouldRenderOverlay) {
4359
+ function getFallbackTextStyle(shouldHideFlowText) {
4360
+ if (!shouldHideFlowText) {
4138
4361
  return FALLBACK_TEXT_STYLE;
4139
4362
  }
4140
4363
  return {
@@ -4167,18 +4390,136 @@ function getExitGlyphStyle(item, stage, visualBridge) {
4167
4390
  transition: getExitTransition(stage)
4168
4391
  };
4169
4392
  }
4170
- function getContextSliceStyle(layoutInlineSize, item) {
4393
+ function getContextSliceStyle(layoutInlineSize, item, whiteSpace) {
4394
+ return {
4395
+ ...CONTEXT_SLICE_TEXT_STYLE,
4396
+ left: -item.left,
4397
+ top: -item.top,
4398
+ width: layoutInlineSize,
4399
+ whiteSpace
4400
+ };
4401
+ }
4402
+ function summarizePreciseRect(rect) {
4403
+ if (rect === null) {
4404
+ return null;
4405
+ }
4406
+ return {
4407
+ left: roundDebugValue(rect.left),
4408
+ top: roundDebugValue(rect.top),
4409
+ width: roundDebugValue(rect.width),
4410
+ height: roundDebugValue(rect.height)
4411
+ };
4412
+ }
4413
+ function summarizePreciseGlyphs(snapshot, rootRect) {
4414
+ if (snapshot === null) {
4415
+ return null;
4416
+ }
4417
+ return snapshot.graphemes.map((grapheme, index) => ({
4418
+ index,
4419
+ glyph: grapheme.glyph,
4420
+ key: grapheme.key,
4421
+ left: roundDebugValue(grapheme.left),
4422
+ top: roundDebugValue(grapheme.top),
4423
+ width: roundDebugValue(grapheme.width),
4424
+ height: roundDebugValue(grapheme.height),
4425
+ viewportLeft: rootRect === null ? null : roundDebugValue(rootRect.left + grapheme.left),
4426
+ viewportTop: rootRect === null ? null : roundDebugValue(rootRect.top + grapheme.top)
4427
+ }));
4428
+ }
4429
+ function summarizeLiveNodeStyles(overlayNode) {
4430
+ if (overlayNode === null) {
4431
+ return null;
4432
+ }
4433
+ return Array.from(overlayNode.querySelectorAll("[data-morph-role='live']")).map((node, index) => {
4434
+ const styles = getComputedStyle(node);
4435
+ const sliceNode = node.firstElementChild;
4436
+ let sliceStyles = null;
4437
+ let sliceRect = null;
4438
+ if (sliceNode instanceof HTMLElement) {
4439
+ sliceStyles = getComputedStyle(sliceNode);
4440
+ sliceRect = sliceNode.getBoundingClientRect();
4441
+ }
4442
+ let nodeRect = null;
4443
+ nodeRect = node.getBoundingClientRect();
4444
+ let sliceScrollWidth = null;
4445
+ let sliceClientWidth = null;
4446
+ let sliceOffsetWidth = null;
4447
+ if (sliceNode instanceof HTMLElement) {
4448
+ sliceScrollWidth = sliceNode.scrollWidth;
4449
+ sliceClientWidth = sliceNode.clientWidth;
4450
+ sliceOffsetWidth = sliceNode.offsetWidth;
4451
+ }
4452
+ return {
4453
+ index,
4454
+ nodeId: getDebugDomNodeId(node),
4455
+ key: node.dataset.morphKey ?? null,
4456
+ glyph: node.dataset.morphGlyph ?? null,
4457
+ kind: node.dataset.morphKind ?? null,
4458
+ transform: styles.transform,
4459
+ inlineTransform: node.style.transform,
4460
+ opacity: styles.opacity,
4461
+ transitionProperty: styles.transitionProperty,
4462
+ transitionDuration: styles.transitionDuration,
4463
+ transitionTimingFunction: styles.transitionTimingFunction,
4464
+ nodeRect: summarizePreciseRect(nodeRect),
4465
+ sliceNodeId: getDebugDomNodeId(sliceNode),
4466
+ sliceInlineLeft: node.firstElementChild instanceof HTMLElement ? node.firstElementChild.style.left : null,
4467
+ sliceInlineTop: node.firstElementChild instanceof HTMLElement ? node.firstElementChild.style.top : null,
4468
+ sliceInlineWidth: node.firstElementChild instanceof HTMLElement ? node.firstElementChild.style.width : null,
4469
+ sliceLeft: sliceStyles?.left ?? null,
4470
+ sliceTop: sliceStyles?.top ?? null,
4471
+ sliceWidth: sliceStyles?.width ?? null,
4472
+ sliceWhiteSpace: sliceStyles?.whiteSpace ?? null,
4473
+ sliceText: sliceNode instanceof HTMLElement ? sliceNode.textContent : null,
4474
+ sliceRect: summarizePreciseRect(sliceRect),
4475
+ sliceScrollWidth: roundDebugValue(sliceScrollWidth),
4476
+ sliceClientWidth: roundDebugValue(sliceClientWidth),
4477
+ sliceOffsetWidth: roundDebugValue(sliceOffsetWidth)
4478
+ };
4479
+ });
4480
+ }
4481
+ function summarizeRootRuntimeStyles(root) {
4482
+ if (root === null) {
4483
+ return null;
4484
+ }
4485
+ const styles = getComputedStyle(root);
4486
+ const parent = root.parentElement;
4487
+ let parentStyles = null;
4488
+ let parentRect = null;
4489
+ if (parent instanceof HTMLElement) {
4490
+ parentStyles = getComputedStyle(parent);
4491
+ parentRect = parent.getBoundingClientRect();
4492
+ }
4493
+ let parentSummary = null;
4494
+ if (parent !== null) {
4495
+ parentSummary = {
4496
+ display: parentStyles?.display ?? null,
4497
+ justifyContent: parentStyles?.justifyContent ?? null,
4498
+ alignItems: parentStyles?.alignItems ?? null,
4499
+ placeItems: parentStyles?.placeItems ?? null,
4500
+ textAlign: parentStyles?.textAlign ?? null,
4501
+ rect: summarizePreciseRect(parentRect)
4502
+ };
4503
+ }
4171
4504
  return {
4172
- ...CONTEXT_SLICE_TEXT_STYLE,
4173
- left: -item.left,
4174
- top: -item.top,
4175
- width: layoutInlineSize
4505
+ inlineWidth: root.style.width || null,
4506
+ computedWidth: styles.width,
4507
+ inlineTransition: root.style.transition || null,
4508
+ computedTransitionProperty: styles.transitionProperty,
4509
+ computedTransitionDuration: styles.transitionDuration,
4510
+ computedTransform: styles.transform,
4511
+ offsetWidth: roundDebugValue(root.offsetWidth),
4512
+ clientWidth: roundDebugValue(root.clientWidth),
4513
+ scrollWidth: roundDebugValue(root.scrollWidth),
4514
+ parent: parentSummary
4176
4515
  };
4177
4516
  }
4178
4517
  function MorphOverlay({
4179
4518
  overlayRef,
4180
4519
  stage,
4181
- plan
4520
+ plan,
4521
+ sourceSliceWhiteSpace,
4522
+ targetSliceWhiteSpace
4182
4523
  }) {
4183
4524
  let exitItems = [];
4184
4525
  if (stage !== "idle") {
@@ -4187,7 +4528,7 @@ function MorphOverlay({
4187
4528
  return /* @__PURE__ */ jsxDEV("div", {
4188
4529
  ref: overlayRef,
4189
4530
  "aria-hidden": "true",
4190
- style: getOverlayStyle(plan),
4531
+ style: getOverlayStyle(stage, plan),
4191
4532
  children: [
4192
4533
  exitItems.map((item) => /* @__PURE__ */ jsxDEV("span", {
4193
4534
  "data-morph-role": "exit",
@@ -4195,7 +4536,8 @@ function MorphOverlay({
4195
4536
  "data-morph-glyph": item.glyph,
4196
4537
  style: getExitGlyphStyle(item, stage, plan.visualBridge),
4197
4538
  children: /* @__PURE__ */ jsxDEV("span", {
4198
- style: getContextSliceStyle(plan.layoutInlineSizeFrom, item),
4539
+ "data-morph-slice": "context",
4540
+ style: getContextSliceStyle(plan.layoutInlineSizeFrom, item, sourceSliceWhiteSpace),
4199
4541
  children: plan.sourceRenderText
4200
4542
  }, undefined, false, undefined, this)
4201
4543
  }, `exit-${item.key}`, false, undefined, this)),
@@ -4203,9 +4545,11 @@ function MorphOverlay({
4203
4545
  "data-morph-role": "live",
4204
4546
  "data-morph-key": item.key,
4205
4547
  "data-morph-glyph": item.glyph,
4548
+ "data-morph-kind": item.kind,
4206
4549
  style: getLiveGlyphStyle(item, stage, plan.visualBridge),
4207
4550
  children: /* @__PURE__ */ jsxDEV("span", {
4208
- style: getContextSliceStyle(plan.layoutInlineSizeTo, item),
4551
+ "data-morph-slice": "context",
4552
+ style: getContextSliceStyle(plan.layoutInlineSizeTo, item, targetSliceWhiteSpace),
4209
4553
  children: plan.targetRenderText
4210
4554
  }, undefined, false, undefined, this)
4211
4555
  }, item.key, false, undefined, this))
@@ -4225,54 +4569,92 @@ function MeasurementLayer({
4225
4569
  children: text
4226
4570
  }, undefined, false, undefined, this);
4227
4571
  }
4572
+ function isMorphOverlayTransformFinalizeEvent(event, hasMoveTransitions) {
4573
+ if (!hasMoveTransitions) {
4574
+ return false;
4575
+ }
4576
+ const target = event.target;
4577
+ if (!(target instanceof HTMLElement)) {
4578
+ return false;
4579
+ }
4580
+ if (target.dataset.morphRole !== "live") {
4581
+ return false;
4582
+ }
4583
+ return event.propertyName === "transform";
4584
+ }
4585
+ function resolveMorphFinalizeSignal(event, hasMoveTransitions) {
4586
+ if (isMorphOverlayTransformFinalizeEvent(event, hasMoveTransitions)) {
4587
+ return "live-transform";
4588
+ }
4589
+ return null;
4590
+ }
4228
4591
  function useMorphTransition(text, className) {
4229
- const { ref, layoutContext } = useObservedLayoutContext([className]);
4230
- const debugInstanceIdRef = useRef(null);
4231
- const flowTextRef = useRef(null);
4232
- const measurementLayerRef = useRef(null);
4233
- const completedDomMeasurementKeyRef = useRef(null);
4234
- const domMeasurementSnapshotCacheRef = useRef(new Map);
4235
- const sessionRef = useRef({ ...EMPTY_SESSION });
4236
- const timelineRef = useRef({ ...EMPTY_TIMELINE });
4237
- const debugDriftSignatureRef = useRef(null);
4238
- const [domMeasurementRequestKey, setDomMeasurementRequestKey] = useState(null);
4239
- const [forceContentMeasurementText, setForceContentMeasurementText] = useState(null);
4240
- const [state, setState] = useState(EMPTY_STATE);
4592
+ const [state, setState] = useState2(EMPTY_STATE);
4593
+ const { ref, layoutContext } = useObservedLayoutContext([
4594
+ className
4595
+ ]);
4596
+ const debugInstanceIdRef = useRef2(null);
4597
+ const debugRenderOrdinalRef = useRef2(0);
4598
+ const flowTextRef = useRef2(null);
4599
+ const measurementLayerRef = useRef2(null);
4600
+ const completedDomMeasurementKeyRef = useRef2(null);
4601
+ const domMeasurementSnapshotCacheRef = useRef2(new Map);
4602
+ const sessionRef = useRef2({ ...EMPTY_SESSION });
4603
+ const timelineRef = useRef2({ ...EMPTY_TIMELINE });
4604
+ const debugDriftSignatureRef = useRef2(null);
4605
+ const [domMeasurementRequestKey, setDomMeasurementRequestKey] = useState2(null);
4606
+ debugRenderOrdinalRef.current += 1;
4607
+ const debugRenderOrdinal = debugRenderOrdinalRef.current;
4241
4608
  if (debugInstanceIdRef.current === null) {
4242
4609
  debugInstanceIdRef.current = nextTorphDebugInstanceId();
4243
4610
  }
4244
- let measurementHint = sessionRef.current.committed;
4245
- if (sessionRef.current.animating) {
4246
- measurementHint = sessionRef.current.target ?? sessionRef.current.committed;
4247
- }
4248
- const forceContentInlineSize = forceContentMeasurementText === text;
4611
+ const measurementHint = selectMorphLayoutHint(sessionRef.current);
4249
4612
  const measurementRequest = useMemo(() => createMorphMeasurementRequest({
4250
4613
  text,
4251
4614
  layoutContext,
4252
- layoutHint: measurementHint,
4253
- forceContentInlineSize
4254
- }), [text, layoutContext, measurementHint, forceContentInlineSize]);
4615
+ layoutHint: measurementHint
4616
+ }), [text, layoutContext, measurementHint]);
4255
4617
  const renderText = measurementRequest?.renderText ?? text;
4256
4618
  const useContentInlineSize = measurementRequest?.useContentInlineSize ?? false;
4257
4619
  const measurementBackend = measurementRequest?.measurementBackend ?? null;
4258
4620
  const segments = measurementRequest?.segments ?? EMPTY_SEGMENTS;
4259
4621
  const domMeasurementKey = measurementRequest?.domMeasurementKey ?? null;
4260
- useLayoutEffect(() => {
4622
+ const logTransitionTrace = (event, payload = {}) => {
4261
4623
  const config = readTorphDebugConfig();
4262
- if (!shouldCaptureTorphTrace(config)) {
4624
+ if (!shouldRunTorphInstrumentation(config)) {
4263
4625
  return;
4264
4626
  }
4627
+ logTorphDebug(debugInstanceIdRef.current, event, {
4628
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
4629
+ renderOrdinal: debugRenderOrdinal,
4630
+ text,
4631
+ renderText,
4632
+ stateStage: state.stage,
4633
+ committed: summarizeDebugMeasurement(sessionRef.current.committed),
4634
+ stateMeasurement: summarizeDebugMeasurement(state.measurement),
4635
+ layoutContext: summarizeDebugLayoutContext(layoutContext),
4636
+ measurementBackend,
4637
+ useContentInlineSize,
4638
+ domMeasurementKey,
4639
+ domMeasurementRequestKey,
4640
+ completedDomMeasurementKey: completedDomMeasurementKeyRef.current,
4641
+ ...payload
4642
+ });
4643
+ };
4644
+ useLayoutEffect2(() => {
4265
4645
  ensureTorphTraceApi();
4266
4646
  }, []);
4267
- useLayoutEffect(() => {
4268
- const finalizeMeasurement = (measurement) => refineMeasurementFromLiveNodes(measurement, ref.current, flowTextRef.current);
4647
+ useLayoutEffect2(() => {
4269
4648
  if (ref.current === null || layoutContext === null) {
4270
4649
  completedDomMeasurementKeyRef.current = null;
4271
4650
  if (domMeasurementRequestKey !== null) {
4651
+ logTransitionTrace("effect:dom-measurement-request-update", {
4652
+ reason: "clear-missing-root-or-layout",
4653
+ nextDomMeasurementRequestKey: null
4654
+ });
4272
4655
  setDomMeasurementRequestKey(null);
4273
4656
  }
4274
4657
  reconcileMorphChange({
4275
- finalizeMeasurement,
4276
4658
  root: ref.current,
4277
4659
  measurementLayer: measurementLayerRef.current,
4278
4660
  measurementBackend,
@@ -4295,10 +4677,14 @@ function useMorphTransition(text, className) {
4295
4677
  if (cachedSnapshot !== null) {
4296
4678
  completedDomMeasurementKeyRef.current = domMeasurementKey;
4297
4679
  if (domMeasurementRequestKey !== null) {
4680
+ logTransitionTrace("effect:dom-measurement-request-update", {
4681
+ reason: "clear-after-cache-hit",
4682
+ nextDomMeasurementRequestKey: null,
4683
+ snapshotSource: "cache"
4684
+ });
4298
4685
  setDomMeasurementRequestKey(null);
4299
4686
  }
4300
4687
  reconcileMorphChange({
4301
- finalizeMeasurement,
4302
4688
  root: ref.current,
4303
4689
  measurementLayer: null,
4304
4690
  measurementBackend,
@@ -4315,14 +4701,20 @@ function useMorphTransition(text, className) {
4315
4701
  }
4316
4702
  if (completedDomMeasurementKeyRef.current !== domMeasurementKey) {
4317
4703
  if (domMeasurementRequestKey !== domMeasurementKey) {
4704
+ logTransitionTrace("effect:dom-measurement-request-update", {
4705
+ reason: "request-measurement-layer",
4706
+ nextDomMeasurementRequestKey: domMeasurementKey
4707
+ });
4318
4708
  setDomMeasurementRequestKey(domMeasurementKey);
4319
4709
  return;
4320
4710
  }
4321
4711
  if (measurementLayerRef.current === null) {
4712
+ logTransitionTrace("effect:dom-measurement-await-layer", {
4713
+ reason: "measurement-layer-not-mounted"
4714
+ });
4322
4715
  return;
4323
4716
  }
4324
4717
  const nextMeasurement2 = reconcileMorphChange({
4325
- finalizeMeasurement,
4326
4718
  root: ref.current,
4327
4719
  measurementLayer: measurementLayerRef.current,
4328
4720
  measurementBackend,
@@ -4342,21 +4734,33 @@ function useMorphTransition(text, className) {
4342
4734
  }
4343
4735
  completedDomMeasurementKeyRef.current = domMeasurementKey;
4344
4736
  if (domMeasurementRequestKey !== null) {
4737
+ logTransitionTrace("effect:dom-measurement-request-update", {
4738
+ reason: "clear-after-live-measurement",
4739
+ nextDomMeasurementRequestKey: null,
4740
+ snapshotSource: "layer"
4741
+ });
4345
4742
  setDomMeasurementRequestKey(null);
4346
4743
  }
4347
4744
  return;
4348
4745
  }
4349
4746
  if (domMeasurementRequestKey !== null) {
4747
+ logTransitionTrace("effect:dom-measurement-request-update", {
4748
+ reason: "clear-completed-measurement",
4749
+ nextDomMeasurementRequestKey: null
4750
+ });
4350
4751
  setDomMeasurementRequestKey(null);
4351
4752
  }
4352
4753
  return;
4353
4754
  }
4354
4755
  completedDomMeasurementKeyRef.current = null;
4355
4756
  if (domMeasurementRequestKey !== null) {
4757
+ logTransitionTrace("effect:dom-measurement-request-update", {
4758
+ reason: "clear-no-dom-measurement-needed",
4759
+ nextDomMeasurementRequestKey: null
4760
+ });
4356
4761
  setDomMeasurementRequestKey(null);
4357
4762
  }
4358
4763
  const nextMeasurement = reconcileMorphChange({
4359
- finalizeMeasurement,
4360
4764
  root: ref.current,
4361
4765
  measurementLayer: measurementLayerRef.current,
4362
4766
  measurementBackend,
@@ -4379,36 +4783,7 @@ function useMorphTransition(text, className) {
4379
4783
  domMeasurementKey,
4380
4784
  domMeasurementRequestKey
4381
4785
  ]);
4382
- useLayoutEffect(() => {
4383
- syncCommittedRootOriginWhenIdle({
4384
- root: ref.current,
4385
- flowTextRef,
4386
- layoutContext,
4387
- state,
4388
- session: sessionRef.current
4389
- });
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(() => {
4786
+ useLayoutEffect2(() => {
4412
4787
  const config = readTorphDebugConfig();
4413
4788
  if (!shouldRunTorphInstrumentation(config)) {
4414
4789
  debugDriftSignatureRef.current = null;
@@ -4451,55 +4826,171 @@ function useMorphTransition(text, className) {
4451
4826
  drift: summarizeSnapshotDrift(drift)
4452
4827
  });
4453
4828
  }, [state, text]);
4454
- useLayoutEffect(() => {
4829
+ useLayoutEffect2(() => {
4455
4830
  return () => {
4456
4831
  cancelTimeline(timelineRef.current);
4457
4832
  };
4458
4833
  }, []);
4834
+ useLayoutEffect2(() => {
4835
+ if (state.stage !== "prepare" || state.measurement === null || state.plan === null) {
4836
+ return;
4837
+ }
4838
+ const root = ref.current;
4839
+ if (root === null) {
4840
+ return;
4841
+ }
4842
+ const nextOrigin = readRootOrigin(root);
4843
+ const nextMeasurement = resolvePreparedMeasurementOrigin(state.measurement, nextOrigin);
4844
+ const nextPlan = resolvePreparedPlanVisualBridge(state.plan, nextOrigin);
4845
+ if (nextMeasurement !== state.measurement || nextPlan !== state.plan) {
4846
+ sessionRef.current.target = nextMeasurement;
4847
+ logTransitionTrace("effect:prepare-refine", {
4848
+ preparedOrigin: {
4849
+ left: roundDebugValue(nextOrigin.left),
4850
+ top: roundDebugValue(nextOrigin.top)
4851
+ },
4852
+ refinedMeasurement: summarizeDebugMeasurement(nextMeasurement),
4853
+ refinedVisualBridge: {
4854
+ offsetX: roundDebugValue(nextPlan.visualBridge.offsetX),
4855
+ offsetY: roundDebugValue(nextPlan.visualBridge.offsetY)
4856
+ }
4857
+ });
4858
+ setState((current) => {
4859
+ if (current.stage !== "prepare" || current.measurement === null || current.plan === null) {
4860
+ return current;
4861
+ }
4862
+ if (current.measurement === nextMeasurement && current.plan === nextPlan) {
4863
+ return current;
4864
+ }
4865
+ return {
4866
+ stage: "prepare",
4867
+ measurement: nextMeasurement,
4868
+ plan: nextPlan
4869
+ };
4870
+ });
4871
+ return;
4872
+ }
4873
+ timelineRef.current.prepareFrame = requestAnimationFrame(() => {
4874
+ timelineRef.current.prepareFrame = null;
4875
+ timelineRef.current.animateFrame = requestAnimationFrame(() => {
4876
+ timelineRef.current.animateFrame = null;
4877
+ logTransitionTrace("effect:prepare-animate", {
4878
+ preparedOrigin: {
4879
+ left: roundDebugValue(nextOrigin.left),
4880
+ top: roundDebugValue(nextOrigin.top)
4881
+ },
4882
+ visualBridge: {
4883
+ offsetX: roundDebugValue(nextPlan.visualBridge.offsetX),
4884
+ offsetY: roundDebugValue(nextPlan.visualBridge.offsetY)
4885
+ }
4886
+ });
4887
+ setState((current) => {
4888
+ if (current.stage !== "prepare" || current.measurement === null || current.plan === null) {
4889
+ return current;
4890
+ }
4891
+ return {
4892
+ stage: "animate",
4893
+ measurement: current.measurement,
4894
+ plan: current.plan
4895
+ };
4896
+ });
4897
+ });
4898
+ });
4899
+ return () => {
4900
+ if (timelineRef.current.prepareFrame !== null) {
4901
+ cancelAnimationFrame(timelineRef.current.prepareFrame);
4902
+ timelineRef.current.prepareFrame = null;
4903
+ }
4904
+ if (timelineRef.current.animateFrame !== null) {
4905
+ cancelAnimationFrame(timelineRef.current.animateFrame);
4906
+ timelineRef.current.animateFrame = null;
4907
+ }
4908
+ };
4909
+ }, [state.measurement, state.plan, state.stage]);
4910
+ const finalizeMorphTransition2 = (measurement, reason) => {
4911
+ logTransitionTrace("effect:finalize-trigger", {
4912
+ reason,
4913
+ measurement: summarizeDebugMeasurement(measurement)
4914
+ });
4915
+ finalizeMorphTransition({
4916
+ session: sessionRef.current,
4917
+ timeline: timelineRef.current,
4918
+ measurement,
4919
+ setState
4920
+ });
4921
+ };
4459
4922
  return {
4460
4923
  debugInstanceId: debugInstanceIdRef.current,
4924
+ debugRenderOrdinal,
4461
4925
  committedMeasurement: sessionRef.current.committed,
4462
4926
  domMeasurementRequestKey,
4463
4927
  flowTextRef,
4464
4928
  ref,
4465
4929
  measurementLayerRef,
4930
+ measurementBackend,
4931
+ domMeasurementKey,
4466
4932
  renderText,
4467
4933
  segments,
4468
4934
  layoutContext,
4469
4935
  state,
4470
- useContentInlineSize
4936
+ useContentInlineSize,
4937
+ finalizeMorphTransition: finalizeMorphTransition2,
4938
+ timelineRef
4471
4939
  };
4472
4940
  }
4473
4941
  function ActiveTorph({
4474
4942
  text,
4475
4943
  className
4476
4944
  }) {
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);
4945
+ const debugRenderOrdinalRef = useRef2(0);
4946
+ const overlayRef = useRef2(null);
4947
+ const debugFinalizeSignatureRef = useRef2(null);
4948
+ const debugFrameHandleRef = useRef2(null);
4949
+ const debugFrameOrdinalRef = useRef2(0);
4950
+ const debugAnimateTailFramesRef = useRef2([]);
4951
+ const debugIdlePostFrameHandleRef = useRef2(null);
4952
+ const debugIdlePostFrameOrdinalRef = useRef2(0);
4953
+ const debugIdlePostFrameTokenRef = useRef2(0);
4954
+ const debugPendingIdlePostFramesRef = useRef2(false);
4955
+ const debugPreviousStageRef = useRef2(null);
4956
+ debugRenderOrdinalRef.current += 1;
4957
+ const debugRenderOrdinal = debugRenderOrdinalRef.current;
4485
4958
  const {
4486
4959
  debugInstanceId,
4960
+ debugRenderOrdinal: hookRenderOrdinal,
4487
4961
  committedMeasurement,
4488
4962
  domMeasurementRequestKey,
4963
+ domMeasurementKey,
4489
4964
  flowTextRef,
4490
4965
  ref,
4491
4966
  measurementLayerRef,
4967
+ measurementBackend,
4492
4968
  renderText,
4493
4969
  segments,
4494
4970
  layoutContext,
4495
4971
  state,
4496
- useContentInlineSize
4972
+ useContentInlineSize,
4973
+ finalizeMorphTransition: finalizeMorphTransition2,
4974
+ timelineRef
4497
4975
  } = useMorphTransition(text, className);
4498
4976
  const plan = state.plan;
4499
- const shouldRenderOverlay = state.stage !== "idle" && plan !== null;
4977
+ let visibleGlyphPlan = plan;
4978
+ if (state.stage === "idle" && state.measurement !== null) {
4979
+ visibleGlyphPlan = createSteadyGlyphPlan(state.measurement);
4980
+ }
4981
+ let sourceSliceWhiteSpace = "inherit";
4982
+ if (committedMeasurement !== null) {
4983
+ sourceSliceWhiteSpace = resolveGlyphSliceWhiteSpace(committedMeasurement.snapshot);
4984
+ }
4985
+ let targetSliceWhiteSpace = "inherit";
4986
+ if (state.measurement !== null) {
4987
+ targetSliceWhiteSpace = resolveGlyphSliceWhiteSpace(state.measurement.snapshot);
4988
+ }
4989
+ const shouldRenderGlyphLayer2 = shouldRenderGlyphLayer(state.stage, visibleGlyphPlan, state.measurement);
4990
+ const shouldHideFlowText = shouldRenderGlyphLayer2;
4500
4991
  const shouldRenderMeasurementLayer = domMeasurementRequestKey !== null;
4501
4992
  const flowText = resolveFlowText(committedMeasurement, state.measurement, text);
4502
- useLayoutEffect(() => {
4993
+ useLayoutEffect2(() => {
4503
4994
  const config = readTorphDebugConfig();
4504
4995
  if (!shouldRunTorphInstrumentation(config)) {
4505
4996
  return;
@@ -4507,11 +4998,264 @@ function ActiveTorph({
4507
4998
  logTorphDebug(debugInstanceId, "effect:trace-meta", {
4508
4999
  traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
4509
5000
  includesIdlePostFrame: true,
5001
+ includesIdlePostFrameLifecycle: true,
4510
5002
  includesViewportAnchors: true,
4511
- includesRootOriginRefine: true
5003
+ includesRootOriginRefine: true,
5004
+ includesFullGlyphLayouts: true,
5005
+ includesIdleVisibleGlyphLayer: true,
5006
+ includesPreciseGlyphGeometry: true,
5007
+ includesAnimateTailFrames: true,
5008
+ includesLiveNodeStyles: true
4512
5009
  });
4513
5010
  }, [debugInstanceId]);
4514
- useLayoutEffect(() => {
5011
+ useLayoutEffect2(() => {
5012
+ const config = readTorphDebugConfig();
5013
+ if (!shouldRunTorphInstrumentation(config)) {
5014
+ debugFinalizeSignatureRef.current = null;
5015
+ return;
5016
+ }
5017
+ if (state.stage !== "animate" || state.measurement === null || plan === null) {
5018
+ debugFinalizeSignatureRef.current = null;
5019
+ }
5020
+ }, [plan, state.measurement, state.stage]);
5021
+ const logAnimateFinalizeSnapshot = (measurement, activePlan, reason, barrier, signal, event) => {
5022
+ const config = readTorphDebugConfig();
5023
+ if (!shouldRunTorphInstrumentation(config)) {
5024
+ return;
5025
+ }
5026
+ const root = ref.current;
5027
+ const overlayNode = overlayRef.current;
5028
+ const flowNode = flowTextRef.current;
5029
+ if (root === null || overlayNode === null || flowNode === null) {
5030
+ return;
5031
+ }
5032
+ const overlayLiveSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "live");
5033
+ const overlayExitSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "exit");
5034
+ const flowSnapshot = measureLiveFlowSnapshot(root, flowNode);
5035
+ if (overlayLiveSnapshot === null || flowSnapshot === null) {
5036
+ return;
5037
+ }
5038
+ const overlayLiveDrift = measureSnapshotDrift(measurement.snapshot, overlayLiveSnapshot);
5039
+ const flowDrift = measureSnapshotDrift(measurement.snapshot, flowSnapshot);
5040
+ const rootRect = root.getBoundingClientRect();
5041
+ const flowRect = flowNode.getBoundingClientRect();
5042
+ const overlayRect = overlayNode.getBoundingClientRect();
5043
+ const signature = JSON.stringify({
5044
+ text,
5045
+ renderText: measurement.snapshot.renderText,
5046
+ reason,
5047
+ signal,
5048
+ barrier: summarizeMorphFinalizeBarrier(barrier),
5049
+ overlayLiveDrift: summarizeSnapshotDrift(overlayLiveDrift),
5050
+ flowDrift: summarizeSnapshotDrift(flowDrift),
5051
+ overlayWidth: roundDebugValue(overlayRect.width),
5052
+ rootWidth: roundDebugValue(rootRect.width)
5053
+ });
5054
+ if (debugFinalizeSignatureRef.current === signature) {
5055
+ return;
5056
+ }
5057
+ debugFinalizeSignatureRef.current = signature;
5058
+ let morphRole = null;
5059
+ let morphKey = null;
5060
+ let morphGlyph = null;
5061
+ const target = event?.target;
5062
+ if (target instanceof HTMLElement) {
5063
+ morphRole = target.dataset.morphRole ?? null;
5064
+ morphKey = target.dataset.morphKey ?? null;
5065
+ morphGlyph = target.dataset.morphGlyph ?? null;
5066
+ }
5067
+ logTorphDebug(debugInstanceId, "effect:animate-finalize-snapshot", {
5068
+ text,
5069
+ reason,
5070
+ propertyName: event?.propertyName ?? null,
5071
+ finalizeSignal: signal,
5072
+ morphRole,
5073
+ morphKey,
5074
+ morphGlyph,
5075
+ barrier: summarizeMorphFinalizeBarrier(barrier),
5076
+ target: {
5077
+ layoutInlineSize: roundDebugValue(measurement.layoutInlineSize),
5078
+ reservedInlineSize: roundDebugValue(measurement.reservedInlineSize),
5079
+ flowInlineSize: roundDebugValue(measurement.flowInlineSize),
5080
+ rootOrigin: {
5081
+ left: roundDebugValue(measurement.rootOrigin.left),
5082
+ top: roundDebugValue(measurement.rootOrigin.top)
5083
+ },
5084
+ snapshot: summarizeDebugSnapshot(measurement.snapshot)
5085
+ },
5086
+ plan: {
5087
+ frameWidth: roundDebugValue(activePlan.frameWidth),
5088
+ frameHeight: roundDebugValue(activePlan.frameHeight),
5089
+ layoutInlineSizeFrom: roundDebugValue(activePlan.layoutInlineSizeFrom),
5090
+ layoutInlineSizeTo: roundDebugValue(activePlan.layoutInlineSizeTo),
5091
+ sourceRenderText: activePlan.sourceRenderText,
5092
+ targetRenderText: activePlan.targetRenderText
5093
+ },
5094
+ overlayLive: summarizeDebugSnapshot(overlayLiveSnapshot),
5095
+ overlayLiveGlyphs: summarizeDebugGlyphs(overlayLiveSnapshot),
5096
+ overlayLiveGlyphsPrecise: summarizePreciseGlyphs(overlayLiveSnapshot, rootRect),
5097
+ overlayLiveDrift: summarizeSnapshotDrift(overlayLiveDrift),
5098
+ overlayExit: summarizeDebugSnapshot(overlayExitSnapshot),
5099
+ overlayExitGlyphs: summarizeDebugGlyphs(overlayExitSnapshot),
5100
+ flow: summarizeDebugSnapshot(flowSnapshot),
5101
+ flowGlyphs: summarizeDebugGlyphs(flowSnapshot),
5102
+ flowGlyphsPrecise: summarizePreciseGlyphs(flowSnapshot, rootRect),
5103
+ flowDrift: summarizeSnapshotDrift(flowDrift),
5104
+ rootOriginDrift: summarizeDebugRootOriginDrift(measurement, rootRect),
5105
+ overlayLiveViewportAnchors: summarizeDebugViewportAnchors(overlayLiveSnapshot, rootRect),
5106
+ overlayExitViewportAnchors: summarizeDebugViewportAnchors(overlayExitSnapshot, rootRect),
5107
+ flowViewportAnchors: summarizeDebugViewportAnchors(flowSnapshot, rootRect),
5108
+ rootBox: {
5109
+ width: roundDebugValue(rootRect.width),
5110
+ height: roundDebugValue(rootRect.height)
5111
+ },
5112
+ overlayBox: {
5113
+ width: roundDebugValue(overlayRect.width),
5114
+ height: roundDebugValue(overlayRect.height)
5115
+ },
5116
+ flowBox: {
5117
+ width: roundDebugValue(flowRect.width),
5118
+ height: roundDebugValue(flowRect.height)
5119
+ },
5120
+ rootBoxPrecise: summarizePreciseRect(rootRect),
5121
+ overlayBoxPrecise: summarizePreciseRect(overlayRect),
5122
+ flowBoxPrecise: summarizePreciseRect(flowRect),
5123
+ rootRuntimeStyles: summarizeRootRuntimeStyles(root),
5124
+ overlayLiveNodeStyles: summarizeLiveNodeStyles(overlayNode),
5125
+ animateTailFrames: debugAnimateTailFramesRef.current
5126
+ });
5127
+ debugAnimateTailFramesRef.current = [];
5128
+ };
5129
+ useLayoutEffect2(() => {
5130
+ if (state.stage !== "animate" || state.measurement === null || plan === null) {
5131
+ return;
5132
+ }
5133
+ const root = ref.current;
5134
+ if (root === null) {
5135
+ return;
5136
+ }
5137
+ const measurement = state.measurement;
5138
+ const hasMoveTransitions = plan.liveItems.some((item) => item.kind === "move");
5139
+ let barrier = createMorphFinalizeBarrier(hasMoveTransitions);
5140
+ let finalizeScheduled = false;
5141
+ let finalizeCommitted = false;
5142
+ let finalizeFrame = null;
5143
+ const armedAt = performance.now();
5144
+ const finalizeNow = (reason, signal, barrierState, event) => {
5145
+ if (finalizeCommitted) {
5146
+ return;
5147
+ }
5148
+ finalizeCommitted = true;
5149
+ const target = event?.target;
5150
+ let morphRole = null;
5151
+ let morphKey = null;
5152
+ let morphGlyph = null;
5153
+ if (target instanceof HTMLElement) {
5154
+ morphRole = target.dataset.morphRole ?? null;
5155
+ morphKey = target.dataset.morphKey ?? null;
5156
+ morphGlyph = target.dataset.morphGlyph ?? null;
5157
+ }
5158
+ logAnimateFinalizeSnapshot(measurement, plan, reason, barrierState, signal, event);
5159
+ finalizeMorphTransition2(measurement, reason);
5160
+ const config = readTorphDebugConfig();
5161
+ if (!shouldRunTorphInstrumentation(config)) {
5162
+ return;
5163
+ }
5164
+ logTorphDebug(debugInstanceId, "effect:finalize-authority", {
5165
+ text,
5166
+ reason,
5167
+ propertyName: event?.propertyName ?? null,
5168
+ morphRole,
5169
+ morphKey,
5170
+ morphGlyph,
5171
+ elapsedMs: roundDebugValue(performance.now() - armedAt),
5172
+ hasMoveTransitions,
5173
+ finalizeSignal: signal,
5174
+ barrier: summarizeMorphFinalizeBarrier(barrierState),
5175
+ measurement: summarizeDebugMeasurement(measurement)
5176
+ });
5177
+ };
5178
+ const scheduleFinalize = (reason, signal, barrierState, event) => {
5179
+ if (finalizeCommitted || finalizeScheduled) {
5180
+ return;
5181
+ }
5182
+ finalizeScheduled = true;
5183
+ if (timelineRef.current.finalizeTimer !== null) {
5184
+ window.clearTimeout(timelineRef.current.finalizeTimer);
5185
+ timelineRef.current.finalizeTimer = null;
5186
+ }
5187
+ logTorphDebug(debugInstanceId, "effect:finalize-raf-schedule", {
5188
+ text,
5189
+ reason,
5190
+ propertyName: event?.propertyName ?? null,
5191
+ finalizeSignal: signal,
5192
+ elapsedMs: roundDebugValue(performance.now() - armedAt),
5193
+ barrier: summarizeMorphFinalizeBarrier(barrierState)
5194
+ });
5195
+ finalizeFrame = requestAnimationFrame(() => {
5196
+ finalizeFrame = null;
5197
+ finalizeNow(reason, signal, barrierState, event);
5198
+ });
5199
+ };
5200
+ const onTransitionEnd = (event) => {
5201
+ const signal = resolveMorphFinalizeSignal(event, hasMoveTransitions);
5202
+ if (signal === null) {
5203
+ return;
5204
+ }
5205
+ barrier = recordMorphFinalizeSignal(barrier, signal);
5206
+ const target = event.target;
5207
+ let morphRole = null;
5208
+ let morphKey = null;
5209
+ let morphGlyph = null;
5210
+ if (target instanceof HTMLElement) {
5211
+ morphRole = target.dataset.morphRole ?? null;
5212
+ morphKey = target.dataset.morphKey ?? null;
5213
+ morphGlyph = target.dataset.morphGlyph ?? null;
5214
+ }
5215
+ logTorphDebug(debugInstanceId, "effect:finalize-barrier-progress", {
5216
+ text,
5217
+ propertyName: event.propertyName,
5218
+ signal,
5219
+ morphRole,
5220
+ morphKey,
5221
+ morphGlyph,
5222
+ elapsedMs: roundDebugValue(performance.now() - armedAt),
5223
+ barrier: summarizeMorphFinalizeBarrier(barrier)
5224
+ });
5225
+ if (!isMorphFinalizeBarrierSatisfied(barrier)) {
5226
+ return;
5227
+ }
5228
+ scheduleFinalize("transitionend", signal, barrier, event);
5229
+ };
5230
+ root.addEventListener("transitionend", onTransitionEnd);
5231
+ if (timelineRef.current.finalizeTimer !== null) {
5232
+ window.clearTimeout(timelineRef.current.finalizeTimer);
5233
+ }
5234
+ timelineRef.current.finalizeTimer = window.setTimeout(() => {
5235
+ timelineRef.current.finalizeTimer = null;
5236
+ scheduleFinalize("watchdog-timeout", null, barrier);
5237
+ }, MORPH.durationMs + 32);
5238
+ return () => {
5239
+ root.removeEventListener("transitionend", onTransitionEnd);
5240
+ if (finalizeFrame !== null) {
5241
+ cancelAnimationFrame(finalizeFrame);
5242
+ finalizeFrame = null;
5243
+ }
5244
+ if (timelineRef.current.finalizeTimer !== null) {
5245
+ window.clearTimeout(timelineRef.current.finalizeTimer);
5246
+ timelineRef.current.finalizeTimer = null;
5247
+ }
5248
+ };
5249
+ }, [
5250
+ debugInstanceId,
5251
+ finalizeMorphTransition2,
5252
+ plan,
5253
+ state.measurement,
5254
+ state.stage,
5255
+ text,
5256
+ timelineRef
5257
+ ]);
5258
+ useLayoutEffect2(() => {
4515
5259
  const config = readTorphDebugConfig();
4516
5260
  if (!shouldRunTorphInstrumentation(config)) {
4517
5261
  debugPendingIdlePostFramesRef.current = false;
@@ -4522,25 +5266,34 @@ function ActiveTorph({
4522
5266
  if (previousStage !== state.stage) {
4523
5267
  if (state.stage === "idle") {
4524
5268
  debugPendingIdlePostFramesRef.current = true;
5269
+ debugIdlePostFrameTokenRef.current += 1;
5270
+ } else {
5271
+ debugPendingIdlePostFramesRef.current = false;
4525
5272
  }
4526
5273
  logTorphDebug(debugInstanceId, "effect:stage-transition", {
5274
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5275
+ renderOrdinal: debugRenderOrdinal,
4527
5276
  text,
4528
5277
  fromStage: previousStage,
4529
5278
  toStage: state.stage,
4530
5279
  committed: summarizeDebugMeasurement(committedMeasurement),
4531
5280
  stateMeasurement: summarizeDebugMeasurement(state.measurement),
4532
- flowText
5281
+ flowText,
5282
+ domMeasurementRequestKey,
5283
+ domMeasurementKey,
5284
+ measurementBackend
4533
5285
  });
4534
5286
  debugPreviousStageRef.current = state.stage;
4535
5287
  }
4536
5288
  }, [committedMeasurement, debugInstanceId, flowText, state, text]);
4537
- useLayoutEffect(() => {
5289
+ useLayoutEffect2(() => {
4538
5290
  const config = readTorphDebugConfig();
4539
5291
  if (!shouldRunTorphInstrumentation(config)) {
4540
5292
  if (debugFrameHandleRef.current !== null) {
4541
5293
  cancelAnimationFrame(debugFrameHandleRef.current);
4542
5294
  debugFrameHandleRef.current = null;
4543
5295
  }
5296
+ debugAnimateTailFramesRef.current = [];
4544
5297
  return;
4545
5298
  }
4546
5299
  if (state.stage === "idle" || state.measurement === null) {
@@ -4549,6 +5302,7 @@ function ActiveTorph({
4549
5302
  debugFrameHandleRef.current = null;
4550
5303
  }
4551
5304
  debugFrameOrdinalRef.current = 0;
5305
+ debugAnimateTailFramesRef.current = [];
4552
5306
  return;
4553
5307
  }
4554
5308
  debugFrameOrdinalRef.current = 0;
@@ -4593,6 +5347,19 @@ function ActiveTorph({
4593
5347
  if (flowSnapshot !== null) {
4594
5348
  flowDrift = summarizeSnapshotDrift(measureSnapshotDrift(measurement.snapshot, flowSnapshot));
4595
5349
  }
5350
+ debugAnimateTailFramesRef.current.push({
5351
+ frame: debugFrameOrdinalRef.current,
5352
+ stateStage: state.stage,
5353
+ rootBoxPrecise: summarizePreciseRect(rootRect),
5354
+ overlayBoxPrecise: summarizePreciseRect(overlayRect),
5355
+ flowBoxPrecise: summarizePreciseRect(flowRect),
5356
+ overlayLiveGlyphsPrecise: summarizePreciseGlyphs(overlayLiveSnapshot, rootRect),
5357
+ flowGlyphsPrecise: summarizePreciseGlyphs(flowSnapshot, rootRect),
5358
+ overlayLiveNodeStyles: summarizeLiveNodeStyles(overlayNode)
5359
+ });
5360
+ if (debugAnimateTailFramesRef.current.length > 8) {
5361
+ debugAnimateTailFramesRef.current.shift();
5362
+ }
4596
5363
  let planSummary = null;
4597
5364
  if (plan !== null) {
4598
5365
  planSummary = {
@@ -4624,13 +5391,17 @@ function ActiveTorph({
4624
5391
  flowBox: summarizeDebugRect(flowRect),
4625
5392
  rootOriginDrift: summarizeDebugRootOriginDrift(measurement, rootRect),
4626
5393
  overlayLive: summarizeDebugSnapshot(overlayLiveSnapshot),
5394
+ overlayLiveGlyphs: summarizeDebugGlyphs(overlayLiveSnapshot),
4627
5395
  overlayLiveViewportAnchors: summarizeDebugViewportAnchors(overlayLiveSnapshot, rootRect),
4628
5396
  overlayLiveDrift,
4629
5397
  overlayExit: summarizeDebugSnapshot(overlayExitSnapshot),
5398
+ overlayExitGlyphs: summarizeDebugGlyphs(overlayExitSnapshot),
4630
5399
  overlayExitViewportAnchors: summarizeDebugViewportAnchors(overlayExitSnapshot, rootRect),
4631
5400
  flow: summarizeDebugSnapshot(flowSnapshot),
5401
+ flowGlyphs: summarizeDebugGlyphs(flowSnapshot),
4632
5402
  flowViewportAnchors: summarizeDebugViewportAnchors(flowSnapshot, rootRect),
4633
- flowDrift
5403
+ flowDrift,
5404
+ rootRuntimeStyles: summarizeRootRuntimeStyles(root)
4634
5405
  });
4635
5406
  debugFrameOrdinalRef.current += 1;
4636
5407
  debugFrameHandleRef.current = requestAnimationFrame(captureFrame);
@@ -4644,11 +5415,22 @@ function ActiveTorph({
4644
5415
  }
4645
5416
  };
4646
5417
  }, [committedMeasurement, debugInstanceId, flowText, plan, ref, state, text]);
4647
- useLayoutEffect(() => {
5418
+ useLayoutEffect2(() => {
4648
5419
  const config = readTorphDebugConfig();
4649
5420
  if (!shouldRunTorphInstrumentation(config)) {
4650
5421
  debugPendingIdlePostFramesRef.current = false;
4651
5422
  if (debugIdlePostFrameHandleRef.current !== null) {
5423
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-cleanup", {
5424
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5425
+ reason: "instrumentation-disabled",
5426
+ renderOrdinal: debugRenderOrdinal,
5427
+ hookRenderOrdinal,
5428
+ token: debugIdlePostFrameTokenRef.current,
5429
+ handle: debugIdlePostFrameHandleRef.current,
5430
+ domMeasurementRequestKey,
5431
+ domMeasurementKey,
5432
+ measurementBackend
5433
+ });
4652
5434
  cancelAnimationFrame(debugIdlePostFrameHandleRef.current);
4653
5435
  debugIdlePostFrameHandleRef.current = null;
4654
5436
  }
@@ -4660,28 +5442,93 @@ function ActiveTorph({
4660
5442
  if (state.stage !== "idle" || state.measurement === null) {
4661
5443
  return;
4662
5444
  }
4663
- debugPendingIdlePostFramesRef.current = false;
4664
5445
  debugIdlePostFrameOrdinalRef.current = 0;
5446
+ const token = debugIdlePostFrameTokenRef.current;
5447
+ const scheduledRenderOrdinal = debugRenderOrdinal;
5448
+ const scheduledHookRenderOrdinal = hookRenderOrdinal;
4665
5449
  const measurement = state.measurement;
4666
5450
  let remainingFrames = 3;
4667
5451
  let cancelled = false;
5452
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-arm", {
5453
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5454
+ renderOrdinal: scheduledRenderOrdinal,
5455
+ hookRenderOrdinal: scheduledHookRenderOrdinal,
5456
+ token,
5457
+ text,
5458
+ stateStage: state.stage,
5459
+ flowText,
5460
+ committed: summarizeDebugMeasurement(committedMeasurement),
5461
+ stateMeasurement: summarizeDebugMeasurement(measurement),
5462
+ domMeasurementRequestKey,
5463
+ domMeasurementKey,
5464
+ measurementBackend
5465
+ });
4668
5466
  const captureIdlePostFrame = () => {
4669
5467
  if (cancelled) {
4670
5468
  return;
4671
5469
  }
5470
+ if (debugPendingIdlePostFramesRef.current) {
5471
+ debugPendingIdlePostFramesRef.current = false;
5472
+ }
5473
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-fire", {
5474
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5475
+ renderOrdinal: debugRenderOrdinalRef.current,
5476
+ hookRenderOrdinal,
5477
+ scheduledRenderOrdinal,
5478
+ scheduledHookRenderOrdinal,
5479
+ token,
5480
+ frame: debugIdlePostFrameOrdinalRef.current,
5481
+ remainingFrames,
5482
+ text,
5483
+ stateStage: state.stage,
5484
+ flowText
5485
+ });
4672
5486
  const root = ref.current;
4673
5487
  const flowNode = flowTextRef.current;
4674
- if (root === null || flowNode === null) {
5488
+ const overlayNode = overlayRef.current;
5489
+ if (root === null || flowNode === null || overlayNode === null) {
5490
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-skip", {
5491
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5492
+ renderOrdinal: debugRenderOrdinalRef.current,
5493
+ hookRenderOrdinal,
5494
+ scheduledRenderOrdinal,
5495
+ scheduledHookRenderOrdinal,
5496
+ token,
5497
+ text,
5498
+ stateStage: state.stage,
5499
+ frame: debugIdlePostFrameOrdinalRef.current,
5500
+ hasRoot: root !== null,
5501
+ hasFlowNode: flowNode !== null,
5502
+ hasOverlayNode: overlayNode !== null,
5503
+ flowText,
5504
+ committed: summarizeDebugMeasurement(committedMeasurement),
5505
+ stateMeasurement: summarizeDebugMeasurement(measurement)
5506
+ });
4675
5507
  return;
4676
5508
  }
4677
5509
  const rootRect = root.getBoundingClientRect();
4678
5510
  const flowRect = flowNode.getBoundingClientRect();
5511
+ const overlayRect = overlayNode.getBoundingClientRect();
4679
5512
  const flowSnapshot = measureLiveFlowSnapshot(root, flowNode);
5513
+ let visibleSnapshot = null;
5514
+ if (overlayNode !== null) {
5515
+ visibleSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "live");
5516
+ }
4680
5517
  let flowDrift = null;
4681
5518
  if (flowSnapshot !== null) {
4682
5519
  flowDrift = summarizeSnapshotDrift(measureSnapshotDrift(measurement.snapshot, flowSnapshot));
4683
5520
  }
5521
+ let visibleDrift = null;
5522
+ if (visibleSnapshot !== null) {
5523
+ visibleDrift = summarizeSnapshotDrift(measureSnapshotDrift(measurement.snapshot, visibleSnapshot));
5524
+ }
4684
5525
  logTorphDebug(debugInstanceId, "effect:idle-post-frame", {
5526
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5527
+ renderOrdinal: debugRenderOrdinalRef.current,
5528
+ hookRenderOrdinal,
5529
+ scheduledRenderOrdinal,
5530
+ scheduledHookRenderOrdinal,
5531
+ token,
4685
5532
  text,
4686
5533
  frame: debugIdlePostFrameOrdinalRef.current,
4687
5534
  stateStage: state.stage,
@@ -4690,11 +5537,24 @@ function ActiveTorph({
4690
5537
  committed: summarizeDebugMeasurement(committedMeasurement),
4691
5538
  stateMeasurement: summarizeDebugMeasurement(measurement),
4692
5539
  rootBox: summarizeDebugRect(rootRect),
5540
+ rootBoxPrecise: summarizePreciseRect(rootRect),
5541
+ overlayBox: summarizeDebugRect(overlayRect),
5542
+ overlayBoxPrecise: summarizePreciseRect(overlayRect),
4693
5543
  flowBox: summarizeDebugRect(flowRect),
5544
+ flowBoxPrecise: summarizePreciseRect(flowRect),
5545
+ rootRuntimeStyles: summarizeRootRuntimeStyles(root),
4694
5546
  rootOriginDrift: summarizeDebugRootOriginDrift(measurement, rootRect),
4695
5547
  flow: summarizeDebugSnapshot(flowSnapshot),
5548
+ flowGlyphs: summarizeDebugGlyphs(flowSnapshot),
5549
+ flowGlyphsPrecise: summarizePreciseGlyphs(flowSnapshot, rootRect),
4696
5550
  flowViewportAnchors: summarizeDebugViewportAnchors(flowSnapshot, rootRect),
4697
- flowDrift
5551
+ flowDrift,
5552
+ visible: summarizeDebugSnapshot(visibleSnapshot),
5553
+ visibleGlyphs: summarizeDebugGlyphs(visibleSnapshot),
5554
+ visibleGlyphsPrecise: summarizePreciseGlyphs(visibleSnapshot, rootRect),
5555
+ visibleViewportAnchors: summarizeDebugViewportAnchors(visibleSnapshot, rootRect),
5556
+ visibleDrift,
5557
+ visibleLiveNodeStyles: summarizeLiveNodeStyles(overlayNode)
4698
5558
  });
4699
5559
  debugIdlePostFrameOrdinalRef.current += 1;
4700
5560
  remainingFrames -= 1;
@@ -4703,121 +5563,77 @@ function ActiveTorph({
4703
5563
  return;
4704
5564
  }
4705
5565
  debugIdlePostFrameHandleRef.current = requestAnimationFrame(captureIdlePostFrame);
5566
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-reschedule", {
5567
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5568
+ renderOrdinal: debugRenderOrdinalRef.current,
5569
+ hookRenderOrdinal,
5570
+ scheduledRenderOrdinal,
5571
+ scheduledHookRenderOrdinal,
5572
+ token,
5573
+ handle: debugIdlePostFrameHandleRef.current,
5574
+ nextFrame: debugIdlePostFrameOrdinalRef.current,
5575
+ remainingFrames,
5576
+ text,
5577
+ stateStage: state.stage,
5578
+ flowText
5579
+ });
4706
5580
  };
4707
5581
  debugIdlePostFrameHandleRef.current = requestAnimationFrame(captureIdlePostFrame);
5582
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-schedule", {
5583
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5584
+ renderOrdinal: scheduledRenderOrdinal,
5585
+ hookRenderOrdinal: scheduledHookRenderOrdinal,
5586
+ token,
5587
+ handle: debugIdlePostFrameHandleRef.current,
5588
+ remainingFrames,
5589
+ text,
5590
+ stateStage: state.stage,
5591
+ flowText,
5592
+ domMeasurementRequestKey,
5593
+ domMeasurementKey,
5594
+ measurementBackend
5595
+ });
4708
5596
  return () => {
4709
5597
  cancelled = true;
4710
5598
  if (debugIdlePostFrameHandleRef.current !== null) {
5599
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-cleanup", {
5600
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5601
+ reason: "effect-rerun-or-unmount",
5602
+ renderOrdinal: debugRenderOrdinalRef.current,
5603
+ hookRenderOrdinal,
5604
+ scheduledRenderOrdinal,
5605
+ scheduledHookRenderOrdinal,
5606
+ token,
5607
+ handle: debugIdlePostFrameHandleRef.current,
5608
+ completedFrames: debugIdlePostFrameOrdinalRef.current,
5609
+ remainingFrames,
5610
+ text,
5611
+ stateStage: state.stage,
5612
+ flowText,
5613
+ domMeasurementRequestKey,
5614
+ domMeasurementKey,
5615
+ measurementBackend
5616
+ });
4711
5617
  cancelAnimationFrame(debugIdlePostFrameHandleRef.current);
4712
5618
  debugIdlePostFrameHandleRef.current = null;
4713
5619
  }
4714
5620
  };
4715
5621
  }, [committedMeasurement, debugInstanceId, flowText, ref, state, text]);
4716
- useLayoutEffect(() => {
5622
+ useLayoutEffect2(() => {
4717
5623
  return () => {
4718
5624
  if (debugIdlePostFrameHandleRef.current !== null) {
5625
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-cleanup", {
5626
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5627
+ reason: "component-unmount",
5628
+ renderOrdinal: debugRenderOrdinalRef.current,
5629
+ token: debugIdlePostFrameTokenRef.current,
5630
+ handle: debugIdlePostFrameHandleRef.current
5631
+ });
4719
5632
  cancelAnimationFrame(debugIdlePostFrameHandleRef.current);
4720
5633
  debugIdlePostFrameHandleRef.current = null;
4721
5634
  }
4722
5635
  };
4723
5636
  }, []);
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]);
4821
5637
  let measurementLayer = null;
4822
5638
  if (shouldRenderMeasurementLayer) {
4823
5639
  measurementLayer = /* @__PURE__ */ jsxDEV(MeasurementLayer, {
@@ -4828,11 +5644,13 @@ function ActiveTorph({
4828
5644
  }, undefined, false, undefined, this);
4829
5645
  }
4830
5646
  let overlay = null;
4831
- if (shouldRenderOverlay) {
5647
+ if (shouldRenderGlyphLayer2 && visibleGlyphPlan !== null) {
4832
5648
  overlay = /* @__PURE__ */ jsxDEV(MorphOverlay, {
4833
5649
  overlayRef,
4834
5650
  stage: state.stage,
4835
- plan
5651
+ plan: visibleGlyphPlan,
5652
+ sourceSliceWhiteSpace,
5653
+ targetSliceWhiteSpace
4836
5654
  }, undefined, false, undefined, this);
4837
5655
  }
4838
5656
  return /* @__PURE__ */ jsxDEV("div", {
@@ -4846,7 +5664,7 @@ function ActiveTorph({
4846
5664
  }, undefined, false, undefined, this),
4847
5665
  /* @__PURE__ */ jsxDEV("span", {
4848
5666
  "aria-hidden": "true",
4849
- style: getFallbackTextStyle(shouldRenderOverlay),
5667
+ style: getFallbackTextStyle(shouldHideFlowText),
4850
5668
  children: /* @__PURE__ */ jsxDEV("span", {
4851
5669
  ref: flowTextRef,
4852
5670
  children: flowText
@@ -4867,23 +5685,5 @@ function Torph({
4867
5685
  }, undefined, false, undefined, this);
4868
5686
  }
4869
5687
  export {
4870
- supportsIntrinsicWidthLock,
4871
- shouldHealIdleMeasurementFromFlow,
4872
- resolveMorphFrameBounds,
4873
- resolveFlowText,
4874
- resolveContentWidthLockInlineSize,
4875
- refineMeasurementWithLiveGeometry,
4876
- pairMorphCharacters,
4877
- needsMeasurementLayer,
4878
- measureMorphSnapshotFromLayer,
4879
- getRootStyle,
4880
- getRootDisplay,
4881
- getMeasurementLayerStyle,
4882
- getLiveTransition,
4883
- getLiveTransform,
4884
- getExitTransition,
4885
- getExitTransform,
4886
- buildMorphVisualBridge,
4887
- buildMorphPlan,
4888
5688
  Torph
4889
5689
  };